Implement markers API

pull/707/head
Dan Watson 2024-04-16 23:43:19 -04:00
rodzic 7c34ac78ed
commit 9759a97463
13 zmienionych plików z 175 dodań i 19 usunięć

1
.gitignore vendored
Wyświetl plik

@ -5,6 +5,7 @@
*.sqlite3
.DS_Store
.idea/*
.nova
.venv
.vscode
/*.env*

Wyświetl plik

@ -503,3 +503,16 @@ class PushSubscription(Schema):
return value
else:
return None
class Marker(Schema):
last_read_id: str
version: int
updated_at: str
@classmethod
def from_marker(
cls,
marker: users_models.Marker,
) -> "Marker":
return cls(**marker.to_mastodon_json())

Wyświetl plik

@ -11,6 +11,7 @@ from api.views import (
follow_requests,
instance,
lists,
markers,
media,
notifications,
polls,
@ -67,6 +68,14 @@ urlpatterns = [
path("v2/instance", instance.instance_info_v2),
# Lists
path("v1/lists", lists.get_lists),
# Markers
path(
"v1/markers",
methods(
get=markers.markers,
post=markers.set_markers,
),
),
# Media
path("v1/media", media.upload_media),
path("v2/media", media.upload_media),

Wyświetl plik

@ -0,0 +1,36 @@
from django.http import HttpRequest
from hatchway import api_view
from api import schemas
from api.decorators import scope_required
@scope_required("read:statuses")
@api_view.get
def markers(request: HttpRequest) -> dict[str, schemas.Marker]:
timelines = set(request.PARAMS.getlist("timeline[]"))
data = {}
for m in request.identity.markers.filter(timeline__in=timelines):
data[m.timeline] = schemas.Marker.from_marker(m)
return data
@scope_required("write:statuses")
@api_view.post
def set_markers(request: HttpRequest) -> dict[str, schemas.Marker]:
markers = {}
for key, last_id in request.PARAMS.items():
if not key.endswith("[last_read_id]"):
continue
timeline = key.replace("[last_read_id]", "")
marker, created = request.identity.markers.get_or_create(
timeline=timeline,
defaults={
"last_read_id": last_id,
},
)
if not created:
marker.last_read_id = last_id
marker.save()
markers[timeline] = schemas.Marker.from_marker(marker)
return markers

Wyświetl plik

@ -1,5 +1,4 @@
import base64
import json
import secrets
import time
from urllib.parse import urlparse, urlunparse
@ -41,19 +40,6 @@ class OauthRedirect(HttpResponseRedirect):
super().__init__(urlunparse(url_parts))
def get_json_and_formdata(request):
# Did they submit JSON?
if request.content_type == "application/json" and request.body.strip():
return json.loads(request.body)
# Fall back to form data
value = {}
for key, item in request.POST.items():
value[key] = item
for key, item in request.GET.items():
value[key] = item
return value
class AuthorizationView(LoginRequiredMixin, View):
"""
Asks the user to authorize access.
@ -106,7 +92,7 @@ class AuthorizationView(LoginRequiredMixin, View):
return render(request, "api/oauth_authorize.html", context)
def post(self, request):
post_data = get_json_and_formdata(request)
post_data = request.PARAMS
# Grab the application and other details again
redirect_uri = post_data["redirect_uri"]
scope = post_data["scope"]
@ -160,7 +146,7 @@ class TokenView(View):
)
def post(self, request):
post_data = get_json_and_formdata(request)
post_data = request.PARAMS.copy()
auth_client_id, auth_client_secret = extract_client_info_from_basic_auth(
request
)
@ -243,7 +229,7 @@ class TokenView(View):
@method_decorator(csrf_exempt, name="dispatch")
class RevokeTokenView(View):
def post(self, request):
post_data = get_json_and_formdata(request)
post_data = request.PARAMS.copy()
auth_client_id, auth_client_secret = extract_client_info_from_basic_auth(
request
)

Wyświetl plik

@ -1,3 +1,4 @@
import json
from time import time
from django.conf import settings
@ -73,3 +74,25 @@ def show_toolbar(request):
Determines whether to show the debug toolbar on a given page.
"""
return settings.DEBUG and request.user.is_authenticated and request.user.admin
class ParamsMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def make_params(self, request):
# See https://docs.joinmastodon.org/client/intro/#parameters
# If they sent JSON, use that.
if request.content_type == "application/json" and request.body.strip():
return json.loads(request.body)
# Otherwise, fall back to form data.
params = {}
for key, value in request.GET.items():
params[key] = value
for key, value in request.POST.items():
params[key] = value
return params
def __call__(self, request):
request.PARAMS = self.make_params(request)
return self.get_response(request)

Wyświetl plik

@ -9,6 +9,7 @@ Currently, it supports:
* A web UI (which can be installed as a PWA as well)
* Mastodon-compatible client applications (beta support)
* Posts with content warnings and visibilities including a local-only option
* Creating polls on posts
* Editing post content
* Viewing images, videos and other post attachments
* Uploading images and attaching image captions
@ -28,6 +29,8 @@ Currently, it supports:
* Server defederation (blocking)
* Signup flow, including auto-cap by user numbers and invite system
* Password reset via email
* Bookmarks
# Markers
Features planned for releases up to 1.0:
@ -41,9 +44,7 @@ Features planned for releases up to 1.0:
Features that may make it into 1.0, or might be further out:
* Creating polls on posts
* Filters
* Bookmarks
* Lists
* Scheduling posts
* Mastodon-compatible account migration target/source

Wyświetl plik

@ -13,3 +13,10 @@ variables. You can generate a keypair via `https://web-push-codelab.glitch.me/`_
Note that users of apps may need to sign out and in again to their accounts for
the app to notice that it can now do push notifications. Some apps, like Elk,
may cache the fact your server didn't support it for a while.
Marker Support
~~~~~~~~~~~~~~
Takahē now supports the `Markers API <https://docs.joinmastodon.org/methods/markers/>`_,
used by clients to sync read positions within timelines.

Wyświetl plik

@ -233,6 +233,7 @@ MIDDLEWARE = [
"django_htmx.middleware.HtmxMiddleware",
"core.middleware.HeadersMiddleware",
"core.middleware.ConfigLoadingMiddleware",
"core.middleware.ParamsMiddleware",
"api.middleware.ApiTokenMiddleware",
"users.middleware.DomainMiddleware",
]

Wyświetl plik

@ -13,6 +13,7 @@ from users.models import (
Identity,
InboxMessage,
Invite,
Marker,
PasswordReset,
Report,
User,
@ -212,6 +213,11 @@ class InviteAdmin(admin.ModelAdmin):
list_display = ["id", "created", "token", "note"]
@admin.register(Marker)
class MarkerAdmin(admin.ModelAdmin):
list_display = ["id", "identity", "timeline", "last_read_id", "updated_at"]
@admin.register(Report)
class ReportAdmin(admin.ModelAdmin):
list_display = ["id", "created", "resolved", "type", "subject_identity"]

Wyświetl plik

@ -0,0 +1,41 @@
# Generated by Django 4.2.11 on 2024-04-17 03:09
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("users", "0022_follow_request"),
]
operations = [
migrations.CreateModel(
name="Marker",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("timeline", models.CharField(max_length=100)),
("last_read_id", models.CharField(max_length=200)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"identity",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="markers",
to="users.identity",
),
),
],
options={
"unique_together": {("identity", "timeline")},
},
),
]

Wyświetl plik

@ -7,6 +7,7 @@ from .hashtag_follow import HashtagFollow # noqa
from .identity import Identity, IdentityStates # noqa
from .inbox_message import InboxMessage, InboxMessageStates # noqa
from .invite import Invite # noqa
from .marker import Marker # noqa
from .password_reset import PasswordReset # noqa
from .report import Report # noqa
from .system_actor import SystemActor # noqa

Wyświetl plik

@ -0,0 +1,31 @@
from django.db import models
from core.ld import format_ld_date
class Marker(models.Model):
"""
A timeline marker.
"""
identity = models.ForeignKey(
"users.Identity",
on_delete=models.CASCADE,
related_name="markers",
)
timeline = models.CharField(max_length=100)
last_read_id = models.CharField(max_length=200)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = [("identity", "timeline")]
def __str__(self):
return f"#{self.id}: {self.identity}{self.timeline}[{self.last_read_id}]"
def to_mastodon_json(self):
return {
"last_read_id": self.last_read_id,
"version": 0,
"updated_at": format_ld_date(self.updated_at),
}