support note
This commit is contained in:
parent
e3fe4b1b4d
commit
b543ad01ee
31 changed files with 752 additions and 92 deletions
|
@ -499,7 +499,7 @@ class Item(PolymorphicModel):
|
|||
return self.title
|
||||
|
||||
@classmethod
|
||||
def get_by_url(cls, url_or_b62: str) -> "Self | None":
|
||||
def get_by_url(cls, url_or_b62: str, resolve_merge=False) -> "Self | None":
|
||||
b62 = url_or_b62.strip().split("/")[-1]
|
||||
if len(b62) not in [21, 22]:
|
||||
r = re.search(r"[A-Za-z0-9]{21,22}", url_or_b62)
|
||||
|
@ -507,6 +507,14 @@ class Item(PolymorphicModel):
|
|||
b62 = r[0]
|
||||
try:
|
||||
item = cls.objects.get(uid=uuid.UUID(int=b62_decode(b62)))
|
||||
if resolve_merge:
|
||||
resolve_cnt = 5
|
||||
while item.merged_to_item and resolve_cnt > 0:
|
||||
item = item.merged_to_item
|
||||
resolve_cnt -= 1
|
||||
if resolve_cnt == 0:
|
||||
logger.error(f"resolve merge loop for {item}")
|
||||
item = None
|
||||
except Exception:
|
||||
item = None
|
||||
return item
|
||||
|
|
|
@ -60,6 +60,40 @@
|
|||
</p>
|
||||
{% endfor %}
|
||||
</section>
|
||||
<section>
|
||||
<h5>
|
||||
{% trans "my notes" %}
|
||||
{% if mark.shelf %}
|
||||
<small>
|
||||
<span class="action inline">
|
||||
<a href="#"
|
||||
hx-get="{% url 'journal:note' item.uuid %}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
<i class="fa-regular fa-square-plus"></i>
|
||||
</a>
|
||||
</span>
|
||||
</small>
|
||||
{% endif %}
|
||||
</h5>
|
||||
{% for note in mark.notes %}
|
||||
<span class="action">
|
||||
<span>
|
||||
<a href="#"
|
||||
hx-get="{% url 'journal:note' note.item.uuid note.uuid %}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"><i class="fa-regular fa-pen-to-square"></i></a>
|
||||
</span>
|
||||
{% if note.latest_post %}
|
||||
{% include "action_like_post.html" with post=note.latest_post %}
|
||||
{% include "action_boost_post.html" with post=note.latest_post %}
|
||||
{% include "action_open_post.html" with post=note.latest_post %}
|
||||
{% endif %}
|
||||
</span>
|
||||
<h6>{{ note.title|default:'' }}</h6>
|
||||
<p>{{ note.content|linebreaks }}</p>
|
||||
{% endfor %}
|
||||
</section>
|
||||
<section>
|
||||
<h5>
|
||||
{% trans "my review" %}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta property="og:title"
|
||||
content="{{ site_name }}{% trans item.category.label %} - {{ item.display_title }}">
|
||||
content="{% trans item.category.label %} - {{ item.display_title }}">
|
||||
<meta property="og:type" content="{{ item.category }}">
|
||||
<meta property="og:url" content="{{ item.absolute_url }}">
|
||||
{% if item.has_cover %}<meta property="og:image" content="{{ item.cover_image_url }}">{% endif %}
|
||||
|
|
|
@ -8,4 +8,34 @@ from catalog.performance.tests import *
|
|||
from catalog.podcast.tests import *
|
||||
from catalog.tv.tests import *
|
||||
|
||||
|
||||
# imported tests with same name might be ignored silently
|
||||
class CatalogCase(TestCase):
|
||||
databases = "__all__"
|
||||
|
||||
def setUp(self):
|
||||
self.hyperion_hardcover = Edition.objects.create(title="Hyperion")
|
||||
self.hyperion_hardcover.pages = 481
|
||||
self.hyperion_hardcover.isbn = "9780385249492"
|
||||
self.hyperion_hardcover.save()
|
||||
self.hyperion_print = Edition.objects.create(title="Hyperion")
|
||||
self.hyperion_print.pages = 500
|
||||
self.hyperion_print.isbn = "9780553283686"
|
||||
self.hyperion_print.save()
|
||||
self.hyperion_ebook = Edition(title="Hyperion")
|
||||
self.hyperion_ebook.asin = "B0043M6780"
|
||||
self.hyperion_ebook.save()
|
||||
self.andymion_print = Edition.objects.create(title="Andymion", pages=42)
|
||||
# serie = Serie(title="Hyperion Cantos")
|
||||
self.hyperion = Work(title="Hyperion")
|
||||
self.hyperion.save()
|
||||
|
||||
def test_merge(self):
|
||||
self.hyperion_hardcover.merge_to(self.hyperion_print)
|
||||
self.assertEqual(self.hyperion_hardcover.merged_to_item, self.hyperion_print)
|
||||
|
||||
def test_merge_resolve(self):
|
||||
self.hyperion_hardcover.merge_to(self.hyperion_print)
|
||||
self.hyperion_print.merge_to(self.hyperion_ebook)
|
||||
resloved = Item.get_by_url(self.hyperion_hardcover.url, True)
|
||||
self.assertEqual(resloved, self.hyperion_ebook)
|
||||
|
|
64
journal/migrations/0002_note.py
Normal file
64
journal/migrations/0002_note.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
# Generated by Django 4.2.13 on 2024-06-13 13:10
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("users", "0001_initial_0_10"),
|
||||
("catalog", "0001_initial_0_10"),
|
||||
("journal", "0001_initial_0_10"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Note",
|
||||
fields=[
|
||||
(
|
||||
"piece_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="journal.piece",
|
||||
),
|
||||
),
|
||||
("visibility", models.PositiveSmallIntegerField(default=0)),
|
||||
(
|
||||
"created_time",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
("edited_time", models.DateTimeField(auto_now=True)),
|
||||
("metadata", models.JSONField(default=dict)),
|
||||
(
|
||||
"remote_id",
|
||||
models.CharField(default=None, max_length=200, null=True),
|
||||
),
|
||||
("title", models.TextField(blank=True, default=None, null=True)),
|
||||
("content", models.TextField()),
|
||||
("sensitive", models.BooleanField(default=False)),
|
||||
("attachements", models.JSONField(default=list)),
|
||||
(
|
||||
"item",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, to="catalog.item"
|
||||
),
|
||||
),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="users.apidentity",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
bases=("journal.piece",),
|
||||
),
|
||||
]
|
|
@ -1,6 +1,7 @@
|
|||
from .collection import Collection, CollectionMember, FeaturedCollection
|
||||
from .comment import Comment
|
||||
from .common import (
|
||||
Content,
|
||||
Piece,
|
||||
PieceInteraction,
|
||||
PiecePost,
|
||||
|
@ -14,6 +15,7 @@ from .common import (
|
|||
from .like import Like
|
||||
from .mark import Mark
|
||||
from .mixins import UserOwnedObjectMixin
|
||||
from .note import Note
|
||||
from .rating import Rating
|
||||
from .renderers import render_md
|
||||
from .review import Review
|
||||
|
@ -43,6 +45,7 @@ __all__ = [
|
|||
"q_piece_visible_to_user",
|
||||
"Like",
|
||||
"Mark",
|
||||
"Note",
|
||||
"Rating",
|
||||
"render_md",
|
||||
"Review",
|
||||
|
|
|
@ -9,6 +9,7 @@ from catalog.collection.models import Collection as CatalogCollection
|
|||
from catalog.common import jsondata
|
||||
from catalog.common.utils import DEFAULT_ITEM_COVER, piece_cover_path
|
||||
from catalog.models import Item
|
||||
from takahe.utils import Takahe
|
||||
from users.models import APIdentity
|
||||
|
||||
from .common import Piece
|
||||
|
@ -113,11 +114,8 @@ class Collection(List):
|
|||
Takahe.post_collection(self)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
existing_post = self.latest_post
|
||||
if existing_post:
|
||||
from takahe.utils import Takahe
|
||||
|
||||
Takahe.delete_posts([existing_post.pk])
|
||||
if self.local:
|
||||
Takahe.delete_posts(self.all_post_ids)
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
@property
|
||||
|
|
|
@ -5,6 +5,7 @@ from django.db import models
|
|||
from django.utils import timezone
|
||||
|
||||
from catalog.models import Item
|
||||
from takahe.utils import Takahe
|
||||
from users.models import APIdentity
|
||||
|
||||
from .common import Content
|
||||
|
@ -33,7 +34,7 @@ class Comment(Content):
|
|||
return d
|
||||
|
||||
@classmethod
|
||||
def update_by_ap_object(cls, owner, item, obj, post_id, visibility):
|
||||
def update_by_ap_object(cls, owner, item, obj, post):
|
||||
p = cls.objects.filter(owner=owner, item=item).first()
|
||||
if p and p.edited_time >= datetime.fromisoformat(obj["updated"]):
|
||||
return p # incoming ap object is older than what we have, no update needed
|
||||
|
@ -45,14 +46,14 @@ class Comment(Content):
|
|||
"text": content,
|
||||
"local": False,
|
||||
"remote_id": obj["id"],
|
||||
"visibility": visibility,
|
||||
"visibility": Takahe.visibility_t2n(post.visibility),
|
||||
"created_time": datetime.fromisoformat(obj["published"]),
|
||||
"edited_time": datetime.fromisoformat(obj["updated"]),
|
||||
}
|
||||
if obj.get("relatedWithItemPosition"):
|
||||
d["metadata"] = {"position": obj["relatedWithItemPosition"]}
|
||||
p, _ = cls.objects.update_or_create(owner=owner, item=item, defaults=d)
|
||||
p.link_post_id(post_id)
|
||||
p.link_post_id(post.id)
|
||||
return p
|
||||
|
||||
@property
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
import re
|
||||
import uuid
|
||||
from abc import abstractmethod
|
||||
from datetime import datetime
|
||||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, Self
|
||||
|
||||
# from deepmerge import always_merger
|
||||
from django.conf import settings
|
||||
from django.core.signing import b62_decode, b62_encode
|
||||
from django.db import connection, models
|
||||
from django.db.models import Avg, CharField, Count, Q
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from loguru import logger
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
from catalog.common.models import AvailableItemCategory, Item, ItemCategory
|
||||
from catalog.models import item_categories, item_content_types
|
||||
from mastodon.api import boost_toot_later, delete_toot, delete_toot_later, post_toot2
|
||||
from takahe.utils import Takahe
|
||||
from users.models import APIdentity, User
|
||||
|
||||
|
@ -123,6 +128,28 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
|||
"takahe.Post", related_name="pieces", through="PiecePost"
|
||||
)
|
||||
|
||||
def get_mastodon_repost_url(self):
|
||||
return (self.metadata or {}).get("shared_link")
|
||||
|
||||
def set_mastodon_repost_url(self, url: str | None):
|
||||
metadata = self.metadata or {}
|
||||
if metadata.get("shared_link", None) == url:
|
||||
return
|
||||
if not url:
|
||||
metadata.pop("shared_link", None)
|
||||
else:
|
||||
metadata["shared_link"] = url
|
||||
self.metadata = metadata
|
||||
self.save(update_fields=["metadata"])
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
if self.local:
|
||||
Takahe.delete_posts(self.all_post_ids)
|
||||
toot_url = self.get_mastodon_repost_url()
|
||||
if toot_url:
|
||||
delete_toot_later(self.owner.user, toot_url)
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def uuid(self):
|
||||
return b62_encode(self.uid.int)
|
||||
|
@ -139,10 +166,6 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
|||
def api_url(self):
|
||||
return f"/api/{self.url}" if self.url_path else None
|
||||
|
||||
@property
|
||||
def shared_link(self):
|
||||
return Takahe.get_post_url(self.latest_post.pk) if self.latest_post else None
|
||||
|
||||
@property
|
||||
def like_count(self):
|
||||
return (
|
||||
|
@ -187,16 +210,13 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
|||
pp = PiecePost.objects.filter(post_id=post_id).first()
|
||||
return pp.piece if pp else None
|
||||
|
||||
@classmethod
|
||||
def update_by_ap_object(cls, owner, item, obj, post_id, visibility):
|
||||
raise NotImplementedError("subclass must implement this")
|
||||
|
||||
@property
|
||||
def ap_object(self):
|
||||
raise NotImplementedError("subclass must implement this")
|
||||
|
||||
def link_post_id(self, post_id: int):
|
||||
PiecePost.objects.get_or_create(piece=self, post_id=post_id)
|
||||
try:
|
||||
del self.latest_post_id
|
||||
del self.latest_post
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def clear_post_ids(self):
|
||||
PiecePost.objects.filter(piece=self).delete()
|
||||
|
@ -219,6 +239,160 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
|||
)
|
||||
return post_ids
|
||||
|
||||
@property
|
||||
def ap_object(self):
|
||||
raise NotImplementedError("subclass must implement this")
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def params_from_ap_object(cls, post, obj, piece):
|
||||
return {}
|
||||
|
||||
@abstractmethod
|
||||
def to_post_params(self):
|
||||
return {}
|
||||
|
||||
@abstractmethod
|
||||
def to_mastodon_params(self):
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def update_by_ap_object(cls, owner: APIdentity, item: Item, obj, post: Post):
|
||||
"""
|
||||
Create or update a content piece with related AP message
|
||||
"""
|
||||
p = cls.get_by_post_id(post.id)
|
||||
if p and p.owner.pk != post.author_id:
|
||||
logger.warning(f"Owner mismatch: {p.owner.pk} != {post.author_id}")
|
||||
return
|
||||
local = post.local
|
||||
visibility = Takahe.visibility_t2n(post.visibility)
|
||||
d = cls.params_from_ap_object(post, obj, p)
|
||||
if p:
|
||||
# update existing piece
|
||||
edited = post.edited if local else datetime.fromisoformat(obj["updated"])
|
||||
if p.edited_time >= edited:
|
||||
# incoming ap object is older than what we have, no update needed
|
||||
return p
|
||||
d["edited_time"] = edited
|
||||
for k, v in d.items():
|
||||
setattr(p, k, v)
|
||||
p.save(update_fields=d.keys())
|
||||
else:
|
||||
# no previously linked piece, create a new one and link to post
|
||||
d.update(
|
||||
{
|
||||
"item": item,
|
||||
"owner": owner,
|
||||
"local": post.local,
|
||||
"visibility": visibility,
|
||||
"remote_id": None if local else obj["id"],
|
||||
}
|
||||
)
|
||||
if local:
|
||||
d["created_time"] = post.published
|
||||
d["edited_time"] = post.edited or post.published
|
||||
else:
|
||||
d["created_time"] = datetime.fromisoformat(obj["published"])
|
||||
d["edited_time"] = datetime.fromisoformat(obj["updated"])
|
||||
p = cls.objects.create(**d)
|
||||
p.link_post_id(post.id)
|
||||
if local:
|
||||
# a local piece is reconstructred from a post, update post and fanout
|
||||
if not post.type_data:
|
||||
post.type_data = {}
|
||||
# always_merger.merge(
|
||||
# post.type_data,
|
||||
# {
|
||||
# "object": {
|
||||
# "tag": [item.ap_object_ref],
|
||||
# "relatedWith": [p.ap_object],
|
||||
# }
|
||||
# },
|
||||
# )
|
||||
post.type_data = {
|
||||
"object": {
|
||||
"tag": [item.ap_object_ref],
|
||||
"relatedWith": [p.ap_object],
|
||||
}
|
||||
}
|
||||
post.save(update_fields=["type_data"])
|
||||
Takahe.update_state(post, "edited")
|
||||
return p
|
||||
|
||||
def sync_to_mastodon(self, delete_existing=False):
|
||||
user = self.owner.user
|
||||
if not user.mastodon_site:
|
||||
return
|
||||
if user.preference.mastodon_repost_mode == 1:
|
||||
if delete_existing:
|
||||
self.delete_mastodon_repost()
|
||||
return self.repost_to_mastodon()
|
||||
elif self.latest_post:
|
||||
return boost_toot_later(user, self.latest_post.url)
|
||||
else:
|
||||
logger.warning("No post found for piece")
|
||||
return False, 404
|
||||
|
||||
def delete_mastodon_repost(self):
|
||||
toot_url = self.get_mastodon_repost_url()
|
||||
if toot_url:
|
||||
self.set_mastodon_repost_url(None)
|
||||
delete_toot(self.owner.user, toot_url)
|
||||
|
||||
def repost_to_mastodon(self):
|
||||
user = self.owner.user
|
||||
d = {
|
||||
"user": user,
|
||||
"visibility": self.visibility,
|
||||
"update_toot_url": self.get_mastodon_repost_url(),
|
||||
}
|
||||
d.update(self.to_mastodon_params())
|
||||
response = post_toot2(**d)
|
||||
if response is not None and response.status_code in [200, 201]:
|
||||
j = response.json()
|
||||
if "url" in j:
|
||||
metadata = {"shared_link": j["url"]}
|
||||
if self.metadata != metadata:
|
||||
self.metadata = metadata
|
||||
self.save(update_fields=["metadata"])
|
||||
return True, 200
|
||||
else:
|
||||
logger.warning(response)
|
||||
return False, response.status_code if response is not None else -1
|
||||
|
||||
def sync_to_timeline(self, delete_existing=False):
|
||||
user = self.owner.user
|
||||
v = Takahe.visibility_n2t(self.visibility, user.preference.post_public_mode)
|
||||
existing_post = self.latest_post
|
||||
if existing_post and existing_post.state in ["deleted", "deleted_fanned_out"]:
|
||||
existing_post = None
|
||||
elif existing_post and delete_existing:
|
||||
Takahe.delete_posts([existing_post.pk])
|
||||
existing_post = None
|
||||
params = {
|
||||
"author_pk": self.owner.pk,
|
||||
"visibility": v,
|
||||
"post_pk": existing_post.pk if existing_post else None,
|
||||
"post_time": self.created_time, # type:ignore subclass must have this
|
||||
"edit_time": self.edited_time, # type:ignore subclass must have this
|
||||
"data": {
|
||||
"object": {
|
||||
"tag": (
|
||||
[self.item.ap_object_ref] # type:ignore
|
||||
if hasattr(self, "item")
|
||||
else []
|
||||
),
|
||||
"relatedWith": [self.ap_object],
|
||||
}
|
||||
},
|
||||
}
|
||||
params.update(self.to_post_params())
|
||||
post = Takahe.post(**params)
|
||||
if post and post != existing_post:
|
||||
self.link_post_id(post.pk)
|
||||
return post
|
||||
|
||||
|
||||
class PiecePost(models.Model):
|
||||
post_id: int
|
||||
|
|
|
@ -26,6 +26,7 @@ from takahe.utils import Takahe
|
|||
from users.models import APIdentity
|
||||
|
||||
from .comment import Comment
|
||||
from .note import Note
|
||||
from .rating import Rating
|
||||
from .review import Review
|
||||
from .shelf import Shelf, ShelfLogEntry, ShelfManager, ShelfMember, ShelfType
|
||||
|
@ -105,6 +106,14 @@ class Mark:
|
|||
else None
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def notes(self):
|
||||
return Note.objects.filter(owner=self.owner, item=self.item)
|
||||
# post_ids = PiecePost.objects.filter(
|
||||
# piece__note__owner_id=self.owner.pk, piece__note__item_id=self.item.pk
|
||||
# ).values_list("post_id", flat=True)
|
||||
# return Takahe.get_posts(list(post_ids))
|
||||
|
||||
@property
|
||||
def created_time(self) -> datetime | None:
|
||||
return self.shelfmember.created_time if self.shelfmember else None
|
||||
|
|
87
journal/models/note.py
Normal file
87
journal/models/note.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
from functools import cached_property
|
||||
from typing import override
|
||||
|
||||
from django.db import models
|
||||
from loguru import logger
|
||||
|
||||
from mastodon.api import delete_toot_later
|
||||
from takahe.utils import Takahe
|
||||
|
||||
from .common import Content
|
||||
from .renderers import render_text
|
||||
from .shelf import ShelfMember
|
||||
|
||||
|
||||
class Note(Content):
|
||||
title = models.TextField(blank=True, null=True, default=None)
|
||||
content = models.TextField(blank=False, null=False)
|
||||
sensitive = models.BooleanField(default=False, null=False)
|
||||
attachements = models.JSONField(default=list)
|
||||
|
||||
@property
|
||||
def html(self):
|
||||
return render_text(self.content)
|
||||
|
||||
@property
|
||||
def ap_object(self):
|
||||
d = {
|
||||
"id": self.absolute_url,
|
||||
"type": "Note",
|
||||
"title": self.title,
|
||||
"content": self.content,
|
||||
"sensitive": self.sensitive,
|
||||
"published": self.created_time.isoformat(),
|
||||
"updated": self.edited_time.isoformat(),
|
||||
"attributedTo": self.owner.actor_uri,
|
||||
"withRegardTo": self.item.absolute_url,
|
||||
"href": self.absolute_url,
|
||||
}
|
||||
return d
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def params_from_ap_object(cls, post, obj, piece):
|
||||
return {
|
||||
"title": obj.get("title", post.summary),
|
||||
"content": obj.get("content", "").strip(),
|
||||
"sensitive": obj.get("sensitive", post.sensitive),
|
||||
# "attachements": obj.get("attachements", []),
|
||||
}
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def update_by_ap_object(cls, owner, item, obj, post):
|
||||
p = super().update_by_ap_object(owner, item, obj, post)
|
||||
if (
|
||||
p
|
||||
and p.local
|
||||
and owner.user.preference.mastodon_default_repost
|
||||
and owner.user.mastodon_username
|
||||
):
|
||||
p.sync_to_mastodon()
|
||||
return p
|
||||
|
||||
@cached_property
|
||||
def shelfmember(self) -> ShelfMember | None:
|
||||
return ShelfMember.objects.filter(item=self.item, owner=self.owner).first()
|
||||
|
||||
def to_mastodon_params(self):
|
||||
return {
|
||||
"spoiler_text": self.title,
|
||||
"content": self.content,
|
||||
"sensitive": self.sensitive,
|
||||
# "attachements": self.attachements,
|
||||
"reply_to_toot_url": (
|
||||
self.shelfmember.get_mastodon_repost_url() if self.shelfmember else None
|
||||
),
|
||||
}
|
||||
|
||||
def to_post_params(self):
|
||||
return {
|
||||
"summary": self.title,
|
||||
"content": self.content,
|
||||
"sensitive": self.sensitive,
|
||||
"reply_to_pk": (
|
||||
self.shelfmember.latest_post_id if self.shelfmember else None
|
||||
),
|
||||
}
|
|
@ -7,6 +7,7 @@ from django.db.models import Avg, Count, Q
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from catalog.models import Item
|
||||
from takahe.utils import Takahe
|
||||
from users.models import APIdentity
|
||||
|
||||
from .common import Content
|
||||
|
@ -39,7 +40,7 @@ class Rating(Content):
|
|||
}
|
||||
|
||||
@classmethod
|
||||
def update_by_ap_object(cls, owner, item, obj, post_id, visibility):
|
||||
def update_by_ap_object(cls, owner, item, obj, post):
|
||||
p = cls.objects.filter(owner=owner, item=item).first()
|
||||
if p and p.edited_time >= datetime.fromisoformat(obj["updated"]):
|
||||
return p # incoming ap object is older than what we have, no update needed
|
||||
|
@ -63,12 +64,12 @@ class Rating(Content):
|
|||
"grade": value,
|
||||
"local": False,
|
||||
"remote_id": obj["id"],
|
||||
"visibility": visibility,
|
||||
"visibility": Takahe.visibility_t2n(post.visibility),
|
||||
"created_time": datetime.fromisoformat(obj["published"]),
|
||||
"edited_time": datetime.fromisoformat(obj["updated"]),
|
||||
}
|
||||
p = cls.objects.update_or_create(owner=owner, item=item, defaults=d)[0]
|
||||
p.link_post_id(post_id)
|
||||
p.link_post_id(post.id)
|
||||
return p
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -9,6 +9,7 @@ from markdownx.models import MarkdownxField
|
|||
|
||||
from catalog.models import Item
|
||||
from mastodon.api import boost_toot_later, share_review
|
||||
from takahe.utils import Takahe
|
||||
from users.models import APIdentity
|
||||
|
||||
from .common import Content
|
||||
|
@ -52,7 +53,7 @@ class Review(Content):
|
|||
}
|
||||
|
||||
@classmethod
|
||||
def update_by_ap_object(cls, owner, item, obj, post_id, visibility):
|
||||
def update_by_ap_object(cls, owner, item, obj, post):
|
||||
p = cls.objects.filter(owner=owner, item=item).first()
|
||||
if p and p.edited_time >= datetime.fromisoformat(obj["updated"]):
|
||||
return p # incoming ap object is older than what we have, no update needed
|
||||
|
@ -66,12 +67,12 @@ class Review(Content):
|
|||
"body": content,
|
||||
"local": False,
|
||||
"remote_id": obj["id"],
|
||||
"visibility": visibility,
|
||||
"visibility": Takahe.visibility_t2n(post.visibility),
|
||||
"created_time": datetime.fromisoformat(obj["published"]),
|
||||
"edited_time": datetime.fromisoformat(obj["updated"]),
|
||||
}
|
||||
p, _ = cls.objects.update_or_create(owner=owner, item=item, defaults=d)
|
||||
p.link_post_id(post_id)
|
||||
p.link_post_id(post.id)
|
||||
return p
|
||||
|
||||
@cached_property
|
||||
|
|
|
@ -10,6 +10,7 @@ from django.utils.translation import pgettext_lazy as __
|
|||
from loguru import logger
|
||||
|
||||
from catalog.models import Item, ItemCategory
|
||||
from takahe.utils import Takahe
|
||||
from users.models import APIdentity
|
||||
|
||||
from .common import q_item_in_category
|
||||
|
@ -335,9 +336,7 @@ class ShelfMember(ListMember):
|
|||
}
|
||||
|
||||
@classmethod
|
||||
def update_by_ap_object(
|
||||
cls, owner: APIdentity, item: Item, obj: dict, post_id: int, visibility: int
|
||||
):
|
||||
def update_by_ap_object(cls, owner: APIdentity, item: Item, obj: dict, post):
|
||||
p = cls.objects.filter(owner=owner, item=item).first()
|
||||
if p and p.edited_time >= datetime.fromisoformat(obj["updated"]):
|
||||
return p # incoming ap object is older than what we have, no update needed
|
||||
|
@ -349,12 +348,12 @@ class ShelfMember(ListMember):
|
|||
"parent": shelf,
|
||||
"position": 0,
|
||||
"local": False,
|
||||
"visibility": visibility,
|
||||
"visibility": Takahe.visibility_t2n(post.visibility),
|
||||
"created_time": datetime.fromisoformat(obj["published"]),
|
||||
"edited_time": datetime.fromisoformat(obj["updated"]),
|
||||
}
|
||||
p, _ = cls.objects.update_or_create(owner=owner, item=item, defaults=d)
|
||||
p.link_post_id(post_id)
|
||||
p.link_post_id(post.id)
|
||||
return p
|
||||
|
||||
@cached_property
|
||||
|
|
|
@ -3,18 +3,17 @@
|
|||
<a target="_blank"
|
||||
rel="noopener"
|
||||
href="{{ post.object_uri }}"
|
||||
onclick="navigator.share({url:'{{ post.object_uri|escapejs }}'});event.preventDefault();"
|
||||
title="{% trans "link for fediverse" %}">
|
||||
onclick="navigator.share({url:'{{ post.object_uri|escapejs }}'});event.preventDefault();">
|
||||
{% if post.visibility == 1 %}
|
||||
<i class="fa-solid fa-lock-open"></i>
|
||||
<i class="fa-solid fa-lock-open" title="{% trans "Public" %}"></i>
|
||||
{% elif post.visibility == 2 %}
|
||||
<i class="fa-solid fa-lock"></i>
|
||||
<i class="fa-solid fa-lock" title="{% trans "Followers Only" %}"></i>
|
||||
{% elif post.visibility == 3 %}
|
||||
<i class="fa-solid fa-at"></i>
|
||||
<i class="fa-solid fa-at" title="{% trans "Mentioned Only" %}"></i>
|
||||
{% elif post.visibility == 4 %}
|
||||
<i class="fa-solid fa-igloo"></i>
|
||||
<i class="fa-solid fa-igloo" title="{% trans "Public" %}"></i>
|
||||
{% else %}
|
||||
<i class="fa-solid fa-globe"></i>
|
||||
<i class="fa-solid fa-globe" title="{% trans "Public" %}"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</span>
|
||||
|
|
|
@ -139,13 +139,9 @@
|
|||
</span>
|
||||
{% endif %}
|
||||
<span class="action inline">
|
||||
<span>
|
||||
<a target="_blank"
|
||||
rel="noopener"
|
||||
href="{{ collection.shared_link }}"
|
||||
onclick="navigator.share({url:'{{ collection.shared_link|escapejs }}'});event.preventDefault();"
|
||||
title="link for fediverse"><i class="fa-solid {% if collection.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
|
||||
</span>
|
||||
{% if collection.latest_post %}
|
||||
{% include "action_open_post.html" with post=collection.latest_post %}
|
||||
{% endif %}
|
||||
<span><a>{% trans "Created date" %}: {{ collection.created_time|date }}</a></span>
|
||||
</span>
|
||||
</section>
|
||||
|
|
71
journal/templates/note.html
Normal file
71
journal/templates/note.html
Normal file
|
@ -0,0 +1,71 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load humanize %}
|
||||
{% load mastodon %}
|
||||
{% load thumb %}
|
||||
<dialog open
|
||||
class="mark-editor"
|
||||
_="on close_dialog add .closing then wait for animationend then remove me">
|
||||
<article>
|
||||
<header>
|
||||
<link to="#"
|
||||
aria-label="Close"
|
||||
class="close"
|
||||
_="on click trigger close_dialog" />
|
||||
<strong>{% trans 'Note' %} - {{ item.display_title }}</strong>
|
||||
</header>
|
||||
<div>
|
||||
<form action="{% url 'journal:note' item.uuid %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="uuid" value="{{ note.uuid|default:'' }}">
|
||||
<textarea name="content" cols="40" rows="10" placeholder="" id="id_content">{{ note.content|default:'' }}</textarea>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<fieldset>
|
||||
<input type="radio"
|
||||
name="visibility"
|
||||
value="0"
|
||||
required=""
|
||||
id="id_visibility_0"
|
||||
{% if note.visibility == 0 or not note %}checked{% endif %}>
|
||||
<label for="id_visibility_0">{% trans "Public" %}</label>
|
||||
<input type="radio"
|
||||
name="visibility"
|
||||
value="1"
|
||||
required=""
|
||||
id="id_visibility_1"
|
||||
{% if note.visibility == 1 %}checked{% endif %}>
|
||||
<label for="id_visibility_1">{% trans "Followers Only" %}</label>
|
||||
<input type="radio"
|
||||
name="visibility"
|
||||
value="2"
|
||||
required=""
|
||||
id="id_visibility_2"
|
||||
{% if note.visibility == 2 %}checked{% endif %}>
|
||||
<label for="id_visibility_2">{% trans "Mentioned Only" %}</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div>
|
||||
<fieldset>
|
||||
{% if request.user.mastodon_acct %}
|
||||
<label for="id_share_to_mastodon">
|
||||
<input role="switch"
|
||||
type="checkbox"
|
||||
name="share_to_mastodon"
|
||||
id="id_share_to_mastodon"
|
||||
value="1"
|
||||
{% if request.user.preference.mastodon_default_repost %}checked{% endif %}>
|
||||
{% trans "Repost to timeline" %}
|
||||
</label>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" class="button float-right" value="{% trans "Save" %}">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
</dialog>
|
|
@ -86,13 +86,9 @@
|
|||
</span>
|
||||
{% endif %}
|
||||
<span class="action inline">
|
||||
<span>
|
||||
<a target="_blank"
|
||||
rel="noopener"
|
||||
href="{{ review.shared_link }}"
|
||||
onclick="navigator.share({url:'{{ review.shared_link |escapejs }}'});event.preventDefault();"
|
||||
title="{% trans "link for fediverse" %}"><i class="fa-solid {% if review.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
|
||||
</span>
|
||||
{% if review.latest_post %}
|
||||
{% include "action_open_post.html" with post=review.latest_post %}
|
||||
{% endif %}
|
||||
<span><a>{{ review.created_time|date }}</a></span>
|
||||
</span>
|
||||
</footer>
|
||||
|
|
|
@ -21,6 +21,8 @@ urlpatterns = [
|
|||
path("wish/<str:item_uuid>", wish, name="wish"),
|
||||
path("mark/<str:item_uuid>", mark, name="mark"),
|
||||
path("comment/<str:item_uuid>", comment, name="comment"),
|
||||
path("note/<str:item_uuid>", note, name="note"),
|
||||
path("note/<str:item_uuid>/<str:note_uuid>", note, name="note"),
|
||||
path("piece/<str:piece_uuid>/replies", piece_replies, name="piece_replies"),
|
||||
path("post/<int:post_id>/replies", post_replies, name="post_replies"),
|
||||
path("post/<int:post_id>/reply", post_reply, name="post_reply"),
|
||||
|
|
|
@ -16,7 +16,7 @@ from .collection import (
|
|||
user_liked_collection_list,
|
||||
)
|
||||
from .common import piece_delete
|
||||
from .mark import comment, mark, mark_log, user_mark_list, wish
|
||||
from .mark import comment, mark, mark_log, note, user_mark_list, wish
|
||||
from .post import (
|
||||
piece_replies,
|
||||
post_boost,
|
||||
|
|
|
@ -16,7 +16,7 @@ from common.utils import AuthedHttpRequest, get_uuid_or_404
|
|||
from mastodon.api import boost_toot_later, share_comment
|
||||
from takahe.utils import Takahe
|
||||
|
||||
from ..models import Comment, Mark, ShelfManager, ShelfType, TagManager
|
||||
from ..models import Comment, Mark, Note, ShelfManager, ShelfType, TagManager
|
||||
from .common import render_list, render_relogin, target_identity_required
|
||||
|
||||
PAGE_SIZE = 10
|
||||
|
@ -192,6 +192,53 @@ def comment(request: AuthedHttpRequest, item_uuid):
|
|||
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def note(request: AuthedHttpRequest, item_uuid: str, note_uuid: str = ""):
|
||||
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
|
||||
note_uuid = request.POST.get("uuid", note_uuid)
|
||||
note = None
|
||||
content = request.POST.get("content")
|
||||
if note_uuid:
|
||||
note = get_object_or_404(
|
||||
Note, owner=request.user.identity, item=item, uid=get_uuid_or_404(note_uuid)
|
||||
)
|
||||
if request.method == "GET":
|
||||
return render(
|
||||
request,
|
||||
"note.html",
|
||||
{
|
||||
"item": item,
|
||||
"note": note,
|
||||
},
|
||||
)
|
||||
else:
|
||||
if request.POST.get("delete", default=False) or not content:
|
||||
if not note:
|
||||
raise Http404(_("Content not found"))
|
||||
note.delete()
|
||||
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
|
||||
share_to_mastodon = bool(request.POST.get("share_to_mastodon", default=False))
|
||||
visibility = int(request.POST.get("visibility", default=0))
|
||||
delete_existing_post = False
|
||||
if note:
|
||||
delete_existing_post = visibility != note.visibility
|
||||
note.content = content
|
||||
note.visibility = visibility
|
||||
note.save()
|
||||
else:
|
||||
note = Note.objects.create(
|
||||
owner=request.user.identity,
|
||||
item=item,
|
||||
content=content,
|
||||
visibility=visibility,
|
||||
)
|
||||
note.sync_to_timeline(delete_existing=delete_existing_post)
|
||||
if share_to_mastodon:
|
||||
note.sync_to_mastodon(delete_existing=delete_existing_post)
|
||||
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
|
||||
|
||||
|
||||
def user_mark_list(request: AuthedHttpRequest, user_name, shelf_type, item_category):
|
||||
return render_list(
|
||||
request, user_name, "mark", shelf_type=shelf_type, item_category=item_category
|
||||
|
|
|
@ -123,9 +123,9 @@ class WrappedShareView(LoginRequiredMixin, TemplateView):
|
|||
)
|
||||
post = Takahe.post(
|
||||
identity.pk,
|
||||
"",
|
||||
comment,
|
||||
Takahe.visibility_n2t(visibility, user.preference.post_public_mode),
|
||||
"",
|
||||
attachments=[media],
|
||||
)
|
||||
classic_repost = user.preference.mastodon_repost_mode == 1
|
||||
|
|
|
@ -227,6 +227,83 @@ def post_toot(
|
|||
return response
|
||||
|
||||
|
||||
def delete_toot(user, toot_url):
|
||||
headers = {
|
||||
"User-Agent": USER_AGENT,
|
||||
"Authorization": f"Bearer {user.mastodon_token}",
|
||||
"Idempotency-Key": random_string_generator(16),
|
||||
}
|
||||
toot_id = get_status_id_by_url(toot_url)
|
||||
url = (
|
||||
"https://"
|
||||
+ get_api_domain(user.mastodon_site)
|
||||
+ API_PUBLISH_TOOT
|
||||
+ "/"
|
||||
+ toot_id
|
||||
)
|
||||
try:
|
||||
response = requests.delete(url, headers=headers)
|
||||
if response.status_code != 200:
|
||||
logger.warning(f"Error DELETE {url} {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error deleting {e}")
|
||||
|
||||
|
||||
def delete_toot_later(user, toot_url):
|
||||
if user and user.mastodon_token and user.mastodon_site and toot_url:
|
||||
django_rq.get_queue("fetch").enqueue(delete_toot, user, toot_url)
|
||||
|
||||
|
||||
def post_toot2(
|
||||
user,
|
||||
content,
|
||||
visibility,
|
||||
update_toot_url: str | None = None,
|
||||
reply_to_toot_url: str | None = None,
|
||||
sensitive: bool = False,
|
||||
spoiler_text: str | None = None,
|
||||
):
|
||||
headers = {
|
||||
"User-Agent": USER_AGENT,
|
||||
"Authorization": f"Bearer {user.mastodon_token}",
|
||||
"Idempotency-Key": random_string_generator(16),
|
||||
}
|
||||
response = None
|
||||
url = "https://" + get_api_domain(user.mastodon_site) + API_PUBLISH_TOOT
|
||||
payload = {
|
||||
"status": content,
|
||||
"visibility": get_toot_visibility(visibility, user),
|
||||
}
|
||||
update_id = get_status_id_by_url(update_toot_url)
|
||||
reply_to_id = get_status_id_by_url(reply_to_toot_url)
|
||||
if reply_to_id:
|
||||
payload["in_reply_to_id"] = reply_to_id
|
||||
# if media_id:
|
||||
# payload["media_ids[]"] = [media_id]
|
||||
if spoiler_text:
|
||||
payload["spoiler_text"] = spoiler_text
|
||||
if sensitive:
|
||||
payload["sensitive"] = True
|
||||
try:
|
||||
if update_id:
|
||||
response = put(url + "/" + update_id, headers=headers, data=payload)
|
||||
if not update_id or (response is not None and response.status_code != 200):
|
||||
headers["Idempotency-Key"] = random_string_generator(16)
|
||||
response = post(url, headers=headers, data=payload)
|
||||
if response is not None and response.status_code != 200:
|
||||
headers["Idempotency-Key"] = random_string_generator(16)
|
||||
payload["in_reply_to_id"] = None
|
||||
response = post(url, headers=headers, data=payload)
|
||||
if response is not None and response.status_code == 201:
|
||||
response.status_code = 200
|
||||
if response is not None and response.status_code != 200:
|
||||
logger.warning(f"Error {url} {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error posting {e}")
|
||||
response = None
|
||||
return response
|
||||
|
||||
|
||||
def _get_redirect_uris(allow_multiple=True) -> str:
|
||||
u = settings.SITE_INFO["site_url"] + "/account/login/oauth"
|
||||
if not allow_multiple:
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 01cacb7dce19f7fadcda7e629b78b2752d74eece
|
||||
Subproject commit 03a4f54a8f9bd3d06dd9f466d40d6f5853699923
|
|
@ -54,6 +54,7 @@ dependencies = [
|
|||
"typesense",
|
||||
"urlman",
|
||||
"validators",
|
||||
"deepmerge>=1.1.1",
|
||||
]
|
||||
|
||||
[tool.rye]
|
||||
|
@ -66,7 +67,7 @@ dev-dependencies = [
|
|||
"djlint~=1.34.1",
|
||||
"isort~=5.13.2",
|
||||
"lxml-stubs",
|
||||
"pyright==1.1.366",
|
||||
"pyright>=1.1.367",
|
||||
"ruff",
|
||||
"mkdocs-material>=9.5.25",
|
||||
]
|
||||
|
|
|
@ -56,6 +56,7 @@ cryptography==42.0.8
|
|||
cssbeautifier==1.15.1
|
||||
# via djlint
|
||||
dateparser==1.2.0
|
||||
deepmerge==1.1.1
|
||||
discord-py==2.3.2
|
||||
distlib==0.3.8
|
||||
# via virtualenv
|
||||
|
@ -219,7 +220,7 @@ pygments==2.18.0
|
|||
# via mkdocs-material
|
||||
pymdown-extensions==10.8.1
|
||||
# via mkdocs-material
|
||||
pyright==1.1.366
|
||||
pyright==1.1.367
|
||||
python-dateutil==2.9.0.post0
|
||||
# via dateparser
|
||||
# via django-auditlog
|
||||
|
|
|
@ -42,6 +42,7 @@ click==8.1.7
|
|||
cryptography==42.0.8
|
||||
# via jwcrypto
|
||||
dateparser==1.2.0
|
||||
deepmerge==1.1.1
|
||||
discord-py==2.3.2
|
||||
django==4.2.13
|
||||
# via django-anymail
|
||||
|
|
|
@ -1,9 +1,20 @@
|
|||
from time import sleep
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from loguru import logger
|
||||
|
||||
from catalog.common import *
|
||||
from journal.models import Comment, Piece, PieceInteraction, Rating, Review, ShelfMember
|
||||
from journal.models import (
|
||||
Comment,
|
||||
Content,
|
||||
Note,
|
||||
Piece,
|
||||
PieceInteraction,
|
||||
Rating,
|
||||
Review,
|
||||
ShelfMember,
|
||||
)
|
||||
from users.models.apidentity import APIdentity
|
||||
|
||||
from .models import Follow, Identity, Post, TimelineEvent
|
||||
|
@ -28,10 +39,11 @@ _supported_ap_journal_types = {
|
|||
"Rating": Rating,
|
||||
"Comment": Comment,
|
||||
"Review": Review,
|
||||
"Note": Note,
|
||||
}
|
||||
|
||||
|
||||
def _parse_items(objects):
|
||||
def _parse_items(objects) -> list[dict[str, Any]]:
|
||||
logger.debug(f"Parsing item links from {objects}")
|
||||
if not objects:
|
||||
return []
|
||||
|
@ -40,7 +52,7 @@ def _parse_items(objects):
|
|||
return items
|
||||
|
||||
|
||||
def _parse_piece_objects(objects):
|
||||
def _parse_piece_objects(objects) -> list[dict[str, Any]]:
|
||||
logger.debug(f"Parsing pieces from {objects}")
|
||||
if not objects:
|
||||
return []
|
||||
|
@ -54,10 +66,15 @@ def _parse_piece_objects(objects):
|
|||
return pieces
|
||||
|
||||
|
||||
def _get_or_create_item(item_obj):
|
||||
def _get_or_create_item(item_obj) -> Item | None:
|
||||
logger.debug(f"Fetching item by ap from {item_obj}")
|
||||
typ = item_obj["type"]
|
||||
url = item_obj["href"]
|
||||
if url.startswith(settings.SITE_INFO["site_url"]):
|
||||
item = Item.get_by_url(url, True)
|
||||
if not item:
|
||||
logger.warning(f"Item not found for {url}")
|
||||
return item
|
||||
if typ in ["TVEpisode", "PodcastEpisode"]:
|
||||
# TODO support episode item
|
||||
# match and fetch parent item first
|
||||
|
@ -74,25 +91,23 @@ def _get_or_create_item(item_obj):
|
|||
return item
|
||||
|
||||
|
||||
def _get_visibility(post_visibility):
|
||||
match post_visibility:
|
||||
case 2:
|
||||
return 1
|
||||
case 3:
|
||||
return 2
|
||||
case _:
|
||||
return 0
|
||||
def post_created(pk, post_data):
|
||||
return post_fetched(pk, post_data)
|
||||
|
||||
|
||||
def post_fetched(pk, obj):
|
||||
def post_edited(pk, post_data):
|
||||
return post_fetched(pk, post_data)
|
||||
|
||||
|
||||
def post_fetched(pk, post_data):
|
||||
post = Post.objects.get(pk=pk)
|
||||
owner = Takahe.get_or_create_remote_apidentity(post.author)
|
||||
if not post.type_data:
|
||||
if not post.type_data and not post_data:
|
||||
logger.warning(f"Post {post} has no type_data")
|
||||
return
|
||||
ap_object = post.type_data.get("object", {})
|
||||
items = _parse_items(ap_object.get("tag"))
|
||||
pieces = _parse_piece_objects(ap_object.get("relatedWith"))
|
||||
ap_objects = post_data or post.type_data.get("object", {})
|
||||
items = _parse_items(ap_objects.get("tag"))
|
||||
pieces = _parse_piece_objects(ap_objects.get("relatedWith"))
|
||||
logger.info(f"Post {post} has items {items} and pieces {pieces}")
|
||||
if len(items) == 0:
|
||||
logger.warning(f"Post {post} has no remote items")
|
||||
|
@ -109,17 +124,20 @@ def post_fetched(pk, obj):
|
|||
if not cls:
|
||||
logger.warning(f'Unknown link type {p["type"]}')
|
||||
continue
|
||||
cls.update_by_ap_object(owner, item, p, pk, _get_visibility(post.visibility))
|
||||
cls.update_by_ap_object(owner, item, p, post)
|
||||
|
||||
|
||||
def post_deleted(pk, obj):
|
||||
for piece in Piece.objects.filter(posts__id=pk, local=False):
|
||||
def post_deleted(pk, post_data):
|
||||
for piece in Piece.objects.filter(posts__id=pk):
|
||||
if piece.local and piece.__class__ != Note:
|
||||
# no delete other than Note, for backward compatibility, should reconsider later
|
||||
return
|
||||
# delete piece if the deleted post is the most recent one for the piece
|
||||
if piece.latest_post_id == pk:
|
||||
logger.debug(f"Deleting remote piece {piece}")
|
||||
logger.debug(f"Deleting piece {piece}")
|
||||
piece.delete()
|
||||
else:
|
||||
logger.debug(f"Matched remote piece {piece} has newer posts, not deleting")
|
||||
logger.debug(f"Matched piece {piece} has newer posts, not deleting")
|
||||
|
||||
|
||||
def post_interacted(interaction_pk, interaction, post_pk, identity_pk):
|
||||
|
|
|
@ -310,6 +310,11 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
("state", models.CharField(default="new", max_length=100)),
|
||||
("state_changed", models.DateTimeField(auto_now_add=True)),
|
||||
("state_next_attempt", models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
"state_locked_until",
|
||||
models.DateTimeField(blank=True, db_index=True, null=True),
|
||||
),
|
||||
("local", models.BooleanField()),
|
||||
(
|
||||
"object_uri",
|
||||
|
|
|
@ -900,6 +900,7 @@ class Post(models.Model):
|
|||
"""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
author_id: int
|
||||
interactions: "models.QuerySet[PostInteraction]"
|
||||
attachments: "models.QuerySet[PostAttachment]"
|
||||
|
||||
|
@ -933,6 +934,8 @@ class Post(models.Model):
|
|||
# state = StateField(PostStates)
|
||||
state = models.CharField(max_length=100, default="new")
|
||||
state_changed = models.DateTimeField(auto_now_add=True)
|
||||
state_next_attempt = models.DateTimeField(blank=True, null=True)
|
||||
state_locked_until = models.DateTimeField(null=True, blank=True, db_index=True)
|
||||
|
||||
# If it is our post or not
|
||||
local = models.BooleanField()
|
||||
|
@ -1090,6 +1093,7 @@ class Post(models.Model):
|
|||
attachments: list | None = None,
|
||||
type_data: dict | None = None,
|
||||
published: datetime.datetime | None = None,
|
||||
edited: datetime.datetime | None = None,
|
||||
) -> "Post":
|
||||
with transaction.atomic():
|
||||
# Find mentions in this post
|
||||
|
@ -1118,6 +1122,8 @@ class Post(models.Model):
|
|||
"hashtags": hashtags,
|
||||
"in_reply_to": reply_to.object_uri if reply_to else None,
|
||||
}
|
||||
if edited:
|
||||
post_obj["edited"] = edited
|
||||
if published:
|
||||
_delta = timezone.now() - published
|
||||
if _delta > datetime.timedelta(0):
|
||||
|
@ -1161,6 +1167,7 @@ class Post(models.Model):
|
|||
attachment_attributes: list | None = None,
|
||||
type_data: dict | None = None,
|
||||
published: datetime.datetime | None = None,
|
||||
edited: datetime.datetime | None = None,
|
||||
):
|
||||
with transaction.atomic():
|
||||
# Strip all HTML and apply linebreaks filter
|
||||
|
@ -1173,7 +1180,7 @@ class Post(models.Model):
|
|||
self.summary = summary or None
|
||||
self.sensitive = bool(summary) if sensitive is None else sensitive
|
||||
self.visibility = visibility
|
||||
self.edited = timezone.now()
|
||||
self.edited = edited or timezone.now()
|
||||
self.mentions.set(self.mentions_from_content(content, self.author))
|
||||
self.emojis.set(Emoji.emojis_from_content(content, None))
|
||||
if attachments is not None:
|
||||
|
@ -1232,7 +1239,9 @@ class Post(models.Model):
|
|||
type=PostInteraction.Types.boost,
|
||||
state__in=["new", "fanned_out"],
|
||||
).count(),
|
||||
"replies": Post.objects.filter(in_reply_to=self.object_uri).count(),
|
||||
"replies": Post.objects.filter(in_reply_to=self.object_uri)
|
||||
.exclude(state__in=["deleted", "deleted_fanned_out"])
|
||||
.count(),
|
||||
}
|
||||
if save:
|
||||
self.save()
|
||||
|
|
|
@ -442,14 +442,15 @@ class Takahe:
|
|||
@staticmethod
|
||||
def post(
|
||||
author_pk: int,
|
||||
pre_conetent: str,
|
||||
content: str,
|
||||
visibility: Visibilities,
|
||||
pre_conetent: str = "",
|
||||
summary: str | None = None,
|
||||
sensitive: bool = False,
|
||||
data: dict | None = None,
|
||||
post_pk: int | None = None,
|
||||
post_time: datetime.datetime | None = None,
|
||||
edit_time: datetime.datetime | None = None,
|
||||
reply_to_pk: int | None = None,
|
||||
attachments: list | None = None,
|
||||
) -> Post | None:
|
||||
|
@ -475,6 +476,7 @@ class Takahe:
|
|||
visibility=visibility,
|
||||
type_data=data,
|
||||
published=post_time,
|
||||
edited=edit_time,
|
||||
attachments=attachments,
|
||||
)
|
||||
else:
|
||||
|
@ -487,6 +489,7 @@ class Takahe:
|
|||
visibility=visibility,
|
||||
type_data=data,
|
||||
published=post_time,
|
||||
edited=edit_time,
|
||||
reply_to=reply_to_post,
|
||||
attachments=attachments,
|
||||
)
|
||||
|
@ -509,9 +512,24 @@ class Takahe:
|
|||
post = Post.objects.filter(pk=post_pk).first() if post_pk else None
|
||||
return post.object_uri if post else None
|
||||
|
||||
@staticmethod
|
||||
def update_post(post_pk, **kwargs):
|
||||
Post.objects.filter(pk=post_pk).update(**kwargs)
|
||||
|
||||
@staticmethod
|
||||
def delete_posts(post_pks):
|
||||
parent_posts = list(
|
||||
Post.objects.filter(
|
||||
object_uri__in=Post.objects.filter(
|
||||
pk__in=post_pks, in_reply_to__isnull=False
|
||||
)
|
||||
.distinct("in_reply_to")
|
||||
.values_list("in_reply_to", flat=True)
|
||||
)
|
||||
)
|
||||
Post.objects.filter(pk__in=post_pks).update(state="deleted")
|
||||
for post in parent_posts:
|
||||
post.calculate_stats()
|
||||
# TimelineEvent.objects.filter(subject_post__in=[post.pk]).delete()
|
||||
PostInteraction.objects.filter(post__in=post_pks).update(state="undone")
|
||||
|
||||
|
@ -538,6 +556,16 @@ class Takahe:
|
|||
else:
|
||||
return Takahe.Visibilities.public
|
||||
|
||||
@staticmethod
|
||||
def visibility_t2n(visibility: int) -> int:
|
||||
match visibility:
|
||||
case 2:
|
||||
return 1
|
||||
case 3:
|
||||
return 2
|
||||
case _:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def post_collection(collection: "Collection"):
|
||||
existing_post = collection.latest_post
|
||||
|
@ -573,9 +601,9 @@ class Takahe:
|
|||
}
|
||||
post = Takahe.post(
|
||||
collection.owner.pk,
|
||||
pre_conetent,
|
||||
content,
|
||||
visibility,
|
||||
pre_conetent,
|
||||
None,
|
||||
False,
|
||||
data,
|
||||
|
@ -621,9 +649,9 @@ class Takahe:
|
|||
existing_post = None if share_as_new_post else comment.latest_post
|
||||
post = Takahe.post(
|
||||
comment.owner.pk,
|
||||
pre_conetent,
|
||||
content,
|
||||
v,
|
||||
pre_conetent,
|
||||
spoiler,
|
||||
spoiler is not None,
|
||||
data,
|
||||
|
@ -668,9 +696,9 @@ class Takahe:
|
|||
existing_post = None if share_as_new_post else review.latest_post
|
||||
post = Takahe.post( # TODO post as Article?
|
||||
review.owner.pk,
|
||||
pre_conetent,
|
||||
content,
|
||||
v,
|
||||
pre_conetent,
|
||||
None,
|
||||
False,
|
||||
data,
|
||||
|
@ -712,9 +740,9 @@ class Takahe:
|
|||
)
|
||||
post = Takahe.post(
|
||||
mark.owner.pk,
|
||||
pre_conetent,
|
||||
content + append_content,
|
||||
v,
|
||||
pre_conetent,
|
||||
spoiler,
|
||||
spoiler is not None,
|
||||
data,
|
||||
|
@ -768,7 +796,7 @@ class Takahe:
|
|||
def reply_post(
|
||||
post_pk: int, identity_pk: int, content: str, visibility: Visibilities
|
||||
):
|
||||
return Takahe.post(identity_pk, "", content, visibility, reply_to_pk=post_pk)
|
||||
return Takahe.post(identity_pk, content, visibility, reply_to_pk=post_pk)
|
||||
|
||||
@staticmethod
|
||||
def boost_post(post_pk: int, identity_pk: int):
|
||||
|
@ -859,7 +887,7 @@ class Takahe:
|
|||
return FediverseHtmlParser(linebreaks_filter(txt)).html
|
||||
|
||||
@staticmethod
|
||||
def update_state(obj, state):
|
||||
def update_state(obj: Post | Relay, state: str):
|
||||
obj.state = state
|
||||
obj.state_changed = timezone.now()
|
||||
obj.state_next_attempt = None
|
||||
|
|
Loading…
Add table
Reference in a new issue