def test_object_to_json_multiple_object_urls(self): self.assert_equals( { 'type': ['h-entry'], 'properties': { 'content': [{ 'html': '✁', 'value': '✁' }], 'in-reply-to': ['https://orig/post/1', 'https://orig/post/2'], }, }, microformats2.object_to_json({ 'objectType': 'activity', 'verb': 'react', 'content': '✁', 'object': [ { 'url': 'https://orig/post/1' }, { 'url': 'https://orig/post/2' }, ], }))
def test_tag_multiple_urls(self): expected_urls = ['http://1', 'https://2'] expected_html = """ <a class="tag" href="http://1"></a> <a class="tag" href="https://2"></a> """ for tag in ({ 'url': 'http://1', 'urls': [{ 'value': 'http://1' }, { 'value': 'https://2' }] }, { 'url': 'http://1', 'urls': [{ 'value': 'https://2' }] }, { 'urls': [{ 'value': 'http://1' }, { 'value': 'https://2' }] }): self.assert_equals( expected_urls, microformats2.object_to_json(tag)['properties']['url'], tag) self.assert_equals(expected_html, microformats2.render_content({'tags': [tag]}), tag)
def test_dont_stop_at_unknown_tag_type(self): obj = { 'tags': [{ 'objectType': 'x', 'url': 'http://x' }, { 'objectType': 'person', 'url': 'http://p', 'displayName': 'p' }], } self.assert_equals( { 'type': ['h-entry'], 'properties': { 'category': [{ 'type': ['h-card'], 'properties': { 'name': ['p'], 'url': ['http://p'], }, }], 'content': [{ 'html': '\n<a class="tag" href="http://x"></a>' }], }, }, microformats2.object_to_json(obj))
def write_response(self, response, actor=None, url=None, title=None): """Converts ActivityStreams activities and writes them out. Args: response: response dict with values based on OpenSocial ActivityStreams REST API, as returned by Source.get_activities_response() actor: optional ActivityStreams actor dict for current user. Only used for Atom output. url: the input URL title: string, Used in Atom output """ expected_formats = ('activitystreams', 'json', 'atom', 'xml', 'html', 'json-mf2') format = self.request.get('format') or self.request.get('output') or 'json' if format not in expected_formats: raise exc.HTTPBadRequest('Invalid format: %s, expected one of %r' % (format, expected_formats)) activities = response['items'] self.response.headers.update({ 'Access-Control-Allow-Origin': '*', 'Strict-Transport-Security': 'max-age=16070400; includeSubDomains; preload', # 6 months }) if format in ('json', 'activitystreams'): # list of official MIME types: # https://www.iana.org/assignments/media-types/media-types.xhtml self.response.headers['Content-Type'] = 'application/json' self.response.out.write(json.dumps(response, indent=2)) elif format == 'atom': self.response.headers['Content-Type'] = 'application/atom+xml' hub = self.request.get('hub') self.response.out.write(atom.activities_to_atom( activities, actor, host_url=url or self.request.host_url + '/', request_url=self.request.url, xml_base=util.base_url(url), title=title, rels={'hub': hub} if hub else None)) self.response.headers.add('Link', str('<%s>; rel="self"' % self.request.url)) if hub: self.response.headers.add('Link', str('<%s>; rel="hub"' % hub)) elif format == 'xml': self.response.headers['Content-Type'] = 'application/xml' self.response.out.write(XML_TEMPLATE % util.to_xml(response)) elif format == 'html': self.response.headers['Content-Type'] = 'text/html' self.response.out.write(microformats2.activities_to_html(activities)) elif format == 'json-mf2': self.response.headers['Content-Type'] = 'application/json' items = [microformats2.object_to_json(a) for a in activities] self.response.out.write(json.dumps({'items': items}, indent=2)) if 'plaintext' in self.request.params: # override response content type self.response.headers['Content-Type'] = 'text/plain'
def test_attachments_to_children(self): obj = { 'attachments': [ { 'objectType': 'note', 'url': 'http://p', 'displayName': 'p' }, { 'objectType': 'x', 'url': 'http://x' }, { 'objectType': 'article', 'url': 'http://a' }, ] } self.assert_equals([{ 'type': ['u-quotation-of', 'h-cite'], 'properties': { 'url': ['http://p'], 'name': ['p'] }, }, { 'type': ['u-quotation-of', 'h-cite'], 'properties': { 'url': ['http://a'] }, }], microformats2.object_to_json(obj)['children']) html = microformats2.object_to_html(obj) self.assert_multiline_in( """\ <article class="u-quotation-of h-cite"> <span class="p-uid"></span> <a class="p-name u-url" href="http://p">p</a> <div class=""> </div> </article> <article class="u-quotation-of h-cite"> <span class="p-uid"></span> <a class="p-name u-url" href="http://a"></a> <div class=""> </div> </article> """, html)
def write_response(self, response, actor=None, url=None, title=None): """Converts ActivityStreams activities and writes them out. Args: response: response dict with values based on OpenSocial ActivityStreams REST API, as returned by Source.get_activities_response() actor: optional ActivityStreams actor dict for current user. Only used for Atom output. url: the input URL title: string, Used in Atom output """ expected_formats = ('activitystreams', 'json', 'atom', 'xml', 'html', 'json-mf2') format = self.request.get('format') or self.request.get('output') or 'json' if format not in expected_formats: raise exc.HTTPBadRequest('Invalid format: %s, expected one of %r' % (format, expected_formats)) activities = response['items'] self.response.headers.update({ 'Access-Control-Allow-Origin': '*', 'Strict-Transport-Security': 'max-age=16070400; includeSubDomains; preload', # 6 months }) if format in ('json', 'activitystreams'): self.response.headers['Content-Type'] = 'application/json' self.response.out.write(json.dumps(response, indent=2)) elif format == 'atom': self.response.headers['Content-Type'] = 'text/xml' hub = self.request.get('hub') self.response.out.write(atom.activities_to_atom( activities, actor, host_url=url or self.request.host_url + '/', request_url=self.request.url, xml_base=util.base_url(url), title=title, rels={'hub': hub} if hub else None)) self.response.headers.add('Link', str('<%s>; rel="self"' % self.request.url)) if hub: self.response.headers.add('Link', str('<%s>; rel="hub"' % hub)) elif format == 'xml': self.response.headers['Content-Type'] = 'text/xml' self.response.out.write(XML_TEMPLATE % util.to_xml(response)) elif format == 'html': self.response.headers['Content-Type'] = 'text/html' self.response.out.write(microformats2.activities_to_html(activities)) elif format == 'json-mf2': self.response.headers['Content-Type'] = 'application/json' items = [microformats2.object_to_json(a) for a in activities] self.response.out.write(json.dumps({'items': items}, indent=2)) if 'plaintext' in self.request.params: # override response content type self.response.headers['Content-Type'] = 'text/plain'
def test_object_to_json_unescapes_html_entities(self): self.assertEquals({ 'type': ['h-entry'], 'properties': {'content': [{ 'html': 'Entity < <a href="http://my/link">link too</a>', 'value': 'Entity < link too', }]}, }, microformats2.object_to_json({ 'content': 'Entity < link too', 'tags': [{'url': 'http://my/link', 'startIndex': 12, 'length': 8}] }))
def test_object_to_json_preserves_url_order(self): self.assertEquals({ 'type': ['h-card'], 'properties': { 'url': ['http://2', 'http://4', 'http://6'], }, }, microformats2.object_to_json({ 'objectType': 'person', 'url': 'http://2', 'urls': [{'value': 'http://4'}, {'value': 'http://6'}], }))
def test_object_to_json_reaction(self): self.assert_equals({ 'type': ['h-entry'], 'properties': { 'content': [{'html': u'✁', 'value': u'✁'}], 'in-reply-to': ['https://orig/post'], }, }, microformats2.object_to_json({ 'objectType': 'activity', 'verb': 'react', 'content': u'✁', 'object': {'url': 'https://orig/post'}, }))
def test_object_to_json_unescapes_html_entities(self): self.assertEquals({ 'type': ['h-entry'], 'properties': {'content': [{ 'html': 'Entity < <a href="http://my/link">link too</a>', 'value': 'Entity < link too', }]}, }, microformats2.object_to_json({ 'verb': 'post', 'object': { 'content': 'Entity < link too', 'tags': [{'url': 'http://my/link', 'startIndex': 12, 'length': 8}] } }))
def test_object_to_json_note_with_in_reply_to(self): self.assertEquals({ 'type': ['h-entry'], 'properties': { 'content': [{ 'html': '@hey great post', 'value': '@hey great post', }], 'in-reply-to': ['http://reply/target'], }, }, microformats2.object_to_json({ 'content': '@hey great post', }, ctx={ 'inReplyTo': [{ 'url': 'http://reply/target', }] }))
def test_object_to_json_reaction(self): self.assert_equals( { 'type': ['h-entry'], 'properties': { 'content': ['✁'], 'in-reply-to': ['https://orig/post'], }, }, microformats2.object_to_json({ 'objectType': 'activity', 'verb': 'react', 'content': '✁', 'object': { 'url': 'https://orig/post' }, }))
def test_tag_multiple_urls(self): expected_urls = ['http://1', 'https://2'] expected_html = """ <a class="tag" href="http://1"></a> <a class="tag" href="https://2"></a> """ for tag in ({'url': 'http://1', 'urls': [{'value': 'http://1'}, {'value': 'https://2'}]}, {'url': 'http://1', 'urls': [{'value': 'https://2'}]}, {'urls': [{'value': 'http://1'}, {'value': 'https://2'}]}): self.assert_equals(expected_urls, microformats2.object_to_json(tag)['properties']['url'], tag) self.assert_equals(expected_html, microformats2.render_content({'tags': [tag]}), tag)
def test_dont_stop_at_unknown_tag_type(self): obj = {'tags': [ {'objectType': 'x', 'url': 'http://x'}, {'objectType': 'person', 'url': 'http://p', 'displayName': 'p'}], } self.assert_equals({ 'type': ['h-entry'], 'properties': { 'category': [{ 'type': ['h-card'], 'properties': { 'name': ['p'], 'url': ['http://p'], }, }], 'content': [{'html': '\n<a class="tag" href="http://x"></a>'}], }, }, microformats2.object_to_json(obj))
def test_object_to_json_note_with_in_reply_to(self): self.assertEquals( { 'type': ['h-entry'], 'properties': { 'content': [{ 'html': '@hey great post', 'value': '@hey great post', }], 'in-reply-to': ['http://reply/target'], }, }, microformats2.object_to_json( { 'content': '@hey great post', }, ctx={'inReplyTo': [{ 'url': 'http://reply/target', }]}))
def test_object_to_json_not_dict(self): """This can happen if we get a dict instead of a list, e.g. with AS 2.0. Found via AS2 on http://evanminto.com/indieweb/activity-stream.php, e.g.: { "@context": "https://www.w3.org/ns/activitystreams", "0": { "name": "Evan reposted another post.", "type": "Announce", "actor": { "type": "Person", "name": "Evan Minto", "url": "http://evanminto.com" }, "object": "http://www.harkavagrant.com/index.php?id=402" }, ... """ self.assert_equals({}, microformats2.object_to_json('foo bar'))
def test_object_to_json_not_dict(self): """This can happen if we get a dict instead of a list, e.g. with AS 2.0. Found via AS2 on http://evanminto.com/indieweb/activity-stream.php, e.g.: { "@context": "https://www.w3.org/ns/activitystreams", "0": { "name": "Evan reposted another post.", "type": "Announce", "actor": { "type": "Person", "name": "Evan Minto", "url": "http://evanminto.com" }, "object": "http://www.harkavagrant.com/index.php?id=402" }, ... """ self.assert_equals({}, microformats2.object_to_json('foo bar'))
def write_response(self, response, actor=None): """Converts ActivityStreams activities and writes them out. Args: response: response dict with values based on OpenSocial ActivityStreams REST API, as returned by Source.get_activities_response() actor: optional ActivityStreams actor dict for current user. Only used for Atom output. """ expected_formats = ('activitystreams', 'json', 'atom', 'xml', 'html', 'json-mf2') format = self.request.get('format') or self.request.get('output') or 'json' if format not in expected_formats: raise exc.HTTPBadRequest('Invalid format: %s, expected one of %r' % (format, expected_formats)) activities = response['items'] self.response.headers['Access-Control-Allow-Origin'] = '*' if format in ('json', 'activitystreams'): self.response.headers['Content-Type'] = 'application/json' self.response.out.write(json.dumps(response, indent=2)) elif format == 'atom': self.response.headers['Content-Type'] = 'text/xml' self.response.out.write(atom.activities_to_atom( activities, actor, host_url=self.request.host_url + '/', request_url=self.request.path_url)) elif format == 'xml': self.response.headers['Content-Type'] = 'text/xml' self.response.out.write(XML_TEMPLATE % util.to_xml(response)) elif format == 'html': self.response.headers['Content-Type'] = 'text/html' self.response.out.write(microformats2.activities_to_html(activities)) elif format == 'json-mf2': self.response.headers['Content-Type'] = 'application/json' items = [microformats2.object_to_json(a['object'], a.get('context', {})) for a in activities] self.response.out.write(json.dumps({'items': items}, indent=2)) if 'plaintext' in self.request.params: # override response content type self.response.headers['Content-Type'] = 'text/plain'
def test_attachments_to_children(self): obj = {'attachments': [ {'objectType': 'note', 'url': 'http://p', 'displayName': 'p'}, {'objectType': 'x', 'url': 'http://x'}, {'objectType': 'article', 'url': 'http://a'}, ]} self.assert_equals([{ 'type': ['h-cite'], 'properties': {'url': ['http://p'], 'name': ['p']}, }, { 'type': ['h-cite'], 'properties': {'url': ['http://a']}, }], microformats2.object_to_json(obj)['children']) html = microformats2.object_to_html(obj) self.assert_multiline_in("""\ <article class="h-cite"> <span class="p-uid"></span> <a class="p-name u-url" href="http://p">p</a> <div class=""> </div> </article> <article class="h-cite"> <span class="p-uid"></span> <a class="u-url" href="http://a">http://a</a> <div class=""> </div> </article> """, html)
def dispatch_request(self, site, key_id, **kwargs): """Handle HTTP request.""" source_cls = models.sources.get(site) if not source_cls: error( f"Source type '{site}' not found. Known sources: {[s for s in models.sources.keys() if s]}" ) self.source = source_cls.get_by_id(key_id) if not self.source: error(f'Source {site} {key_id} not found') elif (self.source.status == 'disabled' or 'listen' not in self.source.features): error( f'Source {self.source.bridgy_path()} is disabled for backfeed') format = request.values.get('format', 'html') if format not in ('html', 'json'): error(f'Invalid format {format}, expected html or json') for id in kwargs.values(): if not self.VALID_ID.match(id): error(f'Invalid id {id}', 404) try: obj = self.get_item(**kwargs) except models.DisableSource: error( "Bridgy's access to your account has expired. Please visit https://brid.gy/ to refresh it!", 401) except ValueError as e: error(f'{self.source.GR_CLASS.NAME} error: {e}') if not obj: error(f'Not found: {site}:{key_id} {kwargs}', 404) if self.source.is_blocked(obj): error('That user is currently blocked', 410) # use https for profile pictures so we don't cause SSL mixed mode errors # when serving over https. author = obj.get('author', {}) image = author.get('image', {}) url = image.get('url') if url: image['url'] = util.update_scheme(url, request) mf2_json = microformats2.object_to_json(obj, synthesize_content=False) # try to include the author's silo profile url author = first_props(mf2_json.get('properties', {})).get('author', {}) author_uid = first_props(author.get('properties', {})).get('uid', '') if author_uid: parsed = util.parse_tag_uri(author_uid) if parsed: urls = author.get('properties', {}).setdefault('url', []) try: silo_url = self.source.gr_source.user_url(parsed[1]) if silo_url not in microformats2.get_string_urls(urls): urls.append(silo_url) except NotImplementedError: # from gr_source.user_url() pass # write the response! if format == 'html': url = obj.get('url', '') return TEMPLATE.substitute({ 'refresh': (f'<meta http-equiv="refresh" content="0;url={url}">' if url else ''), 'url': url, 'body': microformats2.json_to_html(mf2_json), 'title': obj.get('title') or obj.get('content') or 'Bridgy Response', }) elif format == 'json': return mf2_json
return else: raise if not obj: self.abort(404, label) # use https for profile pictures so we don't cause SSL mixed mode errors # when serving over https. author = obj.get('author', {}) image = author.get('image', {}) url = image.get('url') if url: image['url'] = util.update_scheme(url, self) mf2_json = microformats2.object_to_json(obj) # try to include the author's silo profile url author = first_props(mf2_json.get('properties', {})).get('author', {}) author_uid = first_props(author.get('properties', {})).get('uid', '') if author_uid: parsed = util.parse_tag_uri(author_uid) if parsed: silo_url = self.source.gr_source.user_url(parsed[1]) urls = author.get('properties', {}).setdefault('url', []) if silo_url not in microformats2.get_string_urls(urls): urls.append(silo_url) # write the response! self.response.headers['Access-Control-Allow-Origin'] = '*' if format == 'html':
class ItemHandler(webapp2.RequestHandler): """Fetches a post, repost, like, or comment and serves it as mf2 HTML or JSON. """ handle_exception = handlers.handle_exception source = None VALID_ID = re.compile(r'^[\w.+:@-]+$') def head(self, *args): """Return an empty 200 with no caching directives.""" def get_item(self, id): """Fetches and returns an object from the given source. To be implemented by subclasses. Args: source: bridgy.Source subclass id: string Returns: ActivityStreams object dict """ raise NotImplementedError() def get_title(self, obj): """Returns the string to be used in the <title> tag. Args: obj: ActivityStreams object """ return obj.get('title') or obj.get('content') or 'Bridgy Response' def get_post(self, id, **kwargs): """Fetch a post. Args: id: string, site-specific post id is_event: bool kwargs: passed through to get_activities Returns: ActivityStreams object dict """ try: posts = self.source.get_activities(activity_id=id, user_id=self.source.key.id(), **kwargs) if posts: return posts[0] logging.warning('Source post %s not found', id) except Exception as e: util.interpret_http_exception(e) def get(self, type, source_short_name, string_id, *ids): source_cls = models.sources.get(source_short_name) if not source_cls: self.abort( 400, "Source type '%s' not found. Known sources: %s" % (source_short_name, filter(None, models.sources.keys()))) self.source = source_cls.get_by_id(string_id) if not self.source: self.abort( 400, 'Source %s %s not found' % (source_short_name, string_id)) format = self.request.get('format', 'html') if format not in ('html', 'json'): self.abort(400, 'Invalid format %s, expected html or json' % format) for id in ids: if not self.VALID_ID.match(id): self.abort(404, 'Invalid id %s' % id) label = '%s:%s %s %s' % (source_short_name, string_id, type, ids) cache_key = 'H ' + label obj = memcache.get(cache_key) if obj: logging.info('Using cached object for %s', label) else: logging.info('Fetching %s', label) try: obj = self.get_item(*ids) except Exception, e: # pass through all API HTTP errors if we can identify them code, body = util.interpret_http_exception(e) if not code and util.is_connection_failure(e): code = 503 body = str(e) if code: self.response.status_int = int(code) self.response.headers['Content-Type'] = 'text/plain' self.response.write('%s error:\n%s' % (self.source.GR_CLASS.NAME, body)) return else: raise memcache.set(cache_key, obj, time=CACHE_TIME) if not obj: self.abort(404, label) # use https for profile pictures so we don't cause SSL mixed mode errors # when serving over https. author = obj.get('author', {}) image = author.get('image', {}) url = image.get('url') if url: image['url'] = util.update_scheme(url, self) mf2_json = microformats2.object_to_json(obj, synthesize_content=False) # try to include the author's silo profile url author = first_props(mf2_json.get('properties', {})).get('author', {}) author_uid = first_props(author.get('properties', {})).get('uid', '') if author_uid: parsed = util.parse_tag_uri(author_uid) if parsed: silo_url = self.source.gr_source.user_url(parsed[1]) urls = author.get('properties', {}).setdefault('url', []) if silo_url not in microformats2.get_string_urls(urls): urls.append(silo_url) # write the response! self.response.headers['Access-Control-Allow-Origin'] = '*' if format == 'html': self.response.headers['Content-Type'] = 'text/html; charset=utf-8' self.response.out.write( TEMPLATE.substitute({ 'url': obj.get('url', ''), 'body': microformats2.json_to_html(mf2_json), 'title': self.get_title(obj), })) elif format == 'json': self.response.headers[ 'Content-Type'] = 'application/json; charset=utf-8' self.response.out.write(json.dumps(mf2_json, indent=2))
#print trm.name if trm.name in MF2_TYPES: debug_print(trm.name) #if trm.name == 'Image': #debug_print('initial content: ' + post.content) customfields = process_mf2_data(customfields, trm.name) if trm.name == 'Image': debug_print('fields: ') debug_print(customfields) if SYNDLINKS in customfields.keys(): syndlink = parse_response_urlstr(customfields[SYNDLINKS]) if syndlink == '': del customfields[SYNDLINKS] else: customfields[SYNDLINKS] = syndlink post.content = insert_url_content(post,customfields,trm.name) debug_print('returned: ' + post.content) postdict['content'] = post.content #TODO: add tags, from post.terms itemdict = {} #itemdict['@context'] = 'https://www.w3.org/ns/activitystreams' #not sure this is necessary itemdict['object'] = postdict mf2dict['items'].append(itemdict) mf2dict['totalItems'] = totalitems debug_print(mf2dict) debug_print('\n') mf2 = microformats2.object_to_json(mf2dict) print(mf2)
def get(self, type, source_short_name, string_id, *ids): source_cls = models.sources.get(source_short_name) if not source_cls: self.abort( 400, "Source type '%s' not found. Known sources: %s" % (source_short_name, filter(None, models.sources.keys()))) self.source = source_cls.get_by_id(string_id) if not self.source: self.abort( 400, 'Source %s %s not found' % (source_short_name, string_id)) elif (self.source.status == 'disabled' or ('listen' not in self.source.features and 'email' not in self.source.features)): self.abort( 400, 'Source %s is disabled for backfeed' % self.source.bridgy_path()) format = self.request.get('format', 'html') if format not in ('html', 'json'): self.abort(400, 'Invalid format %s, expected html or json' % format) for id in ids: if not self.VALID_ID.match(id): self.abort(404, 'Invalid id %s' % id) label = '%s:%s %s %s' % (source_short_name, string_id, type, ids) cache_key = 'H ' + label obj = memcache.get(cache_key) if obj and not appengine_config.DEBUG: logging.info('Using cached object for %s', label) else: logging.info('Fetching %s', label) try: obj = self.get_item(*ids) except models.DisableSource as e: self.abort( 401, "Bridgy's access to your account has expired. Please visit https://brid.gy/ to refresh it!" ) except ValueError as e: self.abort(400, '%s error:\n%s' % (self.source.GR_CLASS.NAME, e)) except Exception as e: # pass through all API HTTP errors if we can identify them code, body = util.interpret_http_exception(e) # temporary, trying to debug a flaky test failure # eg https://circleci.com/gh/snarfed/bridgy/769 if code: self.response.status_int = int(code) self.response.headers['Content-Type'] = 'text/plain' self.response.write('%s error:\n%s' % (self.source.GR_CLASS.NAME, body)) return else: raise memcache.set(cache_key, obj, time=CACHE_TIME) if not obj: self.abort(404, label) if self.source.is_blocked(obj): self.abort(410, 'That user is currently blocked') # use https for profile pictures so we don't cause SSL mixed mode errors # when serving over https. author = obj.get('author', {}) image = author.get('image', {}) url = image.get('url') if url: image['url'] = util.update_scheme(url, self) mf2_json = microformats2.object_to_json(obj, synthesize_content=False) # try to include the author's silo profile url author = first_props(mf2_json.get('properties', {})).get('author', {}) author_uid = first_props(author.get('properties', {})).get('uid', '') if author_uid: parsed = util.parse_tag_uri(author_uid) if parsed: silo_url = self.source.gr_source.user_url(parsed[1]) urls = author.get('properties', {}).setdefault('url', []) if silo_url not in microformats2.get_string_urls(urls): urls.append(silo_url) # write the response! self.response.headers['Access-Control-Allow-Origin'] = '*' if format == 'html': self.response.headers['Content-Type'] = 'text/html; charset=utf-8' url = obj.get('url', '') self.response.out.write( TEMPLATE.substitute({ 'refresh': (('<meta http-equiv="refresh" content="0;url=%s">' % url) if url else ''), 'url': url, 'body': microformats2.json_to_html(mf2_json), 'title': self.get_title(obj), })) elif format == 'json': self.response.headers[ 'Content-Type'] = 'application/json; charset=utf-8' self.response.out.write(json.dumps(mf2_json, indent=2))
def get(self, type, source_short_name, string_id, *ids): source_cls = models.sources.get(source_short_name) if not source_cls: self.abort(400, "Source type '%s' not found. Known sources: %s" % (source_short_name, filter(None, models.sources.keys()))) self.source = source_cls.get_by_id(string_id) if not self.source: self.abort(400, 'Source %s %s not found' % (source_short_name, string_id)) elif self.source.status == 'disabled' or 'listen' not in self.source.features: self.abort(400, 'Source %s is disabled for backfeed' % self.source.bridgy_path()) format = self.request.get('format', 'html') if format not in ('html', 'json'): self.abort(400, 'Invalid format %s, expected html or json' % format) for id in ids: if not self.VALID_ID.match(id): self.abort(404, 'Invalid id %s' % id) label = '%s:%s %s %s' % (source_short_name, string_id, type, ids) cache_key = 'H ' + label obj = memcache.get(cache_key) if obj: logging.info('Using cached object for %s', label) else: logging.info('Fetching %s', label) try: obj = self.get_item(*ids) except models.DisableSource as e: self.abort(401, "Bridgy's access to your account has expired. Please visit https://brid.gy/ to refresh it!") except Exception as e: # pass through all API HTTP errors if we can identify them code, body = util.interpret_http_exception(e) if not code and util.is_connection_failure(e): code = 503 body = str(e) if code: self.response.status_int = int(code) self.response.headers['Content-Type'] = 'text/plain' self.response.write('%s error:\n%s' % (self.source.GR_CLASS.NAME, body)) return else: raise memcache.set(cache_key, obj, time=CACHE_TIME) if not obj: self.abort(404, label) # use https for profile pictures so we don't cause SSL mixed mode errors # when serving over https. author = obj.get('author', {}) image = author.get('image', {}) url = image.get('url') if url: image['url'] = util.update_scheme(url, self) mf2_json = microformats2.object_to_json(obj, synthesize_content=False) # try to include the author's silo profile url author = first_props(mf2_json.get('properties', {})).get('author', {}) author_uid = first_props(author.get('properties', {})).get('uid', '') if author_uid: parsed = util.parse_tag_uri(author_uid) if parsed: silo_url = self.source.gr_source.user_url(parsed[1]) urls = author.get('properties', {}).setdefault('url', []) if silo_url not in microformats2.get_string_urls(urls): urls.append(silo_url) # write the response! self.response.headers['Access-Control-Allow-Origin'] = '*' if format == 'html': self.response.headers['Content-Type'] = 'text/html; charset=utf-8' self.response.out.write(TEMPLATE.substitute({ 'url': obj.get('url', ''), 'body': microformats2.json_to_html(mf2_json), 'title': self.get_title(obj), })) elif format == 'json': self.response.headers['Content-Type'] = 'application/json; charset=utf-8' self.response.out.write(json.dumps(mf2_json, indent=2))
return else: raise if not obj: self.abort(404, label) # use https for profile pictures so we don't cause SSL mixed mode errors # when serving over https. author = obj.get("author", {}) image = author.get("image", {}) url = image.get("url") if url: image["url"] = util.update_scheme(url, self) mf2_json = microformats2.object_to_json(obj, synthesize_content=False) # try to include the author's silo profile url author = first_props(mf2_json.get("properties", {})).get("author", {}) author_uid = first_props(author.get("properties", {})).get("uid", "") if author_uid: parsed = util.parse_tag_uri(author_uid) if parsed: silo_url = self.source.gr_source.user_url(parsed[1]) urls = author.get("properties", {}).setdefault("url", []) if silo_url not in microformats2.get_string_urls(urls): urls.append(silo_url) # write the response! self.response.headers["Access-Control-Allow-Origin"] = "*" if format == "html":