kopia lustrzana https://github.com/jointakahe/takahe
Implement markers API
rodzic
7c34ac78ed
commit
9759a97463
|
@ -5,6 +5,7 @@
|
|||
*.sqlite3
|
||||
.DS_Store
|
||||
.idea/*
|
||||
.nova
|
||||
.venv
|
||||
.vscode
|
||||
/*.env*
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -233,6 +233,7 @@ MIDDLEWARE = [
|
|||
"django_htmx.middleware.HtmxMiddleware",
|
||||
"core.middleware.HeadersMiddleware",
|
||||
"core.middleware.ConfigLoadingMiddleware",
|
||||
"core.middleware.ParamsMiddleware",
|
||||
"api.middleware.ApiTokenMiddleware",
|
||||
"users.middleware.DomainMiddleware",
|
||||
]
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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")},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
}
|
Ładowanie…
Reference in New Issue