mirror of
https://git.sr.ht/~tsileo/microblog.pub
synced 2025-06-05 21:59:23 +02:00
Compare commits
28 Commits
outbox-fts
...
2.0.0-rc.8
Author | SHA1 | Date | |
---|---|---|---|
d352dc104a | |||
0c5ce67d4e | |||
9db7bdf0fb | |||
793a939046 | |||
c3eb44add7 | |||
9b75020c91 | |||
36a1a6bd9c | |||
164cd9bd00 | |||
698a2bae11 | |||
4613997fe3 | |||
4c995957a6 | |||
5c98b8dbfb | |||
48d5914851 | |||
8f00e522d7 | |||
62c9327500 | |||
a339ff93b1 | |||
afd253a1b4 | |||
509e10e79b | |||
d96ec913d4 | |||
5b505b0e37 | |||
530491ff10 | |||
48740ea8cb | |||
0d7c121781 | |||
a4cfd65009 | |||
540b9d1470 | |||
1c076049cf | |||
242bf7b515 | |||
2843155501 |
@ -154,6 +154,13 @@ if ALSO_KNOWN_AS:
|
||||
if MOVED_TO:
|
||||
ME["movedTo"] = MOVED_TO
|
||||
|
||||
if config.CONFIG.image_url:
|
||||
ME["image"] = {
|
||||
"mediaType": mimetypes.guess_type(config.CONFIG.image_url)[0],
|
||||
"type": "Image",
|
||||
"url": config.CONFIG.image_url,
|
||||
}
|
||||
|
||||
|
||||
class NotAnObjectError(Exception):
|
||||
def __init__(self, url: str, resp: httpx.Response | None = None) -> None:
|
||||
|
50
app/actor.py
50
app/actor.py
@ -82,11 +82,21 @@ class Actor:
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return self.ap_actor.get("icon", {}).get("url")
|
||||
if icon := self.ap_actor.get("icon"):
|
||||
return icon.get("url")
|
||||
return None
|
||||
|
||||
@property
|
||||
def icon_media_type(self) -> str | None:
|
||||
return self.ap_actor.get("icon", {}).get("mediaType")
|
||||
if icon := self.ap_actor.get("icon"):
|
||||
return icon.get("mediaType")
|
||||
return None
|
||||
|
||||
@property
|
||||
def image_url(self) -> str | None:
|
||||
if image := self.ap_actor.get("image"):
|
||||
return image.get("url")
|
||||
return None
|
||||
|
||||
@property
|
||||
def public_key_as_pem(self) -> str:
|
||||
@ -214,24 +224,23 @@ async def fetch_actor(
|
||||
|
||||
if save_if_not_found:
|
||||
ap_actor = await ap.fetch(actor_id)
|
||||
# Some softwares uses URL when we expect ID
|
||||
if actor_id == ap_actor.get("url"):
|
||||
# Which mean we may already have it in DB
|
||||
existing_actor_by_url = (
|
||||
await db_session.scalars(
|
||||
select(models.Actor).where(
|
||||
models.Actor.ap_id == ap.get_id(ap_actor),
|
||||
)
|
||||
# Some softwares uses URL when we expect ID or uses a different casing
|
||||
# (like Birdsite LIVE) , which mean we may already have it in DB
|
||||
existing_actor_by_url = (
|
||||
await db_session.scalars(
|
||||
select(models.Actor).where(
|
||||
models.Actor.ap_id == ap.get_id(ap_actor),
|
||||
)
|
||||
).one_or_none()
|
||||
if existing_actor_by_url:
|
||||
# Update the actor as we had to fetch it anyway
|
||||
await update_actor_if_needed(
|
||||
db_session,
|
||||
existing_actor_by_url,
|
||||
RemoteActor(ap_actor),
|
||||
)
|
||||
return existing_actor_by_url
|
||||
)
|
||||
).one_or_none()
|
||||
if existing_actor_by_url:
|
||||
# Update the actor as we had to fetch it anyway
|
||||
await update_actor_if_needed(
|
||||
db_session,
|
||||
existing_actor_by_url,
|
||||
RemoteActor(ap_actor),
|
||||
)
|
||||
return existing_actor_by_url
|
||||
|
||||
return await save_actor(db_session, ap_actor)
|
||||
else:
|
||||
@ -381,6 +390,9 @@ def _actor_hash(actor: Actor) -> bytes:
|
||||
if actor.icon_url:
|
||||
h.update(actor.icon_url.encode())
|
||||
|
||||
if actor.image_url:
|
||||
h.update(actor.image_url.encode())
|
||||
|
||||
if actor.attachments:
|
||||
for a in actor.attachments:
|
||||
if a.get("type") != "PropertyValue":
|
||||
|
24
app/admin.py
24
app/admin.py
@ -850,6 +850,30 @@ async def admin_profile(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/actions/force_delete")
|
||||
async def admin_actions_force_delete(
|
||||
request: Request,
|
||||
ap_object_id: str = Form(),
|
||||
redirect_url: str = Form(),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> RedirectResponse:
|
||||
ap_object_to_delete = await get_inbox_object_by_ap_id(db_session, ap_object_id)
|
||||
if not ap_object_to_delete:
|
||||
raise ValueError(f"Cannot find {ap_object_id}")
|
||||
|
||||
logger.info(f"Deleting {ap_object_to_delete.ap_type}/{ap_object_to_delete.ap_id}")
|
||||
await boxes._revert_side_effect_for_deleted_object(
|
||||
db_session,
|
||||
None,
|
||||
ap_object_to_delete,
|
||||
None,
|
||||
)
|
||||
ap_object_to_delete.is_deleted = True
|
||||
await db_session.commit()
|
||||
return RedirectResponse(redirect_url, status_code=302)
|
||||
|
||||
|
||||
@router.post("/actions/follow")
|
||||
async def admin_actions_follow(
|
||||
request: Request,
|
||||
|
@ -96,6 +96,9 @@ class Object:
|
||||
def attachments(self) -> list["Attachment"]:
|
||||
attachments = []
|
||||
for obj in ap.as_list(self.ap_object.get("attachment", [])):
|
||||
if obj.get("type") == "PropertyValue":
|
||||
continue
|
||||
|
||||
if obj.get("type") == "Link":
|
||||
attachments.append(
|
||||
Attachment.parse_obj(
|
||||
@ -277,6 +280,9 @@ class Attachment(BaseModel):
|
||||
proxied_url: str | None = None
|
||||
resized_url: str | None = None
|
||||
|
||||
width: int | None = None
|
||||
height: int | None = None
|
||||
|
||||
@property
|
||||
def mimetype(self) -> str:
|
||||
mimetype = self.media_type
|
||||
|
@ -1186,7 +1186,7 @@ async def _get_replies_count(
|
||||
|
||||
async def _revert_side_effect_for_deleted_object(
|
||||
db_session: AsyncSession,
|
||||
delete_activity: models.InboxObject,
|
||||
delete_activity: models.InboxObject | None,
|
||||
deleted_ap_object: models.InboxObject,
|
||||
forwarded_by_actor: models.Actor | None,
|
||||
) -> None:
|
||||
@ -1223,7 +1223,7 @@ async def _revert_side_effect_for_deleted_object(
|
||||
.where(
|
||||
models.OutboxObject.id == replied_object.id,
|
||||
)
|
||||
.values(replies_count=new_replies_count)
|
||||
.values(replies_count=new_replies_count - 1)
|
||||
)
|
||||
else:
|
||||
new_replies_count = await _get_replies_count(
|
||||
@ -1235,7 +1235,7 @@ async def _revert_side_effect_for_deleted_object(
|
||||
.where(
|
||||
models.InboxObject.id == replied_object.id,
|
||||
)
|
||||
.values(replies_count=new_replies_count)
|
||||
.values(replies_count=new_replies_count - 1)
|
||||
)
|
||||
|
||||
if deleted_ap_object.ap_type == "Like" and deleted_ap_object.activity_object_ap_id:
|
||||
@ -1282,7 +1282,8 @@ async def _revert_side_effect_for_deleted_object(
|
||||
# If it's a local replies, it was forwarded, so we also need to forward
|
||||
# the Delete activity if possible
|
||||
if (
|
||||
delete_activity.activity_object_ap_id == deleted_ap_object.ap_id
|
||||
delete_activity
|
||||
and delete_activity.activity_object_ap_id == deleted_ap_object.ap_id
|
||||
and delete_activity.has_ld_signature
|
||||
and is_delete_needs_to_be_forwarded
|
||||
):
|
||||
|
@ -1,4 +1,5 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
@ -91,6 +92,7 @@ class Config(pydantic.BaseModel):
|
||||
summary: str
|
||||
https: bool
|
||||
icon_url: str
|
||||
image_url: str | None = None
|
||||
secret: str
|
||||
debug: bool = False
|
||||
trusted_hosts: list[str] = ["127.0.0.1"]
|
||||
@ -108,10 +110,15 @@ class Config(pydantic.BaseModel):
|
||||
|
||||
inbox_retention_days: int = 15
|
||||
|
||||
custom_content_security_policy: str | None = None
|
||||
|
||||
# Config items to make tests easier
|
||||
sqlalchemy_database: str | None = None
|
||||
key_path: str | None = None
|
||||
|
||||
# Only set when the app is served on a non-root path
|
||||
id: str | None = None
|
||||
|
||||
|
||||
def load_config() -> Config:
|
||||
try:
|
||||
@ -146,6 +153,11 @@ CONFIG = load_config()
|
||||
DOMAIN = CONFIG.domain
|
||||
_SCHEME = "https" if CONFIG.https else "http"
|
||||
ID = f"{_SCHEME}://{DOMAIN}"
|
||||
|
||||
# When running the app on a path, the ID maybe set by the config, but in this
|
||||
# case, a valid webfinger must be served on the root domain
|
||||
if CONFIG.id:
|
||||
ID = CONFIG.id
|
||||
USERNAME = CONFIG.username
|
||||
MANUALLY_APPROVES_FOLLOWERS = CONFIG.manually_approves_followers
|
||||
HIDES_FOLLOWERS = CONFIG.hides_followers
|
||||
@ -156,6 +168,7 @@ if CONFIG.privacy_replace:
|
||||
|
||||
BLOCKED_SERVERS = {blocked_server.hostname for blocked_server in CONFIG.blocked_servers}
|
||||
ALSO_KNOWN_AS = CONFIG.also_known_as
|
||||
CUSTOM_CONTENT_SECURITY_POLICY = CONFIG.custom_content_security_policy
|
||||
|
||||
INBOX_RETENTION_DAYS = CONFIG.inbox_retention_days
|
||||
CUSTOM_FOOTER = (
|
||||
@ -176,7 +189,9 @@ if CONFIG.emoji:
|
||||
EMOJIS = CONFIG.emoji
|
||||
|
||||
# Emoji template for the FE
|
||||
EMOJI_TPL = '<img src="/static/twemoji/{filename}.svg" alt="{raw}" class="emoji">'
|
||||
EMOJI_TPL = (
|
||||
'<img src="{base_url}/static/twemoji/{filename}.svg" alt="{raw}" class="emoji">'
|
||||
)
|
||||
|
||||
_load_emojis(ROOT_DIR, BASE_URL)
|
||||
|
||||
@ -240,3 +255,7 @@ def verify_csrf_token(
|
||||
detail=f"The security token has expired, {please_try_again}",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def hmac_sha256():
|
||||
return hmac.new(CONFIG.secret.encode(), digestmod=hashlib.sha256)
|
||||
|
@ -3,7 +3,6 @@ import traceback
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import select
|
||||
@ -108,6 +107,7 @@ async def process_next_incoming_activity(
|
||||
|
||||
next_activity.tries = next_activity.tries + 1
|
||||
next_activity.last_try = now()
|
||||
await db_session.commit()
|
||||
|
||||
if next_activity.ap_object and next_activity.sent_by_ap_actor_id:
|
||||
try:
|
||||
@ -120,13 +120,16 @@ async def process_next_incoming_activity(
|
||||
),
|
||||
timeout=60,
|
||||
)
|
||||
except httpx.TimeoutException as exc:
|
||||
url = exc._request.url if exc._request else None
|
||||
logger.error(f"Failed, HTTP timeout when fetching {url}")
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
logger.error("Activity took too long to process")
|
||||
await db_session.rollback()
|
||||
await db_session.refresh(next_activity)
|
||||
next_activity.error = traceback.format_exc()
|
||||
_set_next_try(next_activity)
|
||||
except Exception:
|
||||
logger.exception("Failed")
|
||||
await db_session.rollback()
|
||||
await db_session.refresh(next_activity)
|
||||
next_activity.error = traceback.format_exc()
|
||||
_set_next_try(next_activity)
|
||||
else:
|
||||
|
67
app/main.py
67
app/main.py
@ -48,6 +48,7 @@ from app import boxes
|
||||
from app import config
|
||||
from app import httpsig
|
||||
from app import indieauth
|
||||
from app import media
|
||||
from app import micropub
|
||||
from app import models
|
||||
from app import templates
|
||||
@ -136,9 +137,15 @@ class CustomMiddleware:
|
||||
headers["x-frame-options"] = "DENY"
|
||||
headers["permissions-policy"] = "interest-cohort=()"
|
||||
headers["content-security-policy"] = (
|
||||
f"default-src 'self'; "
|
||||
f"style-src 'self' 'sha256-{HIGHLIGHT_CSS_HASH}'; "
|
||||
f"frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
|
||||
(
|
||||
f"default-src 'self'; "
|
||||
f"style-src 'self' 'sha256-{HIGHLIGHT_CSS_HASH}'; "
|
||||
f"frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
|
||||
)
|
||||
if not config.CUSTOM_CONTENT_SECURITY_POLICY
|
||||
else config.CUSTOM_CONTENT_SECURITY_POLICY.format(
|
||||
HIGHLIGHT_CSS_HASH=HIGHLIGHT_CSS_HASH
|
||||
)
|
||||
)
|
||||
if not DEBUG:
|
||||
headers["strict-transport-security"] = "max-age=63072000;"
|
||||
@ -247,6 +254,30 @@ class ActivityPubResponse(JSONResponse):
|
||||
media_type = "application/activity+json"
|
||||
|
||||
|
||||
async def redirect_to_remote_instance(
|
||||
request: Request,
|
||||
db_session: AsyncSession,
|
||||
url: str,
|
||||
) -> templates.TemplateResponse:
|
||||
"""
|
||||
Similar to RedirectResponse, but uses a 200 response with HTML.
|
||||
|
||||
Needed for remote redirects on form submission endpoints,
|
||||
since our CSP policy disallows remote form submission.
|
||||
https://github.com/w3c/webappsec-csp/issues/8#issuecomment-810108984
|
||||
"""
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"redirect_to_remote_instance.html",
|
||||
{
|
||||
"request": request,
|
||||
"url": url,
|
||||
},
|
||||
headers={"Refresh": "0;url=" + url},
|
||||
)
|
||||
|
||||
|
||||
@app.get(config.NavBarItems.NOTES_PATH)
|
||||
async def index(
|
||||
request: Request,
|
||||
@ -733,7 +764,7 @@ async def outbox_by_public_id(
|
||||
|
||||
if maybe_object.ap_type == "Article":
|
||||
return RedirectResponse(
|
||||
f"/articles/{public_id[:7]}/{maybe_object.slug}",
|
||||
f"{BASE_URL}/articles/{public_id[:7]}/{maybe_object.slug}",
|
||||
status_code=301,
|
||||
)
|
||||
|
||||
@ -952,9 +983,10 @@ async def get_remote_follow(
|
||||
@app.post("/remote_follow")
|
||||
async def post_remote_follow(
|
||||
request: Request,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
profile: str = Form(),
|
||||
) -> RedirectResponse:
|
||||
) -> templates.TemplateResponse:
|
||||
if not profile.startswith("@"):
|
||||
profile = f"@{profile}"
|
||||
|
||||
@ -963,9 +995,10 @@ async def post_remote_follow(
|
||||
# TODO(ts): error message to user
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
return RedirectResponse(
|
||||
return await redirect_to_remote_instance(
|
||||
request,
|
||||
db_session,
|
||||
remote_follow_template.format(uri=ID),
|
||||
status_code=302,
|
||||
)
|
||||
|
||||
|
||||
@ -993,10 +1026,11 @@ async def remote_interaction(
|
||||
@app.post("/remote_interaction")
|
||||
async def post_remote_interaction(
|
||||
request: Request,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
profile: str = Form(),
|
||||
ap_id: str = Form(),
|
||||
) -> RedirectResponse:
|
||||
) -> templates.TemplateResponse:
|
||||
if not profile.startswith("@"):
|
||||
profile = f"@{profile}"
|
||||
|
||||
@ -1005,9 +1039,10 @@ async def post_remote_interaction(
|
||||
# TODO(ts): error message to user
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
return RedirectResponse(
|
||||
remote_follow_template.format(uri=ap_id),
|
||||
status_code=302,
|
||||
return await redirect_to_remote_instance(
|
||||
request,
|
||||
db_session,
|
||||
remote_follow_template.format(uri=ID),
|
||||
)
|
||||
|
||||
|
||||
@ -1128,14 +1163,17 @@ def _add_cache_control(headers: dict[str, str]) -> dict[str, str]:
|
||||
return {**headers, "Cache-Control": "max-age=31536000"}
|
||||
|
||||
|
||||
@app.get("/proxy/media/{encoded_url}")
|
||||
@app.get("/proxy/media/{exp}/{sig}/{encoded_url}")
|
||||
async def serve_proxy_media(
|
||||
request: Request,
|
||||
exp: int,
|
||||
sig: str,
|
||||
encoded_url: str,
|
||||
) -> StreamingResponse | PlainTextResponse:
|
||||
# Decode the base64-encoded URL
|
||||
url = base64.urlsafe_b64decode(encoded_url).decode()
|
||||
check_url(url)
|
||||
media.verify_proxied_media_sig(exp, url, sig)
|
||||
|
||||
proxy_resp = await _proxy_get(request, url, stream=True)
|
||||
|
||||
@ -1168,9 +1206,11 @@ async def serve_proxy_media(
|
||||
)
|
||||
|
||||
|
||||
@app.get("/proxy/media/{encoded_url}/{size}")
|
||||
@app.get("/proxy/media/{exp}/{sig}/{encoded_url}/{size}")
|
||||
async def serve_proxy_media_resized(
|
||||
request: Request,
|
||||
exp: int,
|
||||
sig: str,
|
||||
encoded_url: str,
|
||||
size: int,
|
||||
) -> PlainTextResponse:
|
||||
@ -1180,6 +1220,7 @@ async def serve_proxy_media_resized(
|
||||
# Decode the base64-encoded URL
|
||||
url = base64.urlsafe_b64decode(encoded_url).decode()
|
||||
check_url(url)
|
||||
media.verify_proxied_media_sig(exp, url, sig)
|
||||
|
||||
if cached_resp := _RESIZED_CACHE.get((url, size)):
|
||||
resized_content, resized_mimetype, resp_headers = cached_resp
|
||||
|
31
app/media.py
31
app/media.py
@ -1,15 +1,44 @@
|
||||
import base64
|
||||
import time
|
||||
|
||||
from app.config import BASE_URL
|
||||
from app.config import hmac_sha256
|
||||
|
||||
SUPPORTED_RESIZE = [50, 740]
|
||||
EXPIRY_PERIOD = 86400
|
||||
EXPIRY_LENGTH = 7
|
||||
|
||||
|
||||
class InvalidProxySignatureError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def proxied_media_sig(expires: int, url: str) -> str:
|
||||
hm = hmac_sha256()
|
||||
hm.update(f"{expires}".encode())
|
||||
hm.update(b"|")
|
||||
hm.update(url.encode())
|
||||
return base64.urlsafe_b64encode(hm.digest()).decode()
|
||||
|
||||
|
||||
def verify_proxied_media_sig(expires: int, url: str, sig: str) -> None:
|
||||
now = int(time.time() / EXPIRY_PERIOD)
|
||||
expected = proxied_media_sig(expires, url)
|
||||
if now > expires or sig != expected:
|
||||
raise InvalidProxySignatureError("invalid or expired media")
|
||||
|
||||
|
||||
def proxied_media_url(url: str) -> str:
|
||||
if url.startswith(BASE_URL):
|
||||
return url
|
||||
expires = int(time.time() / EXPIRY_PERIOD) + EXPIRY_LENGTH
|
||||
sig = proxied_media_sig(expires, url)
|
||||
|
||||
return "/proxy/media/" + base64.urlsafe_b64encode(url.encode()).decode()
|
||||
return (
|
||||
BASE_URL
|
||||
+ f"/proxy/media/{expires}/{sig}/"
|
||||
+ base64.urlsafe_b64encode(url.encode()).decode()
|
||||
)
|
||||
|
||||
|
||||
def resized_media_url(url: str, size: int) -> str:
|
||||
|
@ -251,6 +251,8 @@ class OutboxObject(Base, BaseObject):
|
||||
"mediaType": attachment.upload.content_type,
|
||||
"name": attachment.alt or attachment.filename,
|
||||
"url": url,
|
||||
"width": attachment.upload.width,
|
||||
"height": attachment.upload.height,
|
||||
"proxiedUrl": url,
|
||||
"resizedUrl": BASE_URL
|
||||
+ (
|
||||
|
@ -531,3 +531,13 @@ a.label-btn {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.ap-place {
|
||||
h3 {
|
||||
display: inline;
|
||||
font-weight: normal;
|
||||
}
|
||||
h3::after {
|
||||
content: ': ';
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import re
|
||||
import typing
|
||||
|
||||
from loguru import logger
|
||||
from mistletoe import Document # type: ignore
|
||||
from mistletoe.html_renderer import HTMLRenderer # type: ignore
|
||||
from mistletoe.span_token import SpanToken # type: ignore
|
||||
@ -78,13 +79,17 @@ class CustomRenderer(HTMLRenderer):
|
||||
|
||||
def render_mention(self, token: Mention) -> str:
|
||||
mention = token.target
|
||||
suffix = ""
|
||||
if mention.endswith("."):
|
||||
mention = mention[:-1]
|
||||
suffix = "."
|
||||
actor = self.mentioned_actors.get(mention)
|
||||
if not actor:
|
||||
return mention
|
||||
|
||||
self.tags.append(dict(type="Mention", href=actor.ap_id, name=mention))
|
||||
|
||||
link = f'<span class="h-card"><a href="{actor.url}" class="u-url mention">{actor.handle}</a></span>' # noqa: E501
|
||||
link = f'<span class="h-card"><a href="{actor.url}" class="u-url mention">{actor.handle}</a></span>{suffix}' # noqa: E501
|
||||
return link
|
||||
|
||||
def render_hashtag(self, token: Hashtag) -> str:
|
||||
@ -118,23 +123,30 @@ async def _prefetch_mentioned_actors(
|
||||
if mention in actors:
|
||||
continue
|
||||
|
||||
_, username, domain = mention.split("@")
|
||||
actor = (
|
||||
await db_session.execute(
|
||||
select(models.Actor).where(
|
||||
models.Actor.handle == mention,
|
||||
models.Actor.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if not actor:
|
||||
actor_url = await webfinger.get_actor_url(mention)
|
||||
if not actor_url:
|
||||
# FIXME(ts): raise an error?
|
||||
continue
|
||||
actor = await fetch_actor(db_session, actor_url)
|
||||
# XXX: the regex catches stuff like `@toto@example.com.`
|
||||
if mention.endswith("."):
|
||||
mention = mention[:-1]
|
||||
|
||||
actors[mention] = actor
|
||||
try:
|
||||
_, username, domain = mention.split("@")
|
||||
actor = (
|
||||
await db_session.execute(
|
||||
select(models.Actor).where(
|
||||
models.Actor.handle == mention,
|
||||
models.Actor.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if not actor:
|
||||
actor_url = await webfinger.get_actor_url(mention)
|
||||
if not actor_url:
|
||||
# FIXME(ts): raise an error?
|
||||
continue
|
||||
actor = await fetch_actor(db_session, actor_url)
|
||||
|
||||
actors[mention] = actor
|
||||
except Exception:
|
||||
logger.exception(f"Failed to prefetch {mention}")
|
||||
|
||||
return actors
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import base64
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
from functools import lru_cache
|
||||
@ -39,7 +38,7 @@ from app.utils.highlight import HIGHLIGHT_CSS
|
||||
from app.utils.highlight import highlight
|
||||
|
||||
_templates = Jinja2Templates(
|
||||
directory="app/templates",
|
||||
directory=["data/templates", "app/templates"], # type: ignore # bad typing
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
)
|
||||
@ -59,13 +58,8 @@ def _filter_domain(text: str) -> str:
|
||||
|
||||
def _media_proxy_url(url: str | None) -> str:
|
||||
if not url:
|
||||
return "/static/nopic.png"
|
||||
|
||||
if url.startswith(BASE_URL):
|
||||
return url
|
||||
|
||||
encoded_url = base64.urlsafe_b64encode(url.encode()).decode()
|
||||
return f"/proxy/media/{encoded_url}"
|
||||
return BASE_URL + "/static/nopic.png"
|
||||
return proxied_media_url(url)
|
||||
|
||||
|
||||
def is_current_user_admin(request: Request) -> bool:
|
||||
@ -91,6 +85,7 @@ async def render_template(
|
||||
template: str,
|
||||
template_args: dict[str, Any] | None = None,
|
||||
status_code: int = 200,
|
||||
headers: dict[str, str] | None = None,
|
||||
) -> TemplateResponse:
|
||||
if template_args is None:
|
||||
template_args = {}
|
||||
@ -135,6 +130,7 @@ async def render_template(
|
||||
**template_args,
|
||||
},
|
||||
status_code=status_code,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
@ -388,7 +384,7 @@ def _html2text(content: str) -> str:
|
||||
|
||||
def _replace_emoji(u: str, _) -> str:
|
||||
filename = "-".join(hex(ord(c))[2:] for c in u)
|
||||
return config.EMOJI_TPL.format(filename=filename, raw=u)
|
||||
return config.EMOJI_TPL.format(base_url=BASE_URL, filename=filename, raw=u)
|
||||
|
||||
|
||||
def _emojify(text: str, is_local: bool) -> str:
|
||||
@ -430,3 +426,4 @@ _templates.env.globals["BASE_URL"] = config.BASE_URL
|
||||
_templates.env.globals["HIDES_FOLLOWERS"] = config.HIDES_FOLLOWERS
|
||||
_templates.env.globals["HIDES_FOLLOWING"] = config.HIDES_FOLLOWING
|
||||
_templates.env.globals["NAVBAR_ITEMS"] = config.NavBarItems
|
||||
_templates.env.globals["ICON_URL"] = config.CONFIG.icon_url
|
||||
|
@ -90,5 +90,5 @@
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
<script src="/static/new.js?v={{ JS_HASH }}"></script>
|
||||
<script src="{{ BASE_URL }}/static/new.js?v={{ JS_HASH }}"></script>
|
||||
{% endblock %}
|
||||
|
@ -14,7 +14,7 @@
|
||||
<meta content="{{ local_actor.display_name }}'s microblog" property="og:site_name" />
|
||||
<meta content="Homepage" property="og:title" />
|
||||
<meta content="{{ local_actor.summary | html2text | trim }}" property="og:description" />
|
||||
<meta content="{{ local_actor.url }}" property="og:image" />
|
||||
<meta content="{{ ICON_URL }}" property="og:image" />
|
||||
<meta content="summary" property="twitter:card" />
|
||||
<meta content="{{ local_actor.handle }}" property="profile:username" />
|
||||
{% endif %}
|
||||
|
@ -25,12 +25,13 @@
|
||||
</div>
|
||||
|
||||
{%- macro header_link(url, text) -%}
|
||||
{% set url_for = request.app.router.url_path_for(url) %}
|
||||
<a href="{{ url_for }}" {% if request.url.path == url_for %}class="active"{% endif %}>{{ text }}</a>
|
||||
{% set url_for = BASE_URL + request.app.router.url_path_for(url) %}
|
||||
<a href="{{ url_for }}" {% if BASE_URL + request.url.path == url_for %}class="active"{% endif %}>{{ text }}</a>
|
||||
{% endmacro %}
|
||||
|
||||
{%- macro navbar_item_link(navbar_item) -%}
|
||||
<a href="{{ navbar_item[0] }}" {% if request.url.path == navbar_item[0] %}class="active"{% endif %}>{{ navbar_item[1] }}</a>
|
||||
{% set url_for = BASE_URL + navbar_item[0] %}
|
||||
<a href="{{ navbar_item[0] }}" {% if BASE_URL + request.url.path == url_for %}class="active"{% endif %}>{{ navbar_item[1] }}</a>
|
||||
{% endmacro %}
|
||||
|
||||
<div class="public-top-menu">
|
||||
|
@ -13,7 +13,7 @@
|
||||
<meta content="{{ local_actor.display_name }}'s microblog" property="og:site_name" />
|
||||
<meta content="Homepage" property="og:title" />
|
||||
<meta content="{{ local_actor.summary | html2text | trim }}" property="og:description" />
|
||||
<meta content="{{ local_actor.url }}" property="og:image" />
|
||||
<meta content="{{ ICON_URL }}" property="og:image" />
|
||||
<meta content="summary" property="twitter:card" />
|
||||
<meta content="{{ local_actor.handle }}" property="profile:username" />
|
||||
{% endblock %}
|
||||
|
@ -4,11 +4,11 @@
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="/static/css/main.css?v={{ CSS_HASH }}">
|
||||
<link rel="stylesheet" href="{{ BASE_URL }}/static/css/main.css?v={{ CSS_HASH }}">
|
||||
<link rel="alternate" title="{{ local_actor.display_name}}'s microblog" type="application/json" href="{{ url_for("json_feed") }}" />
|
||||
<link rel="alternate" href="{{ url_for("rss_feed") }}" type="application/rss+xml" title="{{ local_actor.display_name}}'s microblog">
|
||||
<link rel="alternate" href="{{ url_for("atom_feed") }}" type="application/atom+xml" title="{{ local_actor.display_name}}'s microblog">
|
||||
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
||||
<link rel="icon" type="image/x-icon" href="{{ BASE_URL }}/static/favicon.ico">
|
||||
<style>{{ highlight_css }}</style>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
@ -18,8 +18,8 @@
|
||||
{% if is_admin %}
|
||||
<div id="admin">
|
||||
{% macro admin_link(url, text) %}
|
||||
{% set url_for = request.app.router.url_path_for(url) %}
|
||||
<a href="{{ url_for }}" {% if request.url.path == url_for %}class="active"{% endif %}>{{ text }}</a>
|
||||
{% set url_for = BASE_URL + request.app.router.url_path_for(url) %}
|
||||
<a href="{{ url_for }}" {% if BASE_URL + request.url.path == url_for %}class="active"{% endif %}>{{ text }}</a>
|
||||
{% endmacro %}
|
||||
<div class="admin-menu">
|
||||
<nav class="flexbox">
|
||||
@ -53,7 +53,7 @@
|
||||
</div>
|
||||
</footer>
|
||||
{% if is_admin %}
|
||||
<script src="/static/common-admin.js?v={{ JS_HASH }}"></script>
|
||||
<script src="{{ BASE_URL }}/static/common-admin.js?v={{ JS_HASH }}"></script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -7,7 +7,7 @@
|
||||
{% if error %}
|
||||
<p class="primary-color">Invalid password.</p>
|
||||
{% endif %}
|
||||
<form class="form" action="/admin/login" method="POST">
|
||||
<form class="form" action="{{ BASE_URL }}/admin/login" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="redirect" value="{{ redirect }}">
|
||||
<input type="password" placeholder="password" name="password" autofocus>
|
||||
|
15
app/templates/redirect_to_remote_instance.html
Normal file
15
app/templates/redirect_to_remote_instance.html
Normal file
@ -0,0 +1,15 @@
|
||||
{%- import "utils.html" as utils with context -%}
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ local_actor.display_name }}'s microblog - Redirect</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "header.html" %}
|
||||
|
||||
<div class="box">
|
||||
<p>You are being redirected to your instance: <a href="{{ url }}">{{ url }}</a></p>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -1,168 +1,220 @@
|
||||
{% macro embed_csrf_token() %}
|
||||
{% block embed_csrf_token scoped %}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro embed_redirect_url(permalink_id=None) %}
|
||||
{% block embed_redirect_url scoped %}
|
||||
<input type="hidden" name="redirect_url" value="{{ request.url }}{% if permalink_id %}#{{ permalink_id }}{% endif %}">
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_block_button(actor) %}
|
||||
{% block admin_block_button scoped %}
|
||||
<form action="{{ request.url_for("admin_actions_block") }}" method="POST">
|
||||
{{ embed_csrf_token() }}
|
||||
{{ embed_redirect_url() }}
|
||||
<input type="hidden" name="ap_actor_id" value="{{ actor.ap_id }}">
|
||||
<input type="submit" value="block">
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_unblock_button(actor) %}
|
||||
{% block admin_unblock_button scoped %}
|
||||
<form action="{{ request.url_for("admin_actions_unblock") }}" method="POST">
|
||||
{{ embed_csrf_token() }}
|
||||
{{ embed_redirect_url() }}
|
||||
<input type="hidden" name="ap_actor_id" value="{{ actor.ap_id }}">
|
||||
<input type="submit" value="unblock">
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_follow_button(actor) %}
|
||||
{% block admin_follow_button scoped %}
|
||||
<form action="{{ request.url_for("admin_actions_follow") }}" method="POST">
|
||||
{{ embed_csrf_token() }}
|
||||
{{ embed_redirect_url() }}
|
||||
<input type="hidden" name="ap_actor_id" value="{{ actor.ap_id }}">
|
||||
<input type="submit" value="follow">
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_accept_incoming_follow_button(notif) %}
|
||||
{% block admin_accept_incoming_follow_button scoped %}
|
||||
<form action="{{ request.url_for("admin_actions_accept_incoming_follow") }}" method="POST">
|
||||
{{ embed_csrf_token() }}
|
||||
{{ embed_redirect_url() }}
|
||||
<input type="hidden" name="notification_id" value="{{ notif.id }}">
|
||||
<input type="submit" value="accept follow">
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_reject_incoming_follow_button(notif) %}
|
||||
{% block admin_reject_incoming_follow_button scoped %}
|
||||
<form action="{{ request.url_for("admin_actions_reject_incoming_follow") }}" method="POST">
|
||||
{{ embed_csrf_token() }}
|
||||
{{ embed_redirect_url() }}
|
||||
<input type="hidden" name="notification_id" value="{{ notif.id }}">
|
||||
<input type="submit" value="reject follow">
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_like_button(ap_object_id, permalink_id) %}
|
||||
{% block admin_like_button scoped %}
|
||||
<form action="{{ request.url_for("admin_actions_like") }}" method="POST">
|
||||
{{ embed_csrf_token() }}
|
||||
{{ embed_redirect_url(permalink_id) }}
|
||||
<input type="hidden" name="ap_object_id" value="{{ ap_object_id }}">
|
||||
<input type="submit" value="like">
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_bookmark_button(ap_object_id, permalink_id) %}
|
||||
{% block admin_bookmark_button scoped %}
|
||||
<form action="{{ request.url_for("admin_actions_bookmark") }}" method="POST">
|
||||
{{ embed_csrf_token() }}
|
||||
{{ embed_redirect_url(permalink_id) }}
|
||||
<input type="hidden" name="ap_object_id" value="{{ ap_object_id }}">
|
||||
<input type="submit" value="bookmark">
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_unbookmark_button(ap_object_id, permalink_id) %}
|
||||
{% block admin_unbookmark_button scoped %}
|
||||
<form action="{{ request.url_for("admin_actions_unbookmark") }}" method="POST">
|
||||
{{ embed_csrf_token() }}
|
||||
{{ embed_redirect_url(permalink_id) }}
|
||||
<input type="hidden" name="ap_object_id" value="{{ ap_object_id }}">
|
||||
<input type="submit" value="unbookmark">
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_pin_button(ap_object_id, permalink_id) %}
|
||||
{% block admin_pin_button scoped %}
|
||||
<form action="{{ request.url_for("admin_actions_pin") }}" method="POST">
|
||||
{{ embed_csrf_token() }}
|
||||
{{ embed_redirect_url(permalink_id) }}
|
||||
<input type="hidden" name="ap_object_id" value="{{ ap_object_id }}">
|
||||
<input type="submit" value="pin">
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_unpin_button(ap_object_id, permalink_id) %}
|
||||
{% block admin_unpin_button scoped %}
|
||||
<form action="{{ request.url_for("admin_actions_unpin") }}" method="POST">
|
||||
{{ embed_csrf_token() }}
|
||||
{{ embed_redirect_url(permalink_id) }}
|
||||
<input type="hidden" name="ap_object_id" value="{{ ap_object_id }}">
|
||||
<input type="submit" value="unpin">
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_delete_button(ap_object) %}
|
||||
{% block admin_delete_button scoped %}
|
||||
<form action="{{ request.url_for("admin_actions_delete") }}" class="object-delete-form" method="POST">
|
||||
{{ embed_csrf_token() }}
|
||||
<input type="hidden" name="redirect_url" value="{% if request.url.path.endswith("/" + ap_object.public_id) or (request.url.path == "/admin/object" and request.query_params.ap_id.endswith("/" + ap_object.public_id)) %}{{ request.base_url}}{% else %}{{ request.url }}{% endif %}">
|
||||
<input type="hidden" name="ap_object_id" value="{{ ap_object.ap_id }}">
|
||||
<input type="submit" value="delete">
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_force_delete_button(ap_object_id, permalink_id=None) %}
|
||||
{% block admin_force_delete_button scoped %}
|
||||
<form action="{{ request.url_for("admin_actions_force_delete") }}" class="object-delete-form" method="POST">
|
||||
{{ embed_csrf_token() }}
|
||||
{{ embed_redirect_url(permalink_id) }}
|
||||
<input type="hidden" name="ap_object_id" value="{{ ap_object_id }}">
|
||||
<input type="submit" value="local delete">
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_announce_button(ap_object_id, permalink_id=None) %}
|
||||
{% block admin_announce_button scoped %}
|
||||
<form action="{{ request.url_for("admin_actions_announce") }}" method="POST">
|
||||
{{ embed_csrf_token() }}
|
||||
{{ embed_redirect_url(permalink_id) }}
|
||||
<input type="hidden" name="ap_object_id" value="{{ ap_object_id }}">
|
||||
<input type="submit" value="share">
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_undo_button(ap_object_id, action="undo", permalink_id=None) %}
|
||||
{% block admin_undo_button scoped %}
|
||||
<form action="{{ request.url_for("admin_actions_undo") }}" method="POST">
|
||||
{{ embed_csrf_token() }}
|
||||
{{ embed_redirect_url(permalink_id) }}
|
||||
<input type="hidden" name="ap_object_id" value="{{ ap_object_id }}">
|
||||
<input type="submit" value="{{ action }}">
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_reply_button(ap_object_id) %}
|
||||
<form action="/admin/new" method="GET">
|
||||
{% block admin_reply_button scoped %}
|
||||
<form action="{{ BASE_URL }}/admin/new" method="GET">
|
||||
<input type="hidden" name="in_reply_to" value="{{ ap_object_id }}">
|
||||
<button type="submit">reply</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_dm_button(actor_handle) %}
|
||||
<form action="/admin/new" method="GET">
|
||||
{% block admin_dm_button scoped %}
|
||||
<form action="{{ BASE_URL }}/admin/new" method="GET">
|
||||
<input type="hidden" name="with_content" value="{{ actor_handle }}">
|
||||
<input type="hidden" name="with_visibility" value="DIRECT">
|
||||
<button type="submit">direct message</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_mention_button(actor_handle) %}
|
||||
<form action="/admin/new" method="GET">
|
||||
{% block admin_mention_button scoped %}
|
||||
<form action="{{ BASE_URL }}/admin/new" method="GET">
|
||||
<input type="hidden" name="with_content" value="{{ actor_handle }}">
|
||||
<button type="submit">mention</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
|
||||
{% macro admin_profile_button(ap_actor_id) %}
|
||||
{% block admin_profile_button scoped %}
|
||||
<form action="{{ url_for("admin_profile") }}" method="GET">
|
||||
<input type="hidden" name="actor_id" value="{{ ap_actor_id }}">
|
||||
<button type="submit">profile</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_expand_button(ap_object) %}
|
||||
{% block admin_expand_button scoped %}
|
||||
{# TODO turn these into a regular link and append permalink ID if it's a reply #}
|
||||
<form action="{{ url_for("admin_object") }}" method="GET">
|
||||
<input type="hidden" name="ap_id" value="{{ ap_object.ap_id }}">
|
||||
<button type="submit">expand</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro display_box_filters(route) %}
|
||||
{% block display_box_filters scoped %}
|
||||
<nav class="flexbox box">
|
||||
<ul>
|
||||
<li>Filter by</li>
|
||||
@ -179,13 +231,17 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro display_tiny_actor_icon(actor) %}
|
||||
{% block display_tiny_actor_icon scoped %}
|
||||
<img class="tiny-actor-icon" src="{{ actor.resized_icon_url }}" alt="{{ actor.display_name }}'s avatar">
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro actor_action(inbox_object, text, with_icon=False) %}
|
||||
{% block actor_action scoped %}
|
||||
<div class="actor-action">
|
||||
<a href="{{ url_for("admin_profile") }}?actor_id={{ inbox_object.actor.ap_id }}">
|
||||
{% if with_icon %}{{ display_tiny_actor_icon(inbox_object.actor) }}{% endif %} {{ inbox_object.actor.display_name | clean_html(inbox_object.actor) | safe }}
|
||||
@ -193,9 +249,11 @@
|
||||
<span title="{{ inbox_object.ap_published_at.isoformat() }}">{{ inbox_object.ap_published_at | timeago }}</span>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro display_actor(actor, actors_metadata={}, embedded=False, with_details=False, pending_incoming_follow_notif=None) %}
|
||||
{% block display_actor scoped %}
|
||||
{% set metadata = actors_metadata.get(actor.ap_id) %}
|
||||
|
||||
{% if not embedded %}
|
||||
@ -306,9 +364,11 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro display_og_meta(object) %}
|
||||
{% block display_og_meta scoped %}
|
||||
{% if object.og_meta %}
|
||||
{% for og_meta in object.og_meta[:1] %}
|
||||
<div class="activity-og-meta">
|
||||
@ -326,22 +386,29 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro display_attachments(object) %}
|
||||
{% block display_attachments scoped %}
|
||||
|
||||
{% for attachment in object.attachments %}
|
||||
{% if attachment.type != "PropertyValue" %}
|
||||
{% set orientation = "unknown" %}
|
||||
{% if attachment.width %}
|
||||
{% set orientation = "portrait" if attachment.width < attachment.height else "landscape" %}
|
||||
{% endif %}
|
||||
{% if object.sensitive and (attachment.type == "Image" or (attachment | has_media_type("image")) or attachment.type == "Video" or (attachment | has_media_type("video"))) %}
|
||||
<div class="attachment-wrapper">
|
||||
<label for="{{attachment.proxied_url}}" class="label-btn show-hide-sensitive-btn">show/hide sensitive content</label>
|
||||
<div>
|
||||
<div class="sensitive-attachment">
|
||||
<input class="sensitive-attachment-state" type="checkbox" id="{{attachment.proxied_url}}" aria-hidden="true">
|
||||
<div class="sensitive-attachment-box">
|
||||
<div class="sensitive-attachment-box attachment-orientation-{{orientation}}">
|
||||
<div></div>
|
||||
{% else %}
|
||||
<div class="attachment-item">
|
||||
<div class="attachment-item attachment-orientation-{{orientation}}">
|
||||
{% endif %}
|
||||
|
||||
{% if attachment.type == "Image" or (attachment | has_media_type("image")) %}
|
||||
@ -367,12 +434,15 @@
|
||||
{% else %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro display_object(object, likes=[], shares=[], webmentions=[], expanded=False, actors_metadata={}, is_object_page=False) %}
|
||||
{% block display_object scoped %}
|
||||
{% set is_article_mode = object.is_from_outbox and object.ap_type == "Article" and is_object_page %}
|
||||
{% if object.ap_type in ["Note", "Article", "Video", "Page", "Question"] %}
|
||||
{% if object.ap_type in ["Note", "Article", "Video", "Page", "Question", "Event"] %}
|
||||
<div class="ap-object {% if expanded %}ap-object-expanded {% endif %}h-entry" id="{{ object.permalink_id }}">
|
||||
|
||||
{% if is_article_mode %}
|
||||
@ -391,10 +461,32 @@
|
||||
</a></p>
|
||||
{% endif %}
|
||||
|
||||
{% if object.ap_type == "Article" %}
|
||||
{% if object.ap_type in ["Article", "Event"] %}
|
||||
<h2 class="p-name no-margin-top">{{ object.name }}</h2>
|
||||
{% endif %}
|
||||
|
||||
{% if object.ap_type == "Event" %}
|
||||
{% if object.ap_object.get("endTime") and object.ap_object.get("startTime") %}
|
||||
<p>On {{ object.ap_object.startTime | parse_datetime | format_date }}
|
||||
(ends {{ object.ap_object.endTime | parse_datetime | format_date }})</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if object.ap_object.get("location") %}
|
||||
{% set loc = object.ap_object.get("location") %}
|
||||
{% if loc.type == "Place" and loc.latitude and loc.longitude %}
|
||||
<div class="ap-place">
|
||||
<h3>Location</h3>
|
||||
{% if loc.name %}{{ loc.name }}{% endif %}
|
||||
<span class="h-geo">
|
||||
<data class="p-latitude" value="{{ loc.latitude}}"></data>
|
||||
<data class="p-longitude" value="{{ loc.longitude }}"></data>
|
||||
<a href="https://www.openstreetmap.org/?mlat={{ loc.latitude }}&mlon={{ loc.longitude }}#map=16/{{loc.latitude}}/{{loc.longitude}}">{{loc.latitude}},{{loc.longitude}}</a>
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_article_mode %}
|
||||
<time class="dt-published muted" datetime="{{ object.ap_published_at.replace(microsecond=0).isoformat() }}" title="{{ object.ap_published_at.replace(microsecond=0).isoformat() }}">{{ object.ap_published_at.strftime("%b %d, %Y") }}</time>
|
||||
{% endif %}
|
||||
@ -601,6 +693,11 @@
|
||||
{{ admin_expand_button(object) }}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if object.is_from_inbox %}
|
||||
<li>
|
||||
{{ admin_force_delete_button(object.ap_id) }}
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
@ -663,4 +760,5 @@
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
@ -1,12 +1,15 @@
|
||||
import asyncio
|
||||
import mimetypes
|
||||
import re
|
||||
import signal
|
||||
from concurrent.futures import TimeoutError
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
from bs4 import BeautifulSoup # type: ignore
|
||||
from loguru import logger
|
||||
from pebble import concurrent # type: ignore
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app import activitypub as ap
|
||||
@ -29,7 +32,11 @@ class OpenGraphMeta(BaseModel):
|
||||
site_name: str
|
||||
|
||||
|
||||
@concurrent.process(timeout=5)
|
||||
def _scrap_og_meta(url: str, html: str) -> OpenGraphMeta | None:
|
||||
# Prevent SIGTERM to bubble up to the worker
|
||||
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
||||
|
||||
soup = BeautifulSoup(html, "html5lib")
|
||||
ogs = {
|
||||
og.attrs["property"]: og.attrs.get("content")
|
||||
@ -58,6 +65,10 @@ def _scrap_og_meta(url: str, html: str) -> OpenGraphMeta | None:
|
||||
return OpenGraphMeta.parse_obj(raw)
|
||||
|
||||
|
||||
def scrap_og_meta(url: str, html: str) -> OpenGraphMeta | None:
|
||||
return _scrap_og_meta(url, html).result()
|
||||
|
||||
|
||||
async def external_urls(
|
||||
db_session: AsyncSession,
|
||||
ro: ap_object.RemoteObject | OutboxObject | InboxObject,
|
||||
@ -126,7 +137,10 @@ async def _og_meta_from_url(url: str) -> OpenGraphMeta | None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return _scrap_og_meta(url, resp.text)
|
||||
return scrap_og_meta(url, resp.text)
|
||||
except TimeoutError:
|
||||
logger.info(f"Timed out when scraping OG meta for {url}")
|
||||
return None
|
||||
except Exception:
|
||||
logger.info(f"Failed to scrap OG meta for {url}")
|
||||
return None
|
||||
|
@ -69,5 +69,5 @@ class Worker(Generic[T]):
|
||||
logger.info("stopping loop")
|
||||
|
||||
async def _shutdown(self, sig: signal.Signals) -> None:
|
||||
logger.info(f"Caught {signal=}")
|
||||
logger.info(f"Caught {sig=}")
|
||||
self._stop_event.set()
|
||||
|
@ -12,6 +12,7 @@ async def webfinger(
|
||||
resource: str,
|
||||
) -> dict[str, Any] | None: # noqa: C901
|
||||
"""Mastodon-like WebFinger resolution to retrieve the activity stream Actor URL."""
|
||||
resource = resource.strip()
|
||||
logger.info(f"performing webfinger resolution for {resource}")
|
||||
protos = ["https", "http"]
|
||||
if resource.startswith("http://"):
|
||||
|
1
data/templates/app
Symbolic link
1
data/templates/app
Symbolic link
@ -0,0 +1 @@
|
||||
../../app/templates/
|
@ -5,6 +5,7 @@ admin_password = "$2b$12$OwCyZM33uXQUVrChgER.h.qgFJ4fBp6tdFwArR3Lm1LV8NgMvIxVa"
|
||||
name = "test"
|
||||
summary = "<p>Hello</p>"
|
||||
https = false
|
||||
id = "http://localhost:8000"
|
||||
icon_url = "https://localhost:8000/static/nopic.png"
|
||||
secret = "1dd4079e0474d1a519052b8fe3cb5fa6"
|
||||
debug = true
|
||||
|
@ -127,9 +127,23 @@ $secondary-color: #32cd32;
|
||||
|
||||
See `app/scss/main.scss` to see what variables can be overridden.
|
||||
|
||||
#### Custom templates
|
||||
|
||||
If you'd like to customize your instance's theme beyond CSS, you can modify the app's HTML by placing templates in `data/templates` which overwrite the defaults in `app/templates`.
|
||||
|
||||
#### Custom Content Security Policy (CSP)
|
||||
|
||||
You can override the default Content Security Policy by adding a line in `data/profile.toml`:
|
||||
|
||||
```toml
|
||||
custom_content_security_policy = "default-src 'self'; style-src 'self' 'sha256-{HIGHLIGHT_CSS_HASH}'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
|
||||
```
|
||||
|
||||
This example will output the default CSP, note that `{HIGHLIGHT_CSS_HASH}` will be dynamically replaced by the correct value (the hash of the CSS needed for syntax highlighting).
|
||||
|
||||
#### Code highlighting theme
|
||||
|
||||
You can switch to one of the [styles supported by Pygments](https://pygments.org/styles/) by adding a line in `profile.toml`:
|
||||
You can switch to one of the [styles supported by Pygments](https://pygments.org/styles/) by adding a line in `data/profile.toml`:
|
||||
|
||||
```toml
|
||||
code_highlighting_theme = "solarized-dark"
|
||||
|
168
poetry.lock
generated
168
poetry.lock
generated
@ -271,7 +271,7 @@ dev = ["pytest", "coverage", "coveralls"]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -297,7 +297,7 @@ doc = ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"]
|
||||
|
||||
[[package]]
|
||||
name = "faker"
|
||||
version = "15.1.2"
|
||||
version = "15.2.0"
|
||||
description = "Faker is a Python package that generates fake data for you."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -689,6 +689,14 @@ category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "pebble"
|
||||
version = "5.0.2"
|
||||
description = "Threading and multiprocessing eye-candy."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "9.3.0"
|
||||
@ -703,15 +711,15 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "2.5.2"
|
||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
version = "2.5.3"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
|
||||
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
|
||||
docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.19.4)", "sphinx (>=5.3)"]
|
||||
test = ["appdirs (==1.4.4)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest (>=7.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
@ -727,7 +735,7 @@ testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.31"
|
||||
version = "3.0.32"
|
||||
description = "Library for building powerful interactive command lines in Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -979,7 +987,7 @@ python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "1.4.42"
|
||||
version = "1.4.43"
|
||||
description = "Database Abstraction Library"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -1216,14 +1224,14 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "watchfiles"
|
||||
version = "0.18.0"
|
||||
version = "0.18.1"
|
||||
description = "Simple, modern and high performance file watching and code reload in python."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.0.0,<4"
|
||||
anyio = ">=3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
@ -1263,7 +1271,7 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "89df524a545a19a20440d1872c93151bbf3f68d3b3d20cc50bc9049dd0e6d25f"
|
||||
content-hash = "13a1f5fc3f65c56e753062dca6ab74a50f7270d78a08ebf6297f7b4fa26b5eac"
|
||||
|
||||
[metadata.files]
|
||||
aiosqlite = [
|
||||
@ -1504,16 +1512,16 @@ emoji = [
|
||||
{file = "emoji-1.7.0.tar.gz", hash = "sha256:65c54533ea3c78f30d0729288998715f418d7467de89ec258a31c0ce8660a1d1"},
|
||||
]
|
||||
exceptiongroup = [
|
||||
{file = "exceptiongroup-1.0.0-py3-none-any.whl", hash = "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41"},
|
||||
{file = "exceptiongroup-1.0.0.tar.gz", hash = "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad"},
|
||||
{file = "exceptiongroup-1.0.1-py3-none-any.whl", hash = "sha256:4d6c0aa6dd825810941c792f53d7b8d71da26f5e5f84f20f9508e8f2d33b140a"},
|
||||
{file = "exceptiongroup-1.0.1.tar.gz", hash = "sha256:73866f7f842ede6cb1daa42c4af078e2035e5f7607f0e2c762cc51bb31bbe7b2"},
|
||||
]
|
||||
factory-boy = [
|
||||
{file = "factory_boy-3.2.1-py2.py3-none-any.whl", hash = "sha256:eb02a7dd1b577ef606b75a253b9818e6f9eaf996d94449c9d5ebb124f90dc795"},
|
||||
{file = "factory_boy-3.2.1.tar.gz", hash = "sha256:a98d277b0c047c75eb6e4ab8508a7f81fb03d2cb21986f627913546ef7a2a55e"},
|
||||
]
|
||||
faker = [
|
||||
{file = "Faker-15.1.2-py3-none-any.whl", hash = "sha256:37c8bfcbd9e0e99cebcd22e70dcf895ff92fb46aa8a32c7772df0c1f1f32ea48"},
|
||||
{file = "Faker-15.1.2.tar.gz", hash = "sha256:39c4e7915813923829675488cafef07ddf11cf59ecbaac518f53dd8e7b0df5cf"},
|
||||
{file = "Faker-15.2.0-py3-none-any.whl", hash = "sha256:8066d5ef0ca116469292d947593ae4cdf1d97dd83f557dd8a749826cf960020a"},
|
||||
{file = "Faker-15.2.0.tar.gz", hash = "sha256:f35b9b47fb84d7334645feba0dd87bbf5aba2b617cd83ec8e1b8c6dcd859a710"},
|
||||
]
|
||||
fastapi = [
|
||||
{file = "fastapi-0.78.0-py3-none-any.whl", hash = "sha256:15fcabd5c78c266fa7ae7d8de9b384bfc2375ee0503463a6febbe3bab69d6f65"},
|
||||
@ -1871,6 +1879,10 @@ pathspec = [
|
||||
{file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"},
|
||||
{file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"},
|
||||
]
|
||||
pebble = [
|
||||
{file = "Pebble-5.0.2-py3-none-any.whl", hash = "sha256:61b2dfd52b1a8c083b4e6cf3e0f1ff2e8a430a6283c53969a7057a1c91bed3cd"},
|
||||
{file = "Pebble-5.0.2.tar.gz", hash = "sha256:9c58c03eaf920c31287444c6fef39dc53baeac9de221ead104f5c9b48e8bd587"},
|
||||
]
|
||||
pillow = [
|
||||
{file = "Pillow-9.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:0b7257127d646ff8676ec8a15520013a698d1fdc48bc2a79ba4e53df792526f2"},
|
||||
{file = "Pillow-9.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b90f7616ea170e92820775ed47e136208e04c967271c9ef615b6fbd08d9af0e3"},
|
||||
@ -1933,16 +1945,16 @@ pillow = [
|
||||
{file = "Pillow-9.3.0.tar.gz", hash = "sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f"},
|
||||
]
|
||||
platformdirs = [
|
||||
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
|
||||
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
|
||||
{file = "platformdirs-2.5.3-py3-none-any.whl", hash = "sha256:0cb405749187a194f444c25c82ef7225232f11564721eabffc6ec70df83b11cb"},
|
||||
{file = "platformdirs-2.5.3.tar.gz", hash = "sha256:6e52c21afff35cb659c6e52d8b4d61b9bd544557180440538f255d9382c8cbe0"},
|
||||
]
|
||||
pluggy = [
|
||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||
]
|
||||
prompt-toolkit = [
|
||||
{file = "prompt_toolkit-3.0.31-py3-none-any.whl", hash = "sha256:9696f386133df0fc8ca5af4895afe5d78f5fcfe5258111c2a79a1c3e41ffa96d"},
|
||||
{file = "prompt_toolkit-3.0.31.tar.gz", hash = "sha256:9ada952c9d1787f52ff6d5f3484d0b4df8952787c087edf6a1f7c2cb1ea88148"},
|
||||
{file = "prompt_toolkit-3.0.32-py3-none-any.whl", hash = "sha256:24becda58d49ceac4dc26232eb179ef2b21f133fecda7eed6018d341766ed76e"},
|
||||
{file = "prompt_toolkit-3.0.32.tar.gz", hash = "sha256:e7f2129cba4ff3b3656bbdda0e74ee00d2f874a8bcdb9dd16f5fec7b3e173cae"},
|
||||
]
|
||||
pyaml = [
|
||||
{file = "pyaml-21.10.1-py2.py3-none-any.whl", hash = "sha256:19985ed303c3a985de4cf8fd329b6d0a5a5b5c9035ea240eccc709ebacbaf4a0"},
|
||||
@ -2121,47 +2133,47 @@ soupsieve = [
|
||||
{file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"},
|
||||
]
|
||||
sqlalchemy = [
|
||||
{file = "SQLAlchemy-1.4.42-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:28e881266a172a4d3c5929182fde6bb6fba22ac93f137d5380cc78a11a9dd124"},
|
||||
{file = "SQLAlchemy-1.4.42-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ca9389a00f639383c93ed00333ed763812f80b5ae9e772ea32f627043f8c9c88"},
|
||||
{file = "SQLAlchemy-1.4.42-cp27-cp27m-win32.whl", hash = "sha256:1d0c23ecf7b3bc81e29459c34a3f4c68ca538de01254e24718a7926810dc39a6"},
|
||||
{file = "SQLAlchemy-1.4.42-cp27-cp27m-win_amd64.whl", hash = "sha256:6c9d004eb78c71dd4d3ce625b80c96a827d2e67af9c0d32b1c1e75992a7916cc"},
|
||||
{file = "SQLAlchemy-1.4.42-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9e3a65ce9ed250b2f096f7b559fe3ee92e6605fab3099b661f0397a9ac7c8d95"},
|
||||
{file = "SQLAlchemy-1.4.42-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:2e56dfed0cc3e57b2f5c35719d64f4682ef26836b81067ee6cfad062290fd9e2"},
|
||||
{file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b42c59ffd2d625b28cdb2ae4cde8488543d428cba17ff672a543062f7caee525"},
|
||||
{file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22459fc1718785d8a86171bbe7f01b5c9d7297301ac150f508d06e62a2b4e8d2"},
|
||||
{file = "SQLAlchemy-1.4.42-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df76e9c60879fdc785a34a82bf1e8691716ffac32e7790d31a98d7dec6e81545"},
|
||||
{file = "SQLAlchemy-1.4.42-cp310-cp310-win32.whl", hash = "sha256:e7e740453f0149437c101ea4fdc7eea2689938c5760d7dcc436c863a12f1f565"},
|
||||
{file = "SQLAlchemy-1.4.42-cp310-cp310-win_amd64.whl", hash = "sha256:effc89e606165ca55f04f3f24b86d3e1c605e534bf1a96e4e077ce1b027d0b71"},
|
||||
{file = "SQLAlchemy-1.4.42-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:97ff50cd85bb907c2a14afb50157d0d5486a4b4639976b4a3346f34b6d1b5272"},
|
||||
{file = "SQLAlchemy-1.4.42-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12c6949bae10f1012ab5c0ea52ab8db99adcb8c7b717938252137cdf694c775"},
|
||||
{file = "SQLAlchemy-1.4.42-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11b2ec26c5d2eefbc3e6dca4ec3d3d95028be62320b96d687b6e740424f83b7d"},
|
||||
{file = "SQLAlchemy-1.4.42-cp311-cp311-win32.whl", hash = "sha256:6045b3089195bc008aee5c273ec3ba9a93f6a55bc1b288841bd4cfac729b6516"},
|
||||
{file = "SQLAlchemy-1.4.42-cp311-cp311-win_amd64.whl", hash = "sha256:0501f74dd2745ec38f44c3a3900fb38b9db1ce21586b691482a19134062bf049"},
|
||||
{file = "SQLAlchemy-1.4.42-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:6e39e97102f8e26c6c8550cb368c724028c575ec8bc71afbbf8faaffe2b2092a"},
|
||||
{file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15d878929c30e41fb3d757a5853b680a561974a0168cd33a750be4ab93181628"},
|
||||
{file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fa5b7eb2051e857bf83bade0641628efe5a88de189390725d3e6033a1fff4257"},
|
||||
{file = "SQLAlchemy-1.4.42-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1c5f8182b4f89628d782a183d44db51b5af84abd6ce17ebb9804355c88a7b5"},
|
||||
{file = "SQLAlchemy-1.4.42-cp36-cp36m-win32.whl", hash = "sha256:a7dd5b7b34a8ba8d181402d824b87c5cee8963cb2e23aa03dbfe8b1f1e417cde"},
|
||||
{file = "SQLAlchemy-1.4.42-cp36-cp36m-win_amd64.whl", hash = "sha256:5ede1495174e69e273fad68ad45b6d25c135c1ce67723e40f6cf536cb515e20b"},
|
||||
{file = "SQLAlchemy-1.4.42-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:9256563506e040daddccaa948d055e006e971771768df3bb01feeb4386c242b0"},
|
||||
{file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4948b6c5f4e56693bbeff52f574279e4ff972ea3353f45967a14c30fb7ae2beb"},
|
||||
{file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1811a0b19a08af7750c0b69e38dec3d46e47c4ec1d74b6184d69f12e1c99a5e0"},
|
||||
{file = "SQLAlchemy-1.4.42-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b01d9cd2f9096f688c71a3d0f33f3cd0af8549014e66a7a7dee6fc214a7277d"},
|
||||
{file = "SQLAlchemy-1.4.42-cp37-cp37m-win32.whl", hash = "sha256:bd448b262544b47a2766c34c0364de830f7fb0772d9959c1c42ad61d91ab6565"},
|
||||
{file = "SQLAlchemy-1.4.42-cp37-cp37m-win_amd64.whl", hash = "sha256:04f2598c70ea4a29b12d429a80fad3a5202d56dce19dd4916cc46a965a5ca2e9"},
|
||||
{file = "SQLAlchemy-1.4.42-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:3ab7c158f98de6cb4f1faab2d12973b330c2878d0c6b689a8ca424c02d66e1b3"},
|
||||
{file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee377eb5c878f7cefd633ab23c09e99d97c449dd999df639600f49b74725b80"},
|
||||
{file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:934472bb7d8666727746a75670a1f8d91a9cae8c464bba79da30a0f6faccd9e1"},
|
||||
{file = "SQLAlchemy-1.4.42-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb94a3d1ba77ff2ef11912192c066f01e68416f554c194d769391638c8ad09a"},
|
||||
{file = "SQLAlchemy-1.4.42-cp38-cp38-win32.whl", hash = "sha256:f0f574465b78f29f533976c06b913e54ab4980b9931b69aa9d306afff13a9471"},
|
||||
{file = "SQLAlchemy-1.4.42-cp38-cp38-win_amd64.whl", hash = "sha256:a85723c00a636eed863adb11f1e8aaa36ad1c10089537823b4540948a8429798"},
|
||||
{file = "SQLAlchemy-1.4.42-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5ce6929417d5dce5ad1d3f147db81735a4a0573b8fb36e3f95500a06eaddd93e"},
|
||||
{file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723e3b9374c1ce1b53564c863d1a6b2f1dc4e97b1c178d9b643b191d8b1be738"},
|
||||
{file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:876eb185911c8b95342b50a8c4435e1c625944b698a5b4a978ad2ffe74502908"},
|
||||
{file = "SQLAlchemy-1.4.42-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd49af453e590884d9cdad3586415922a8e9bb669d874ee1dc55d2bc425aacd"},
|
||||
{file = "SQLAlchemy-1.4.42-cp39-cp39-win32.whl", hash = "sha256:e4ef8cb3c5b326f839bfeb6af5f406ba02ad69a78c7aac0fbeeba994ad9bb48a"},
|
||||
{file = "SQLAlchemy-1.4.42-cp39-cp39-win_amd64.whl", hash = "sha256:5f966b64c852592469a7eb759615bbd351571340b8b344f1d3fa2478b5a4c934"},
|
||||
{file = "SQLAlchemy-1.4.42.tar.gz", hash = "sha256:177e41914c476ed1e1b77fd05966ea88c094053e17a85303c4ce007f88eff363"},
|
||||
{file = "SQLAlchemy-1.4.43-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:491d94879f9ec0dea7e1cb053cd9cc65a28d2467960cf99f7b3c286590406060"},
|
||||
{file = "SQLAlchemy-1.4.43-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eeb55a555eef1a9607c1635bbdddd0b8a2bb9713bcb5bc8da1e8fae8ee46d1d8"},
|
||||
{file = "SQLAlchemy-1.4.43-cp27-cp27m-win32.whl", hash = "sha256:7d6293010aa0af8bd3b0c9993259f8979db2422d6abf85a31d70ec69cb2ee4dc"},
|
||||
{file = "SQLAlchemy-1.4.43-cp27-cp27m-win_amd64.whl", hash = "sha256:27479b5a1e110e64c56b18ffbf8cf99e101572a3d1a43943ea02158f1304108e"},
|
||||
{file = "SQLAlchemy-1.4.43-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:13ce4f3a068ec4ef7598d2a77f42adc3d90c76981f5a7c198756b25c4f4a22ea"},
|
||||
{file = "SQLAlchemy-1.4.43-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:aa12e27cb465b4b006ffb777624fc6023363e01cfed2d3f89d33fb6da80f6de2"},
|
||||
{file = "SQLAlchemy-1.4.43-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d16aca30fad4753aeb4ebde564bbd4a248b9673e4f879b940f4e806a17be87f"},
|
||||
{file = "SQLAlchemy-1.4.43-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cde363fb5412ab178f1cc1e596e9cfc396464da8a4fe8e733cc6d6b4e2c23aa9"},
|
||||
{file = "SQLAlchemy-1.4.43-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4abda3e693d24169221ffc7aa0444ccef3dc43dfeab6ad8665d3836751cd6af7"},
|
||||
{file = "SQLAlchemy-1.4.43-cp310-cp310-win32.whl", hash = "sha256:fa46d86a17cccd48c6762df1a60aecf5aaa2e0c0973efacf146c637694b62ffd"},
|
||||
{file = "SQLAlchemy-1.4.43-cp310-cp310-win_amd64.whl", hash = "sha256:962c7c80c54a42836c47cb0d8a53016986c8584e8d98e90e2ea723a4ed0ba85b"},
|
||||
{file = "SQLAlchemy-1.4.43-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f6e036714a586f757a3e12ff0798ce9a90aa04a60cff392d8bcacc5ecf79c95e"},
|
||||
{file = "SQLAlchemy-1.4.43-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05d7365c2d1df03a69d90157a3e9b3e7b62088cca8ee6686aed2598659a6e14"},
|
||||
{file = "SQLAlchemy-1.4.43-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59bd0ae166253f7fed8c3f4f6265d2637f25d2f6614d00df34d7ee0d95d29c91"},
|
||||
{file = "SQLAlchemy-1.4.43-cp311-cp311-win32.whl", hash = "sha256:0c8a174f23bc021aac97bcb27fbe2ae3d4652d3d23e5768bc2ec3d44e386c7eb"},
|
||||
{file = "SQLAlchemy-1.4.43-cp311-cp311-win_amd64.whl", hash = "sha256:5d5937e1bf7921e4d1acdfad72dd98d9e7f9ea5c52aeb12b3b05b534b527692d"},
|
||||
{file = "SQLAlchemy-1.4.43-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:ed1c950aba723b7a5b702b88f05d883607c587de918d7d8c2014fe7f55cf67e0"},
|
||||
{file = "SQLAlchemy-1.4.43-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5438f6c768b7e928f0463777b545965648ba0d55877afd14a4e96d2a99702e7"},
|
||||
{file = "SQLAlchemy-1.4.43-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:41df873cdae1d56fde97a1b4f6ffa118f40e4b2d6a6aa8c25c50eea31ecbeb08"},
|
||||
{file = "SQLAlchemy-1.4.43-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22f46440e61d90100e0f378faac40335fb5bbf278472df0d83dc15b653b9896"},
|
||||
{file = "SQLAlchemy-1.4.43-cp36-cp36m-win32.whl", hash = "sha256:529e2cc8af75811114e5ab2eb116fd71b6e252c6bdb32adbfcd5e0c5f6d5ab06"},
|
||||
{file = "SQLAlchemy-1.4.43-cp36-cp36m-win_amd64.whl", hash = "sha256:c1ced2fae7a1177a36cf94d0a5567452d195d3b4d7d932dd61f123fb15ddf87b"},
|
||||
{file = "SQLAlchemy-1.4.43-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:736d4e706adb3c95a0a7e660073a5213dfae78ff2df6addf8ff2918c83fbeebe"},
|
||||
{file = "SQLAlchemy-1.4.43-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23a4569d3db1ce44370d05c5ad79be4f37915fcc97387aef9da232b95db7b695"},
|
||||
{file = "SQLAlchemy-1.4.43-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:42bff29eaecbb284f614f4bb265bb0c268625f5b93ce6268f8017811e0afbdde"},
|
||||
{file = "SQLAlchemy-1.4.43-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee9613b0460dce970414cfc990ca40afe518bc139e697243fcdf890285fb30ac"},
|
||||
{file = "SQLAlchemy-1.4.43-cp37-cp37m-win32.whl", hash = "sha256:dc1e005d490c101d27657481a05765851ab795cc8aedeb8d9425595088b20736"},
|
||||
{file = "SQLAlchemy-1.4.43-cp37-cp37m-win_amd64.whl", hash = "sha256:c9a6e878e63286392b262d86d21fe16e6eec12b95ccb0a92c392f2b1e0acca03"},
|
||||
{file = "SQLAlchemy-1.4.43-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:c6de20de7c19b965c007c9da240268dde1451865099ca10f0f593c347041b845"},
|
||||
{file = "SQLAlchemy-1.4.43-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fef01240d32ada9007387afd8e0b2230f99efdc4b57ca6f1d1192fca4fcf6a5"},
|
||||
{file = "SQLAlchemy-1.4.43-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b6fd58e25e6cdd2a131d7e97f9713f8f2142360cd40c75af8aa5b83d535f811c"},
|
||||
{file = "SQLAlchemy-1.4.43-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35dc0a5e934c41e282e019c889069b01ff4cd356b2ea452c9985e1542734cfb1"},
|
||||
{file = "SQLAlchemy-1.4.43-cp38-cp38-win32.whl", hash = "sha256:fb9a44e7124f72b79023ab04e1c8fcd8f392939ef0d7a75beae8634e15605d30"},
|
||||
{file = "SQLAlchemy-1.4.43-cp38-cp38-win_amd64.whl", hash = "sha256:4a791e7a1e5ac33f70a3598f8f34fdd3b60c68593bbb038baf58bc50e02d7468"},
|
||||
{file = "SQLAlchemy-1.4.43-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:c9b59863e2b1f1e1ebf9ee517f86cdfa82d7049c8d81ad71ab58d442b137bbe9"},
|
||||
{file = "SQLAlchemy-1.4.43-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd80300d81d92661e2488a4bf4383f0c5dc6e7b05fa46d2823e231af4e30539a"},
|
||||
{file = "SQLAlchemy-1.4.43-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c3dde668edea70dc8d55a74d933d5446e5a97786cdd1c67c8e4971c73bd087ad"},
|
||||
{file = "SQLAlchemy-1.4.43-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b462c070769f0ef06ea5fe65206b970bcf2b59cb3fda2bec2f4729e1be89c13"},
|
||||
{file = "SQLAlchemy-1.4.43-cp39-cp39-win32.whl", hash = "sha256:c1f5bfffc3227d05d90c557b10604962f655b4a83c9f3ad507a81ac8d6847679"},
|
||||
{file = "SQLAlchemy-1.4.43-cp39-cp39-win_amd64.whl", hash = "sha256:a7fa3e57a7b0476fbcba72b231150503d53dbcbdd23f4a86be5152912a923b6e"},
|
||||
{file = "SQLAlchemy-1.4.43.tar.gz", hash = "sha256:c628697aad7a141da8fc3fd81b4874a711cc84af172e1b1e7bbfadf760446496"},
|
||||
]
|
||||
sqlalchemy2-stubs = [
|
||||
{file = "sqlalchemy2-stubs-0.0.2a29.tar.gz", hash = "sha256:1bbc6aebd76db7c0351a9f45cc1c4e8ac335ba150094c2af091e8b87b9118419"},
|
||||
@ -2283,24 +2295,24 @@ watchdog = [
|
||||
{file = "watchdog-2.1.9.tar.gz", hash = "sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609"},
|
||||
]
|
||||
watchfiles = [
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:76a4c4a8e25a2c9a4f7fa3d373bbaf5558c17b97b4cf8411d33de368fe6b68a9"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:d5d799614d4c56d29c5ba56f4f619f967210dc10a0d6965b62d326b9e2f72c9e"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:39b932b044fef6c43e813e0bef908e0edf185bf7b5d8d53246651cb7ac9efe79"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1686bc4ac40ffde7256b6543b0f9a2cc8b531ae45243786f1d3f1dda2fe39e24"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:320bcde0adaa972403ed3b70f784409437325a1a4df2de54ba0672203d8847e5"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ba6d8c2f957cae3e888bc250bc60ed09fe869b3f55f09d020ed3fecbefb6a4c"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd4215badad1e3d1ad5fb79f21432dd5157e2e7b0765d27a19dc2a28580c6979"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cfdbfc4b6797c28dd1a8524581fed00ca333971b4111af8cd42fb7a92dcdc227"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-win32.whl", hash = "sha256:8eddc2d19bf6f49aee224072ec0f4f3258125a49f11b5dcff1448e68718a745e"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-win_amd64.whl", hash = "sha256:be87c9b1fe2b02105a9ac6d9df7500a110652bbd97cf46b13964eeaef9a6c89c"},
|
||||
{file = "watchfiles-0.18.0-cp37-abi3-win_arm64.whl", hash = "sha256:184799818c4fa7dbc6a1e4ca20bcbc6b85e4e0db07ce4554ea2f29b75ccd0cdc"},
|
||||
{file = "watchfiles-0.18.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:7f39fcdac5d5b9815a0c2ab9005d39854296b11fa15386a9a69c09cbbc5dde2c"},
|
||||
{file = "watchfiles-0.18.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78b1e7c29b92dfc8fc32f15949019232b493767d236c2bff31848df13fdb9e8a"},
|
||||
{file = "watchfiles-0.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27d64a6ed5e0aebef97c70fa3899a6958d4f7f049effc659e7dc3e81f3170a7b"},
|
||||
{file = "watchfiles-0.18.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4bbc8bfa0f3871b1867af42837a5635a9c1cbb2b68d039754b4750642c34aaee"},
|
||||
{file = "watchfiles-0.18.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3a12e4de5446fb6e286b720d0cb3a080811caf0ef43e556c2db5fe10ef0342"},
|
||||
{file = "watchfiles-0.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e611a90482ac14ef3ec234c1604ed921d1b0c68970eba82f1cf0d59a3e4eb76"},
|
||||
{file = "watchfiles-0.18.0.tar.gz", hash = "sha256:bbe10d134eef1666451382015e48f092c941a6d4562a98ffa1a288f79a897c46"},
|
||||
{file = "watchfiles-0.18.1-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:9891d3c94272108bcecf5597a592e61105279def1313521e637f2d5acbe08bc9"},
|
||||
{file = "watchfiles-0.18.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:7102342d60207fa635e24c02a51c6628bf0472e5fef067f78a612386840407fc"},
|
||||
{file = "watchfiles-0.18.1-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:00ea0081eca5e8e695cffbc3a726bb90da77f4e3f78ce29b86f0d95db4e70ef7"},
|
||||
{file = "watchfiles-0.18.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b8e6db99e49cd7125d8a4c9d33c0735eea7b75a942c6ad68b75be3e91c242fb"},
|
||||
{file = "watchfiles-0.18.1-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc7c726855f04f22ac79131b51bf0c9f728cb2117419ed830a43828b2c4a5fcb"},
|
||||
{file = "watchfiles-0.18.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbaff354d12235002e62d9d3fa8bcf326a8490c1179aa5c17195a300a9e5952f"},
|
||||
{file = "watchfiles-0.18.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:888db233e06907c555eccd10da99b9cd5ed45deca47e41766954292dc9f7b198"},
|
||||
{file = "watchfiles-0.18.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:dde79930d1b28f15994ad6613aa2865fc7a403d2bb14585a8714a53233b15717"},
|
||||
{file = "watchfiles-0.18.1-cp37-abi3-win32.whl", hash = "sha256:e2b2bdd26bf8d6ed90763e6020b475f7634f919dbd1730ea1b6f8cb88e21de5d"},
|
||||
{file = "watchfiles-0.18.1-cp37-abi3-win_amd64.whl", hash = "sha256:c541e0f2c3e95e83e4f84561c893284ba984e9d0025352057396d96dceb09f44"},
|
||||
{file = "watchfiles-0.18.1-cp37-abi3-win_arm64.whl", hash = "sha256:9a26272ef3e930330fc0c2c148cc29706cc2c40d25760c7ccea8d768a8feef8b"},
|
||||
{file = "watchfiles-0.18.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:9fb12a5e2b42e0b53769455ff93546e6bc9ab14007fbd436978d827a95ca5bd1"},
|
||||
{file = "watchfiles-0.18.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:548d6b42303d40264118178053c78820533b683b20dfbb254a8706ca48467357"},
|
||||
{file = "watchfiles-0.18.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0d8fdfebc50ac7569358f5c75f2b98bb473befccf9498cf23b3e39993bb45a"},
|
||||
{file = "watchfiles-0.18.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:0f9a22fff1745e2bb930b1e971c4c5b67ea3b38ae17a6adb9019371f80961219"},
|
||||
{file = "watchfiles-0.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b02e7fa03cd4059dd61ff0600080a5a9e7a893a85cb8e5178943533656eec65e"},
|
||||
{file = "watchfiles-0.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a868ce2c7565137f852bd4c863a164dc81306cae7378dbdbe4e2aca51ddb8857"},
|
||||
{file = "watchfiles-0.18.1.tar.gz", hash = "sha256:4ec0134a5e31797eb3c6c624dbe9354f2a8ee9c720e0b46fc5b7bab472b7c6d4"},
|
||||
]
|
||||
wcwidth = [
|
||||
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
|
||||
|
@ -44,6 +44,7 @@ uvicorn = {extras = ["standard"], version = "^0.18.3"}
|
||||
Brotli = "^1.0.9"
|
||||
greenlet = "^1.1.3"
|
||||
mistletoe = "^0.9.0"
|
||||
Pebble = "^5.0.2"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "^22.3.0"
|
||||
|
Reference in New Issue
Block a user