diff --git a/catalog/podcast/models.py b/catalog/podcast/models.py index e2bd6047..52eb36b0 100644 --- a/catalog/podcast/models.py +++ b/catalog/podcast/models.py @@ -111,11 +111,11 @@ class PodcastEpisode(Item): def cover_image_url(self): return self.cover_url or self.program.cover_image_url - def get_absolute_url_with_position(self, position=None): + def get_url_with_position(self, position=None): return ( - self.absolute_url + self.url if position is None or position == "" - else f"{self.absolute_url}?position={position}" + else f"{self.url}?position={position}" ) class Meta: diff --git a/journal/models/comment.py b/journal/models/comment.py index 78189ff5..29453dc8 100644 --- a/journal/models/comment.py +++ b/journal/models/comment.py @@ -17,7 +17,7 @@ class Comment(Content): @property def ap_object(self): - return { + d = { "id": self.absolute_url, "type": "Comment", "content": self.text, @@ -27,6 +27,10 @@ class Comment(Content): "relatedWith": self.item.absolute_url, "href": self.absolute_url, } + if self.metadata.get("position"): + d["relatedWithItemPosition"] = self.metadata["position"] + d["relatedWithItemPositionType"] = "time" + return d @classmethod def update_by_ap_object(cls, owner, item, obj, post_id, visibility): @@ -42,6 +46,8 @@ class Comment(Content): "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) return p @@ -65,7 +71,7 @@ class Comment(Content): @property def item_url(self): if self.metadata.get("position"): - return self.item.get_absolute_url_with_position(self.metadata["position"]) + return self.item.get_url_with_position(self.metadata["position"]) else: return self.item.url diff --git a/journal/models/shelf.py b/journal/models/shelf.py index 0560325f..fd6abb9a 100644 --- a/journal/models/shelf.py +++ b/journal/models/shelf.py @@ -80,6 +80,7 @@ class ShelfMember(ListMember): def update_by_ap_object( cls, owner: APIdentity, item: Identity, obj: dict, post_id: int, visibility: int ): + # TODO check timestamp? (update may come in with inconsistent sequence) if not obj: cls.objects.filter(owner=owner, item=item).delete() return diff --git a/journal/views/__init__.py b/journal/views/__init__.py index aa58787f..5e78ce29 100644 --- a/journal/views/__init__.py +++ b/journal/views/__init__.py @@ -15,16 +15,7 @@ from .collection import ( user_liked_collection_list, ) from .common import piece_delete -from .mark import ( - comment, - like, - mark, - mark_log, - share_comment, - unlike, - user_mark_list, - wish, -) +from .mark import comment, like, mark, mark_log, unlike, user_mark_list, wish from .post import piece_replies, post_like, post_replies, post_reply, post_unlike from .profile import profile, user_calendar_data from .review import ReviewFeed, review_edit, review_retrieve, user_review_list diff --git a/journal/views/mark.py b/journal/views/mark.py index 3a93bd8b..f5821ece 100644 --- a/journal/views/mark.py +++ b/journal/views/mark.py @@ -13,12 +13,7 @@ from django.utils.translation import gettext_lazy as _ from catalog.models import * from common.utils import AuthedHttpRequest, PageLinksGenerator, get_uuid_or_404 -from mastodon.api import ( - get_spoiler_text, - get_status_id_by_url, - get_visibility, - post_toot, -) +from mastodon.api import boost_toot from takahe.utils import Takahe from ..models import Comment, Mark, Piece, ShelfType, ShelfTypeNames, TagManager @@ -168,36 +163,6 @@ def mark(request: AuthedHttpRequest, item_uuid): raise BadRequest() -def share_comment(user, item, text, visibility, shared_link=None, position=None): - post_error = False - status_id = get_status_id_by_url(shared_link) - link = ( - item.get_absolute_url_with_position(position) if position else item.absolute_url - ) - action_label = "评论" if text else "分享" - status = f"{action_label}{ItemCategory(item.category).label}《{item.display_title}》\n{link}\n\n{text}" - spoiler, status = get_spoiler_text(status, item) - try: - response = post_toot( - user.mastodon_site, - status, - get_visibility(visibility, user), - user.mastodon_token, - False, - status_id, - spoiler, - ) - if response and response.status_code in [200, 201]: - j = response.json() - if "url" in j: - shared_link = j["url"] - except Exception as e: - if settings.DEBUG: - raise - post_error = True - return post_error, shared_link - - @login_required def mark_log(request: AuthedHttpRequest, item_uuid, log_id): """ @@ -220,15 +185,7 @@ def comment(request: AuthedHttpRequest, item_uuid): item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) if not item.class_name in ["podcastepisode", "tvepisode"]: raise BadRequest("不支持评论此类型的条目") - # episode = None - # if item.class_name == "tvseason": - # try: - # episode = int(request.POST.get("episode", 0)) - # except: - # episode = 0 - # if episode <= 0: - # raise BadRequest("请输入正确的集数") - comment = Comment.objects.filter(owner=request.user, item=item).first() + comment = Comment.objects.filter(owner=request.user.identity, item=item).first() if request.method == "GET": return render( request, @@ -256,49 +213,22 @@ def comment(request: AuthedHttpRequest, item_uuid): if settings.DEBUG: raise position = None - share_to_mastodon = bool(request.POST.get("share_to_mastodon", default=False)) - shared_link = comment.metadata.get("shared_link") if comment else None - post_error = False - if share_to_mastodon and request.user.mastodon_username: - post_error, shared_link = share_comment( - request.user, item, text, visibility, shared_link, position - ) - Comment.objects.update_or_create( - owner=request.user, - item=item, - # metadata__episode=episode, - defaults={ - "text": text, - "visibility": visibility, - "metadata": { - "shared_link": shared_link, - "position": position, - }, - }, + d = {"text": text, "visibility": visibility} + if position: + d["metadata"] = {"position": position} + comment, _ = Comment.objects.update_or_create( + owner=request.user.identity, item=item, defaults=d ) - - # if comment: - # comment.visibility = visibility - # comment.text = text - # comment.metadata["position"] = position - # comment.metadata["episode"] = episode - # if shared_link: - # comment.metadata["shared_link"] = shared_link - # comment.save() - # else: - # comment = Comment.objects.create( - # owner=request.user, - # item=item, - # text=text, - # visibility=visibility, - # metadata={ - # "shared_link": shared_link, - # "position": position, - # "episode": episode, - # }, - # ) - if post_error: - return render_relogin(request) + post = Takahe.post_comment(comment, False) + share_to_mastodon = bool(request.POST.get("share_to_mastodon", default=False)) + if post and share_to_mastodon and request.user.mastodon_username: + boost_toot( + request.user.mastodon_site, + request.user.mastodon_token, + post.url, + ) + # if post_error: + # return render_relogin(request) return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) raise BadRequest() diff --git a/takahe/ap_handlers.py b/takahe/ap_handlers.py index b5aabce7..029a6118 100644 --- a/takahe/ap_handlers.py +++ b/takahe/ap_handlers.py @@ -18,6 +18,7 @@ _supported_ap_catalog_item_types = [ "Album", "Game", "Podcast", + "PodcastEpisode", "Performance", "PerformanceProduction", ] @@ -30,14 +31,12 @@ _supported_ap_journal_types = { } -def _parse_item_links(objects): +def _parse_items(objects): logger.debug(f"Parsing item links from {objects}") if not objects: return [] objs = objects if isinstance(objects, list) else [objects] - items = [ - obj["href"] for obj in objs if obj["type"] in _supported_ap_catalog_item_types - ] + items = [obj for obj in objs if obj["type"] in _supported_ap_catalog_item_types] return items @@ -55,8 +54,14 @@ def _parse_piece_objects(objects): return pieces -def _get_or_create_item_by_ap_url(url): - logger.debug(f"Fetching item by ap from {url}") +def _get_or_create_item(item_obj): + logger.debug(f"Fetching item by ap from {item_obj}") + typ = item_obj["type"] + url = item_obj["href"] + if typ in ["TVEpisode", "PodcastEpisode"]: + # TODO support episode item + # match and fetch parent item first + return None site = SiteManager.get_site_by_url(url) if not site: return None @@ -75,13 +80,13 @@ def _get_visibility(post_visibility): return 0 -def _update_or_create_post(pk, obj): +def post_fetched(pk, obj): post = Post.objects.get(pk=pk) owner = Takahe.get_or_create_remote_apidentity(post.author) if not post.type_data: logger.warning(f"Post {post} has no type_data") return - items = _parse_item_links(post.type_data["object"]["tag"]) + items = _parse_items(post.type_data["object"]["tag"]) pieces = _parse_piece_objects(post.type_data["object"]["relatedWith"]) logger.info(f"Post {post} has items {items} and pieces {pieces}") if len(items) == 0: @@ -90,20 +95,15 @@ def _update_or_create_post(pk, obj): elif len(items) > 1: logger.warning(f"Post {post} has more than one remote item") return - remote_url = items[0] - item = _get_or_create_item_by_ap_url(remote_url) + item = _get_or_create_item(items[0]) if not item: - logger.warning(f"Post {post} has no local item") + logger.warning(f"Post {post} has no local item matched or created") return for p in pieces: cls = _supported_ap_journal_types[p["type"]] cls.update_by_ap_object(owner, item, p, pk, _get_visibility(post.visibility)) -def post_fetched(pk, obj): - _update_or_create_post(pk, obj) - - def post_deleted(pk, obj): Piece.objects.filter(posts__id=pk, local=False).delete() diff --git a/takahe/utils.py b/takahe/utils.py index 897f357a..4fbc543e 100644 --- a/takahe/utils.py +++ b/takahe/utils.py @@ -397,6 +397,50 @@ class Takahe: def delete_posts(post_pks): Post.objects.filter(pk__in=post_pks).update(state="deleted") + @staticmethod + def post_comment(comment, share_as_new_post: bool) -> Post | None: + from catalog.common import ItemCategory + + user = comment.owner.user + category = str(ItemCategory(comment.item.category).label) + tags = ( + "\n" + user.preference.mastodon_append_tag.replace("[category]", category) + if user.preference.mastodon_append_tag + else "" + ) + item_link = f"{settings.SITE_INFO['site_url']}/~neodb~{comment.item_url}" + action_label = "评论" if comment.text else "分享" + pre_conetent = f'{action_label}{category}《{comment.item.display_title}》
' + content = f"{comment.text}\n{tags}" + data = { + "object": { + "tag": [comment.item.ap_object_ref], + "relatedWith": [comment.ap_object], + } + } + if comment.visibility == 1: + v = Takahe.Visibilities.followers + elif comment.visibility == 2: + v = Takahe.Visibilities.mentioned + elif user.preference.mastodon_publish_public: + v = Takahe.Visibilities.public + else: + v = Takahe.Visibilities.unlisted + existing_post = None if share_as_new_post else comment.latest_post + post = Takahe.post( # TODO post as Article? + comment.owner.pk, + pre_conetent, + content, + v, + data, + existing_post.pk if existing_post else None, + comment.created_time, + ) + if not post: + return + comment.link_post(post) + return post + @staticmethod def post_review(review, share_as_new_post: bool) -> Post | None: from catalog.common import ItemCategory