mirror of
https://git.sr.ht/~tsileo/microblog.pub
synced 2025-06-05 21:59:23 +02:00
Compare commits
10 Commits
2.0.0-rc.6
...
outbox-fts
Author | SHA1 | Date | |
---|---|---|---|
0dac5ec8fd | |||
ee37803987 | |||
0badf0bc1f | |||
32692a7dcd | |||
817dd98c5c | |||
b6f0cd01d3 | |||
c985dd84c3 | |||
3d049da2e5 | |||
fd5293a05c | |||
3729500e3e |
16
Makefile
16
Makefile
@ -12,32 +12,32 @@ config:
|
||||
|
||||
.PHONY: update
|
||||
update:
|
||||
-docker run --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv update --no-update-deps
|
||||
-docker run --rm --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv update --no-update-deps
|
||||
|
||||
.PHONY: prune-old-data
|
||||
prune-old-data:
|
||||
-docker run --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv prune-old-data
|
||||
-docker run --rm --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv prune-old-data
|
||||
|
||||
.PHONY: webfinger
|
||||
webfinger:
|
||||
-docker run --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv webfinger $(account)
|
||||
-docker run --rm --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv webfinger $(account)
|
||||
|
||||
.PHONY: move-to
|
||||
move-to:
|
||||
-docker run --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv move-to $(account)
|
||||
-docker run --rm --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv move-to $(account)
|
||||
|
||||
.PHONY: self-destruct
|
||||
self-destruct:
|
||||
-docker run --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv self-destruct
|
||||
-docker run --rm --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv self-destruct
|
||||
|
||||
.PHONY: reset-password
|
||||
reset-password:
|
||||
-docker run --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv reset-password
|
||||
-docker run --rm -it --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv reset-password
|
||||
|
||||
.PHONY: check-config
|
||||
check-config:
|
||||
-docker run --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv check-config
|
||||
-docker run --rm --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv check-config
|
||||
|
||||
.PHONY: compile-scss
|
||||
compile-scss:
|
||||
-docker run --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv compile-scss
|
||||
-docker run --rm --volume `pwd`/data:/app/data --volume `pwd`/app/static:/app/app/static microblogpub/microblogpub inv compile-scss
|
||||
|
@ -0,0 +1,48 @@
|
||||
"""Add a slug field for outbox objects
|
||||
|
||||
Revision ID: b28c0551c236
|
||||
Revises: 604d125ea2fb
|
||||
Create Date: 2022-10-30 14:09:14.540461+00:00
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b28c0551c236'
|
||||
down_revision = '604d125ea2fb'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('outbox', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('slug', sa.String(), nullable=True))
|
||||
batch_op.create_index(batch_op.f('ix_outbox_slug'), ['slug'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
# Backfill the slug for existing articles
|
||||
from app.models import OutboxObject
|
||||
from app.utils.text import slugify
|
||||
sess = Session(op.get_bind())
|
||||
articles = sess.execute(select(OutboxObject).where(
|
||||
OutboxObject.ap_type == "Article")
|
||||
).scalars()
|
||||
for article in articles:
|
||||
title = article.ap_object["name"]
|
||||
article.slug = slugify(title)
|
||||
sess.commit()
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('outbox', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_outbox_slug'))
|
||||
batch_op.drop_column('slug')
|
||||
|
||||
# ### end Alembic commands ###
|
115
alembic/versions/2022_11_02_1914-368f511ad954_outbox_fts.py
Normal file
115
alembic/versions/2022_11_02_1914-368f511ad954_outbox_fts.py
Normal file
@ -0,0 +1,115 @@
|
||||
"""Outbox FTS
|
||||
|
||||
Revision ID: 368f511ad954
|
||||
Revises: b28c0551c236
|
||||
Create Date: 2022-11-02 19:14:37.865923+00:00
|
||||
|
||||
"""
|
||||
from sqlalchemy import insert
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '368f511ad954'
|
||||
down_revision = 'b28c0551c236'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# ### end Alembic commands ###
|
||||
op.execute(
|
||||
"CREATE VIRTUAL TABLE outbox_fts USING "
|
||||
"fts5(summary, name, source, content='');"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE TRIGGER outbox_fts_ai AFTER "
|
||||
"INSERT ON outbox WHEN new.ap_type in ('Article', 'Note', 'Question') BEGIN"
|
||||
" INSERT INTO outbox_fts (rowid, source, name, summary)"
|
||||
" VALUES ("
|
||||
" new.id, "
|
||||
" new.source, "
|
||||
' json_extract(new.ap_object, "$.name"), '
|
||||
' json_extract(new.ap_object, "$.summary")'
|
||||
" ); "
|
||||
"END;"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE TRIGGER outbox_fts_ad AFTER "
|
||||
"DELETE ON outbox WHEN old.ap_type in ('Article', 'Note', 'Question') BEGIN"
|
||||
" INSERT INTO outbox_fts (outbox_fts, rowid, source, name, summary)"
|
||||
" VALUES ("
|
||||
" 'delete', "
|
||||
" old.id, "
|
||||
" old.source, "
|
||||
' json_extract(old.ap_object, "$.name"), '
|
||||
' json_extract(old.ap_object, "$.summary")'
|
||||
" ); "
|
||||
"END;"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE TRIGGER outbox_fts_au_softdelete AFTER "
|
||||
"UPDATE ON outbox WHEN new.is_deleted = 1 AND "
|
||||
"new.ap_type in ('Article', 'Note', 'Question') BEGIN"
|
||||
" INSERT INTO outbox_fts (outbox_fts, rowid, source, name, summary)"
|
||||
" VALUES ("
|
||||
" 'delete', "
|
||||
" old.id, "
|
||||
" old.source, "
|
||||
' json_extract(old.ap_object, "$.name"), '
|
||||
' json_extract(old.ap_object, "$.summary")'
|
||||
" ); "
|
||||
"END; "
|
||||
)
|
||||
op.execute(
|
||||
"CREATE TRIGGER outbox_fts_au AFTER "
|
||||
"UPDATE ON outbox "
|
||||
"WHEN (new.source <> old.source OR new.ap_object <> old.ap_object) AND "
|
||||
"new.ap_type in ('Note', 'Article', 'Quesion') BEGIN"
|
||||
" INSERT INTO outbox_fts (outbox_fts, rowid, source, name, summary)"
|
||||
" VALUES ("
|
||||
" 'delete', "
|
||||
" old.id, "
|
||||
" old.source, "
|
||||
' json_extract(old.ap_object, "$.name"), '
|
||||
' json_extract(old.ap_object, "$.summary")'
|
||||
" );"
|
||||
" INSERT INTO outbox_fts (rowid, source, name, summary)"
|
||||
" VALUES ("
|
||||
" new.id, "
|
||||
" new.source, "
|
||||
' json_extract(new.ap_object, "$.name"), '
|
||||
' json_extract(new.ap_object, "$.summary")'
|
||||
" );"
|
||||
"END;"
|
||||
)
|
||||
from app.models import OutboxObject
|
||||
from app.models import outbox_fts
|
||||
sess = Session(op.get_bind())
|
||||
|
||||
# Backfill the index
|
||||
outbox_objects = sess.execute(select(OutboxObject).where(
|
||||
OutboxObject.ap_type.in_(["Article", "Note", "Question"]))
|
||||
).scalars()
|
||||
for outbox_object in outbox_objects:
|
||||
row = {"source": outbox_object.source, "rowid": outbox_object.id}
|
||||
if name := outbox_object.ap_object.get("name"):
|
||||
row["name"] = name
|
||||
if summary := outbox_object.ap_object.get("summary"):
|
||||
row["summary"] = summary
|
||||
sess.execute(insert(outbox_fts).values(row))
|
||||
|
||||
sess.commit()
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# ### end Alembic commands ###
|
||||
op.drop_table('outbox_fts')
|
||||
op.execute("DROP TRIGGER outbox_fts_ai;")
|
||||
op.execute("DROP TRIGGER outbox_fts_ad;")
|
||||
op.execute("DROP TRIGGER outbox_fts_au_softdelete;")
|
||||
op.execute("DROP TRIGGER outbox_fts_au;")
|
64
app/admin.py
64
app/admin.py
@ -1,4 +1,5 @@
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter
|
||||
@ -25,7 +26,9 @@ from app.actor import fetch_actor
|
||||
from app.actor import get_actors_metadata
|
||||
from app.boxes import get_inbox_object_by_ap_id
|
||||
from app.boxes import get_outbox_object_by_ap_id
|
||||
from app.boxes import send_block
|
||||
from app.boxes import send_follow
|
||||
from app.boxes import send_unblock
|
||||
from app.config import EMOJIS
|
||||
from app.config import generate_csrf_token
|
||||
from app.config import session_serializer
|
||||
@ -147,6 +150,57 @@ async def get_lookup(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/search")
|
||||
async def admin_search(
|
||||
request: Request,
|
||||
query: str | None = None,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> templates.TemplateResponse | RedirectResponse:
|
||||
|
||||
results: list[Any] = []
|
||||
if query:
|
||||
results = (
|
||||
(
|
||||
await db_session.execute(
|
||||
select(models.OutboxObject)
|
||||
.join(
|
||||
models.outbox_fts,
|
||||
models.outbox_fts.c.rowid == models.OutboxObject.id,
|
||||
)
|
||||
.options(
|
||||
joinedload(
|
||||
models.OutboxObject.outbox_object_attachments
|
||||
).options(joinedload(models.OutboxObjectAttachment.upload)),
|
||||
joinedload(models.OutboxObject.relates_to_inbox_object).options(
|
||||
joinedload(models.InboxObject.actor),
|
||||
),
|
||||
joinedload(
|
||||
models.OutboxObject.relates_to_outbox_object
|
||||
).options(
|
||||
joinedload(
|
||||
models.OutboxObject.outbox_object_attachments
|
||||
).options(joinedload(models.OutboxObjectAttachment.upload)),
|
||||
),
|
||||
)
|
||||
.where(models.outbox_fts.c.outbox_fts.op("MATCH")(query))
|
||||
.limit(20)
|
||||
)
|
||||
) # type: ignore
|
||||
.unique()
|
||||
.scalars()
|
||||
)
|
||||
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"admin_search.html",
|
||||
{
|
||||
"query": query,
|
||||
"results": results,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/new")
|
||||
async def admin_new(
|
||||
request: Request,
|
||||
@ -340,6 +394,7 @@ async def admin_inbox(
|
||||
"Update",
|
||||
"Undo",
|
||||
"Read",
|
||||
"Reject",
|
||||
"Add",
|
||||
"Remove",
|
||||
"EmojiReact",
|
||||
@ -868,10 +923,7 @@ async def admin_actions_block(
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> RedirectResponse:
|
||||
logger.info(f"Blocking {ap_actor_id}")
|
||||
actor = await fetch_actor(db_session, ap_actor_id)
|
||||
actor.is_blocked = True
|
||||
await db_session.commit()
|
||||
await send_block(db_session, ap_actor_id)
|
||||
return RedirectResponse(redirect_url, status_code=302)
|
||||
|
||||
|
||||
@ -884,9 +936,7 @@ async def admin_actions_unblock(
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> RedirectResponse:
|
||||
logger.info(f"Unblocking {ap_actor_id}")
|
||||
actor = await fetch_actor(db_session, ap_actor_id)
|
||||
actor.is_blocked = False
|
||||
await db_session.commit()
|
||||
await send_unblock(db_session, ap_actor_id)
|
||||
return RedirectResponse(redirect_url, status_code=302)
|
||||
|
||||
|
||||
|
132
app/boxes.py
132
app/boxes.py
@ -41,6 +41,7 @@ from app.utils import webmentions
|
||||
from app.utils.datetime import as_utc
|
||||
from app.utils.datetime import now
|
||||
from app.utils.datetime import parse_isoformat
|
||||
from app.utils.text import slugify
|
||||
|
||||
AnyboxObject = models.InboxObject | models.OutboxObject
|
||||
|
||||
@ -63,6 +64,7 @@ async def save_outbox_object(
|
||||
source: str | None = None,
|
||||
is_transient: bool = False,
|
||||
conversation: str | None = None,
|
||||
slug: str | None = None,
|
||||
) -> models.OutboxObject:
|
||||
ro = await RemoteObject.from_raw_object(raw_object)
|
||||
|
||||
@ -82,6 +84,7 @@ async def save_outbox_object(
|
||||
source=source,
|
||||
is_transient=is_transient,
|
||||
conversation=conversation,
|
||||
slug=slug,
|
||||
)
|
||||
db_session.add(outbox_object)
|
||||
await db_session.flush()
|
||||
@ -90,6 +93,87 @@ async def save_outbox_object(
|
||||
return outbox_object
|
||||
|
||||
|
||||
async def send_unblock(db_session: AsyncSession, ap_actor_id: str) -> None:
|
||||
actor = await fetch_actor(db_session, ap_actor_id)
|
||||
|
||||
block_activity = (
|
||||
await db_session.scalars(
|
||||
select(models.OutboxObject).where(
|
||||
models.OutboxObject.activity_object_ap_id == actor.ap_id,
|
||||
models.OutboxObject.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
).one_or_none()
|
||||
if not block_activity:
|
||||
raise ValueError(f"No Block activity for {ap_actor_id}")
|
||||
|
||||
await _send_undo(db_session, block_activity.ap_id)
|
||||
|
||||
await db_session.commit()
|
||||
|
||||
|
||||
async def send_block(db_session: AsyncSession, ap_actor_id: str) -> None:
|
||||
logger.info(f"Blocking {ap_actor_id}")
|
||||
actor = await fetch_actor(db_session, ap_actor_id)
|
||||
actor.is_blocked = True
|
||||
|
||||
# 1. Unfollow the actor
|
||||
following = (
|
||||
await db_session.scalars(
|
||||
select(models.Following)
|
||||
.options(joinedload(models.Following.outbox_object))
|
||||
.where(
|
||||
models.Following.ap_actor_id == actor.ap_id,
|
||||
)
|
||||
)
|
||||
).one_or_none()
|
||||
if following:
|
||||
await _send_undo(db_session, following.outbox_object.ap_id)
|
||||
|
||||
# 2. If the blocked actor is a follower, reject the follow request
|
||||
follower = (
|
||||
await db_session.scalars(
|
||||
select(models.Follower)
|
||||
.options(joinedload(models.Follower.inbox_object))
|
||||
.where(
|
||||
models.Follower.ap_actor_id == actor.ap_id,
|
||||
)
|
||||
)
|
||||
).one_or_none()
|
||||
if follower:
|
||||
await _send_reject(db_session, actor, follower.inbox_object)
|
||||
await db_session.delete(follower)
|
||||
|
||||
# 3. Send a block
|
||||
block_id = allocate_outbox_id()
|
||||
block = {
|
||||
"@context": ap.AS_EXTENDED_CTX,
|
||||
"id": outbox_object_id(block_id),
|
||||
"type": "Block",
|
||||
"actor": LOCAL_ACTOR.ap_id,
|
||||
"object": actor.ap_id,
|
||||
}
|
||||
outbox_object = await save_outbox_object(
|
||||
db_session,
|
||||
block_id,
|
||||
block,
|
||||
)
|
||||
if not outbox_object.id:
|
||||
raise ValueError("Should never happen")
|
||||
|
||||
await new_outgoing_activity(db_session, actor.inbox_url, outbox_object.id)
|
||||
|
||||
# 4. Create a notification
|
||||
notif = models.Notification(
|
||||
notification_type=models.NotificationType.BLOCK,
|
||||
actor_id=actor.id,
|
||||
outbox_object_id=outbox_object.id,
|
||||
)
|
||||
db_session.add(notif)
|
||||
|
||||
await db_session.commit()
|
||||
|
||||
|
||||
async def send_delete(db_session: AsyncSession, ap_object_id: str) -> None:
|
||||
outbox_object_to_delete = await get_outbox_object_by_ap_id(db_session, ap_object_id)
|
||||
if not outbox_object_to_delete:
|
||||
@ -266,7 +350,7 @@ async def _send_undo(db_session: AsyncSession, ap_object_id: str) -> None:
|
||||
if not outbox_object_to_undo:
|
||||
raise ValueError(f"{ap_object_id} not found in the outbox")
|
||||
|
||||
if outbox_object_to_undo.ap_type not in ["Follow", "Like", "Announce"]:
|
||||
if outbox_object_to_undo.ap_type not in ["Follow", "Like", "Announce", "Block"]:
|
||||
raise ValueError(
|
||||
f"Cannot build Undo for {outbox_object_to_undo.ap_type} activity"
|
||||
)
|
||||
@ -339,6 +423,30 @@ async def _send_undo(db_session: AsyncSession, ap_object_id: str) -> None:
|
||||
recipients = await _compute_recipients(db_session, outbox_object.ap_object)
|
||||
for rcp in recipients:
|
||||
await new_outgoing_activity(db_session, rcp, outbox_object.id)
|
||||
elif outbox_object_to_undo.ap_type == "Block":
|
||||
if not outbox_object_to_undo.activity_object_ap_id:
|
||||
raise ValueError(f"Invalid block activity {outbox_object_to_undo.ap_id}")
|
||||
|
||||
# Send the Undo to the blocked actor
|
||||
blocked_actor = await fetch_actor(
|
||||
db_session, outbox_object_to_undo.activity_object_ap_id
|
||||
)
|
||||
|
||||
blocked_actor.is_blocked = False
|
||||
|
||||
await new_outgoing_activity(
|
||||
db_session,
|
||||
blocked_actor.inbox_url, # type: ignore
|
||||
outbox_object.id,
|
||||
)
|
||||
|
||||
notif = models.Notification(
|
||||
notification_type=models.NotificationType.UNBLOCK,
|
||||
actor_id=blocked_actor.id,
|
||||
outbox_object_id=outbox_object.id,
|
||||
)
|
||||
db_session.add(notif)
|
||||
|
||||
else:
|
||||
raise ValueError("Should never happen")
|
||||
|
||||
@ -509,6 +617,9 @@ async def send_create(
|
||||
else:
|
||||
raise ValueError(f"Unhandled visibility {visibility}")
|
||||
|
||||
slug = None
|
||||
url = outbox_object_id(note_id)
|
||||
|
||||
extra_obj_attrs = {}
|
||||
if ap_type == "Question":
|
||||
if not poll_answers or len(poll_answers) < 2:
|
||||
@ -538,6 +649,8 @@ async def send_create(
|
||||
if not name:
|
||||
raise ValueError("Article must have a name")
|
||||
|
||||
slug = slugify(name)
|
||||
url = f"{BASE_URL}/articles/{note_id[:7]}/{slug}"
|
||||
extra_obj_attrs = {"name": name}
|
||||
|
||||
obj = {
|
||||
@ -551,7 +664,7 @@ async def send_create(
|
||||
"published": published,
|
||||
"context": context,
|
||||
"conversation": context,
|
||||
"url": outbox_object_id(note_id),
|
||||
"url": url,
|
||||
"tag": dedup_tags(tags),
|
||||
"summary": content_warning,
|
||||
"inReplyTo": in_reply_to,
|
||||
@ -565,6 +678,7 @@ async def send_create(
|
||||
obj,
|
||||
source=source,
|
||||
conversation=conversation,
|
||||
slug=slug,
|
||||
)
|
||||
if not outbox_object.id:
|
||||
raise ValueError("Should never happen")
|
||||
@ -2034,8 +2148,10 @@ async def save_to_inbox(
|
||||
await _process_transient_object(db_session, raw_object, actor)
|
||||
return None
|
||||
|
||||
if actor.is_blocked:
|
||||
logger.warning("Actor {actor.ap_id} is blocked, ignoring object")
|
||||
# If we just blocked an actor, we want to process any undo sent as side
|
||||
# effects
|
||||
if actor.is_blocked and ap.as_list(raw_object["type"])[0] != "Undo":
|
||||
logger.warning(f"Actor {actor.ap_id} is blocked, ignoring object")
|
||||
return None
|
||||
|
||||
raw_object_id = ap.get_id(raw_object)
|
||||
@ -2377,7 +2493,9 @@ async def get_replies_tree(
|
||||
.where(
|
||||
models.InboxObject.conversation
|
||||
== requested_object.conversation,
|
||||
models.InboxObject.ap_type.in_(["Note", "Page", "Article"]),
|
||||
models.InboxObject.ap_type.in_(
|
||||
["Note", "Page", "Article", "Question"]
|
||||
),
|
||||
models.InboxObject.is_deleted.is_(False),
|
||||
models.InboxObject.visibility.in_(allowed_visibility),
|
||||
)
|
||||
@ -2395,7 +2513,9 @@ async def get_replies_tree(
|
||||
models.OutboxObject.conversation
|
||||
== requested_object.conversation,
|
||||
models.OutboxObject.is_deleted.is_(False),
|
||||
models.OutboxObject.ap_type.in_(["Note", "Page", "Article"]),
|
||||
models.OutboxObject.ap_type.in_(
|
||||
["Note", "Page", "Article", "Question"]
|
||||
),
|
||||
models.OutboxObject.visibility.in_(allowed_visibility),
|
||||
)
|
||||
.options(
|
||||
|
@ -14,6 +14,7 @@ from itsdangerous import URLSafeTimedSerializer
|
||||
from loguru import logger
|
||||
from mistletoe import markdown # type: ignore
|
||||
|
||||
from app.customization import _CUSTOM_ROUTES
|
||||
from app.utils.emoji import _load_emojis
|
||||
from app.utils.version import get_version_commit
|
||||
|
||||
@ -184,6 +185,31 @@ CODE_HIGHLIGHTING_THEME = CONFIG.code_highlighting_theme
|
||||
MOVED_TO = _get_moved_to()
|
||||
|
||||
|
||||
_NavBarItem = tuple[str, str]
|
||||
|
||||
|
||||
class NavBarItems:
|
||||
EXTRA_NAVBAR_ITEMS: list[_NavBarItem] = []
|
||||
INDEX_NAVBAR_ITEM: _NavBarItem | None = None
|
||||
NOTES_PATH = "/"
|
||||
|
||||
|
||||
def load_custom_routes() -> None:
|
||||
try:
|
||||
from data import custom_routes # type: ignore # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
for path, custom_handler in _CUSTOM_ROUTES.items():
|
||||
# If a handler wants to replace the root, move the index to /notes
|
||||
if path == "/":
|
||||
NavBarItems.NOTES_PATH = "/notes"
|
||||
NavBarItems.INDEX_NAVBAR_ITEM = (path, custom_handler.title)
|
||||
else:
|
||||
if custom_handler.show_in_navbar:
|
||||
NavBarItems.EXTRA_NAVBAR_ITEMS.append((path, custom_handler.title))
|
||||
|
||||
|
||||
session_serializer = URLSafeTimedSerializer(
|
||||
CONFIG.secret,
|
||||
salt=f"{ID}.session",
|
||||
|
112
app/customization.py
Normal file
112
app/customization.py
Normal file
@ -0,0 +1,112 @@
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import Request
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
_DATA_DIR = Path().parent.resolve() / "data"
|
||||
_Handler = Callable[..., Any]
|
||||
|
||||
|
||||
class HTMLPage:
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
html_file: str,
|
||||
show_in_navbar: bool,
|
||||
) -> None:
|
||||
self.title = title
|
||||
self.html_file = _DATA_DIR / html_file
|
||||
self.show_in_navbar = show_in_navbar
|
||||
|
||||
|
||||
class RawHandler:
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
handler: Any,
|
||||
show_in_navbar: bool,
|
||||
) -> None:
|
||||
self.title = title
|
||||
self.handler = handler
|
||||
self.show_in_navbar = show_in_navbar
|
||||
|
||||
|
||||
_CUSTOM_ROUTES: dict[str, HTMLPage | RawHandler] = {}
|
||||
|
||||
|
||||
def register_html_page(
|
||||
path: str,
|
||||
*,
|
||||
title: str,
|
||||
html_file: str,
|
||||
show_in_navbar: bool = True,
|
||||
) -> None:
|
||||
if path in _CUSTOM_ROUTES:
|
||||
raise ValueError(f"{path} is already registered")
|
||||
|
||||
_CUSTOM_ROUTES[path] = HTMLPage(title, html_file, show_in_navbar)
|
||||
|
||||
|
||||
def register_raw_handler(
|
||||
path: str,
|
||||
*,
|
||||
title: str,
|
||||
handler: _Handler,
|
||||
show_in_navbar: bool = True,
|
||||
) -> None:
|
||||
if path in _CUSTOM_ROUTES:
|
||||
raise ValueError(f"{path} is already registered")
|
||||
|
||||
_CUSTOM_ROUTES[path] = RawHandler(title, handler, show_in_navbar)
|
||||
|
||||
|
||||
class ActivityPubResponse(JSONResponse):
|
||||
media_type = "application/activity+json"
|
||||
|
||||
|
||||
def _custom_page_handler(path: str, html_page: HTMLPage) -> Any:
|
||||
from app import templates
|
||||
from app.actor import LOCAL_ACTOR
|
||||
from app.config import is_activitypub_requested
|
||||
from app.database import AsyncSession
|
||||
from app.database import get_db_session
|
||||
|
||||
async def _handler(
|
||||
request: Request,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> templates.TemplateResponse | ActivityPubResponse:
|
||||
if path == "/" and is_activitypub_requested(request):
|
||||
return ActivityPubResponse(LOCAL_ACTOR.ap_actor)
|
||||
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"custom_page.html",
|
||||
{
|
||||
"page_content": html_page.html_file.read_text(),
|
||||
"title": html_page.title,
|
||||
},
|
||||
)
|
||||
|
||||
return _handler
|
||||
|
||||
|
||||
def get_custom_router() -> APIRouter | None:
|
||||
if not _CUSTOM_ROUTES:
|
||||
return None
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
for path, handler in _CUSTOM_ROUTES.items():
|
||||
if isinstance(handler, HTMLPage):
|
||||
router.add_api_route(
|
||||
path, _custom_page_handler(path, handler), methods=["GET"]
|
||||
)
|
||||
else:
|
||||
router.add_api_route(path, handler.handler)
|
||||
|
||||
return router
|
168
app/main.py
168
app/main.py
@ -63,6 +63,7 @@ from app.config import USER_AGENT
|
||||
from app.config import USERNAME
|
||||
from app.config import is_activitypub_requested
|
||||
from app.config import verify_csrf_token
|
||||
from app.customization import get_custom_router
|
||||
from app.database import AsyncSession
|
||||
from app.database import async_session
|
||||
from app.database import get_db_session
|
||||
@ -192,6 +193,9 @@ app.include_router(admin.unauthenticated_router, prefix="/admin")
|
||||
app.include_router(indieauth.router)
|
||||
app.include_router(micropub.router)
|
||||
app.include_router(webmentions.router)
|
||||
config.load_custom_routes()
|
||||
if custom_router := get_custom_router():
|
||||
app.include_router(custom_router)
|
||||
|
||||
# XXX: order matters, the proxy middleware needs to be last
|
||||
app.add_middleware(CustomMiddleware)
|
||||
@ -243,7 +247,7 @@ class ActivityPubResponse(JSONResponse):
|
||||
media_type = "application/activity+json"
|
||||
|
||||
|
||||
@app.get("/")
|
||||
@app.get(config.NavBarItems.NOTES_PATH)
|
||||
async def index(
|
||||
request: Request,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
@ -632,13 +636,75 @@ async def _check_outbox_object_acl(
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
|
||||
async def _fetch_likes(
|
||||
db_session: AsyncSession,
|
||||
outbox_object: models.OutboxObject,
|
||||
) -> list[models.InboxObject]:
|
||||
return (
|
||||
(
|
||||
await db_session.scalars(
|
||||
select(models.InboxObject)
|
||||
.where(
|
||||
models.InboxObject.ap_type == "Like",
|
||||
models.InboxObject.activity_object_ap_id == outbox_object.ap_id,
|
||||
models.InboxObject.is_deleted.is_(False),
|
||||
)
|
||||
.options(joinedload(models.InboxObject.actor))
|
||||
.order_by(models.InboxObject.ap_published_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
async def _fetch_shares(
|
||||
db_session: AsyncSession,
|
||||
outbox_object: models.OutboxObject,
|
||||
) -> list[models.InboxObject]:
|
||||
return (
|
||||
(
|
||||
await db_session.scalars(
|
||||
select(models.InboxObject)
|
||||
.filter(
|
||||
models.InboxObject.ap_type == "Announce",
|
||||
models.InboxObject.activity_object_ap_id == outbox_object.ap_id,
|
||||
models.InboxObject.is_deleted.is_(False),
|
||||
)
|
||||
.options(joinedload(models.InboxObject.actor))
|
||||
.order_by(models.InboxObject.ap_published_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
async def _fetch_webmentions(
|
||||
db_session: AsyncSession,
|
||||
outbox_object: models.OutboxObject,
|
||||
) -> list[models.Webmention]:
|
||||
return (
|
||||
await db_session.scalars(
|
||||
select(models.Webmention)
|
||||
.filter(
|
||||
models.Webmention.outbox_object_id == outbox_object.id,
|
||||
models.Webmention.is_deleted.is_(False),
|
||||
)
|
||||
.limit(10)
|
||||
)
|
||||
).all()
|
||||
|
||||
|
||||
@app.get("/o/{public_id}")
|
||||
async def outbox_by_public_id(
|
||||
public_id: str,
|
||||
request: Request,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
httpsig_info: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> ActivityPubResponse | templates.TemplateResponse:
|
||||
) -> ActivityPubResponse | templates.TemplateResponse | RedirectResponse:
|
||||
maybe_object = (
|
||||
(
|
||||
await db_session.execute(
|
||||
@ -665,59 +731,79 @@ async def outbox_by_public_id(
|
||||
if is_activitypub_requested(request):
|
||||
return ActivityPubResponse(maybe_object.ap_object)
|
||||
|
||||
if maybe_object.ap_type == "Article":
|
||||
return RedirectResponse(
|
||||
f"/articles/{public_id[:7]}/{maybe_object.slug}",
|
||||
status_code=301,
|
||||
)
|
||||
|
||||
replies_tree = await boxes.get_replies_tree(
|
||||
db_session,
|
||||
maybe_object,
|
||||
is_current_user_admin=is_current_user_admin(request),
|
||||
)
|
||||
|
||||
likes = (
|
||||
likes = await _fetch_likes(db_session, maybe_object)
|
||||
shares = await _fetch_shares(db_session, maybe_object)
|
||||
webmentions = await _fetch_webmentions(db_session, maybe_object)
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"object.html",
|
||||
{
|
||||
"replies_tree": replies_tree,
|
||||
"outbox_object": maybe_object,
|
||||
"likes": likes,
|
||||
"shares": shares,
|
||||
"webmentions": webmentions,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/articles/{short_id}/{slug}")
|
||||
async def article_by_slug(
|
||||
short_id: str,
|
||||
slug: str,
|
||||
request: Request,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
httpsig_info: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> ActivityPubResponse | templates.TemplateResponse | RedirectResponse:
|
||||
maybe_object = (
|
||||
(
|
||||
await db_session.scalars(
|
||||
select(models.InboxObject)
|
||||
await db_session.execute(
|
||||
select(models.OutboxObject)
|
||||
.options(
|
||||
joinedload(models.OutboxObject.outbox_object_attachments).options(
|
||||
joinedload(models.OutboxObjectAttachment.upload)
|
||||
)
|
||||
)
|
||||
.where(
|
||||
models.InboxObject.ap_type == "Like",
|
||||
models.InboxObject.activity_object_ap_id == maybe_object.ap_id,
|
||||
models.InboxObject.is_deleted.is_(False),
|
||||
models.OutboxObject.public_id.like(f"{short_id}%"),
|
||||
models.OutboxObject.slug == slug,
|
||||
models.OutboxObject.is_deleted.is_(False),
|
||||
)
|
||||
.options(joinedload(models.InboxObject.actor))
|
||||
.order_by(models.InboxObject.ap_published_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
.scalar_one_or_none()
|
||||
)
|
||||
if not maybe_object:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
await _check_outbox_object_acl(request, db_session, maybe_object, httpsig_info)
|
||||
|
||||
if is_activitypub_requested(request):
|
||||
return ActivityPubResponse(maybe_object.ap_object)
|
||||
|
||||
replies_tree = await boxes.get_replies_tree(
|
||||
db_session,
|
||||
maybe_object,
|
||||
is_current_user_admin=is_current_user_admin(request),
|
||||
)
|
||||
|
||||
shares = (
|
||||
(
|
||||
await db_session.scalars(
|
||||
select(models.InboxObject)
|
||||
.filter(
|
||||
models.InboxObject.ap_type == "Announce",
|
||||
models.InboxObject.activity_object_ap_id == maybe_object.ap_id,
|
||||
models.InboxObject.is_deleted.is_(False),
|
||||
)
|
||||
.options(joinedload(models.InboxObject.actor))
|
||||
.order_by(models.InboxObject.ap_published_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
|
||||
webmentions = (
|
||||
await db_session.scalars(
|
||||
select(models.Webmention)
|
||||
.filter(
|
||||
models.Webmention.outbox_object_id == maybe_object.id,
|
||||
models.Webmention.is_deleted.is_(False),
|
||||
)
|
||||
.limit(10)
|
||||
)
|
||||
).all()
|
||||
|
||||
likes = await _fetch_likes(db_session, maybe_object)
|
||||
shares = await _fetch_shares(db_session, maybe_object)
|
||||
webmentions = await _fetch_webmentions(db_session, maybe_object)
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
|
@ -158,6 +158,7 @@ class OutboxObject(Base, BaseObject):
|
||||
is_hidden_from_homepage = Column(Boolean, nullable=False, default=False)
|
||||
|
||||
public_id = Column(String, nullable=False, index=True)
|
||||
slug = Column(String, nullable=True, index=True)
|
||||
|
||||
ap_type = Column(String, nullable=False, index=True)
|
||||
ap_id: Mapped[str] = Column(String, nullable=False, unique=True, index=True)
|
||||
@ -281,6 +282,13 @@ class OutboxObject(Base, BaseObject):
|
||||
def is_from_outbox(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def url(self) -> str | None:
|
||||
# XXX: rewrite old URL here for compat
|
||||
if self.ap_type == "Article" and self.slug and self.public_id:
|
||||
return f"{BASE_URL}/articles/{self.public_id[:7]}/{self.slug}"
|
||||
return super().url
|
||||
|
||||
|
||||
class Follower(Base):
|
||||
__tablename__ = "follower"
|
||||
@ -551,9 +559,14 @@ class NotificationType(str, enum.Enum):
|
||||
UPDATED_WEBMENTION = "updated_webmention"
|
||||
DELETED_WEBMENTION = "deleted_webmention"
|
||||
|
||||
# incoming
|
||||
BLOCKED = "blocked"
|
||||
UNBLOCKED = "unblocked"
|
||||
|
||||
# outgoing
|
||||
BLOCK = "block"
|
||||
UNBLOCK = "unblock"
|
||||
|
||||
|
||||
class Notification(Base):
|
||||
__tablename__ = "notifications"
|
||||
|
@ -291,6 +291,10 @@ ALLOWED_ATTRIBUTES: dict[str, list[str] | Callable[[str, str, str], bool]] = {
|
||||
}
|
||||
|
||||
|
||||
def _allow_all_attributes(tag: Any, name: Any, value: Any) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
@lru_cache(maxsize=256)
|
||||
def _update_inline_imgs(content):
|
||||
soup = BeautifulSoup(content, "html5lib")
|
||||
@ -320,7 +324,11 @@ def _clean_html(html: str, note: Object) -> str:
|
||||
_update_inline_imgs(highlight(html))
|
||||
),
|
||||
tags=ALLOWED_TAGS,
|
||||
attributes=ALLOWED_ATTRIBUTES,
|
||||
attributes=(
|
||||
_allow_all_attributes
|
||||
if note.ap_id.startswith(config.ID)
|
||||
else ALLOWED_ATTRIBUTES
|
||||
),
|
||||
strip=True,
|
||||
),
|
||||
note,
|
||||
@ -421,3 +429,4 @@ _templates.env.globals["CSS_HASH"] = config.CSS_HASH
|
||||
_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
|
||||
|
27
app/templates/admin_search.html
Normal file
27
app/templates/admin_search.html
Normal file
@ -0,0 +1,27 @@
|
||||
{%- import "utils.html" as utils with context -%}
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ local_actor.display_name }} - Search</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="box">
|
||||
|
||||
<form class="form" action="{{ url_for("admin_search") }}" method="GET">
|
||||
<input type="text" name="query" value="{{ query if query else "" }}" autofocus>
|
||||
<select name="what">
|
||||
<option value="outbox">Outbox</option>
|
||||
</select>
|
||||
<input type="submit" value="Search">
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
{% for result in results %}
|
||||
{{ utils.display_object(result) }}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
{% endblock %}
|
30
app/templates/custom_page.html
Normal file
30
app/templates/custom_page.html
Normal file
@ -0,0 +1,30 @@
|
||||
{%- import "utils.html" as utils with context -%}
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ title }}</title>
|
||||
{% if request.url.path == "/" %}
|
||||
<link rel="indieauth-metadata" href="{{ url_for("well_known_authorization_server") }}">
|
||||
<link rel="authorization_endpoint" href="{{ url_for("indieauth_authorization_endpoint") }}">
|
||||
<link rel="token_endpoint" href="{{ url_for("indieauth_token_endpoint") }}">
|
||||
<link rel="micropub" href="{{ url_for("micropub_endpoint") }}">
|
||||
<link rel="alternate" href="{{ local_actor.url }}" title="ActivityPub profile" type="application/activity+json">
|
||||
<meta content="profile" property="og:type" />
|
||||
<meta content="{{ local_actor.url }}" property="og:url" />
|
||||
<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="summary" property="twitter:card" />
|
||||
<meta content="{{ local_actor.handle }}" property="profile:username" />
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "header.html" %}
|
||||
|
||||
<div class="box">
|
||||
{{ page_content | safe }}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -29,9 +29,16 @@
|
||||
<a href="{{ url_for }}" {% if 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>
|
||||
{% endmacro %}
|
||||
|
||||
<div class="public-top-menu">
|
||||
<nav class="flexbox">
|
||||
<ul>
|
||||
{% if NAVBAR_ITEMS.INDEX_NAVBAR_ITEM %}
|
||||
<li>{{ navbar_item_link(NAVBAR_ITEMS.INDEX_NAVBAR_ITEM) }}</li>
|
||||
{% endif %}
|
||||
<li>{{ header_link("index", "Notes") }}</li>
|
||||
{% if articles_count %}
|
||||
<li>{{ header_link("articles", "Articles") }}</li>
|
||||
@ -43,6 +50,9 @@
|
||||
<li>{{ header_link("following", "Following") }} <span class="counter">{{ following_count }}</span></li>
|
||||
{% endif %}
|
||||
<li>{{ header_link("get_remote_follow", "Remote follow") }}</li>
|
||||
{% for navbar_item in NAVBAR_ITEMS.EXTRA_NAVBAR_ITEMS %}
|
||||
{{ navbar_item_link(navbar_item) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -30,6 +30,7 @@
|
||||
<li>{{ admin_link("admin_inbox", "Inbox") }} / {{ admin_link("admin_outbox", "Outbox") }}</li>
|
||||
<li>{{ admin_link("admin_direct_messages", "DMs") }}</li>
|
||||
<li>{{ admin_link("get_notifications", "Notifications") }} {% if notifications_count %}({{ notifications_count }}){% endif %}</li>
|
||||
<li>{{ admin_link("admin_search", "Search") }}</li>
|
||||
<li>{{ admin_link("get_lookup", "Lookup") }}</li>
|
||||
<li>{{ admin_link("admin_bookmarks", "Bookmarks") }}</li>
|
||||
<li><a href="{{ url_for("logout")}}">Logout</a></li>
|
||||
|
@ -42,6 +42,12 @@
|
||||
{% elif notif.notification_type.value == "unblocked" %}
|
||||
{{ notif_actor_action(notif, "unblocked you") }}
|
||||
{{ utils.display_actor(notif.actor, actors_metadata) }}
|
||||
{% elif notif.notification_type.value == "block" %}
|
||||
{{ notif_actor_action(notif, "was blocked") }}
|
||||
{{ utils.display_actor(notif.actor, actors_metadata) }}
|
||||
{% elif notif.notification_type.value == "unblock" %}
|
||||
{{ notif_actor_action(notif, "was unblocked") }}
|
||||
{{ utils.display_actor(notif.actor, actors_metadata) }}
|
||||
{%- elif notif.notification_type.value == "move" %}
|
||||
{# for move notif, the actor is the target and the inbox object the Move activity #}
|
||||
<div class="actor-action">
|
||||
|
32
app/utils/custom_index_handler.py
Normal file
32
app/utils/custom_index_handler.py
Normal file
@ -0,0 +1,32 @@
|
||||
from typing import Any
|
||||
from typing import Awaitable
|
||||
from typing import Callable
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.actor import LOCAL_ACTOR
|
||||
from app.config import is_activitypub_requested
|
||||
from app.database import AsyncSession
|
||||
from app.database import get_db_session
|
||||
|
||||
_Handler = Callable[[Request, AsyncSession], Awaitable[Any]]
|
||||
|
||||
|
||||
def build_custom_index_handler(handler: _Handler) -> _Handler:
|
||||
async def custom_index(
|
||||
request: Request,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> Any:
|
||||
# Serve the AP actor if requested
|
||||
if is_activitypub_requested(request):
|
||||
return JSONResponse(
|
||||
LOCAL_ACTOR.ap_actor,
|
||||
media_type="application/activity+json",
|
||||
)
|
||||
|
||||
# Defer to the custom handler
|
||||
return await handler(request, db_session)
|
||||
|
||||
return custom_index
|
8
app/utils/text.py
Normal file
8
app/utils/text.py
Normal file
@ -0,0 +1,8 @@
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
|
||||
def slugify(text: str) -> str:
|
||||
value = unicodedata.normalize("NFKC", text)
|
||||
value = re.sub(r"[^\w\s-]", "", value.lower())
|
||||
return re.sub(r"[-\s]+", "-", value).strip("-_")
|
@ -333,13 +333,13 @@ make compile-scss
|
||||
|
||||
### Password reset
|
||||
|
||||
If have lost your password, you can generate a new one using the `password-reset` task.
|
||||
If have lost your password, you can generate a new one using the `reset-password` task.
|
||||
|
||||
#### Python edition
|
||||
|
||||
```bash
|
||||
# shutdown supervisord
|
||||
poetry run inv password-reset
|
||||
poetry run inv reset-password
|
||||
# edit data/profile.toml
|
||||
# restart supervisord
|
||||
```
|
||||
@ -348,7 +348,7 @@ poetry run inv password-reset
|
||||
|
||||
```bash
|
||||
docker compose stop
|
||||
make password-reset
|
||||
make reset-password
|
||||
# edit data/profile.toml
|
||||
docker compose up -d
|
||||
```
|
||||
|
295
poetry.lock
generated
295
poetry.lock
generated
@ -238,11 +238,11 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
|
||||
[[package]]
|
||||
name = "colorlog"
|
||||
@ -269,6 +269,17 @@ python-versions = "*"
|
||||
[package.extras]
|
||||
dev = ["pytest", "coverage", "coveralls"]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.0.0"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "factory-boy"
|
||||
version = "3.2.1"
|
||||
@ -286,7 +297,7 @@ doc = ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"]
|
||||
|
||||
[[package]]
|
||||
name = "faker"
|
||||
version = "15.1.1"
|
||||
version = "15.1.2"
|
||||
description = "Faker is a Python package that generates fake data for you."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -680,7 +691,7 @@ python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "9.2.0"
|
||||
version = "9.3.0"
|
||||
description = "Python Imaging Library (Fork)"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -725,14 +736,6 @@ python-versions = ">=3.6.2"
|
||||
[package.dependencies]
|
||||
wcwidth = "*"
|
||||
|
||||
[[package]]
|
||||
name = "py"
|
||||
version = "1.11.0"
|
||||
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "pyaml"
|
||||
version = "21.10.1"
|
||||
@ -834,7 +837,7 @@ diagrams = ["railroad-diagrams", "jinja2"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.1.3"
|
||||
version = "7.2.0"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -843,11 +846,11 @@ python-versions = ">=3.7"
|
||||
[package.dependencies]
|
||||
attrs = ">=19.2.0"
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
py = ">=1.8.2"
|
||||
tomli = ">=1.0.0"
|
||||
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||
@ -1103,7 +1106,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "types-pillow"
|
||||
version = "9.2.2.2"
|
||||
version = "9.3.0.0"
|
||||
description = "Typing stubs for Pillow"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -1240,7 +1243,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "10.3"
|
||||
version = "10.4"
|
||||
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -1490,8 +1493,8 @@ click = [
|
||||
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
|
||||
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
colorlog = [
|
||||
{file = "colorlog-6.7.0-py2.py3-none-any.whl", hash = "sha256:0d33ca236784a1ba3ff9c532d4964126d8a2c44f1f0cb1d2b0728196f512f662"},
|
||||
@ -1500,13 +1503,17 @@ colorlog = [
|
||||
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"},
|
||||
]
|
||||
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.1-py3-none-any.whl", hash = "sha256:096c15e136adb365db24d8c3964fe26bfc68fe060c9385071a339f8c14e09c8a"},
|
||||
{file = "Faker-15.1.1.tar.gz", hash = "sha256:a741b77f484215c3aab2604100669657189548f440fcb2ed0f8b7ee21c385629"},
|
||||
{file = "Faker-15.1.2-py3-none-any.whl", hash = "sha256:37c8bfcbd9e0e99cebcd22e70dcf895ff92fb46aa8a32c7772df0c1f1f32ea48"},
|
||||
{file = "Faker-15.1.2.tar.gz", hash = "sha256:39c4e7915813923829675488cafef07ddf11cf59ecbaac518f53dd8e7b0df5cf"},
|
||||
]
|
||||
fastapi = [
|
||||
{file = "fastapi-0.78.0-py3-none-any.whl", hash = "sha256:15fcabd5c78c266fa7ae7d8de9b384bfc2375ee0503463a6febbe3bab69d6f65"},
|
||||
@ -1865,64 +1872,65 @@ pathspec = [
|
||||
{file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"},
|
||||
]
|
||||
pillow = [
|
||||
{file = "Pillow-9.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb"},
|
||||
{file = "Pillow-9.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f"},
|
||||
{file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5"},
|
||||
{file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831e648102c82f152e14c1a0938689dbb22480c548c8d4b8b248b3e50967b88c"},
|
||||
{file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cc1d2451e8a3b4bfdb9caf745b58e6c7a77d2e469159b0d527a4554d73694d1"},
|
||||
{file = "Pillow-9.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:136659638f61a251e8ed3b331fc6ccd124590eeff539de57c5f80ef3a9594e58"},
|
||||
{file = "Pillow-9.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6e8c66f70fb539301e064f6478d7453e820d8a2c631da948a23384865cd95544"},
|
||||
{file = "Pillow-9.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e"},
|
||||
{file = "Pillow-9.2.0-cp310-cp310-win32.whl", hash = "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28"},
|
||||
{file = "Pillow-9.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d"},
|
||||
{file = "Pillow-9.2.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:408673ed75594933714482501fe97e055a42996087eeca7e5d06e33218d05aa8"},
|
||||
{file = "Pillow-9.2.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:727dd1389bc5cb9827cbd1f9d40d2c2a1a0c9b32dd2261db522d22a604a6eec9"},
|
||||
{file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004"},
|
||||
{file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0"},
|
||||
{file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4"},
|
||||
{file = "Pillow-9.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:15928f824870535c85dbf949c09d6ae7d3d6ac2d6efec80f3227f73eefba741c"},
|
||||
{file = "Pillow-9.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a"},
|
||||
{file = "Pillow-9.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5b87da55a08acb586bad5c3aa3b86505f559b84f39035b233d5bf844b0834b1"},
|
||||
{file = "Pillow-9.2.0-cp311-cp311-win32.whl", hash = "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf"},
|
||||
{file = "Pillow-9.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6bf088c1ce160f50ea40764f825ec9b72ed9da25346216b91361eef8ad1b8f8c"},
|
||||
{file = "Pillow-9.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069"},
|
||||
{file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eef7592281f7c174d3d6cbfbb7ee5984a671fcd77e3fc78e973d492e9bf0eb3f"},
|
||||
{file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcd7b9c7139dc8258d164b55696ecd16c04607f1cc33ba7af86613881ffe4ac8"},
|
||||
{file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a138441e95562b3c078746a22f8fca8ff1c22c014f856278bdbdd89ca36cff1b"},
|
||||
{file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:93689632949aff41199090eff5474f3990b6823404e45d66a5d44304e9cdc467"},
|
||||
{file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:f3fac744f9b540148fa7715a435d2283b71f68bfb6d4aae24482a890aed18b59"},
|
||||
{file = "Pillow-9.2.0-cp37-cp37m-win32.whl", hash = "sha256:fa768eff5f9f958270b081bb33581b4b569faabf8774726b283edb06617101dc"},
|
||||
{file = "Pillow-9.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:69bd1a15d7ba3694631e00df8de65a8cb031911ca11f44929c97fe05eb9b6c1d"},
|
||||
{file = "Pillow-9.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:030e3460861488e249731c3e7ab59b07c7853838ff3b8e16aac9561bb345da14"},
|
||||
{file = "Pillow-9.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:74a04183e6e64930b667d321524e3c5361094bb4af9083db5c301db64cd341f3"},
|
||||
{file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402"},
|
||||
{file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fd6f5e3c0e4697fa7eb45b6e93996299f3feee73a3175fa451f49a74d092b9f"},
|
||||
{file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8"},
|
||||
{file = "Pillow-9.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:4134d3f1ba5f15027ff5c04296f13328fecd46921424084516bdb1b2548e66ff"},
|
||||
{file = "Pillow-9.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1"},
|
||||
{file = "Pillow-9.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1536ad017a9f789430fb6b8be8bf99d2f214c76502becc196c6f2d9a75b01b76"},
|
||||
{file = "Pillow-9.2.0-cp38-cp38-win32.whl", hash = "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f"},
|
||||
{file = "Pillow-9.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:ec52c351b35ca269cb1f8069d610fc45c5bd38c3e91f9ab4cbbf0aebc136d9c8"},
|
||||
{file = "Pillow-9.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ed2c4ef2451de908c90436d6e8092e13a43992f1860275b4d8082667fbb2ffc"},
|
||||
{file = "Pillow-9.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ad2f835e0ad81d1689f1b7e3fbac7b01bb8777d5a985c8962bedee0cc6d43da"},
|
||||
{file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea98f633d45f7e815db648fd7ff0f19e328302ac36427343e4432c84432e7ff4"},
|
||||
{file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7761afe0126d046974a01e030ae7529ed0ca6a196de3ec6937c11df0df1bc91c"},
|
||||
{file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a54614049a18a2d6fe156e68e188da02a046a4a93cf24f373bffd977e943421"},
|
||||
{file = "Pillow-9.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:5aed7dde98403cd91d86a1115c78d8145c83078e864c1de1064f52e6feb61b20"},
|
||||
{file = "Pillow-9.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:13b725463f32df1bfeacbf3dd197fb358ae8ebcd8c5548faa75126ea425ccb60"},
|
||||
{file = "Pillow-9.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:808add66ea764ed97d44dda1ac4f2cfec4c1867d9efb16a33d158be79f32b8a4"},
|
||||
{file = "Pillow-9.2.0-cp39-cp39-win32.whl", hash = "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885"},
|
||||
{file = "Pillow-9.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:fac2d65901fb0fdf20363fbd345c01958a742f2dc62a8dd4495af66e3ff502a4"},
|
||||
{file = "Pillow-9.2.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3"},
|
||||
{file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c7b502bc34f6e32ba022b4a209638f9e097d7a9098104ae420eb8186217ebbb"},
|
||||
{file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be"},
|
||||
{file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:dfe4c1fedfde4e2fbc009d5ad420647f7730d719786388b7de0999bf32c0d9fd"},
|
||||
{file = "Pillow-9.2.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:f07f1f00e22b231dd3d9b9208692042e29792d6bd4f6639415d2f23158a80013"},
|
||||
{file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1802f34298f5ba11d55e5bb09c31997dc0c6aed919658dfdf0198a2fe75d5490"},
|
||||
{file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17d4cafe22f050b46d983b71c707162d63d796a1235cdf8b9d7a112e97b15bac"},
|
||||
{file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96b5e6874431df16aee0c1ba237574cb6dff1dcb173798faa6a9d8b399a05d0e"},
|
||||
{file = "Pillow-9.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927"},
|
||||
{file = "Pillow-9.2.0.tar.gz", hash = "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04"},
|
||||
{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"},
|
||||
{file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68943d632f1f9e3dce98908e873b3a090f6cba1cbb1b892a9e8d97c938871fbe"},
|
||||
{file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be55f8457cd1eac957af0c3f5ece7bc3f033f89b114ef30f710882717670b2a8"},
|
||||
{file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d77adcd56a42d00cc1be30843d3426aa4e660cab4a61021dc84467123f7a00c"},
|
||||
{file = "Pillow-9.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:829f97c8e258593b9daa80638aee3789b7df9da5cf1336035016d76f03b8860c"},
|
||||
{file = "Pillow-9.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:801ec82e4188e935c7f5e22e006d01611d6b41661bba9fe45b60e7ac1a8f84de"},
|
||||
{file = "Pillow-9.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:871b72c3643e516db4ecf20efe735deb27fe30ca17800e661d769faab45a18d7"},
|
||||
{file = "Pillow-9.3.0-cp310-cp310-win32.whl", hash = "sha256:655a83b0058ba47c7c52e4e2df5ecf484c1b0b0349805896dd350cbc416bdd91"},
|
||||
{file = "Pillow-9.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:9f47eabcd2ded7698106b05c2c338672d16a6f2a485e74481f524e2a23c2794b"},
|
||||
{file = "Pillow-9.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:57751894f6618fd4308ed8e0c36c333e2f5469744c34729a27532b3db106ee20"},
|
||||
{file = "Pillow-9.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7db8b751ad307d7cf238f02101e8e36a128a6cb199326e867d1398067381bff4"},
|
||||
{file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3033fbe1feb1b59394615a1cafaee85e49d01b51d54de0cbf6aa8e64182518a1"},
|
||||
{file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22b012ea2d065fd163ca096f4e37e47cd8b59cf4b0fd47bfca6abb93df70b34c"},
|
||||
{file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a65733d103311331875c1dca05cb4606997fd33d6acfed695b1232ba1df193"},
|
||||
{file = "Pillow-9.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:502526a2cbfa431d9fc2a079bdd9061a2397b842bb6bc4239bb176da00993812"},
|
||||
{file = "Pillow-9.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:90fb88843d3902fe7c9586d439d1e8c05258f41da473952aa8b328d8b907498c"},
|
||||
{file = "Pillow-9.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:89dca0ce00a2b49024df6325925555d406b14aa3efc2f752dbb5940c52c56b11"},
|
||||
{file = "Pillow-9.3.0-cp311-cp311-win32.whl", hash = "sha256:3168434d303babf495d4ba58fc22d6604f6e2afb97adc6a423e917dab828939c"},
|
||||
{file = "Pillow-9.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:18498994b29e1cf86d505edcb7edbe814d133d2232d256db8c7a8ceb34d18cef"},
|
||||
{file = "Pillow-9.3.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:772a91fc0e03eaf922c63badeca75e91baa80fe2f5f87bdaed4280662aad25c9"},
|
||||
{file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa4107d1b306cdf8953edde0534562607fe8811b6c4d9a486298ad31de733b2"},
|
||||
{file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4012d06c846dc2b80651b120e2cdd787b013deb39c09f407727ba90015c684f"},
|
||||
{file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77ec3e7be99629898c9a6d24a09de089fa5356ee408cdffffe62d67bb75fdd72"},
|
||||
{file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:6c738585d7a9961d8c2821a1eb3dcb978d14e238be3d70f0a706f7fa9316946b"},
|
||||
{file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:828989c45c245518065a110434246c44a56a8b2b2f6347d1409c787e6e4651ee"},
|
||||
{file = "Pillow-9.3.0-cp37-cp37m-win32.whl", hash = "sha256:82409ffe29d70fd733ff3c1025a602abb3e67405d41b9403b00b01debc4c9a29"},
|
||||
{file = "Pillow-9.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:41e0051336807468be450d52b8edd12ac60bebaa97fe10c8b660f116e50b30e4"},
|
||||
{file = "Pillow-9.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:b03ae6f1a1878233ac620c98f3459f79fd77c7e3c2b20d460284e1fb370557d4"},
|
||||
{file = "Pillow-9.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4390e9ce199fc1951fcfa65795f239a8a4944117b5935a9317fb320e7767b40f"},
|
||||
{file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40e1ce476a7804b0fb74bcfa80b0a2206ea6a882938eaba917f7a0f004b42502"},
|
||||
{file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0a06a052c5f37b4ed81c613a455a81f9a3a69429b4fd7bb913c3fa98abefc20"},
|
||||
{file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03150abd92771742d4a8cd6f2fa6246d847dcd2e332a18d0c15cc75bf6703040"},
|
||||
{file = "Pillow-9.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:15c42fb9dea42465dfd902fb0ecf584b8848ceb28b41ee2b58f866411be33f07"},
|
||||
{file = "Pillow-9.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:51e0e543a33ed92db9f5ef69a0356e0b1a7a6b6a71b80df99f1d181ae5875636"},
|
||||
{file = "Pillow-9.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3dd6caf940756101205dffc5367babf288a30043d35f80936f9bfb37f8355b32"},
|
||||
{file = "Pillow-9.3.0-cp38-cp38-win32.whl", hash = "sha256:f1ff2ee69f10f13a9596480335f406dd1f70c3650349e2be67ca3139280cade0"},
|
||||
{file = "Pillow-9.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:276a5ca930c913f714e372b2591a22c4bd3b81a418c0f6635ba832daec1cbcfc"},
|
||||
{file = "Pillow-9.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:73bd195e43f3fadecfc50c682f5055ec32ee2c933243cafbfdec69ab1aa87cad"},
|
||||
{file = "Pillow-9.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c7c8ae3864846fc95f4611c78129301e203aaa2af813b703c55d10cc1628535"},
|
||||
{file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0918e03aa0c72ea56edbb00d4d664294815aa11291a11504a377ea018330d3"},
|
||||
{file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0915e734b33a474d76c28e07292f196cdf2a590a0d25bcc06e64e545f2d146c"},
|
||||
{file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0372acb5d3598f36ec0914deed2a63f6bcdb7b606da04dc19a88d31bf0c05b"},
|
||||
{file = "Pillow-9.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ad58d27a5b0262c0c19b47d54c5802db9b34d38bbf886665b626aff83c74bacd"},
|
||||
{file = "Pillow-9.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:97aabc5c50312afa5e0a2b07c17d4ac5e865b250986f8afe2b02d772567a380c"},
|
||||
{file = "Pillow-9.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9aaa107275d8527e9d6e7670b64aabaaa36e5b6bd71a1015ddd21da0d4e06448"},
|
||||
{file = "Pillow-9.3.0-cp39-cp39-win32.whl", hash = "sha256:bac18ab8d2d1e6b4ce25e3424f709aceef668347db8637c2296bcf41acb7cf48"},
|
||||
{file = "Pillow-9.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:b472b5ea442148d1c3e2209f20f1e0bb0eb556538690fa70b5e1f79fa0ba8dc2"},
|
||||
{file = "Pillow-9.3.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ab388aaa3f6ce52ac1cb8e122c4bd46657c15905904b3120a6248b5b8b0bc228"},
|
||||
{file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbb8e7f2abee51cef77673be97760abff1674ed32847ce04b4af90f610144c7b"},
|
||||
{file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca31dd6014cb8b0b2db1e46081b0ca7d936f856da3b39744aef499db5d84d02"},
|
||||
{file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c7025dce65566eb6e89f56c9509d4f628fddcedb131d9465cacd3d8bac337e7e"},
|
||||
{file = "Pillow-9.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ebf2029c1f464c59b8bdbe5143c79fa2045a581ac53679733d3a91d400ff9efb"},
|
||||
{file = "Pillow-9.3.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b59430236b8e58840a0dfb4099a0e8717ffb779c952426a69ae435ca1f57210c"},
|
||||
{file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12ce4932caf2ddf3e41d17fc9c02d67126935a44b86df6a206cf0d7161548627"},
|
||||
{file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae5331c23ce118c53b172fa64a4c037eb83c9165aba3a7ba9ddd3ec9fa64a699"},
|
||||
{file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0b07fffc13f474264c336298d1b4ce01d9c5a011415b79d4ee5527bb69ae6f65"},
|
||||
{file = "Pillow-9.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:073adb2ae23431d3b9bcbcff3fe698b62ed47211d0716b067385538a1b0f28b8"},
|
||||
{file = "Pillow-9.3.0.tar.gz", hash = "sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f"},
|
||||
]
|
||||
platformdirs = [
|
||||
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
|
||||
@ -1936,10 +1944,6 @@ 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"},
|
||||
]
|
||||
py = [
|
||||
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
||||
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||
]
|
||||
pyaml = [
|
||||
{file = "pyaml-21.10.1-py2.py3-none-any.whl", hash = "sha256:19985ed303c3a985de4cf8fd329b6d0a5a5b5c9035ea240eccc709ebacbaf4a0"},
|
||||
{file = "pyaml-21.10.1.tar.gz", hash = "sha256:c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383"},
|
||||
@ -2038,8 +2042,8 @@ pyparsing = [
|
||||
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
|
||||
]
|
||||
pytest = [
|
||||
{file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"},
|
||||
{file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"},
|
||||
{file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
|
||||
{file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
|
||||
]
|
||||
pytest-asyncio = [
|
||||
{file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"},
|
||||
@ -2191,8 +2195,8 @@ types-markdown = [
|
||||
{file = "types_Markdown-3.4.2.1-py3-none-any.whl", hash = "sha256:b2333f6f4b8f69af83de359e10a097e4a3f14bbd6d2484e1829d9b0ec56fa0cb"},
|
||||
]
|
||||
types-pillow = [
|
||||
{file = "types-Pillow-9.2.2.2.tar.gz", hash = "sha256:b88bd03d6b1d467d2dd1d54808b04631ca88941fe16f9eeb92bc114e5a145ec0"},
|
||||
{file = "types_Pillow-9.2.2.2-py3-none-any.whl", hash = "sha256:49a633ad811446efeb2abbfea4596cff470b1a48adba6c944fae57b3a667e5cb"},
|
||||
{file = "types-Pillow-9.3.0.0.tar.gz", hash = "sha256:0851a1b3ff002253a7af8f7eaf74d79fb761430933bd1aeb73d853a17f2a0a9d"},
|
||||
{file = "types_Pillow-9.3.0.0-py3-none-any.whl", hash = "sha256:df09de7e557706c16fb30db887327c7f1c81e8ebc703d9d4739bfda7cad0e733"},
|
||||
]
|
||||
types-python-dateutil = [
|
||||
{file = "types-python-dateutil-2.8.19.2.tar.gz", hash = "sha256:e6e32ce18f37765b08c46622287bc8d8136dc0c562d9ad5b8fd158c59963d7a7"},
|
||||
@ -2307,54 +2311,75 @@ webencodings = [
|
||||
{file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
|
||||
]
|
||||
websockets = [
|
||||
{file = "websockets-10.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978"},
|
||||
{file = "websockets-10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500"},
|
||||
{file = "websockets-10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b"},
|
||||
{file = "websockets-10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c"},
|
||||
{file = "websockets-10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8"},
|
||||
{file = "websockets-10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677"},
|
||||
{file = "websockets-10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e"},
|
||||
{file = "websockets-10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f"},
|
||||
{file = "websockets-10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47"},
|
||||
{file = "websockets-10.3-cp310-cp310-win32.whl", hash = "sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae"},
|
||||
{file = "websockets-10.3-cp310-cp310-win_amd64.whl", hash = "sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079"},
|
||||
{file = "websockets-10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916"},
|
||||
{file = "websockets-10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb"},
|
||||
{file = "websockets-10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79"},
|
||||
{file = "websockets-10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d"},
|
||||
{file = "websockets-10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98"},
|
||||
{file = "websockets-10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e"},
|
||||
{file = "websockets-10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6"},
|
||||
{file = "websockets-10.3-cp37-cp37m-win32.whl", hash = "sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1"},
|
||||
{file = "websockets-10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4"},
|
||||
{file = "websockets-10.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36"},
|
||||
{file = "websockets-10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69"},
|
||||
{file = "websockets-10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd"},
|
||||
{file = "websockets-10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2"},
|
||||
{file = "websockets-10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c"},
|
||||
{file = "websockets-10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e"},
|
||||
{file = "websockets-10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991"},
|
||||
{file = "websockets-10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442"},
|
||||
{file = "websockets-10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76"},
|
||||
{file = "websockets-10.3-cp38-cp38-win32.whl", hash = "sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559"},
|
||||
{file = "websockets-10.3-cp38-cp38-win_amd64.whl", hash = "sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d"},
|
||||
{file = "websockets-10.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094"},
|
||||
{file = "websockets-10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667"},
|
||||
{file = "websockets-10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731"},
|
||||
{file = "websockets-10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9"},
|
||||
{file = "websockets-10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680"},
|
||||
{file = "websockets-10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247"},
|
||||
{file = "websockets-10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af"},
|
||||
{file = "websockets-10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3"},
|
||||
{file = "websockets-10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8"},
|
||||
{file = "websockets-10.3-cp39-cp39-win32.whl", hash = "sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582"},
|
||||
{file = "websockets-10.3-cp39-cp39-win_amd64.whl", hash = "sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02"},
|
||||
{file = "websockets-10.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7"},
|
||||
{file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f"},
|
||||
{file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4"},
|
||||
{file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755"},
|
||||
{file = "websockets-10.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55"},
|
||||
{file = "websockets-10.3.tar.gz", hash = "sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4"},
|
||||
{file = "websockets-10.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d58804e996d7d2307173d56c297cf7bc132c52df27a3efaac5e8d43e36c21c48"},
|
||||
{file = "websockets-10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc0b82d728fe21a0d03e65f81980abbbcb13b5387f733a1a870672c5be26edab"},
|
||||
{file = "websockets-10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba089c499e1f4155d2a3c2a05d2878a3428cf321c848f2b5a45ce55f0d7d310c"},
|
||||
{file = "websockets-10.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33d69ca7612f0ddff3316b0c7b33ca180d464ecac2d115805c044bf0a3b0d032"},
|
||||
{file = "websockets-10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62e627f6b6d4aed919a2052efc408da7a545c606268d5ab5bfab4432734b82b4"},
|
||||
{file = "websockets-10.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ea7b82bfcae927eeffc55d2ffa31665dc7fec7b8dc654506b8e5a518eb4d50"},
|
||||
{file = "websockets-10.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e0cb5cc6ece6ffa75baccfd5c02cffe776f3f5c8bf486811f9d3ea3453676ce8"},
|
||||
{file = "websockets-10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae5e95cfb53ab1da62185e23b3130e11d64431179debac6dc3c6acf08760e9b1"},
|
||||
{file = "websockets-10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7c584f366f46ba667cfa66020344886cf47088e79c9b9d39c84ce9ea98aaa331"},
|
||||
{file = "websockets-10.4-cp310-cp310-win32.whl", hash = "sha256:b029fb2032ae4724d8ae8d4f6b363f2cc39e4c7b12454df8df7f0f563ed3e61a"},
|
||||
{file = "websockets-10.4-cp310-cp310-win_amd64.whl", hash = "sha256:8dc96f64ae43dde92530775e9cb169979f414dcf5cff670455d81a6823b42089"},
|
||||
{file = "websockets-10.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47a2964021f2110116cc1125b3e6d87ab5ad16dea161949e7244ec583b905bb4"},
|
||||
{file = "websockets-10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e789376b52c295c4946403bd0efecf27ab98f05319df4583d3c48e43c7342c2f"},
|
||||
{file = "websockets-10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3f0b61c45c3fa9a349cf484962c559a8a1d80dae6977276df8fd1fa5e3cb8c"},
|
||||
{file = "websockets-10.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f55b5905705725af31ccef50e55391621532cd64fbf0bc6f4bac935f0fccec46"},
|
||||
{file = "websockets-10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00c870522cdb69cd625b93f002961ffb0c095394f06ba8c48f17eef7c1541f96"},
|
||||
{file = "websockets-10.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f38706e0b15d3c20ef6259fd4bc1700cd133b06c3c1bb108ffe3f8947be15fa"},
|
||||
{file = "websockets-10.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f2c38d588887a609191d30e902df2a32711f708abfd85d318ca9b367258cfd0c"},
|
||||
{file = "websockets-10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fe10ddc59b304cb19a1bdf5bd0a7719cbbc9fbdd57ac80ed436b709fcf889106"},
|
||||
{file = "websockets-10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:90fcf8929836d4a0e964d799a58823547df5a5e9afa83081761630553be731f9"},
|
||||
{file = "websockets-10.4-cp311-cp311-win32.whl", hash = "sha256:b9968694c5f467bf67ef97ae7ad4d56d14be2751000c1207d31bf3bb8860bae8"},
|
||||
{file = "websockets-10.4-cp311-cp311-win_amd64.whl", hash = "sha256:a7a240d7a74bf8d5cb3bfe6be7f21697a28ec4b1a437607bae08ac7acf5b4882"},
|
||||
{file = "websockets-10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:74de2b894b47f1d21cbd0b37a5e2b2392ad95d17ae983e64727e18eb281fe7cb"},
|
||||
{file = "websockets-10.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3a686ecb4aa0d64ae60c9c9f1a7d5d46cab9bfb5d91a2d303d00e2cd4c4c5cc"},
|
||||
{file = "websockets-10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d15c968ea7a65211e084f523151dbf8ae44634de03c801b8bd070b74e85033"},
|
||||
{file = "websockets-10.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41"},
|
||||
{file = "websockets-10.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e23173580d740bf8822fd0379e4bf30aa1d5a92a4f252d34e893070c081050df"},
|
||||
{file = "websockets-10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:dd500e0a5e11969cdd3320935ca2ff1e936f2358f9c2e61f100a1660933320ea"},
|
||||
{file = "websockets-10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4239b6027e3d66a89446908ff3027d2737afc1a375f8fd3eea630a4842ec9a0c"},
|
||||
{file = "websockets-10.4-cp37-cp37m-win32.whl", hash = "sha256:8a5cc00546e0a701da4639aa0bbcb0ae2bb678c87f46da01ac2d789e1f2d2038"},
|
||||
{file = "websockets-10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a9f9a735deaf9a0cadc2d8c50d1a5bcdbae8b6e539c6e08237bc4082d7c13f28"},
|
||||
{file = "websockets-10.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c1289596042fad2cdceb05e1ebf7aadf9995c928e0da2b7a4e99494953b1b94"},
|
||||
{file = "websockets-10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0cff816f51fb33c26d6e2b16b5c7d48eaa31dae5488ace6aae468b361f422b63"},
|
||||
{file = "websockets-10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dd9becd5fe29773d140d68d607d66a38f60e31b86df75332703757ee645b6faf"},
|
||||
{file = "websockets-10.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45ec8e75b7dbc9539cbfafa570742fe4f676eb8b0d3694b67dabe2f2ceed8aa6"},
|
||||
{file = "websockets-10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f72e5cd0f18f262f5da20efa9e241699e0cf3a766317a17392550c9ad7b37d8"},
|
||||
{file = "websockets-10.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185929b4808b36a79c65b7865783b87b6841e852ef5407a2fb0c03381092fa3b"},
|
||||
{file = "websockets-10.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d27a7e34c313b3a7f91adcd05134315002aaf8540d7b4f90336beafaea6217c"},
|
||||
{file = "websockets-10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:884be66c76a444c59f801ac13f40c76f176f1bfa815ef5b8ed44321e74f1600b"},
|
||||
{file = "websockets-10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:931c039af54fc195fe6ad536fde4b0de04da9d5916e78e55405436348cfb0e56"},
|
||||
{file = "websockets-10.4-cp38-cp38-win32.whl", hash = "sha256:db3c336f9eda2532ec0fd8ea49fef7a8df8f6c804cdf4f39e5c5c0d4a4ad9a7a"},
|
||||
{file = "websockets-10.4-cp38-cp38-win_amd64.whl", hash = "sha256:48c08473563323f9c9debac781ecf66f94ad5a3680a38fe84dee5388cf5acaf6"},
|
||||
{file = "websockets-10.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:40e826de3085721dabc7cf9bfd41682dadc02286d8cf149b3ad05bff89311e4f"},
|
||||
{file = "websockets-10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:56029457f219ade1f2fc12a6504ea61e14ee227a815531f9738e41203a429112"},
|
||||
{file = "websockets-10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5fc088b7a32f244c519a048c170f14cf2251b849ef0e20cbbb0fdf0fdaf556f"},
|
||||
{file = "websockets-10.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc8709c00704194213d45e455adc106ff9e87658297f72d544220e32029cd3d"},
|
||||
{file = "websockets-10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0154f7691e4fe6c2b2bc275b5701e8b158dae92a1ab229e2b940efe11905dff4"},
|
||||
{file = "websockets-10.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c6d2264f485f0b53adf22697ac11e261ce84805c232ed5dbe6b1bcb84b00ff0"},
|
||||
{file = "websockets-10.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9bc42e8402dc5e9905fb8b9649f57efcb2056693b7e88faa8fb029256ba9c68c"},
|
||||
{file = "websockets-10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:edc344de4dac1d89300a053ac973299e82d3db56330f3494905643bb68801269"},
|
||||
{file = "websockets-10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:84bc2a7d075f32f6ed98652db3a680a17a4edb21ca7f80fe42e38753a58ee02b"},
|
||||
{file = "websockets-10.4-cp39-cp39-win32.whl", hash = "sha256:c94ae4faf2d09f7c81847c63843f84fe47bf6253c9d60b20f25edfd30fb12588"},
|
||||
{file = "websockets-10.4-cp39-cp39-win_amd64.whl", hash = "sha256:bbccd847aa0c3a69b5f691a84d2341a4f8a629c6922558f2a70611305f902d74"},
|
||||
{file = "websockets-10.4-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:82ff5e1cae4e855147fd57a2863376ed7454134c2bf49ec604dfe71e446e2193"},
|
||||
{file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d210abe51b5da0ffdbf7b43eed0cfdff8a55a1ab17abbec4301c9ff077dd0342"},
|
||||
{file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:942de28af58f352a6f588bc72490ae0f4ccd6dfc2bd3de5945b882a078e4e179"},
|
||||
{file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9b27d6c1c6cd53dc93614967e9ce00ae7f864a2d9f99fe5ed86706e1ecbf485"},
|
||||
{file = "websockets-10.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3d3cac3e32b2c8414f4f87c1b2ab686fa6284a980ba283617404377cd448f631"},
|
||||
{file = "websockets-10.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:da39dd03d130162deb63da51f6e66ed73032ae62e74aaccc4236e30edccddbb0"},
|
||||
{file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389f8dbb5c489e305fb113ca1b6bdcdaa130923f77485db5b189de343a179393"},
|
||||
{file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09a1814bb15eff7069e51fed0826df0bc0702652b5cb8f87697d469d79c23576"},
|
||||
{file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff64a1d38d156d429404aaa84b27305e957fd10c30e5880d1765c9480bea490f"},
|
||||
{file = "websockets-10.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b343f521b047493dc4022dd338fc6db9d9282658862756b4f6fd0e996c1380e1"},
|
||||
{file = "websockets-10.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:932af322458da7e4e35df32f050389e13d3d96b09d274b22a7aa1808f292fee4"},
|
||||
{file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a4162139374a49eb18ef5b2f4da1dd95c994588f5033d64e0bbfda4b6b6fcf"},
|
||||
{file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c57e4c1349fbe0e446c9fa7b19ed2f8a4417233b6984277cce392819123142d3"},
|
||||
{file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b627c266f295de9dea86bd1112ed3d5fafb69a348af30a2422e16590a8ecba13"},
|
||||
{file = "websockets-10.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:05a7233089f8bd355e8cbe127c2e8ca0b4ea55467861906b80d2ebc7db4d6b72"},
|
||||
{file = "websockets-10.4.tar.gz", hash = "sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3"},
|
||||
]
|
||||
win32-setctime = [
|
||||
{file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"},
|
||||
|
Reference in New Issue
Block a user