def get(self, domain): url = 'http://%s/' % domain resp = common.requests_get(url) mf2 = mf2py.parse(resp.text, url=resp.url, img_with_alt=True) # logging.info('Parsed mf2 for %s: %s', resp.url, json.dumps(mf2, indent=2)) hcard = mf2util.representative_hcard(mf2, resp.url) logging.info('Representative h-card: %s', json.dumps(hcard, indent=2)) if not hcard: common.error( self, """\ Couldn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on %s""" % resp.url) key = MagicKey.get_or_create(domain) obj = common.postprocess_as2(as2.from_as1( microformats2.json_to_object(hcard)), key=key) obj.update({ 'inbox': '%s/%s/inbox' % (appengine_config.HOST_URL, domain), 'outbox': '%s/%s/outbox' % (appengine_config.HOST_URL, domain), 'following': '%s/%s/following' % (appengine_config.HOST_URL, domain), 'followers': '%s/%s/followers' % (appengine_config.HOST_URL, domain), }) logging.info('Returning: %s', json.dumps(obj, indent=2)) self.response.headers.update({ 'Content-Type': common.CONTENT_TYPE_AS2, 'Access-Control-Allow-Origin': '*', }) self.response.write(json.dumps(obj, indent=2))
def actor(domain): """Serves /[DOMAIN], fetches its mf2, converts to AS Actor, and serves it.""" tld = domain.split('.')[-1] if tld in common.TLD_BLOCKLIST: error('', status=404) mf2 = util.fetch_mf2(f'http://{domain}/', gateway=True, headers=common.HEADERS) hcard = mf2util.representative_hcard(mf2, mf2['url']) logging.info(f'Representative h-card: {json_dumps(hcard, indent=2)}') if not hcard: error( f"Couldn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on {mf2['url']}" ) key = MagicKey.get_or_create(domain) obj = common.postprocess_as2(as2.from_as1( microformats2.json_to_object(hcard)), key=key) obj.update({ 'preferredUsername': domain, 'inbox': f'{request.host_url}{domain}/inbox', 'outbox': f'{request.host_url}{domain}/outbox', 'following': f'{request.host_url}{domain}/following', 'followers': f'{request.host_url}{domain}/followers', }) logging.info(f'Returning: {json_dumps(obj, indent=2)}') return (obj, { 'Content-Type': common.CONTENT_TYPE_AS2, 'Access-Control-Allow-Origin': '*', })
def test_magic_key_get_or_create(self): assert self.key.mod assert self.key.public_exponent assert self.key.private_exponent same = MagicKey.get_or_create('y.z') self.assertEquals(same, self.key)
def get(self, domain): tld = domain.split('.')[-1] if tld in common.TLD_BLOCKLIST: self.error('', status=404) mf2 = util.fetch_mf2('http://%s/' % domain, gateway=True, headers=common.HEADERS) # logging.info('Parsed mf2 for %s: %s', resp.url, json_dumps(mf2, indent=2)) hcard = mf2util.representative_hcard(mf2, mf2['url']) logging.info('Representative h-card: %s', json_dumps(hcard, indent=2)) if not hcard: self.error("""\ Couldn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on %s""" % mf2['url']) key = MagicKey.get_or_create(domain) obj = self.postprocess_as2(as2.from_as1(microformats2.json_to_object(hcard)), key=key) obj.update({ 'inbox': '%s/%s/inbox' % (self.request.host_url, domain), 'outbox': '%s/%s/outbox' % (self.request.host_url, domain), 'following': '%s/%s/following' % (self.request.host_url, domain), 'followers': '%s/%s/followers' % (self.request.host_url, domain), }) logging.info('Returning: %s', json_dumps(obj, indent=2)) self.response.headers.update({ 'Content-Type': common.CONTENT_TYPE_AS2, 'Access-Control-Allow-Origin': '*', }) self.response.write(json_dumps(obj, indent=2))
def try_activitypub(self): """Returns True if we attempted ActivityPub delivery, False otherwise.""" targets = self._activitypub_targets() if not targets: return False key = MagicKey.get_or_create(self.source_domain) error = None last_success = None # TODO: collect by inbox, add 'to' fields, de-dupe inboxes and recipients for resp, inbox in targets: target_obj = json_loads( resp.target_as2) if resp.target_as2 else None source_activity = self.postprocess_as2(as2.from_as1( self.source_obj), target=target_obj, key=key) if resp.status == 'complete': source_activity['type'] = 'Update' try: last = activitypub.send(source_activity, inbox, self.source_domain) resp.status = 'complete' last_success = last except BaseException as e: error = e resp.status = 'error' resp.put() # Pass the AP response status code and body through as our response if last_success: self.response.status_int = last_success.status_code self.response.write(last_success.text) elif isinstance(error, requests.HTTPError): self.response.status_int = error.status_code self.response.write(error.text) else: self.response.write(str(error)) return bool(last_success)
def send(activity, inbox_url, user_domain): """Sends an ActivityPub request to an inbox. Args: activity: dict, AS2 activity inbox_url: string user_domain: string, domain of the bridgy fed user sending the request Returns: requests.Response """ logging.info( 'Sending AP request from {user_domain}: {json_dumps(activity, indent=2)}' ) # prepare HTTP Signature (required by Mastodon) # https://w3c.github.io/activitypub/#authorization # https://tools.ietf.org/html/draft-cavage-http-signatures-07 # https://github.com/tootsuite/mastodon/issues/4906#issuecomment-328844846 acct = 'acct:%s@%s' % (user_domain, user_domain) key = MagicKey.get_or_create(user_domain) auth = HTTPSignatureAuth(secret=key.private_pem(), key_id=acct, algorithm='rsa-sha256', sign_header='signature', headers=('Date', 'Digest', 'Host')) # deliver to inbox body = json_dumps(activity).encode() headers = { 'Content-Type': common.CONTENT_TYPE_AS2, # required for HTTP Signature # https://tools.ietf.org/html/draft-cavage-http-signatures-07#section-2.1.3 'Date': datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'), # required by Mastodon # https://github.com/tootsuite/mastodon/pull/14556#issuecomment-674077648 'Digest': 'SHA-256=' + b64encode(sha256(body).digest()).decode(), 'Host': util.domain_from_link(inbox_url), } return common.requests_post(inbox_url, data=body, auth=auth, headers=headers)
def send(activity, inbox_url, user_domain): """Sends an ActivityPub request to an inbox. Args: activity: dict, AS2 activity inbox_url: string user_domain: string, domain of the bridgy fed user sending the request Returns: requests.Response """ logging.info('Sending AP request from %s: %s', user_domain, json.dumps(activity, indent=2)) # prepare HTTP Signature (required by Mastodon) # https://w3c.github.io/activitypub/#authorization-lds # https://tools.ietf.org/html/draft-cavage-http-signatures-07 # https://github.com/tootsuite/mastodon/issues/4906#issuecomment-328844846 acct = 'acct:%s@%s' % (user_domain, user_domain) key = MagicKey.get_or_create(user_domain) auth = HTTPSignatureAuth(secret=key.private_pem(), key_id=acct, algorithm='rsa-sha256') # deliver to inbox headers = { 'Content-Type': common.CONTENT_TYPE_AS2, # required for HTTP Signature # https://tools.ietf.org/html/draft-cavage-http-signatures-07#section-2.1.3 'Date': datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'), } return common.requests_post(inbox_url, json=activity, auth=auth, headers=headers)
def _try_salmon(self, resp): """ Args: resp: Response """ # fetch target HTML page, extract Atom rel-alternate link target = resp.target() if not self.target_resp: self.target_resp = common.requests_get(target) parsed = util.parse_html(self.target_resp) atom_url = parsed.find('link', rel='alternate', type=common.CONTENT_TYPE_ATOM) if not atom_url or not atom_url.get('href'): self.error('Target post %s has no Atom link' % resp.target(), status=400) # fetch Atom target post, extract and inject id into source object base_url = '' base = parsed.find('base') if base and base.get('href'): base_url = base['href'] atom_link = parsed.find('link', rel='alternate', type=common.CONTENT_TYPE_ATOM) atom_url = urllib.parse.urljoin( resp.target(), urllib.parse.urljoin(base_url, atom_link['href'])) feed = common.requests_get(atom_url).text parsed = feedparser.parse(feed) logging.info('Parsed: %s', json_dumps(parsed, indent=2)) entry = parsed.entries[0] target_id = entry.id in_reply_to = self.source_obj.get('inReplyTo') source_obj_obj = self.source_obj.get('object') if in_reply_to: for elem in in_reply_to: if elem.get('url') == target: elem['id'] = target_id elif isinstance(source_obj_obj, dict): source_obj_obj['id'] = target_id # Mastodon (and maybe others?) require a rel-mentioned link to the # original post's author to make it show up as a reply: # app/services/process_interaction_service.rb # ...so add them as a tag, which atom renders as a rel-mention link. authors = entry.get('authors', None) if authors: url = entry.authors[0].get('href') if url: self.source_obj.setdefault('tags', []).append({'url': url}) # extract and discover salmon endpoint logging.info('Discovering Salmon endpoint in %s', atom_url) endpoint = django_salmon.discover_salmon_endpoint(feed) if not endpoint: # try webfinger parsed = urllib.parse.urlparse(resp.target()) # TODO: test missing email author = entry.get('author_detail', {}) email = author.get('email') or '@'.join( (author.get('name', ''), parsed.netloc)) try: # TODO: always https? profile = common.requests_get( '%s://%s/.well-known/webfinger?resource=acct:%s' % (parsed.scheme, parsed.netloc, email), verify=False) endpoint = django_salmon.get_salmon_replies_link( profile.json()) except requests.HTTPError as e: pass if not endpoint: self.error('No salmon endpoint found!', status=400) logging.info('Discovered Salmon endpoint %s', endpoint) # construct reply Atom object self.source_url = resp.source() activity = self.source_obj if self.source_obj.get('verb') not in source.VERBS_WITH_OBJECT: activity = {'object': self.source_obj} entry = atom.activity_to_atom(activity, xml_base=self.source_url) logging.info('Converted %s to Atom:\n%s', self.source_url, entry) # sign reply and wrap in magic envelope domain = urllib.parse.urlparse(self.source_url).netloc key = MagicKey.get_or_create(domain) logging.info('Using key for %s: %s', domain, key) magic_envelope = magicsigs.magic_envelope(entry, common.CONTENT_TYPE_ATOM, key).decode() logging.info('Sending Salmon slap to %s', endpoint) common.requests_post( endpoint, data=common.XML_UTF8 + magic_envelope, headers={'Content-Type': common.CONTENT_TYPE_MAGIC_ENVELOPE}) return True
def setUp(self): super(MagicKeyTest, self).setUp() self.key = MagicKey.get_or_create('y.z')
def try_activitypub(self): source = util.get_required_param(self, 'source') # fetch source page, convert to ActivityStreams source_resp = common.requests_get(source) source_url = source_resp.url or source source_mf2 = mf2py.parse(source_resp.text, url=source_url) # logging.debug('Parsed mf2 for %s: %s', source_resp.url, json.dumps(source_mf2, indent=2)) entry = mf2util.find_first_entry(source_mf2, ['h-entry']) logging.info('First entry: %s', json.dumps(entry, indent=2)) # make sure it has url, since we use that for AS2 id, which is required # for ActivityPub. props = entry.setdefault('properties', {}) if not props.get('url'): props['url'] = [source_url] source_obj = microformats2.json_to_object(entry, fetch_mf2=True) logging.info('Converted to AS: %s', json.dumps(source_obj, indent=2)) # fetch target page as AS object. target is first in-reply-to, like-of, # or repost-of, *not* target query param.) target = util.get_url(util.get_first(source_obj, 'inReplyTo') or util.get_first(source_obj, 'object')) if not target: common.error(self, 'No u-in-reply-to, u-like-of, or u-repost-of ' 'found in %s' % source_url) try: target_resp = common.get_as2(target) except (requests.HTTPError, exc.HTTPBadGateway) as e: if (e.response.status_code // 100 == 2 and common.content_type(e.response).startswith('text/html')): self.resp = Response.get_or_create( source=source_url, target=e.response.url or target, direction='out', source_mf2=json.dumps(source_mf2)) return self.send_salmon(source_obj, target_resp=e.response) raise target_url = target_resp.url or target self.resp = Response.get_or_create( source=source_url, target=target_url, direction='out', protocol='activitypub', source_mf2=json.dumps(source_mf2)) # find actor's inbox target_obj = target_resp.json() inbox_url = target_obj.get('inbox') if not inbox_url: # TODO: test actor/attributedTo and not, with/without inbox actor = target_obj.get('actor') or target_obj.get('attributedTo') if isinstance(actor, dict): inbox_url = actor.get('inbox') actor = actor.get('url') if not inbox_url and not actor: common.error(self, 'Target object has no actor or attributedTo URL') if not inbox_url: # fetch actor as AS object actor = common.get_as2(actor).json() inbox_url = actor.get('inbox') if not inbox_url: # TODO: probably need a way to save errors like this so that we can # return them if ostatus fails too. # common.error(self, 'Target actor has no inbox') return self.send_salmon(source_obj, target_resp=target_resp) # convert to AS2 source_domain = urlparse.urlparse(source_url).netloc key = MagicKey.get_or_create(source_domain) source_activity = common.postprocess_as2( as2.from_as1(source_obj), target=target_obj, key=key) if self.resp.status == 'complete': source_activity['type'] = 'Update' # prepare HTTP Signature (required by Mastodon) # https://w3c.github.io/activitypub/#authorization-lds # https://tools.ietf.org/html/draft-cavage-http-signatures-07 # https://github.com/tootsuite/mastodon/issues/4906#issuecomment-328844846 acct = 'acct:%s@%s' % (source_domain, source_domain) auth = HTTPSignatureAuth(secret=key.private_pem(), key_id=acct, algorithm='rsa-sha256') # deliver source object to target actor's inbox. headers = { 'Content-Type': common.CONTENT_TYPE_AS2, # required for HTTP Signature # https://tools.ietf.org/html/draft-cavage-http-signatures-07#section-2.1.3 'Date': datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'), } inbox_url = urlparse.urljoin(target_url, inbox_url) resp = common.requests_post(inbox_url, json=source_activity, auth=auth, headers=headers) self.response.status_int = resp.status_code if resp.status_code == 202: self.response.write('202 response! If this is Mastodon 1.x, their ' 'signature verification probably failed. :(\n') self.response.write(resp.text)
def send_salmon(self, source_obj, target_resp=None): self.resp.protocol = 'ostatus' # fetch target HTML page, extract Atom rel-alternate link if not target_resp: target_resp = common.requests_get(self.resp.target()) parsed = BeautifulSoup(target_resp.content, from_encoding=target_resp.encoding) atom_url = parsed.find('link', rel='alternate', type=common.CONTENT_TYPE_ATOM) if not atom_url or not atom_url.get('href'): common.error(self, 'Target post %s has no Atom link' % self.resp.target(), status=400) # fetch Atom target post, extract and inject id into source object feed = common.requests_get(atom_url['href']).text parsed = feedparser.parse(feed) logging.info('Parsed: %s', json.dumps(parsed, indent=2, default=lambda key: '-')) entry = parsed.entries[0] target_id = entry.id in_reply_to = source_obj.get('inReplyTo') source_obj_obj = source_obj.get('object') if in_reply_to: in_reply_to[0]['id'] = target_id elif isinstance(source_obj_obj, dict): source_obj_obj['id'] = target_id # Mastodon (and maybe others?) require a rel-mentioned link to the # original post's author to make it show up as a reply: # app/services/process_interaction_service.rb # ...so add them as a tag, which atom renders as a rel-mention link. authors = entry.get('authors', None) if authors: url = entry.authors[0].get('href') if url: source_obj.setdefault('tags', []).append({'url': url}) # extract and discover salmon endpoint logging.info('Discovering Salmon endpoint in %s', atom_url['href']) endpoint = django_salmon.discover_salmon_endpoint(feed) if not endpoint: # try webfinger parsed = urlparse.urlparse(self.resp.target()) # TODO: test missing email email = entry.author_detail.get('email') or '@'.join( (entry.author_detail.name, parsed.netloc)) try: # TODO: always https? resp = common.requests_get( '%s://%s/.well-known/webfinger?resource=acct:%s' % (parsed.scheme, parsed.netloc, email), verify=False) endpoint = django_salmon.get_salmon_replies_link(resp.json()) except requests.HTTPError as e: pass if not endpoint: common.error(self, 'No salmon endpoint found!', status=400) logging.info('Discovered Salmon endpoint %s', endpoint) # construct reply Atom object source_url = self.resp.source() activity = (source_obj if source_obj.get('verb') in source.VERBS_WITH_OBJECT else {'object': source_obj}) entry = atom.activity_to_atom(activity, xml_base=source_url) logging.info('Converted %s to Atom:\n%s', source_url, entry) # sign reply and wrap in magic envelope domain = urlparse.urlparse(source_url).netloc key = MagicKey.get_or_create(domain) logging.info('Using key for %s: %s', domain, key) magic_envelope = magicsigs.magic_envelope( entry, common.CONTENT_TYPE_ATOM, key) logging.info('Sending Salmon slap to %s', endpoint) common.requests_post( endpoint, data=common.XML_UTF8 + magic_envelope, headers={'Content-Type': common.CONTENT_TYPE_MAGIC_ENVELOPE})
def try_activitypub(self): """Attempts ActivityPub delivery. Returns Flask response (string body or tuple) if we succeeded or failed, None if ActivityPub was not available. """ targets = self._activitypub_targets() if not targets: return None key = MagicKey.get_or_create(self.source_domain) error = None last_success = None # TODO: collect by inbox, add 'to' fields, de-dupe inboxes and recipients for resp, inbox in targets: target_obj = json_loads( resp.target_as2) if resp.target_as2 else None source_activity = common.postprocess_as2(as2.from_as1( self.source_obj), target=target_obj, key=key) if resp.status == 'complete': if resp.source_mf2: def content(mf2): items = mf2.get('items') if items: return microformats2.first_props( items[0].get('properties')).get('content') orig_content = content(json_loads(resp.source_mf2)) new_content = content(self.source_mf2) if orig_content and new_content and orig_content == new_content: msg = f'Skipping; new content is same as content published before at {resp.updated}' logging.info(msg) return msg source_activity['type'] = 'Update' try: last = activitypub.send(source_activity, inbox, self.source_domain) resp.status = 'complete' last_success = last except BaseException as e: error = e resp.status = 'error' resp.put() # Pass the AP response status code and body through as our response if last_success: return last_success.text or 'Sent!', last_success.status_code elif isinstance(error, BadGateway): raise error elif isinstance(error, requests.HTTPError): return str(error), error.status_code else: return str(error)
def setUp(self): super(WebmentionTest, self).setUp() self.key = MagicKey.get_or_create('a') self.orig_html_as2 = requests_response("""\ <html> <meta> <link href='http://orig/atom' rel='alternate' type='application/atom+xml'> <link href='http://orig/as2' rel='alternate' type='application/activity+json'> </meta> </html> """, url='http://orig/post', content_type=CONTENT_TYPE_HTML) self.orig_html_atom = requests_response("""\ <html> <meta> <link href='http://orig/atom' rel='alternate' type='application/atom+xml'> </meta> </html> """, url='http://orig/post', content_type=CONTENT_TYPE_HTML) self.orig_atom = requests_response("""\ <?xml version="1.0"?> <entry xmlns="http://www.w3.org/2005/Atom"> <id>tag:fed.brid.gy,2017-08-22:orig-post</id> <link rel="salmon" href="http://orig/salmon"/> <content type="html">baz ☕ baj</content> </entry> """) self.orig_as2_data = { '@context': ['https://www.w3.org/ns/activitystreams'], 'type': 'Article', 'id': 'tag:orig,2017:as2', 'content': 'Lots of ☕ words...', 'actor': { 'url': 'http://orig/author' }, 'to': ['http://orig/recipient'], 'cc': ['http://orig/bystander', AS2_PUBLIC_AUDIENCE], } self.orig_as2 = requests_response(self.orig_as2_data, url='http://orig/as2', content_type=CONTENT_TYPE_AS2 + '; charset=utf-8') self.reply_html = """\ <html> <body> <div class="h-entry"> <a class="u-url" href="http://a/reply"></a> <p class="e-content p-name"> <a class="u-in-reply-to" href="http://orig/post">foo ☕ bar</a> <a href="http://localhost/"></a> </p> <a class="p-author h-card" href="http://orig">Ms. ☕ Baz</a> </div> </body> </html> """ self.reply = requests_response(self.reply_html, content_type=CONTENT_TYPE_HTML) self.reply_mf2 = mf2py.parse(self.reply_html, url='http://a/reply') self.repost_html = REPOST_HTML self.repost = requests_response(self.repost_html, content_type=CONTENT_TYPE_HTML) self.repost_mf2 = mf2py.parse(self.repost_html, url='http://a/repost') self.repost_as2 = REPOST_AS2 self.like_html = """\ <html> <body class="h-entry"> <a class="u-url" href="http://a/like"></a> <a class="u-like-of" href="http://orig/post"></a> <!--<a class="u-like-of p-name" href="http://orig/post">liked!</a>--> <a class="p-author h-card" href="http://orig">Ms. ☕ Baz</a> <a href="http://localhost/"></a> </body> </html> """ self.like = requests_response(self.like_html, content_type=CONTENT_TYPE_HTML) self.like_mf2 = mf2py.parse(self.like_html, url='http://a/like') self.actor = requests_response( { 'objectType': 'person', 'displayName': 'Mrs. ☕ Foo', 'url': 'https://foo.com/about-me', 'inbox': 'https://foo.com/inbox', }, content_type=CONTENT_TYPE_AS2) self.activitypub_gets = [self.reply, self.orig_as2, self.actor] self.as2_create = { '@context': 'https://www.w3.org/ns/activitystreams', 'type': 'Create', 'object': { '@context': 'https://www.w3.org/ns/activitystreams', 'type': 'Note', 'id': 'http://localhost/r/http://a/reply', 'url': 'http://localhost/r/http://a/reply', 'name': 'foo ☕ bar', 'content': '<a class="u-in-reply-to" href="http://orig/post">foo ☕ bar</a>\n<a href="http://localhost/"></a>', 'inReplyTo': 'tag:orig,2017:as2', 'cc': [ AS2_PUBLIC_AUDIENCE, 'http://orig/author', 'http://orig/recipient', 'http://orig/bystander', ], 'attributedTo': [{ 'type': 'Person', 'id': 'http://localhost/orig', 'url': 'http://localhost/r/http://orig', 'preferredUsername': '******', 'name': 'Ms. ☕ Baz', }], 'tag': [{ 'type': 'Mention', 'href': 'http://orig/author', }], }, } self.as2_update = copy.deepcopy(self.as2_create) self.as2_update['type'] = 'Update' self.follow_html = """\ <html> <body class="h-entry"> <a class="u-url" href="http://a/follow"></a> <a class="u-follow-of" href="http://followee"></a> <a class="p-author h-card" href="https://orig">Ms. ☕ Baz</a> <a href="http://localhost/"></a> </body> </html> """ self.follow = requests_response(self.follow_html, content_type=CONTENT_TYPE_HTML) self.follow_mf2 = mf2py.parse(self.follow_html, url='http://a/follow') self.follow_as2 = { '@context': 'https://www.w3.org/ns/activitystreams', 'type': 'Follow', 'id': 'http://localhost/r/http://a/follow', 'url': 'http://localhost/r/http://a/follow', 'object': 'http://followee', 'actor': { 'id': 'http://localhost/orig', 'name': 'Ms. ☕ Baz', 'preferredUsername': '******', 'type': 'Person', 'url': 'http://localhost/r/https://orig', }, 'cc': ['https://www.w3.org/ns/activitystreams#Public'], } self.actor = requests_response( { 'objectType': 'person', 'displayName': 'Mrs. ☕ Foo', 'url': 'https://foo.com/about-me', 'inbox': 'https://foo.com/inbox', }, content_type=CONTENT_TYPE_AS2) self.activitypub_gets = [self.reply, self.orig_as2, self.actor] self.create_html = """\ <html> <body class="h-entry"> <a class="u-url" href="http://orig/post"></a> <p class="e-content p-name">hello i am a post</p> <a class="p-author h-card" href="https://orig">Ms. ☕ Baz</a> <a href="http://localhost/"></a> </body> </html> """ self.create = requests_response(self.create_html, content_type=CONTENT_TYPE_HTML) self.create_mf2 = mf2py.parse(self.create_html, url='http://a/create') self.create_as2 = { '@context': 'https://www.w3.org/ns/activitystreams', 'type': 'Create', 'object': { '@context': 'https://www.w3.org/ns/activitystreams', 'type': 'Note', 'id': 'http://localhost/r/http://orig/post', 'url': 'http://localhost/r/http://orig/post', 'name': 'hello i am a post', 'content': 'hello i am a post', 'attributedTo': [{ 'type': 'Person', 'id': 'http://localhost/orig', 'url': 'http://localhost/r/https://orig', 'name': 'Ms. ☕ Baz', 'preferredUsername': '******', }], 'cc': ['https://www.w3.org/ns/activitystreams#Public'], }, } self.actor = requests_response( { 'objectType': 'person', 'displayName': 'Mrs. ☕ Foo', 'url': 'https://foo.com/about-me', 'inbox': 'https://foo.com/inbox', }, content_type=CONTENT_TYPE_AS2) self.activitypub_gets = [self.reply, self.orig_as2, self.actor]
def setUp(self): super(SalmonTest, self).setUp() self.key = MagicKey.get_or_create('alice')
def setUp(self): super().setUp() self.key = MagicKey.get_or_create('alice')
def setUp(self): super().setUp() MagicKey.get_or_create('foo.com')