def test_atom_to_activity_reply(self): expected = { 'objectType': 'activity', 'id': 'reply-url', 'url': 'reply-url', 'inReplyTo': [{ 'id': 'foo-id', 'url': 'foo-url' }], 'object': { 'id': 'reply-url', 'url': 'reply-url', 'content': 'I hereby ☕ reply.', 'inReplyTo': [{ 'id': 'foo-id', 'url': 'foo-url' }], }, } self.assert_equals( expected, atom.atom_to_activity(u"""\ <?xml version="1.0" encoding="UTF-8"?> <entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0"> <uri>reply-url</uri> <thr:in-reply-to ref="foo-id" href="foo-url" /> <content>I hereby ☕ reply.</content> </entry> """))
def render(): """Fetches a stored Response and renders it as HTML.""" source = flask_util.get_required_param('source') target = flask_util.get_required_param('target') id = f'{source} {target}' resp = Response.get_by_id(id) if not resp: error(f'No stored response for {id}', status=404) if resp.source_mf2: as1 = microformats2.json_to_object(json_loads(resp.source_mf2)) elif resp.source_as2: as1 = as2.to_as1(json_loads(resp.source_as2)) elif resp.source_atom: as1 = atom.atom_to_activity(resp.source_atom) else: error(f'Stored response for {id} has no data', status=404) # add HTML meta redirect to source page. should trigger for end users in # browsers but not for webmention receivers (hopefully). html = microformats2.activities_to_html([as1]) utf8 = '<meta charset="utf-8">' refresh = f'<meta http-equiv="refresh" content="0;url={source}">' return html.replace(utf8, utf8 + '\n' + refresh)
def get(self): source = util.get_required_param(self, 'source') target = util.get_required_param(self, 'target') id = '%s %s' % (source, target) resp = Response.get_by_id(id) if not resp: self.abort(404, 'No stored response for %s' % id) if resp.source_mf2: as1 = microformats2.json_to_object(json.loads(resp.source_mf2)) elif resp.source_as2: as1 = as2.to_as1(json.loads(resp.source_as2)) elif resp.source_atom: as1 = atom.atom_to_activity(resp.source_atom) else: self.abort(404, 'Stored response for %s has no data' % id) # add HTML meta redirect to source page. should trigger for end users in # browsers but not for webmention receivers (hopefully). html = microformats2.activities_to_html([as1]) utf8 = '<meta charset="utf-8">' refresh = '<meta http-equiv="refresh" content="0;url=%s">' % source html = html.replace(utf8, utf8 + '\n' + refresh) self.response.write(html)
def test_atom_to_activity_like(self): for atom_obj, as_obj in ( ('foo', { 'id': 'foo', 'url': 'foo' }), ('<id>foo</id>', { 'id': 'foo' }), ('<uri>foo</uri>', { 'id': 'foo', 'url': 'foo' }), ): self.assert_equals( { 'url': 'like-url', 'objectType': 'activity', 'verb': 'like', 'object': as_obj, }, atom.atom_to_activity(u"""\ <?xml version="1.0" encoding="UTF-8"?> <entry xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0/"> <uri>like-url</uri> <activity:verb>http://activitystrea.ms/schema/1.0/like</activity:verb> <activity:object>%s</activity:object> </entry> """ % atom_obj))
def slap(acct): """Accepts POSTs to /[ACCT]/salmon and converts to outbound webmentions.""" # TODO: unify with activitypub body = request.get_data(as_text=True) logging.info(f'Got: {body}') try: parsed = utils.parse_magic_envelope(body) except ParseError as e: error('Could not parse POST body as XML', exc_info=True) data = parsed['data'] logging.info(f'Decoded: {data}') # check that we support this activity type try: activity = atom.atom_to_activity(data) except ParseError as e: error('Could not parse envelope data as XML', exc_info=True) verb = activity.get('verb') if verb and verb not in SUPPORTED_VERBS: error(f'Sorry, {verb} activities are not supported yet.', status=501) # verify author and signature author = util.get_url(activity.get('actor')) if ':' not in author: author = f'acct:{author}' elif not author.startswith('acct:'): error(f'Author URI {author} has unsupported scheme; expected acct:') logging.info(f'Fetching Salmon key for {author}') if not magicsigs.verify(data, parsed['sig'], author_uri=author): error('Could not verify magic signature.') logging.info('Verified magic signature.') # Verify that the timestamp is recent. Required by spec. # I get that this helps prevent spam, but in practice it's a bit silly, # and other major implementations don't (e.g. Mastodon), so forget it. # # updated = utils.parse_updated_from_atom(data) # if not utils.verify_timestamp(updated): # error('Timestamp is more than 1h old.') # send webmentions to each target activity = atom.atom_to_activity(data) common.send_webmentions(activity, protocol='ostatus', source_atom=data) return ''
def post(self, username, domain): logging.info('Got: %s', self.request.body) try: parsed = utils.parse_magic_envelope(self.request.body) except ParseError as e: self.error('Could not parse POST body as XML', exc_info=True) data = parsed['data'] logging.info('Decoded: %s', data) # check that we support this activity type try: activity = atom.atom_to_activity(data) except ParseError as e: self.error('Could not parse envelope data as XML', exc_info=True) verb = activity.get('verb') if verb and verb not in SUPPORTED_VERBS: self.error('Sorry, %s activities are not supported yet.' % verb, status=501) # verify author and signature author = util.get_url(activity.get('actor')) if ':' not in author: author = 'acct:%s' % author elif not author.startswith('acct:'): self.error('Author URI %s has unsupported scheme; expected acct:' % author) logging.info('Fetching Salmon key for %s' % author) if not magicsigs.verify(author, data, parsed['sig']): self.error('Could not verify magic signature.') logging.info('Verified magic signature.') # Verify that the timestamp is recent. Required by spec. # I get that this helps prevent spam, but in practice it's a bit silly, # and other major implementations don't (e.g. Mastodon), so forget it. # # updated = utils.parse_updated_from_atom(data) # if not utils.verify_timestamp(updated): # self.error('Timestamp is more than 1h old.') # send webmentions to each target activity = atom.atom_to_activity(data) self.send_webmentions(activity, protocol='ostatus', source_atom=data)
def test_atom_to_activity_unicode_title(self): """Unicode smart quote in the <title> element.""" self.assert_equals({ 'objectType': 'activity', 'object': { 'title': 'How quill’s editor looks', }, }, atom.atom_to_activity(u"""\ <?xml version='1.0' encoding='UTF-8'?> <entry xmlns='http://www.w3.org/2005/Atom'> <title>How quill’s editor looks</title> </entry> """))
def test_atom_to_activity_in_reply_to_text(self): expected = { 'objectType': 'activity', 'inReplyTo': [{'id': 'my-inreplyto', 'url': 'my-inreplyto'}], 'object': { 'inReplyTo': [{'id': 'my-inreplyto', 'url': 'my-inreplyto'}], }, } self.assert_equals(expected, atom.atom_to_activity(u"""\ <?xml version="1.0" encoding="UTF-8"?> <entry xmlns:thr="http://purl.org/syndication/thread/1.0"> <thr:in-reply-to>my-inreplyto</thr:in-reply-to> </entry> """))
def test_atom_to_activity_unicode_title(self): """Unicode smart quote in the <title> element.""" self.assert_equals( { 'objectType': 'activity', 'object': { 'title': 'How quill’s editor looks', }, }, atom.atom_to_activity(u"""\ <?xml version='1.0' encoding='UTF-8'?> <entry xmlns='http://www.w3.org/2005/Atom'> <title>How quill’s editor looks</title> </entry> """))
def get(self): source = util.get_required_param(self, 'source') target = util.get_required_param(self, 'target') id = '%s %s' % (source, target) resp = Response.get_by_id(id) if not resp: self.abort(404, 'No stored response for %s' % id) if resp.source_mf2: as1 = microformats2.json_to_object(json.loads(resp.source_mf2)) elif resp.source_as2: as1 = as2.to_as1(json.loads(resp.source_as2)) elif resp.source_atom: as1 = atom.atom_to_activity(resp.source_atom) else: self.abort(404, 'Stored response for %s has no data' % id) self.response.write(microformats2.activities_to_html([as1]))
def test_atom_to_activity_like(self): for atom_obj, as_obj in ( ('foo', {'id': 'foo', 'url': 'foo'}), ('<id>foo</id>', {'id': 'foo'}), ('<uri>foo</uri>', {'id': 'foo', 'url': 'foo'}), ): self.assert_equals({ 'url': 'like-url', 'objectType': 'activity', 'verb': 'like', 'object': as_obj, }, atom.atom_to_activity(u"""\ <?xml version="1.0" encoding="UTF-8"?> <entry xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0/"> <uri>like-url</uri> <activity:verb>http://activitystrea.ms/schema/1.0/like</activity:verb> <activity:object>%s</activity:object> </entry> """ % atom_obj))
def test_atom_to_activity_in_reply_to_text(self): expected = { 'objectType': 'activity', 'inReplyTo': [{ 'id': 'my-inreplyto', 'url': 'my-inreplyto' }], 'object': { 'inReplyTo': [{ 'id': 'my-inreplyto', 'url': 'my-inreplyto' }], }, } self.assert_equals( expected, atom.atom_to_activity(u"""\ <?xml version="1.0" encoding="UTF-8"?> <entry xmlns:thr="http://purl.org/syndication/thread/1.0"> <thr:in-reply-to>my-inreplyto</thr:in-reply-to> </entry> """))
def test_atom_to_activity_reply(self): expected = { 'objectType': 'activity', 'id': 'reply-url', 'url': 'reply-url', 'inReplyTo': [{'id': 'foo-id', 'url': 'foo-url'}], 'object': { 'id': 'reply-url', 'url': 'reply-url', 'content': 'I hereby ☕ reply.', 'inReplyTo': [{'id': 'foo-id', 'url': 'foo-url'}], }, } self.assert_equals(expected, atom.atom_to_activity(u"""\ <?xml version="1.0" encoding="UTF-8"?> <entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0"> <uri>reply-url</uri> <thr:in-reply-to ref="foo-id" href="foo-url" /> <content>I hereby ☕ reply.</content> </entry> """))
def test_atom_to_activity(self): self.assert_equals(INSTAGRAM_ACTIVITY, atom.atom_to_activity(INSTAGRAM_ENTRY))