def test_site_lookup_fails(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', 'my resp body', status=402) self.mox.ReplayAll() with self.assertRaises(urllib.error.HTTPError): WordPress.new(auth_entity=self.auth_entity)
def test_site_lookup_api_disabled_error_start(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', '{"error": "unauthorized",' ' "message": "API calls to this blog have been disabled."}', status=403) self.mox.ReplayAll() self.assertIsNone(WordPress.new(self.handler, auth_entity=self.auth_entity)) self.assertIsNone(WordPress.query().get()) self.assertIn('enable the Jetpack JSON API', next(iter(self.handler.messages)))
def test_site_lookup_api_disabled_error_start(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', '{"error": "unauthorized", "message": "API calls to this blog have been disabled."}', status=403) self.mox.ReplayAll() with app.test_request_context(): with self.assertRaises(RequestRedirect): self.assertIsNone(WordPress.new(auth_entity=self.auth_entity)) self.assertIsNone(WordPress.query().get()) self.assertIn('enable the Jetpack JSON API', get_flashed_messages()[0])
def setUp(self): super(WordPressTest, self).setUp() self.auth_entity = WordPressAuth(id='my.wp.com', user_json=json.dumps({ 'display_name': 'Ryan', 'username': '******', 'avatar_URL': 'http://ava/tar'}), blog_id='123', blog_url='http://my.wp.com/', access_token_str='my token') self.auth_entity.put() self.wp = WordPress(id='my.wp.com', auth_entity=self.auth_entity.key, url='http://my.wp.com/', domains=['my.wp.com'])
def test_new_site_domain_same_gr_blog_url(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', json.dumps({'ID': 123, 'URL': 'http://my.wp.com/'})) self.mox.ReplayAll() w = WordPress.new(self.handler, auth_entity=self.auth_entity) self.assertEquals(['http://my.wp.com/'], w.domain_urls) self.assertEquals(['my.wp.com'], w.domains)
def test_new_site_domain_same_gr_blog_url(self): self.expect_urlopen( "https://public-api.wordpress.com/rest/v1/sites/123?pretty=true", json.dumps({"ID": 123, "URL": "http://my.wp.com/"}), ) self.mox.ReplayAll() w = WordPress.new(self.handler, auth_entity=self.auth_entity) self.assertEquals(["http://my.wp.com/"], w.domain_urls) self.assertEquals(["my.wp.com"], w.domains)
def test_new(self): self.expect_urlopen("https://public-api.wordpress.com/rest/v1/sites/123?pretty=true", json.dumps({})) self.mox.ReplayAll() w = WordPress.new(self.handler, auth_entity=self.auth_entity) self.assertEquals(self.auth_entity.key, w.auth_entity) self.assertEquals("my.wp.com", w.key.id()) self.assertEquals("Ryan", w.name) self.assertEquals(["http://my.wp.com/"], w.domain_urls) self.assertEquals(["my.wp.com"], w.domains) self.assertEquals("http://ava/tar", w.picture)
def test_site_lookup_api_disabled_error_finish(self): self.expect_urlopen( "https://public-api.wordpress.com/rest/v1/sites/123?pretty=true", '{"error": "unauthorized",' ' "message": "API calls to this blog have been disabled."}', status=403, ) self.mox.ReplayAll() handler = AddWordPress(self.request, self.response) handler.finish(self.auth_entity) self.assertIsNone(WordPress.query().get()) self.assertIn("enable the Jetpack JSON API", next(iter(handler.messages)))
def test_new_with_site_domain(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', json.dumps({'ID': 123, 'URL': 'https://vanity.domain/'})) self.mox.ReplayAll() w = WordPress.new(self.handler, auth_entity=self.auth_entity) self.assertEquals('vanity.domain', w.key.id()) self.assertEquals('https://vanity.domain/', w.url) self.assertEquals(['https://vanity.domain/', 'http://my.wp.com/'], w.domain_urls) self.assertEquals(['vanity.domain', 'my.wp.com'], w.domains)
def test_new_with_site_domain(self): self.expect_urlopen( "https://public-api.wordpress.com/rest/v1/sites/123?pretty=true", json.dumps({"ID": 123, "URL": "https://vanity.domain/"}), ) self.mox.ReplayAll() w = WordPress.new(self.handler, auth_entity=self.auth_entity) self.assertEquals("vanity.domain", w.key.id()) self.assertEquals("https://vanity.domain/", w.url) self.assertEquals(["https://vanity.domain/", "http://my.wp.com/"], w.domain_urls) self.assertEquals(["vanity.domain", "my.wp.com"], w.domains)
def setUp(self): super(WordPressTest, self).setUp() self.auth_entity = WordPressAuth( id="my.wp.com", user_json=json.dumps({"display_name": "Ryan", "username": "******", "avatar_URL": "http://ava/tar"}), blog_id="123", blog_url="http://my.wp.com/", access_token_str="my token", ) self.auth_entity.put() self.wp = WordPress( id="my.wp.com", auth_entity=self.auth_entity.key, url="http://my.wp.com/", domains=["my.wp.com"] )
def test_new(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', json_dumps({})) self.mox.ReplayAll() w = WordPress.new(self.handler, auth_entity=self.auth_entity) self.assertEquals(self.auth_entity.key, w.auth_entity) self.assertEquals('my.wp.com', w.key.id()) self.assertEquals('Ryan', w.name) self.assertEquals(['http://my.wp.com/'], w.domain_urls) self.assertEquals(['my.wp.com'], w.domains) self.assertEquals('http://ava/tar', w.picture)
class WordPressTest(testutil.HandlerTest): def setUp(self): super(WordPressTest, self).setUp() self.auth_entity = WordPressAuth(id='my.wp.com', user_json=json.dumps({ 'display_name': 'Ryan', 'username': '******', 'avatar_URL': 'http://ava/tar'}), blog_id='123', blog_url='http://my.wp.com/', access_token_str='my token') self.auth_entity.put() self.wp = WordPress(id='my.wp.com', auth_entity=self.auth_entity.key, url='http://my.wp.com/', domains=['my.wp.com']) def test_new(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', json.dumps({})) self.mox.ReplayAll() w = WordPress.new(self.handler, auth_entity=self.auth_entity) self.assertEquals(self.auth_entity.key, w.auth_entity) self.assertEquals('my.wp.com', w.key.id()) self.assertEquals('Ryan', w.name) self.assertEquals(['http://my.wp.com/'], w.domain_urls) self.assertEquals(['my.wp.com'], w.domains) self.assertEquals('http://ava/tar', w.picture) def test_new_with_site_domain(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', json.dumps({'ID': 123, 'URL': 'https://vanity.domain/'})) self.mox.ReplayAll() w = WordPress.new(self.handler, auth_entity=self.auth_entity) self.assertEquals('vanity.domain', w.key.id()) self.assertEquals('https://vanity.domain/', w.url) self.assertEquals(['https://vanity.domain/', 'http://my.wp.com/'], w.domain_urls) self.assertEquals(['vanity.domain', 'my.wp.com'], w.domains) def test_create_comment_with_slug_lookup(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123/posts/' 'slug:the-slug?pretty=true', json.dumps({'ID': 456})) self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123/posts/' '456/replies/new?pretty=true', json.dumps({'ID': 789, 'ok': 'sgtm'}), data=urllib.urlencode({'content': '<a href="http://who">who</a>: foo bar'})) self.mox.ReplayAll() resp = self.wp.create_comment('http://primary/post/123999/the-slug?asdf', 'who', 'http://who', 'foo bar') self.assertEquals({'id': 789, 'ok': 'sgtm'}, resp) def test_create_comment_with_unicode_chars(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123/posts/' '123/replies/new?pretty=true', json.dumps({}), data=urllib.urlencode({ 'content': '<a href="http://who">Degenève</a>: foo Degenève bar'})) self.mox.ReplayAll() resp = self.wp.create_comment('http://primary/post/123', u'Degenève', 'http://who', u'foo Degenève bar') def test_create_comment_gives_up_on_invalid_input_error(self): # see https://github.com/snarfed/bridgy/issues/161 self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123/posts/' '123/replies/new?pretty=true', json.dumps({'error': 'invalid_input'}), status=400, data=urllib.urlencode({'content': '<a href="http://who">name</a>: foo'})) self.mox.ReplayAll() resp = self.wp.create_comment('http://primary/post/123', 'name', 'http://who', 'foo') # shouldn't raise an exception self.assertEquals({'error': 'invalid_input'}, resp)
class WordPressTest(testutil.HandlerTest): def setUp(self): super(WordPressTest, self).setUp() self.auth_entity = WordPressAuth(id='my.wp.com', user_json=json_dumps({ 'display_name': 'Ryan', 'username': '******', 'avatar_URL': 'http://ava/tar' }), blog_id='123', blog_url='http://my.wp.com/', access_token_str='my token') self.auth_entity.put() self.wp = WordPress(id='my.wp.com', auth_entity=self.auth_entity.key, url='http://my.wp.com/', domains=['my.wp.com']) def expect_new_reply( self, url='https://public-api.wordpress.com/rest/v1/sites/123/posts/456/replies/new?pretty=true', content='<a href="http://who">name</a>: foo bar', response='{}', status=200, **kwargs): self.expect_urlopen(url, response, data=urllib.parse.urlencode({'content': content}), status=status, **kwargs) self.mox.ReplayAll() def test_new(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', json_dumps({})) self.mox.ReplayAll() w = WordPress.new(self.handler, auth_entity=self.auth_entity) self.assertEquals(self.auth_entity.key, w.auth_entity) self.assertEquals('my.wp.com', w.key.id()) self.assertEquals('Ryan', w.name) self.assertEquals(['http://my.wp.com/'], w.domain_urls) self.assertEquals(['my.wp.com'], w.domains) self.assertEquals('http://ava/tar', w.picture) def test_new_with_site_domain(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', json_dumps({ 'ID': 123, 'URL': 'https://vanity.domain/' })) self.mox.ReplayAll() w = WordPress.new(self.handler, auth_entity=self.auth_entity) self.assertEquals('vanity.domain', w.key.id()) self.assertEquals('https://vanity.domain/', w.url) self.assertEquals(['https://vanity.domain/', 'http://my.wp.com/'], w.domain_urls) self.assertEquals(['vanity.domain', 'my.wp.com'], w.domains) def test_new_site_domain_same_gr_blog_url(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', json_dumps({ 'ID': 123, 'URL': 'http://my.wp.com/' })) self.mox.ReplayAll() w = WordPress.new(self.handler, auth_entity=self.auth_entity) self.assertEquals(['http://my.wp.com/'], w.domain_urls) self.assertEquals(['my.wp.com'], w.domains) def test_site_lookup_fails(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', 'my resp body', status=402) self.mox.ReplayAll() self.assertRaises(urllib_error_py2.HTTPError, WordPress.new, self.handler, auth_entity=self.auth_entity) def test_site_lookup_api_disabled_error_start(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', '{"error": "unauthorized",' ' "message": "API calls to this blog have been disabled."}', status=403) self.mox.ReplayAll() self.assertIsNone( WordPress.new(self.handler, auth_entity=self.auth_entity)) self.assertIsNone(WordPress.query().get()) self.assertIn('enable the Jetpack JSON API', next(iter(self.handler.messages))) def test_site_lookup_api_disabled_error_finish(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', '{"error": "unauthorized",' ' "message": "API calls to this blog have been disabled."}', status=403) self.mox.ReplayAll() handler = AddWordPress(self.request, self.response) handler.finish(self.auth_entity) self.assertIsNone(WordPress.query().get()) self.assertIn('enable the Jetpack JSON API', next(iter(handler.messages))) def test_create_comment_with_slug_lookup(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123/posts/' 'slug:the-slug?pretty=true', json_dumps({'ID': 456})) self.expect_new_reply(response=json_dumps({'ID': 789, 'ok': 'sgtm'})) resp = self.wp.create_comment( 'http://primary/post/123999/the-slug?asdf', 'name', 'http://who', 'foo bar') # ID field gets converted to lower case id self.assertEquals({'id': 789, 'ok': 'sgtm'}, resp) def test_create_comment_with_unicode_chars(self): self.expect_new_reply( content='<a href="http://who">Degenève</a>: foo Degenève bar') resp = self.wp.create_comment('http://primary/post/456', 'Degenève', 'http://who', 'foo Degenève bar') self.assertEquals({'id': None}, resp) def test_create_comment_with_unicode_chars_in_slug(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123/posts/slug:✁?pretty=true', json_dumps({'ID': 456})) self.expect_new_reply() resp = self.wp.create_comment('http://primary/post/✁', 'name', 'http://who', 'foo bar') self.assertEquals({'id': None}, resp) def test_create_comment_gives_up_on_invalid_input_error(self): # see https://github.com/snarfed/bridgy/issues/161 self.expect_new_reply(status=400, response=json_dumps({'error': 'invalid_input'})) resp = self.wp.create_comment('http://primary/post/456', 'name', 'http://who', 'foo bar') # shouldn't raise an exception self.assertEquals({'error': 'invalid_input'}, resp) def test_create_comment_gives_up_on_coments_closed(self): resp = { 'error': 'unauthorized', 'message': 'Comments on this post are closed' } self.expect_new_reply(status=403, response=json_dumps(resp)) # shouldn't raise an exception got = self.wp.create_comment('http://primary/post/456', 'name', 'http://who', 'foo bar') self.assertEquals(resp, got) def test_create_comment_returns_non_json(self): self.expect_new_reply(status=403, response='Forbidden') self.assertRaises(urllib_error_py2.HTTPError, self.wp.create_comment, 'http://primary/post/456', 'name', 'http://who', 'foo bar')
def user(site, id): """View for a user page.""" cls = models.sources.get(site) if not cls: return render_template('user_not_found.html'), 404 source = cls.lookup(id) if not source: key = cls.query( ndb.OR(*[ ndb.GenericProperty(prop) == id for prop in ('domains', 'inferred_username', 'name', 'username') ])).get(keys_only=True) if key: return redirect(cls(key=key).bridgy_path(), code=301) if not source or not source.features: return render_template('user_not_found.html'), 404 source.verify() source = util.preprocess_source(source) vars = { 'source': source, 'logs': logs, 'REFETCH_HFEED_TRIGGER': models.REFETCH_HFEED_TRIGGER, 'RECENT_PRIVATE_POSTS_THRESHOLD': RECENT_PRIVATE_POSTS_THRESHOLD, } # Blog webmention promos if 'webmention' not in source.features: if source.SHORT_NAME in ('blogger', 'medium', 'tumblr', 'wordpress'): vars[source.SHORT_NAME + '_promo'] = True else: for domain in source.domains: if ('.blogspot.' in domain and # Blogger uses country TLDs not Blogger.query(Blogger.domains == domain).get()): vars['blogger_promo'] = True elif (util.domain_or_parent_in(domain, ['tumblr.com']) and not Tumblr.query(Tumblr.domains == domain).get()): vars['tumblr_promo'] = True elif (util.domain_or_parent_in(domain, 'wordpress.com') and not WordPress.query(WordPress.domains == domain).get()): vars['wordpress_promo'] = True # Responses if 'listen' in source.features or 'email' in source.features: vars['responses'] = [] query = Response.query().filter(Response.source == source.key) # if there's a paging param (responses_before or responses_after), update # query with it def get_paging_param(param): val = request.values.get(param) try: return util.parse_iso8601(val.replace(' ', '+')) if val else None except BaseException: error(f"Couldn't parse {param}, {val!r} as ISO8601") before = get_paging_param('responses_before') after = get_paging_param('responses_after') if before and after: error("can't handle both responses_before and responses_after") elif after: query = query.filter(Response.updated > after).order( Response.updated) elif before: query = query.filter( Response.updated < before).order(-Response.updated) else: query = query.order(-Response.updated) query_iter = query.iter() for i, r in enumerate(query_iter): r.response = json_loads(r.response_json) r.activities = [json_loads(a) for a in r.activities_json] if (not source.is_activity_public(r.response) or not all( source.is_activity_public(a) for a in r.activities)): continue elif r.type == 'post': r.activities = [] verb = r.response.get('verb') r.actor = (r.response.get('object') if verb == 'invite' else r.response.get('author') or r.response.get('actor')) or {} activity_content = '' for a in r.activities + [r.response]: if not a.get('content'): obj = a.get('object', {}) a['content'] = activity_content = ( obj.get('content') or obj.get('displayName') or # historical, from a Reddit bug fixed in granary@4f9df7c obj.get('name') or '') response_content = r.response.get('content') phrases = { 'like': 'liked this', 'repost': 'reposted this', 'rsvp-yes': 'is attending', 'rsvp-no': 'is not attending', 'rsvp-maybe': 'might attend', 'rsvp-interested': 'is interested', 'invite': 'is invited', } phrase = phrases.get(r.type) or phrases.get(verb) if phrase and (r.type != 'repost' or activity_content.startswith(response_content)): r.response[ 'content'] = f'{r.actor.get("displayName") or ""} {phrase}.' # convert image URL to https if we're serving over SSL image_url = r.actor.setdefault('image', {}).get('url') if image_url: r.actor['image']['url'] = util.update_scheme( image_url, request) # generate original post links r.links = process_webmention_links(r) r.original_links = [ util.pretty_link(url, new_tab=True) for url in r.original_posts ] vars['responses'].append(r) if len(vars['responses']) >= 10 or i > 200: break vars['responses'].sort(key=lambda r: r.updated, reverse=True) # calculate new paging param(s) new_after = (before if before else vars['responses'][0].updated if vars['responses'] and query_iter.probably_has_next() and (before or after) else None) if new_after: vars[ 'responses_after_link'] = f'?responses_after={new_after.isoformat()}#responses' new_before = (after if after else vars['responses'][-1].updated if vars['responses'] and query_iter.probably_has_next() else None) if new_before: vars[ 'responses_before_link'] = f'?responses_before={new_before.isoformat()}#responses' vars['next_poll'] = max( source.last_poll_attempt + source.poll_period(), # lower bound is 1 minute from now util.now_fn() + datetime.timedelta(seconds=90)) # Publishes if 'publish' in source.features: publishes = Publish.query().filter(Publish.source == source.key)\ .order(-Publish.updated)\ .fetch(10) for p in publishes: p.pretty_page = util.pretty_link( p.key.parent().id(), attrs={'class': 'original-post u-url u-name'}, new_tab=True) vars['publishes'] = publishes if 'webmention' in source.features: # Blog posts blogposts = BlogPost.query().filter(BlogPost.source == source.key)\ .order(-BlogPost.created)\ .fetch(10) for b in blogposts: b.links = process_webmention_links(b) try: text = b.feed_item.get('title') except ValueError: text = None b.pretty_url = util.pretty_link( b.key.id(), text=text, attrs={'class': 'original-post u-url u-name'}, max_length=40, new_tab=True) # Blog webmentions webmentions = BlogWebmention.query()\ .filter(BlogWebmention.source == source.key)\ .order(-BlogWebmention.updated)\ .fetch(10) for w in webmentions: w.pretty_source = util.pretty_link( w.source_url(), attrs={'class': 'original-post'}, new_tab=True) try: target_is_source = (urllib.parse.urlparse( w.target_url()).netloc in source.domains) except BaseException: target_is_source = False w.pretty_target = util.pretty_link( w.target_url(), attrs={'class': 'original-post'}, new_tab=True, keep_host=target_is_source) vars.update({'blogposts': blogposts, 'webmentions': webmentions}) return render_template(f'{source.SHORT_NAME}_user.html', **vars)
def template_vars(self): if not self.source: return {} vars = super(UserHandler, self).template_vars() vars.update({ 'source': self.source, 'epoch': util.EPOCH, }) # Blog webmention promos if 'webmention' not in self.source.features: if self.source.SHORT_NAME in ('blogger', 'tumblr', 'wordpress'): vars[self.source.SHORT_NAME + '_promo'] = True else: for domain in self.source.domains: if ('.blogspot.' in domain and # Blogger uses country TLDs not Blogger.query(Blogger.domains == domain).get()): vars['blogger_promo'] = True elif (domain.endswith('tumblr.com') and not Tumblr.query(Tumblr.domains == domain).get()): vars['tumblr_promo'] = True elif (domain.endswith('wordpress.com') and not WordPress.query(WordPress.domains == domain).get()): vars['wordpress_promo'] = True # Responses if 'listen' in self.source.features: vars['responses'] = [] for i, r in enumerate(Response.query() .filter(Response.source == self.source.key)\ .order(-Response.updated)): r.response = json.loads(r.response_json) if r.activity_json: # handle old entities r.activities_json.append(r.activity_json) r.activities = [json.loads(a) for a in r.activities_json] if (not gr_source.Source.is_public(r.response) or not all(gr_source.Source.is_public(a) for a in r.activities)): continue r.actor = r.response.get('author') or r.response.get('actor', {}) if not r.response.get('content'): phrases = { 'like': 'liked this', 'repost': 'reposted this', 'rsvp-yes': 'is attending', 'rsvp-no': 'is not attending', 'rsvp-maybe': 'might attend', 'invite': 'is invited', } r.response['content'] = '%s %s.' % ( r.actor.get('displayName') or '', phrases.get(r.type) or phrases.get(r.response.get('verb'))) # convert image URL to https if we're serving over SSL image_url = r.actor.setdefault('image', {}).get('url') if image_url: r.actor['image']['url'] = util.update_scheme(image_url, self) # generate original post links r.links = self.process_webmention_links(r) vars['responses'].append(r) if len(vars['responses']) >= 10 or i > 200: break # Publishes if 'publish' in self.source.features: publishes = Publish.query().filter(Publish.source == self.source.key)\ .order(-Publish.updated)\ .fetch(10) for p in publishes: p.pretty_page = util.pretty_link( p.key.parent().id(), a_class='original-post', new_tab=True) vars['publishes'] = publishes if 'webmention' in self.source.features: # Blog posts blogposts = BlogPost.query().filter(BlogPost.source == self.source.key)\ .order(-BlogPost.created)\ .fetch(10) for b in blogposts: b.links = self.process_webmention_links(b) try: text = b.feed_item.get('title') except ValueError: text = None b.pretty_url = util.pretty_link(b.key.id(), text=text, a_class='original-post', max_length=40, new_tab=True) # Blog webmentions webmentions = BlogWebmention.query()\ .filter(BlogWebmention.source == self.source.key)\ .order(-BlogWebmention.updated)\ .fetch(10) for w in webmentions: w.pretty_source = util.pretty_link(w.source_url(), a_class='original-post', new_tab=True) try: target_is_source = (urlparse.urlparse(w.target_url()).netloc in self.source.domains) except BaseException: target_is_source = False w.pretty_target = util.pretty_link(w.target_url(), a_class='original-post', new_tab=True, keep_host=target_is_source) vars.update({'blogposts': blogposts, 'webmentions': webmentions}) return vars
class WordPressTest(testutil.HandlerTest): def setUp(self): super(WordPressTest, self).setUp() self.auth_entity = WordPressAuth(id='my.wp.com', user_json=json.dumps({ 'display_name': 'Ryan', 'username': '******', 'avatar_URL': 'http://ava/tar'}), blog_id='123', blog_url='http://my.wp.com/', access_token_str='my token') self.auth_entity.put() self.wp = WordPress(id='my.wp.com', auth_entity=self.auth_entity.key, url='http://my.wp.com/', domains=['my.wp.com']) def expect_new_reply( self, url='https://public-api.wordpress.com/rest/v1/sites/123/posts/456/replies/new?pretty=true', content='<a href="http://who">name</a>: foo bar', response='{}', status=200, **kwargs): self.expect_urlopen( url, response, data=urllib.parse.urlencode({'content': content}), status=status, **kwargs) self.mox.ReplayAll() def test_new(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', json.dumps({})) self.mox.ReplayAll() w = WordPress.new(self.handler, auth_entity=self.auth_entity) self.assertEquals(self.auth_entity.key, w.auth_entity) self.assertEquals('my.wp.com', w.key.id()) self.assertEquals('Ryan', w.name) self.assertEquals(['http://my.wp.com/'], w.domain_urls) self.assertEquals(['my.wp.com'], w.domains) self.assertEquals('http://ava/tar', w.picture) def test_new_with_site_domain(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', json.dumps({'ID': 123, 'URL': 'https://vanity.domain/'})) self.mox.ReplayAll() w = WordPress.new(self.handler, auth_entity=self.auth_entity) self.assertEquals('vanity.domain', w.key.id()) self.assertEquals('https://vanity.domain/', w.url) self.assertEquals(['https://vanity.domain/', 'http://my.wp.com/'], w.domain_urls) self.assertEquals(['vanity.domain', 'my.wp.com'], w.domains) def test_new_site_domain_same_gr_blog_url(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', json.dumps({'ID': 123, 'URL': 'http://my.wp.com/'})) self.mox.ReplayAll() w = WordPress.new(self.handler, auth_entity=self.auth_entity) self.assertEquals(['http://my.wp.com/'], w.domain_urls) self.assertEquals(['my.wp.com'], w.domains) def test_site_lookup_fails(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', 'my resp body', status=402) self.mox.ReplayAll() self.assertRaises(urllib2.HTTPError, WordPress.new, self.handler, auth_entity=self.auth_entity) def test_site_lookup_api_disabled_error_start(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', '{"error": "unauthorized",' ' "message": "API calls to this blog have been disabled."}', status=403) self.mox.ReplayAll() self.assertIsNone(WordPress.new(self.handler, auth_entity=self.auth_entity)) self.assertIsNone(WordPress.query().get()) self.assertIn('enable the Jetpack JSON API', next(iter(self.handler.messages))) def test_site_lookup_api_disabled_error_finish(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123?pretty=true', '{"error": "unauthorized",' ' "message": "API calls to this blog have been disabled."}', status=403) self.mox.ReplayAll() handler = AddWordPress(self.request, self.response) handler.finish(self.auth_entity) self.assertIsNone(WordPress.query().get()) self.assertIn('enable the Jetpack JSON API', next(iter(handler.messages))) def test_create_comment_with_slug_lookup(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123/posts/' 'slug:the-slug?pretty=true', json.dumps({'ID': 456})) self.expect_new_reply(response=json.dumps({'ID': 789, 'ok': 'sgtm'})) resp = self.wp.create_comment('http://primary/post/123999/the-slug?asdf', 'name', 'http://who', 'foo bar') # ID field gets converted to lower case id self.assertEquals({'id': 789, 'ok': 'sgtm'}, resp) def test_create_comment_with_unicode_chars(self): self.expect_new_reply(content='<a href="http://who">Degenève</a>: foo Degenève bar') resp = self.wp.create_comment('http://primary/post/456', 'Degenève', 'http://who', 'foo Degenève bar') self.assertEquals({'id': None}, resp) def test_create_comment_with_unicode_chars_in_slug(self): self.expect_urlopen( 'https://public-api.wordpress.com/rest/v1/sites/123/posts/slug:✁?pretty=true', json.dumps({'ID': 456})) self.expect_new_reply() resp = self.wp.create_comment('http://primary/post/✁', 'name', 'http://who', 'foo bar') self.assertEquals({'id': None}, resp) def test_create_comment_gives_up_on_invalid_input_error(self): # see https://github.com/snarfed/bridgy/issues/161 self.expect_new_reply(status=400, response=json.dumps({'error': 'invalid_input'})) resp = self.wp.create_comment('http://primary/post/456', 'name', 'http://who', 'foo bar') # shouldn't raise an exception self.assertEquals({'error': 'invalid_input'}, resp) def test_create_comment_gives_up_on_coments_closed(self): resp = {'error': 'unauthorized', 'message': 'Comments on this post are closed'} self.expect_new_reply(status=403, response=json.dumps(resp)) # shouldn't raise an exception got = self.wp.create_comment('http://primary/post/456', 'name', 'http://who', 'foo bar') self.assertEquals(resp, got) def test_create_comment_returns_non_json(self): self.expect_new_reply(status=403, response='Forbidden') self.assertRaises(urllib2.HTTPError, self.wp.create_comment, 'http://primary/post/456', 'name', 'http://who', 'foo bar')
def template_vars(self): vars = super(UserHandler, self).template_vars() vars.update({ 'source': self.source, 'EPOCH': util.EPOCH, 'REFETCH_HFEED_TRIGGER': models.REFETCH_HFEED_TRIGGER, 'RECENT_PRIVATE_POSTS_THRESHOLD': RECENT_PRIVATE_POSTS_THRESHOLD, }) if not self.source: return vars if isinstance(self.source, instagram.Instagram): auth = self.source.auth_entity vars['indieauth_me'] = ( auth.id if isinstance(auth, indieauth.IndieAuth) else self.source.domain_urls[0] if self.source.domain_urls else None) # Blog webmention promos if 'webmention' not in self.source.features: if self.source.SHORT_NAME in ('blogger', 'tumblr', 'wordpress'): vars[self.source.SHORT_NAME + '_promo'] = True else: for domain in self.source.domains: if ('.blogspot.' in domain and # Blogger uses country TLDs not Blogger.query(Blogger.domains == domain).get()): vars['blogger_promo'] = True elif (domain.endswith('tumblr.com') and not Tumblr.query(Tumblr.domains == domain).get()): vars['tumblr_promo'] = True elif (domain.endswith('wordpress.com') and not WordPress.query(WordPress.domains == domain).get()): vars['wordpress_promo'] = True # Responses if 'listen' in self.source.features: vars['responses'] = [] query = Response.query().filter(Response.source == self.source.key) # if there's a paging param (responses_before or responses_after), update # query with it def get_paging_param(param): val = self.request.get(param) try: return util.parse_iso8601(val) if val else None except: msg = "Couldn't parse %s %r as ISO8601" % (param, val) logging.exception(msg) self.abort(400, msg) before = get_paging_param('responses_before') after = get_paging_param('responses_after') if before and after: self.abort(400, "can't handle both responses_before and responses_after") elif after: query = query.filter(Response.updated > after).order(Response.updated) elif before: query = query.filter(Response.updated < before).order(-Response.updated) else: query = query.order(-Response.updated) query_iter = query.iter() for i, r in enumerate(query_iter): r.response = json.loads(r.response_json) r.activities = [json.loads(a) for a in r.activities_json] if (not self.source.is_activity_public(r.response) or not all(self.source.is_activity_public(a) for a in r.activities)): continue elif r.type == 'post': r.activities = [] r.actor = r.response.get('author') or r.response.get('actor', {}) for a in r.activities + [r.response]: if not a.get('content'): a['content'] = a.get('object', {}).get('content') if not r.response.get('content'): phrases = { 'like': 'liked this', 'repost': 'reposted this', 'rsvp-yes': 'is attending', 'rsvp-no': 'is not attending', 'rsvp-maybe': 'might attend', 'rsvp-interested': 'is interested', 'invite': 'is invited', } r.response['content'] = '%s %s.' % ( r.actor.get('displayName') or '', phrases.get(r.type) or phrases.get(r.response.get('verb'))) # convert image URL to https if we're serving over SSL image_url = r.actor.setdefault('image', {}).get('url') if image_url: r.actor['image']['url'] = util.update_scheme(image_url, self) # generate original post links r.links = self.process_webmention_links(r) r.original_links = [util.pretty_link(url, new_tab=True) for url in r.original_posts] vars['responses'].append(r) if len(vars['responses']) >= 10 or i > 200: break vars['responses'].sort(key=lambda r: r.updated, reverse=True) # calculate new paging param(s) new_after = ( before if before else vars['responses'][0].updated if vars['responses'] and query_iter.probably_has_next() and (before or after) else None) if new_after: vars['responses_after_link'] = ('?responses_after=%s#responses' % new_after.isoformat()) new_before = ( after if after else vars['responses'][-1].updated if vars['responses'] and query_iter.probably_has_next() else None) if new_before: vars['responses_before_link'] = ('?responses_before=%s#responses' % new_before.isoformat()) vars['next_poll'] = max( self.source.last_poll_attempt + self.source.poll_period(), # lower bound is 1 minute from now util.now_fn() + datetime.timedelta(seconds=90)) # Publishes if 'publish' in self.source.features: publishes = Publish.query().filter(Publish.source == self.source.key)\ .order(-Publish.updated)\ .fetch(10) for p in publishes: p.pretty_page = util.pretty_link( p.key.parent().id(), attrs={'class': 'original-post u-url u-name'}, new_tab=True) vars['publishes'] = publishes if 'webmention' in self.source.features: # Blog posts blogposts = BlogPost.query().filter(BlogPost.source == self.source.key)\ .order(-BlogPost.created)\ .fetch(10) for b in blogposts: b.links = self.process_webmention_links(b) try: text = b.feed_item.get('title') except ValueError: text = None b.pretty_url = util.pretty_link( b.key.id(), text=text, attrs={'class': 'original-post u-url u-name'}, max_length=40, new_tab=True) # Blog webmentions webmentions = BlogWebmention.query()\ .filter(BlogWebmention.source == self.source.key)\ .order(-BlogWebmention.updated)\ .fetch(10) for w in webmentions: w.pretty_source = util.pretty_link( w.source_url(), attrs={'class': 'original-post'}, new_tab=True) try: target_is_source = (urlparse.urlparse(w.target_url()).netloc in self.source.domains) except BaseException: target_is_source = False w.pretty_target = util.pretty_link( w.target_url(), attrs={'class': 'original-post'}, new_tab=True, keep_host=target_is_source) vars.update({'blogposts': blogposts, 'webmentions': webmentions}) return vars
def template_vars(self): vars = super(UserHandler, self).template_vars() vars.update({ 'source': self.source, 'EPOCH': util.EPOCH, 'REFETCH_HFEED_TRIGGER': models.REFETCH_HFEED_TRIGGER, 'RECENT_PRIVATE_POSTS_THRESHOLD': RECENT_PRIVATE_POSTS_THRESHOLD, }) if not self.source: return vars if isinstance(self.source, instagram.Instagram): auth = self.source.auth_entity vars['indieauth_me'] = ( auth.id if isinstance(auth, indieauth.IndieAuth) else self.source.domain_urls[0] if self.source.domain_urls else None) # Blog webmention promos if 'webmention' not in self.source.features: if self.source.SHORT_NAME in ('blogger', 'medium', 'tumblr', 'wordpress'): vars[self.source.SHORT_NAME + '_promo'] = True else: for domain in self.source.domains: if ('.blogspot.' in domain and # Blogger uses country TLDs not Blogger.query(Blogger.domains == domain).get()): vars['blogger_promo'] = True elif (domain.endswith('tumblr.com') and not Tumblr.query(Tumblr.domains == domain).get()): vars['tumblr_promo'] = True elif (domain.endswith('wordpress.com') and not WordPress.query(WordPress.domains == domain).get()): vars['wordpress_promo'] = True # Responses if 'listen' in self.source.features: vars['responses'] = [] query = Response.query().filter(Response.source == self.source.key) # if there's a paging param (responses_before or responses_after), update # query with it def get_paging_param(param): val = self.request.get(param) try: return util.parse_iso8601(val) if val else None except: msg = "Couldn't parse %s %r as ISO8601" % (param, val) logging.exception(msg) self.abort(400, msg) before = get_paging_param('responses_before') after = get_paging_param('responses_after') if before and after: self.abort(400, "can't handle both responses_before and responses_after") elif after: query = query.filter(Response.updated > after).order(Response.updated) elif before: query = query.filter(Response.updated < before).order(-Response.updated) else: query = query.order(-Response.updated) query_iter = query.iter() for i, r in enumerate(query_iter): r.response = json.loads(r.response_json) r.activities = [json.loads(a) for a in r.activities_json] if (not self.source.is_activity_public(r.response) or not all(self.source.is_activity_public(a) for a in r.activities)): continue elif r.type == 'post': r.activities = [] r.actor = r.response.get('author') or r.response.get('actor', {}) for a in r.activities + [r.response]: if not a.get('content'): a['content'] = a.get('object', {}).get('content') if not r.response.get('content'): phrases = { 'like': 'liked this', 'repost': 'reposted this', 'rsvp-yes': 'is attending', 'rsvp-no': 'is not attending', 'rsvp-maybe': 'might attend', 'rsvp-interested': 'is interested', 'invite': 'is invited', } r.response['content'] = '%s %s.' % ( r.actor.get('displayName') or '', phrases.get(r.type) or phrases.get(r.response.get('verb'))) # convert image URL to https if we're serving over SSL image_url = r.actor.setdefault('image', {}).get('url') if image_url: r.actor['image']['url'] = util.update_scheme(image_url, self) # generate original post links r.links = self.process_webmention_links(r) r.original_links = [util.pretty_link(url, new_tab=True) for url in r.original_posts] vars['responses'].append(r) if len(vars['responses']) >= 10 or i > 200: break vars['responses'].sort(key=lambda r: r.updated, reverse=True) # calculate new paging param(s) new_after = ( before if before else vars['responses'][0].updated if vars['responses'] and query_iter.probably_has_next() and (before or after) else None) if new_after: vars['responses_after_link'] = ('?responses_after=%s#responses' % new_after.isoformat()) new_before = ( after if after else vars['responses'][-1].updated if vars['responses'] and query_iter.probably_has_next() else None) if new_before: vars['responses_before_link'] = ('?responses_before=%s#responses' % new_before.isoformat()) vars['next_poll'] = max( self.source.last_poll_attempt + self.source.poll_period(), # lower bound is 1 minute from now util.now_fn() + datetime.timedelta(seconds=90)) # Publishes if 'publish' in self.source.features: publishes = Publish.query().filter(Publish.source == self.source.key)\ .order(-Publish.updated)\ .fetch(10) for p in publishes: p.pretty_page = util.pretty_link( p.key.parent().id().decode('utf-8'), attrs={'class': 'original-post u-url u-name'}, new_tab=True) vars['publishes'] = publishes if 'webmention' in self.source.features: # Blog posts blogposts = BlogPost.query().filter(BlogPost.source == self.source.key)\ .order(-BlogPost.created)\ .fetch(10) for b in blogposts: b.links = self.process_webmention_links(b) try: text = b.feed_item.get('title') except ValueError: text = None b.pretty_url = util.pretty_link( b.key.id(), text=text, attrs={'class': 'original-post u-url u-name'}, max_length=40, new_tab=True) # Blog webmentions webmentions = BlogWebmention.query()\ .filter(BlogWebmention.source == self.source.key)\ .order(-BlogWebmention.updated)\ .fetch(10) for w in webmentions: w.pretty_source = util.pretty_link( w.source_url(), attrs={'class': 'original-post'}, new_tab=True) try: target_is_source = (urlparse.urlparse(w.target_url()).netloc in self.source.domains) except BaseException: target_is_source = False w.pretty_target = util.pretty_link( w.target_url(), attrs={'class': 'original-post'}, new_tab=True, keep_host=target_is_source) vars.update({'blogposts': blogposts, 'webmentions': webmentions}) return vars
class WordPressTest(testutil.HandlerTest): def setUp(self): super(WordPressTest, self).setUp() self.auth_entity = WordPressAuth( id="my.wp.com", user_json=json.dumps({"display_name": "Ryan", "username": "******", "avatar_URL": "http://ava/tar"}), blog_id="123", blog_url="http://my.wp.com/", access_token_str="my token", ) self.auth_entity.put() self.wp = WordPress( id="my.wp.com", auth_entity=self.auth_entity.key, url="http://my.wp.com/", domains=["my.wp.com"] ) def expect_new_reply( self, url="https://public-api.wordpress.com/rest/v1/sites/123/posts/456/replies/new?pretty=true", content='<a href="http://who">name</a>: foo bar', response="{}", status=200, **kwargs ): self.expect_urlopen(url, response, data=urllib.urlencode({"content": content}), status=status, **kwargs) self.mox.ReplayAll() def test_new(self): self.expect_urlopen("https://public-api.wordpress.com/rest/v1/sites/123?pretty=true", json.dumps({})) self.mox.ReplayAll() w = WordPress.new(self.handler, auth_entity=self.auth_entity) self.assertEquals(self.auth_entity.key, w.auth_entity) self.assertEquals("my.wp.com", w.key.id()) self.assertEquals("Ryan", w.name) self.assertEquals(["http://my.wp.com/"], w.domain_urls) self.assertEquals(["my.wp.com"], w.domains) self.assertEquals("http://ava/tar", w.picture) def test_new_with_site_domain(self): self.expect_urlopen( "https://public-api.wordpress.com/rest/v1/sites/123?pretty=true", json.dumps({"ID": 123, "URL": "https://vanity.domain/"}), ) self.mox.ReplayAll() w = WordPress.new(self.handler, auth_entity=self.auth_entity) self.assertEquals("vanity.domain", w.key.id()) self.assertEquals("https://vanity.domain/", w.url) self.assertEquals(["https://vanity.domain/", "http://my.wp.com/"], w.domain_urls) self.assertEquals(["vanity.domain", "my.wp.com"], w.domains) def test_new_site_domain_same_gr_blog_url(self): self.expect_urlopen( "https://public-api.wordpress.com/rest/v1/sites/123?pretty=true", json.dumps({"ID": 123, "URL": "http://my.wp.com/"}), ) self.mox.ReplayAll() w = WordPress.new(self.handler, auth_entity=self.auth_entity) self.assertEquals(["http://my.wp.com/"], w.domain_urls) self.assertEquals(["my.wp.com"], w.domains) def test_site_lookup_fails(self): self.expect_urlopen( "https://public-api.wordpress.com/rest/v1/sites/123?pretty=true", "my resp body", status=402 ) self.mox.ReplayAll() self.assertRaises(urllib2.HTTPError, WordPress.new, self.handler, auth_entity=self.auth_entity) def test_site_lookup_api_disabled_error_start(self): self.expect_urlopen( "https://public-api.wordpress.com/rest/v1/sites/123?pretty=true", '{"error": "unauthorized",' ' "message": "API calls to this blog have been disabled."}', status=403, ) self.mox.ReplayAll() self.assertIsNone(WordPress.new(self.handler, auth_entity=self.auth_entity)) self.assertIsNone(WordPress.query().get()) self.assertIn("enable the Jetpack JSON API", next(iter(self.handler.messages))) def test_site_lookup_api_disabled_error_finish(self): self.expect_urlopen( "https://public-api.wordpress.com/rest/v1/sites/123?pretty=true", '{"error": "unauthorized",' ' "message": "API calls to this blog have been disabled."}', status=403, ) self.mox.ReplayAll() handler = AddWordPress(self.request, self.response) handler.finish(self.auth_entity) self.assertIsNone(WordPress.query().get()) self.assertIn("enable the Jetpack JSON API", next(iter(handler.messages))) def test_create_comment_with_slug_lookup(self): self.expect_urlopen( "https://public-api.wordpress.com/rest/v1/sites/123/posts/" "slug:the-slug?pretty=true", json.dumps({"ID": 456}), ) self.expect_new_reply(response=json.dumps({"ID": 789, "ok": "sgtm"})) resp = self.wp.create_comment("http://primary/post/123999/the-slug?asdf", "name", "http://who", "foo bar") # ID field gets converted to lower case id self.assertEquals({"id": 789, "ok": "sgtm"}, resp) def test_create_comment_with_unicode_chars(self): self.expect_new_reply(content='<a href="http://who">Degenève</a>: foo Degenève bar') resp = self.wp.create_comment("http://primary/post/456", u"Degenève", "http://who", u"foo Degenève bar") self.assertEquals({"id": None}, resp) def test_create_comment_with_unicode_chars_in_slug(self): self.expect_urlopen( u"https://public-api.wordpress.com/rest/v1/sites/123/posts/slug:✁?pretty=true", json.dumps({"ID": 456}) ) self.expect_new_reply() resp = self.wp.create_comment(u"http://primary/post/✁", "name", "http://who", "foo bar") self.assertEquals({"id": None}, resp) def test_create_comment_gives_up_on_invalid_input_error(self): # see https://github.com/snarfed/bridgy/issues/161 self.expect_new_reply(status=400, response=json.dumps({"error": "invalid_input"})) resp = self.wp.create_comment("http://primary/post/456", "name", "http://who", "foo bar") # shouldn't raise an exception self.assertEquals({"error": "invalid_input"}, resp) def test_create_comment_gives_up_on_coments_closed(self): resp = {"error": "unauthorized", "message": "Comments on this post are closed"} self.expect_new_reply(status=403, response=json.dumps(resp)) # shouldn't raise an exception got = self.wp.create_comment("http://primary/post/456", "name", "http://who", "foo bar") self.assertEquals(resp, got) def test_create_comment_returns_non_json(self): self.expect_new_reply(status=403, response="Forbidden") self.assertRaises( urllib2.HTTPError, self.wp.create_comment, "http://primary/post/456", "name", "http://who", "foo bar" )