示例#1
0
  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)
示例#2
0
  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)))
示例#3
0
  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)))
示例#4
0
  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])
示例#5
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'])
示例#6
0
  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)
示例#7
0
  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)
示例#8
0
    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)
示例#9
0
    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)
示例#10
0
    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)))
示例#11
0
  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)
示例#12
0
    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)
示例#13
0
  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)
示例#14
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(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)
示例#16
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'])
示例#17
0
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')
示例#19
0
文件: pages.py 项目: snarfed/bridgy
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)
示例#20
0
  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
示例#21
0
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')
示例#22
0
  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
示例#23
0
  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
示例#24
0
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"
        )