Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Publish comment as ActivityPub Note #385

Merged
merged 3 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 42 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
6 changes: 3 additions & 3 deletions catalog/podcast/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 8 additions & 2 deletions journal/models/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Comment(Content):

@property
def ap_object(self):
return {
d = {
"id": self.absolute_url,
"type": "Comment",
"content": self.text,
Expand All @@ -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):
Expand All @@ -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
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions journal/models/shelf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 1 addition & 10 deletions journal/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
102 changes: 16 additions & 86 deletions journal/views/mark.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand All @@ -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,
Expand Down Expand Up @@ -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()

Expand Down
30 changes: 15 additions & 15 deletions takahe/ap_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"Album",
"Game",
"Podcast",
"PodcastEpisode",
"Performance",
"PerformanceProduction",
]
Expand All @@ -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


Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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()

Expand Down
Loading