From 76298004c73001fdd43bad4f15cc973da61426f0 Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Thu, 5 Jul 2018 23:41:44 -0700 Subject: [PATCH] mf2, as2: add tag-of support https://indieweb.org/tag-reply for snarfed/bridgy#811 --- granary/as2.py | 13 +++++++++-- granary/github.py | 2 +- granary/microformats2.py | 32 ++++++++++++++++++++------- granary/test/test_github.py | 2 +- granary/test/testdata/tag_of.as.json | 12 ++++++++++ granary/test/testdata/tag_of.as2.json | 9 ++++++++ granary/test/testdata/tag_of.mf2.html | 9 ++++++++ granary/test/testdata/tag_of.mf2.json | 10 +++++++++ 8 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 granary/test/testdata/tag_of.as.json create mode 100644 granary/test/testdata/tag_of.as2.json create mode 100644 granary/test/testdata/tag_of.mf2.html create mode 100644 granary/test/testdata/tag_of.mf2.json diff --git a/granary/as2.py b/granary/as2.py index 335d1e62..f8869679 100644 --- a/granary/as2.py +++ b/granary/as2.py @@ -25,6 +25,7 @@ def _invert(d): 'collection': 'Collection', 'comment': 'Note', 'event': 'Event', + 'hashtag': 'Tag', # not in AS2 spec; needed for correct round trip conversion 'image': 'Image', 'note': 'Note', 'person': 'Person', @@ -80,6 +81,10 @@ def all_from_as1(field, type=None): for elem in util.pop_list(obj, field)] images = all_from_as1('image', type='Image') + inner_objs = all_from_as1('object') + if len(inner_objs) == 1: + inner_objs = inner_objs[0] + obj.update({ 'type': type, 'name': obj.pop('displayName', None), @@ -89,7 +94,7 @@ def all_from_as1(field, type=None): 'image': images, 'inReplyTo': util.trim_nulls([orig.get('id') or orig.get('url') for orig in obj.get('inReplyTo', [])]), - 'object': from_as1(obj.get('object'), context=None), + 'object': inner_objs, 'tag': all_from_as1('tags') }) @@ -158,6 +163,10 @@ def all_to_as1(field): if as1_img not in images: images.append(as1_img) + inner_objs = all_to_as1('object') + if len(inner_objs) == 1: + inner_objs = inner_objs[0] + obj.update({ 'displayName': obj.pop('name', None), 'actor': to_as1(obj.get('actor')), @@ -165,7 +174,7 @@ def all_to_as1(field): 'image': images, 'inReplyTo': [url_or_as1(orig) for orig in util.get_list(obj, 'inReplyTo')], 'location': url_or_as1(obj.get('location')), - 'object': to_as1(obj.get('object')), + 'object': inner_objs, 'tags': all_to_as1('tag'), }) diff --git a/granary/github.py b/granary/github.py index 140acd5c..c576bf24 100644 --- a/granary/github.py +++ b/granary/github.py @@ -648,9 +648,9 @@ def _create(self, obj, preview=None, include_link=source.OMIT_LINK, else: resp = self.rest(REST_API_ISSUE_LABELS % (owner, repo, number), labels).json() return source.creation_result({ - 'id': resp.get('id'), 'url': base_url, 'type': 'tag', + 'tags': labels, }) else: # new issue diff --git a/granary/microformats2.py b/granary/microformats2.py index e06fa223..e328b79e 100644 --- a/granary/microformats2.py +++ b/granary/microformats2.py @@ -46,7 +46,7 @@ $event_times $location $categories -$in_reply_tos +$links $children $comments @@ -59,7 +59,7 @@ $photos """) -IN_REPLY_TO = string.Template(' ') +LINK = string.Template(' ') AS_TO_MF2_TYPE = { 'event': ['h-event'], 'person': ['h-card'], @@ -76,6 +76,7 @@ 'reply': ('comment', None), 'repost': ('activity', 'share'), 'rsvp': ('activity', None), # json_to_object() will generate verb from rsvp + 'tag': ('activity', 'tag'), } # ISO 6709 location string. http://en.wikipedia.org/wiki/ISO_6709 ISO_6709_RE = re.compile(r'^([-+][0-9.]+)([-+][0-9.]+).*/$') @@ -198,8 +199,7 @@ def object_to_json(obj, trim_nulls=True, entry_class='h-entry', summary = primary.get('summary') author = obj.get('author', obj.get('actor', {})) - in_reply_tos = obj.get( - 'inReplyTo', obj.get('context', {}).get('inReplyTo', [])) + in_reply_tos = obj.get('inReplyTo', obj.get('context', {}).get('inReplyTo', [])) is_rsvp = obj_type in ('rsvp-yes', 'rsvp-no', 'rsvp-maybe') if (is_rsvp or obj_type == 'react') and obj.get('object'): objs = obj['object'] @@ -253,13 +253,18 @@ def object_to_json(obj, trim_nulls=True, entry_class='h-entry', } # hashtags and person tags + if obj_type == 'tag': + ret['properties']['tag-of'] = util.get_urls(obj, 'target') + tags = obj.get('tags', []) or get_first(obj, 'object', {}).get('tags', []) + if not tags and obj_type == 'tag': + tags = util.get_list(obj, 'object') ret['properties']['category'] = [] for tag in tags: if tag.get('objectType') == 'person': ret['properties']['category'].append( object_to_json(tag, entry_class='u-category h-card')) - elif tag.get('objectType') == 'hashtag': + elif tag.get('objectType') == 'hashtag' or obj_type == 'tag': name = tag.get('displayName') if name: ret['properties']['category'].append(name) @@ -357,6 +362,10 @@ def fetch(url): mf2_types = mf2.get('type') or [] if 'h-geo' in mf2_types or 'p-location' in mf2_types: mf2_type = 'location' + elif 'tag-of' in props: + # TODO: remove once this is in mf2util + # https://github.com/kylewm/mf2util/issues/18 + mf2_type = 'tag' else: # mf2 'photo' type is a note or article *with* a photo, but AS 'photo' type # *is* a photo. so, special case photo type to fall through to underlying @@ -449,6 +458,10 @@ def absolute_urls(prop): 'object': objects[0] if len(objects) == 1 else objects, 'actor': author, }) + if as_verb == 'tag': + obj['target'] = {'url': prop['tag-of']} + assert not obj.get('object') + obj['object'] = obj.pop('tags') else: obj.update({ 'inReplyTo': [{'url': url} for url in in_reply_tos], @@ -555,8 +568,11 @@ def json_to_html(obj, parent_props=None): return hcard_to_html(obj, parent_props) props = copy.copy(obj.get('properties', {})) - in_reply_tos = '\n'.join(IN_REPLY_TO.substitute(url=url) - for url in get_string_urls(props.get('in-reply-to', []))) + + links = [] + for prop in 'in-reply-to', 'tag-of': + links.extend(LINK.substitute(cls=prop, url=url) + for url in get_string_urls(props.get(prop, []))) prop = first_props(props) prop.setdefault('uid', '') @@ -663,7 +679,7 @@ def json_to_html(obj, parent_props=None): location=hcard_to_html(location, ['p-location']), categories='\n'.join(people + tags), attachments='\n'.join(attachments), - in_reply_tos=in_reply_tos, + links='\n'.join(links), invitees='\n'.join([hcard_to_html(i, ['p-invitee']) for i in props.get('invitee', [])]), content=content_html, diff --git a/granary/test/test_github.py b/granary/test/test_github.py index bfb231b4..55310464 100644 --- a/granary/test/test_github.py +++ b/granary/test/test_github.py @@ -972,9 +972,9 @@ def test_create_add_label(self): result = self.gh.create(TAG_ACTIVITY) self.assert_equals({ - 'id': 'DEF456', 'url': 'https://github.com/foo/bar/issues/456', 'type': 'tag', + 'tags': ['one'], }, result.content, result) def test_preview_add_label(self): diff --git a/granary/test/testdata/tag_of.as.json b/granary/test/testdata/tag_of.as.json new file mode 100644 index 00000000..f2c3d207 --- /dev/null +++ b/granary/test/testdata/tag_of.as.json @@ -0,0 +1,12 @@ +{ + "objectType": "activity", + "verb": "tag", + "object": [{ + "objectType": "hashtag", + "displayName": "one" + }, { + "objectType": "hashtag", + "displayName": "two" + }], + "target": {"url": "https://github.com/foo/bar/issues/456"} +} diff --git a/granary/test/testdata/tag_of.as2.json b/granary/test/testdata/tag_of.as2.json new file mode 100644 index 00000000..82736155 --- /dev/null +++ b/granary/test/testdata/tag_of.as2.json @@ -0,0 +1,9 @@ +{ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Add", + "object": [ + {"name": "one", "type": "Tag"}, + {"name": "two", "type": "Tag"} + ], + "target": {"url": "https://github.com/foo/bar/issues/456"} +} diff --git a/granary/test/testdata/tag_of.mf2.html b/granary/test/testdata/tag_of.mf2.html new file mode 100644 index 00000000..6d4ae232 --- /dev/null +++ b/granary/test/testdata/tag_of.mf2.html @@ -0,0 +1,9 @@ +
+ + +
+
+ one + two + +
diff --git a/granary/test/testdata/tag_of.mf2.json b/granary/test/testdata/tag_of.mf2.json new file mode 100644 index 00000000..c1a8a38a --- /dev/null +++ b/granary/test/testdata/tag_of.mf2.json @@ -0,0 +1,10 @@ +{ + "type": ["h-entry"], + "properties": { + "category": [ + "one", + "two" + ], + "tag-of": ["https://github.com/foo/bar/issues/456"] + } +}