import datetime import logging import json import os import urllib.parse import traceback from pelican import signals log = logging.getLogger(__name__) __version__ = '0.1' pagination = 25 def ap_article(generator, writer): now = datetime.datetime.utcnow() author = generator.settings['AUTHOR'] domain = urllib.parse.urlparse(generator.settings['SITEURL']).netloc wkhmpath = os.path.join(writer.output_path, '.well-known/host-meta') wknipath = os.path.join(writer.output_path, '.well-known/nodeinfo') nipath = os.path.join(writer.output_path, 'activitypub/nodeinfo') wfpath = os.path.join(writer.output_path, '.well-known/webfinger') awfpath = os.path.join(writer.output_path, '.well-known/_webfinger') authorpath = os.path.join(writer.output_path, 'activitypub/users') os.makedirs(os.path.join(writer.output_path, '.well-known'), exist_ok=True) os.makedirs(awfpath, exist_ok=True) os.makedirs(os.path.join(writer.output_path, 'activitypub/users'), exist_ok=True) os.makedirs(os.path.join(writer.output_path, 'activitypub/posts'), exist_ok=True) os.makedirs(os.path.join(writer.output_path, 'activitypub/tags'), exist_ok=True) os.makedirs(os.path.join(writer.output_path, 'activitypub/collections/inbox'), exist_ok=True) os.makedirs(os.path.join(writer.output_path, 'activitypub/collections/outbox'), exist_ok=True) for author, _ in generator.authors: os.makedirs(os.path.join(writer.output_path, 'activitypub/collections/outbox_page', author.slug), exist_ok=True) os.makedirs(os.path.join(writer.output_path, 'activitypub/collections/following'), exist_ok=True) os.makedirs(os.path.join(writer.output_path, 'activitypub/collections/followers'), exist_ok=True) wknodeinfo = { 'links': [ { 'href': os.path.join(writer.site_url, 'activitypub/nodeinfo'), 'rel': 'http://nodeinfo.diaspora.software/ns/schema/2.0' } ] } nodeinfo = { 'version': '2.0', 'software': { 'name': 'pelican-activitypub', 'version': __version__ }, 'protocols': ['activitypub'], 'services': { 'inbound': [], 'outbound': ['atom1.0', 'rss2.0'] }, 'openRegistrations': False, 'usage': { 'users': { 'total': len(generator.authors), }, 'localPosts': len(generator.articles) }, 'metadata': { 'nodeName': generator.settings['SITENAME'], } } wfurl = os.path.join(generator.settings['SITEURL'], '.well-known/webfinger?resource={uri}') hostmeta = f'' webfinger = { 'subject': f'acct:{author}@{domain}', 'aliases': [ os.path.join(generator.settings['SITEURL'], 'author', author.slug + '.html'), os.path.join(generator.settings['SITEURL'], 'activitypub/users/', author.slug) ], 'links': [ { 'rel': 'http://webfinger.net/rel/profile-page', 'type': 'text/html', 'href': os.path.join(generator.settings['SITEURL'], 'author', author.slug + '.html') }, { 'rel': 'self', 'type': 'application/activity+json', 'href': os.path.join(generator.settings['SITEURL'], 'activitypub/users', author.slug) } ] } with open(wkhmpath, 'w') as hf: hf.write(hostmeta) with open(wknipath, 'w') as nf: json.dump(wknodeinfo, nf) with open(nipath, 'w') as nf: json.dump(nodeinfo, nf) with open(wfpath, 'w') as wf: json.dump(webfinger, wf) for t in generator.tags: url = os.path.join(generator.settings['SITEURL'], 'activitypub/tags', t.slug) path = os.path.join(writer.output_path, 'activitypub/tags', t.slug) articles = [] for article in generator.articles: if t.name not in article.tags: continue articles.append( os.path.join(generator.settings['SITEURL'], 'activitypub/posts', article.slug) ) tag = { '@context': ['https://www.w3.org/ns/activitystreams'], 'id': url, 'type': 'OrderedCollection', 'totalItems': len(articles), 'orderedItems': articles } with open(path, 'w') as f: json.dump(tag, f) articlemap = {} for article in generator.articles: aurl = os.path.join(generator.settings['SITEURL'], 'activitypub/posts', article.slug) apath = os.path.join(writer.output_path, 'activitypub/posts', article.slug) tags = [] for tag in article.tags: tags.append({ 'type': 'Hashtag', 'name': '#' + tag.slug, 'href': os.path.join(generator.settings['SITEURL'], 'activitypub/tags', tag.slug) }) replyto = None if 'series' in article.metadata and 'seriesindex' in article.metadata: series = article.metadata['series'] sindex = int(article.metadata['seriesindex']) for sa in generator.articles: if 'series' in sa.metadata and 'seriesindex' in sa.metadata \ and sa.metadata['series'] == series and int(sa.metadata['seriesindex']) == sindex - 1: replyto = os.path.join(generator.settings['SITEURL'], 'activitypub/posts', sa.slug) break cc = [os.path.join(generator.settings['SITEURL'], 'activitypub/collections/followers', article.author.slug)] aa = generator.settings.get('ACTIVITYPUB_AUTHORS', {}).get(author.name, {}) if 'movedTo' in aa: cc.append(aa['movedTo']) tags.append({ 'type': 'Mention', 'href': aa['movedTo'], 'name': aa['_movedTo_name'] }) cmap = {} tmap = {} for lang in article.translations + [article]: tmap[lang.lang] = lang.title cmap[lang.lang] = lang.content articlemap[article.slug] = { '@context': ['https://www.w3.org/ns/activitystreams'], 'type': 'Article', 'id': aurl, 'published': article.date.isoformat(timespec='seconds'), 'inReplyTo': replyto, 'url': os.path.join(generator.settings['SITEURL'], article.url), 'attributedTo': os.path.join(generator.settings['SITEURL'], 'activitypub/users', article.author.slug), 'to': ['https://www.w3.org/ns/activitystreams#Public'], 'cc': cc, 'name': article.title, 'nameMap': tmap, 'content': article.content, 'contentMap': cmap, 'summary': None, 'attachment': [], 'tag': tags } with open(apath, 'w') as f: json.dump(articlemap[article.slug], f) for author, articles in generator.authors: aa = generator.settings.get('ACTIVITYPUB_AUTHORS', {}).get(author.name, {}) url = os.path.join(generator.settings['SITEURL'], 'activitypub/users', author.slug) wwwurl = os.path.join(generator.settings['SITEURL'], 'author', author.slug + '.html') inboxurl = os.path.join(generator.settings['SITEURL'], 'activitypub/collections/inbox', author.slug) inboxpath = os.path.join(writer.output_path, 'activitypub/collections/inbox', author.slug) outboxurl = os.path.join(generator.settings['SITEURL'], 'activitypub/collections/outbox', author.slug) outboxpath = os.path.join(writer.output_path, 'activitypub/collections/outbox', author.slug) followersurl = os.path.join(generator.settings['SITEURL'], 'activitypub/collections/followers', author.slug) followerspath = os.path.join(writer.output_path, 'activitypub/collections/followers', author.slug) followingurl = os.path.join(generator.settings['SITEURL'], 'activitypub/collections/following', author.slug) followingpath = os.path.join(writer.output_path, 'activitypub/collections/following', author.slug) creates = [] for article in articles: art = articlemap[article.slug] creates.append({ '@context': ['https://www.w3.org/ns/activitystreams'], 'type': 'Create', 'id': os.path.join(generator.settings['SITEURL'], 'activitypub/posts', article.slug), 'actor': url, 'published': article.date.isoformat(timespec='seconds'), 'to': art['to'], 'cc': art['cc'], 'object': art }) if len(articles) == 0: published = now.isoformat(timespec='seconds') + 'Z' else: published = min([x.date for x in articles]).isoformat(timespec='seconds') a = { '@context': [ 'https://www.w3.org/ns/activitystreams', { 'schema': 'http://schema.org#', 'toot': 'http://joinmastodon.org/ns#', 'PropertyValue': 'schema:PropertyValue', 'value': 'schema:value', 'alsoKnownAs': { '@id': 'as:alsoKnownAs', '@type': '@id' }, 'movedTo': { '@id': 'as:movedTo', '@type': '@id' }, 'discoverable': 'toot:discoverable', } ], 'type': 'Person', 'id': url, 'inbox': inboxurl, 'outbox': outboxurl, 'following': followingurl, 'followers': followersurl, 'preferredUsername': author.name, 'url': wwwurl, 'name': author.slug, 'discoverable': True, 'manuallyApprovesFollowers': True, 'published': published, 'updated': now.isoformat(timespec='seconds') + 'Z', 'tag': [], 'attachment': [], 'endpoints': { 'sharedInbox': inboxurl }, 'icon': {}, 'image': {} } a.update({k: v for k, v in aa.items() if not k.startswith('_')}) inbox = { '@context': ['https://www.w3.org/ns/activitystreams'], 'type': 'OrderedCollection', 'id': inboxurl, 'totalItems': 0, 'orderedItems': [] } maxpage = len(creates) // pagination outbox = { '@context': ['https://www.w3.org/ns/activitystreams'], 'type': 'OrderedCollection', 'id': outboxurl, 'totalItems': len(creates), 'first': os.path.join(generator.settings['SITEURL'], 'activitypub/collections/outbox_page/', author.slug, str(0)), 'last': os.path.join(generator.settings['SITEURL'], 'activitypub/collections/outbox_page/', author.slug, str(maxpage)) } following = { '@context': ['https://www.w3.org/ns/activitystreams'], 'type': 'OrderedCollection', 'id': followingurl, 'totalItems': 0, 'orderedItems': [] } followers = { '@context': ['https://www.w3.org/ns/activitystreams'], 'type': 'OrderedCollection', 'id': followersurl, 'totalItems': 0, 'orderedItems': [] } for i in range(0, len(creates), pagination): ipage = i // pagination outpageurl = os.path.join(generator.settings['SITEURL'], 'activitypub/collections/outbox_page', author.slug, str(ipage)) outpagepath = os.path.join(writer.output_path, 'activitypub/collections/outbox_page', author.slug, str(ipage)) page = { '@context': ['https://www.w3.org/ns/activitystreams'], 'type': 'OrderedCollectionPage', 'id': outpageurl, 'totalItems': len(creates), 'partOf': outboxurl, 'orderedItems': creates[i:i+pagination] } if ipage > 0: page['prev'] = os.path.join(generator.settings['SITEURL'], 'activitypub/collections/outbox_page', author.slug, str(ipage-1)) if ipage < maxpage: page['next'] = os.path.join(generator.settings['SITEURL'], 'activitypub/collections/outbox_page', author.slug, str(ipage+1)) with open(outpagepath, 'w') as f: json.dump(page, f) author_webfinger = { 'subject': f'acct:{author.name}@{domain}', 'aliases': [ os.path.join(generator.settings['SITEURL'], 'author', author.name + '.html'), os.path.join(generator.settings['SITEURL'], 'activitypub/users/', author.name) ], 'links': [ { 'rel': 'http://webfinger.net/rel/profile-page', 'type': 'text/html', 'href': os.path.join(generator.settings['SITEURL'], 'author', author.name + '.html') }, { 'rel': 'self', 'type': 'application/activity+json', 'href': os.path.join(generator.settings['SITEURL'], 'activitypub/users', author.name) } ] } with open(os.path.join(awfpath, author.name), 'w') as f: json.dump(author_webfinger, f) with open(os.path.join(authorpath, author.name), 'w') as f: json.dump(a, f) with open(inboxpath, 'w') as f: json.dump(inbox, f) with open(outboxpath, 'w') as f: json.dump(outbox, f) with open(followingpath, 'w') as f: json.dump(following, f) with open(followerspath, 'w') as f: json.dump(followers, f) def register(): signals.article_writer_finalized.connect(ap_article)