"""app.bsky.feed.* XRPC methods.""" import json import logging import re from granary import bluesky, microformats2 import mf2util from oauth_dropins.webutil import util from oauth_dropins.webutil.util import json_loads from app import xrpc_server from models import Object logger = logging.getLogger(__name__) @xrpc_server.method('app.bsky.feed.getAuthorFeed') def getAuthorFeed(input, author=None, limit=None, before=None): """ lexicons/app/bsky/feed/getAuthorFeed.json, feedViewPost.json """ if not author or not re.match(util.DOMAIN_RE, author): raise ValueError(f'{author} is not a domain') url = f'https://{author}/' mf2 = util.fetch_mf2(url, gateway=True) logger.info(f'Got mf2: {json.dumps(mf2, indent=2)}') feed_author = mf2util.find_author(mf2, source_url=url, fetch_mf2_func=util.fetch_mf2) if feed_author: logger.info(f'Authorship found: {feed_author}') actor = { 'url': feed_author.get('url') or url, 'displayName': feed_author.get('name'), 'image': {'url': feed_author.get('photo')}, } else: logger.info(f'No authorship result on {url} ; generated {feed_author}') actor = { 'url': url, 'displayName': author, } activities = microformats2.json_to_activities(mf2) # default actor to feed author for a in activities: a.setdefault('actor', actor) logger.info(f'AS1 activities: {json.dumps(activities, indent=2)}') return {'feed': [bluesky.from_as1(a) for a in activities]} @xrpc_server.method('app.bsky.feed.getPostThread') def getPostThread(input, uri=None, depth=None): """ lexicons/app/bsky/feed/getPostThread.json """ if not uri: raise ValueError('Missing uri') mf2 = util.fetch_mf2(uri, gateway=True) logger.info(f'Got mf2: {json.dumps(mf2, indent=2)}') entry = mf2util.find_first_entry(mf2, ['h-entry']) logger.info(f'Entry: {json.dumps(entry, indent=2)}') if not entry: raise ValueError(f"No h-entry on {uri}") obj = microformats2.json_to_object(entry) logger.info(f'AS1: {json.dumps(obj, indent=2)}') return { 'thread': { '$type': 'app.bsky.feed.getPostThread#threadViewPost', 'post': bluesky.from_as1(obj)['post'], 'replies': [{ '$type': 'app.bsky.feed.getPostThread#threadViewPost', 'post': bluesky.from_as1(reply)['post'], } for reply in obj.get('replies', {}).get('items', [])], }, } # TODO # what's the mf2 for repost children of an h-entry? u-repost, like u-comment? # nothing about markup on https://indieweb.org/reposts # based on https://indieweb.org/comments-display , it would be u-repost # @xrpc_server.method('app.bsky.feed.getRepostedBy') # def getRepostedBy(input, uri=None, cid=None, limit=None, before=None): # """ # lexicons/app/bsky/feed/getRepostedBy.json # """ # mf2 = util.fetch_mf2(uri, gateway=True) # logger.info(f'Got mf2: {json.dumps(mf2, indent=2)}') # entry = mf2util.find_first_entry(mf2, ['h-entry']) # logger.info(f'Entry: {json.dumps(entry, indent=2)}') # if not entry: # raise ValueError(f"No h-entry on {uri}") # obj = microformats2.json_to_object(entry) # logger.info(f'AS1: {json.dumps(obj, indent=2)}') # return { # 'uri': 'http://orig/post', # 'repostBy': [{ # '$type': 'app.bsky.feed.getRepostedBy#repostedBy', # 'did': 'did:web:eve.net', # 'declaration': { # '$type': 'app.bsky.system.declRef', # 'cid': 'TODO', # 'actorType': 'app.bsky.system.actorUser', # }, # 'handle': 'eve.net', # 'displayName': 'Eve', # 'indexedAt': '2022-01-02T03:04:05+00:00', # }], # } # TODO: cursor @xrpc_server.method('app.bsky.feed.getTimeline') def getTimeline(input, algorithm=None, limit=50, before=None): """ lexicons/app/bsky/feed/getTimeline.json """ # TODO: how to get authed user? user = 'foo.com' # TODO: de-dupe with pages.feed() logger.info(f'Fetching {limit} objects for {user}') objects, _, _ = Object.query( Object.domains == user, Object.labels == 'feed' ).order(-Object.created ).fetch_page(limit) return {'feed': [bluesky.from_as1(json_loads(obj.as1)) for obj in objects]} # TODO: use likes as votes? @xrpc_server.method('app.bsky.feed.getVotes') def getVotes(input, uri=None, direction=None, cid=None, limit=None, before=None): """ lexicons/app/bsky/feed/getVotes.json """ return { 'uri': uri, 'votes': [], }