From f0f5b7819f15869f19ca450be0941dbc475f29e7 Mon Sep 17 00:00:00 2001 From: Her Email Date: Sun, 19 Nov 2023 13:39:43 -0500 Subject: [PATCH 1/3] update readme with more AP details --- README.md | 79 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 34b4f8b9..7984f650 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,56 @@ -# Boofilsic/NeoDB +# NeoDB ![](https://github.com/neodb-social/neodb/actions/workflows/check.yml/badge.svg?branch=main) ![](https://github.com/neodb-social/neodb/actions/workflows/tests.yml/badge.svg?branch=main) ![](https://github.com/neodb-social/neodb/actions/workflows/publish.yml/badge.svg?branch=main) -Boofilsic/NeoDB is an open source project and free service to help users manage, share and discover collections, reviews and ratings for culture products (e.g. books, movies, music, podcasts, games and performances) in Fediverse. +NeoDB (fka boofilsic) is an open source project and free service to help users manage, share and discover collections, reviews and ratings for culture products (e.g. books, movies, music, podcasts, games and performances) in Fediverse. [NeoDB.social](https://neodb.social) and [NiceDB](https://nicedb.org) are free instances hosted by volunteers. Your support is essential to keep the service free and open-sourced. [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/neodb) ## Features - - Manage a shared catalog of books/movies/tv shows/music album/games/podcasts/performances - + search or create catalog items in each category - + one click create item with links to 3rd party sites: - * Goodreads - * IMDB - * The Movie Database - * Douban - * Google Books - * Discogs - * Spotify - * Apple Music - * Bandcamp - * Steam - * IGDB - * Bangumi - * any RSS link to a podcast - - Logged in users can manage their collections: - + mark an item as wishlist/in progress/complete - + rate and write reviews for an item - + create tags for an item, either privately or publicly - + create and share list of items - + tracking progress of a list (e.g. personal reading challenges) - + Import and export full user data archive - + import list or archives from some 3rd party sites: - * Goodreads reading list - * Douban archive (via Doufen) - - Social features: - + view home feed with friends' activities - * every activity can be set as viewable to self/follower-only/public - * eligible items, e.g. podcasts and albums, are playable in feed - + link Fediverse account and import social graph - + share collections and reviews to Fediverse ~~and Twitter~~ feed - + ActivityPub support is under active development - - Other - + i18n/language support are planned +- Manage a shared catalog of books/movies/tv shows/music album/games/podcasts/performances + + search or create catalog items in each category + + one click create item with links to 3rd party sites: + * Goodreads + * IMDB + * The Movie Database + * Douban + * Google Books + * Discogs + * Spotify + * Apple Music + * Bandcamp + * Steam + * IGDB + * Bangumi + * any RSS link to a podcast +- Logged in users can manage their collections: + + mark an item as wishlist/in progress/complete + + rate and write reviews for an item + + create tags for an item, either privately or publicly + + create and share list of items + + tracking progress of a list (e.g. personal reading challenges) + + Import and export full user data archive + + import list or archives from some 3rd party sites: + * Goodreads reading list + * Douban archive (via Doufen) +- Social features: + + view home feed with friends' activities + * every activity can be set as viewable to self/follower-only/public + * eligible items, e.g. podcasts and albums, are playable in feed + + login with other Fediverse server account and import social graph + * supported servers: Mastodon/Pleroma/Firefish/GoToSocial/Pixelfed/friendica/Takahē + + share collections and reviews to Fediverse ~~and Twitter~~ feed + + ActivityPub support is under active development + * NeoDB users can interact with users on other ActivityPub services like Mastodon and Pleroma + * NeoDB instances communicate via an extended version of ActivityPub + * NeoDB instances may share public rating and reviews with relays + * implementation is based on [Takahē](https://jointakahe.org/) server +- Other + + i18n/language support are planned ## Install Please see [doc/install.md](doc/install.md) From 3ce10db5974bcfffc2de1bc39516303392af7b1d Mon Sep 17 00:00:00 2001 From: Her Email Date: Sun, 19 Nov 2023 15:19:49 -0500 Subject: [PATCH 2/3] post comment as ap --- catalog/podcast/models.py | 6 +-- journal/models/comment.py | 10 +++- journal/models/shelf.py | 1 + journal/views/__init__.py | 11 +--- journal/views/mark.py | 102 ++++++-------------------------------- takahe/ap_handlers.py | 30 +++++------ takahe/utils.py | 44 ++++++++++++++++ 7 files changed, 88 insertions(+), 116 deletions(-) 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 @@ def display_title(self): 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 @@ def ap_object(self): "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 @@ def update_by_ap_object(cls, owner, item, obj, post_id, 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) return p @@ -65,7 +71,7 @@ def mark(self): @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 @@ def ap_object(self): 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 @@ 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 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 + 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 + ) + post = Takahe.post_comment(comment, False) 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 + if post and share_to_mastodon and request.user.mastodon_username: + boost_toot( + request.user.mastodon_site, + request.user.mastodon_token, + post.url, ) - 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, - }, - }, - ) - - # 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) + # 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 @@ "Album", "Game", "Podcast", + "PodcastEpisode", "Performance", "PerformanceProduction", ] @@ -30,14 +31,12 @@ } -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 @@ def get_post_url(post_pk: int) -> str | None: 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 From e110158dfa79b159667bdc308f98ee30d8aacd20 Mon Sep 17 00:00:00 2001 From: Her Email Date: Sun, 19 Nov 2023 15:27:17 -0500 Subject: [PATCH 3/3] sync takahe:main --- neodb-takahe | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neodb-takahe b/neodb-takahe index c1f202a9..a8f8f9d5 160000 --- a/neodb-takahe +++ b/neodb-takahe @@ -1 +1 @@ -Subproject commit c1f202a958b0c90a605042d9157f7dc246a5ac9a +Subproject commit a8f8f9d5931b062d1b8bb9455db7e7f0ad79ff41