def test_maybe_add_or_delete_source_delete_declined(self): state = { 'feature': 'webmention', 'operation': 'delete', } msg = 'If you want to disable, please approve the FakeSource prompt.' # no source with app.test_request_context(): with self.assertRaises(RequestRedirect) as rr: util.maybe_add_or_delete_source(FakeSource, None, util.encode_oauth_state(state)) self.assert_equals(302, rr.exception.code) self.assert_equals('http://localhost/', rr.exception.new_url) self.assertEqual([msg], get_flashed_messages()) # source state['source'] = self.sources[0].key.urlsafe().decode() with app.test_request_context(), self.assertRaises( RequestRedirect) as rr: util.maybe_add_or_delete_source(FakeSource, None, util.encode_oauth_state(state)) self.assert_equals(302, rr.exception.code) self.assert_equals(self.source_bridgy_url, rr.exception.new_url) self.assertEqual([msg], get_flashed_messages())
def test_registration_api_start_handler_post(self): self.expect_site_fetch() self.mox.ReplayAll() resp = instagram.application.get_response('/instagram/start', method='POST', body=urllib.urlencode( self.bridgy_api_state)) self.assertEquals(302, resp.status_code) state_json = util.encode_oauth_state(self.bridgy_api_state) expected_auth_url = indieauth.INDIEAUTH_URL + '?' + urllib.urlencode({ 'me': 'http://snarfed.org', 'client_id': appengine_config.INDIEAUTH_CLIENT_ID, 'redirect_uri': 'http://localhost/instagram/callback', 'state': util.encode_oauth_state({ 'endpoint': indieauth.INDIEAUTH_URL, 'me': 'http://snarfed.org', 'state': state_json, }), }) self.assertEquals(expected_auth_url, resp.headers['Location'])
def post(self): source = self.load_source(param='key') kind = source.key.kind() feature = util.get_required_param(self, 'feature') state = util.encode_oauth_state({ 'operation': 'delete', 'feature': feature, 'source': source.key.urlsafe().decode(), 'callback': self.request.get('callback'), }) # Blogger don't support redirect_url() yet if kind == 'Blogger': return self.redirect('/blogger/delete/start?state=%s' % state) path = ('/reddit/callback' if kind == 'Reddit' else '/wordpress/add' if kind == 'WordPress' else '/%s/delete/finish' % source.SHORT_NAME) kwargs = {} if kind == 'Twitter': kwargs['access_type'] = 'read' if feature == 'listen' else 'write' handler = source.OAUTH_START_HANDLER.to(path, **kwargs)(self.request, self.response) try: self.redirect(handler.redirect_url(state=state)) except Exception as e: code, body = util.interpret_http_exception(e) if not code and util.is_connection_failure(e): code = '-' body = str(e) if code: self.messages.add('%s API error %s: %s' % (source.GR_CLASS.NAME, code, body)) self.redirect(source.bridgy_url(self)) else: raise
def post(self): source = self.load_source(param='key') module = self.OAUTH_MODULES[source.key.kind()] feature = util.get_required_param(self, 'feature') state = util.encode_oauth_state({ 'operation': 'delete', 'feature': feature, 'source': source.key.urlsafe(), 'callback': self.request.get('callback'), }) # Blogger don't support redirect_url() yet if module is oauth_blogger_v2: return self.redirect('/blogger/delete/start?state=%s' % state) path = ('/instagram/callback' if module is indieauth else '/wordpress/add' if module is oauth_wordpress_rest else '/%s/delete/finish' % source.SHORT_NAME) kwargs = {} if module is oauth_twitter: kwargs['access_type'] = 'read' if feature == 'listen' else 'write' handler = module.StartHandler.to(path, **kwargs)(self.request, self.response) try: self.redirect(handler.redirect_url(state=state)) except Exception as e: code, body = util.interpret_http_exception(e) if not code and util.is_connection_failure(e): code = '-' body = unicode(e) if code: self.messages.add('%s API error %s: %s' % (source.GR_CLASS.NAME, code, body)) self.redirect(source.bridgy_url(self)) else: raise
def _render_preview(self, result, include_link=False): """Renders a preview CreationResult as HTML. Args: result: CreationResult include_link: boolean Returns: CreationResult with the rendered HTML in content """ state = { 'source_key': self.source.key.urlsafe().decode(), 'source_url': self.source_url(), 'target_url': self.target_url(), 'include_link': include_link, } vars = { 'source': self.preprocess_source(self.source), 'preview': result.content, 'description': result.description, 'webmention_endpoint': util.host_url(self) + '/publish/webmention', 'state': util.encode_oauth_state(state), } vars.update(state) logging.info('Rendering preview with template vars %s', pprint.pformat(vars)) return gr_source.creation_result( JINJA_ENV.get_template('preview.html').render(**vars))
def _render_preview(self, result, include_link=False): """Renders a preview CreationResult as HTML. Args: result: CreationResult include_link: boolean Returns: CreationResult with the rendered HTML in content """ state = { 'source_key': self.source.key.urlsafe(), 'source_url': self.source_url(), 'target_url': self.target_url(), 'include_link': include_link, } vars = { 'source': self.preprocess_source(self.source), 'preview': result.content, 'description': result.description, 'webmention_endpoint': util.host_url(self) + '/publish/webmention', 'state': util.encode_oauth_state(state), } vars.update(state) logging.info('Rendering preview with template vars %s', pprint.pformat(vars)) return gr_source.creation_result( JINJA_ENV.get_template('preview.html').render(**vars))
def callback(self, state=''): resp = instagram.application.get_response( '/instagram/callback?code=my_code&state=%s' % util.encode_oauth_state({ 'endpoint': indieauth.INDIEAUTH_URL, 'me': 'http://snarfed.org', 'state': state, })) self.assertEquals(302, resp.status_int) return resp
def callback(self, token='towkin'): resp = app.application.get_response( '/indieauth/callback?code=my_code&state=%s' % util.encode_oauth_state({ 'endpoint': indieauth.INDIEAUTH_URL, 'me': 'http://snarfed.org', 'state': token, })) self.assertEqual(302, resp.status_int) return resp
def callback(self, token='towkin'): state = util.encode_oauth_state({ 'endpoint': indieauth.INDIEAUTH_URL, 'me': 'http://snarfed.org', 'state': token, }) resp = self.client.get( f'/indieauth/callback?code=my_code&state={state}') self.assertEqual(302, resp.status_code) return resp
def test_registration_api_finish_no_rel_me(self): state = util.encode_oauth_state(self.bridgy_api_state) self.expect_indieauth_check(state=state) self.expect_site_fetch('') self.mox.ReplayAll() resp = self.callback(state=urllib.quote_plus(state)) self.assertEquals(302, resp.status_int) location = urllib.unquote_plus(resp.headers['Location']) self.assertTrue(location.startswith( 'http://localhost/#!No Instagram profile found.'), location)
def test_registration_api_start_handler_post(self): self.expect_site_fetch() self.mox.ReplayAll() resp = instagram.application.get_response( '/instagram/start', method='POST', body=urllib.urlencode(self.bridgy_api_state)) self.assertEquals(302, resp.status_code) state_json = util.encode_oauth_state(self.bridgy_api_state) expected_auth_url = indieauth.INDIEAUTH_URL + '?' + urllib.urlencode({ 'me': 'http://snarfed.org', 'client_id': appengine_config.INDIEAUTH_CLIENT_ID, 'redirect_uri': 'http://localhost/instagram/callback', 'state': util.encode_oauth_state({ 'endpoint': indieauth.INDIEAUTH_URL, 'me': 'http://snarfed.org', 'state': state_json, }), }) self.assertEquals(expected_auth_url, resp.headers['Location'])
def test_maybe_add_or_delete_source_delete_declined(self): state = { 'feature': 'webmention', 'operation': 'delete', } msg = '#!If%20you%20want%20to%20disable%2C%20please%20approve%20the%20FakeSource%20prompt.' # no source self.assertIsNone(self.handler.maybe_add_or_delete_source( FakeSource, None, util.encode_oauth_state(state))) self.assert_equals(302, self.handler.response.status_code) self.assert_equals('http://localhost/' + msg, self.handler.response.headers['location']) # source state['source'] = self.sources[0].key.urlsafe().decode() self.assertIsNone(self.handler.maybe_add_or_delete_source( FakeSource, None, util.encode_oauth_state(state))) self.assert_equals(302, self.handler.response.status_code) self.assert_equals(self.sources[0].bridgy_url(self.handler) + msg, self.handler.response.headers['location'])
def test_maybe_add_or_delete_source_delete_declined(self): state = { 'feature': 'webmention', 'operation': 'delete', } msg = '#!If%20you%20want%20to%20disable%2C%20please%20approve%20the%20FakeSource%20prompt.' # no source self.assertIsNone(self.handler.maybe_add_or_delete_source( FakeSource, None, util.encode_oauth_state(state))) self.assert_equals(302, self.handler.response.status_code) self.assert_equals('http://localhost/' + msg, self.handler.response.headers['location']) # source state['source'] = self.sources[0].key.urlsafe() self.assertIsNone(self.handler.maybe_add_or_delete_source( FakeSource, None, util.encode_oauth_state(state))) self.assert_equals(302, self.handler.response.status_code) self.assert_equals(self.sources[0].bridgy_url(self.handler) + msg, self.handler.response.headers['location'])
def test_registration_api_finish_success(self): state = util.encode_oauth_state(self.bridgy_api_state) self.expect_indieauth_check(state=state) self.expect_site_fetch() self.expect_instagram_fetch() self.expect_webmention_discovery() self.mox.ReplayAll() resp = self.callback(state=urllib.quote_plus(state)) self.assertEquals(302, resp.status_int) self.assertEquals('http://my.site/call-back?' + urllib.urlencode({ 'result': 'success', 'key': self.inst.key.urlsafe(), 'user': '******', }), resp.headers['Location'])
def post(self): key = ndb.Key(urlsafe=util.get_required_param(self, 'key')) module = self.OAUTH_MODULES[key.kind()] feature = util.get_required_param(self, 'feature') state = util.encode_oauth_state({ 'operation': 'delete', 'feature': feature, 'source': key.urlsafe(), 'callback': self.request.get('callback'), }) # Google+ and Blogger don't support redirect_url() yet if module is oauth_googleplus: return self.redirect('/googleplus/delete/start?state=%s' % state) if module is oauth_blogger_v2: return self.redirect('/blogger/delete/start?state=%s' % state) source = key.get() path = ('/instagram/callback' if module is indieauth else '/wordpress/add' if module is oauth_wordpress_rest else '/%s/delete/finish' % source.SHORT_NAME) kwargs = {} if module is oauth_twitter: kwargs['access_type'] = 'read' if feature == 'listen' else 'write' handler = module.StartHandler.to(path, **kwargs)(self.request, self.response) try: self.redirect(handler.redirect_url(state=state)) except Exception as e: code, body = util.interpret_http_exception(e) if not code and util.is_connection_failure(e): code = '-' body = unicode(e) if code: self.messages.add('%s API error %s: %s' % (source.GR_CLASS.NAME, code, body)) self.redirect(source.bridgy_url(self)) else: raise
def delete_start(): source = util.load_source() kind = source.key.kind() feature = request.form['feature'] state = util.encode_oauth_state({ 'operation': 'delete', 'feature': feature, 'source': source.key.urlsafe().decode(), 'callback': request.values.get('callback'), }) # Blogger don't support redirect_url() yet if kind == 'Blogger': return redirect(f'/blogger/delete/start?state={state}') path = ('/reddit/callback' if kind == 'Reddit' else '/wordpress/add' if kind == 'WordPress' else f'/{source.SHORT_NAME}/delete/finish') kwargs = {} if kind == 'Twitter': kwargs['access_type'] = 'read' if feature == 'listen' else 'write' try: return redirect(source.OAUTH_START(path).redirect_url(state=state)) except werkzeug.exceptions.HTTPException: # raised by us, probably via self.error() raise except Exception as e: code, body = util.interpret_http_exception(e) if not code and util.is_connection_failure(e): code = '-' body = str(e) if code: flash(f'{source.GR_CLASS.NAME} API error {code}: {body}') return redirect(source.bridgy_url()) else: raise
def attempt_single_item(self, item): """Attempts to preview or publish a single mf2 item. Args: item: mf2 item dict from mf2py Returns: CreationResult """ self.maybe_inject_silo_content(item) obj = microformats2.json_to_object(item) ignore_formatting = self.ignore_formatting(item) if ignore_formatting: prop = microformats2.first_props(item.get('properties', {})) content = microformats2.get_text(prop.get('content')) if content: obj['content'] = content.strip() # which original post URL to include? in order of preference: # 1. rel-shortlink (background: https://github.com/snarfed/bridgy/issues/173) # 2. original user-provided URL if it redirected # 3. u-url if available # 4. actual final fetched URL if self.shortlink: obj['url'] = self.shortlink elif self.source_url() != self.fetched.url: obj['url'] = self.source_url() elif 'url' not in obj: obj['url'] = self.fetched.url logging.debug('Converted to ActivityStreams object: %s', json.dumps(obj, indent=2)) # posts and comments need content obj_type = obj.get('objectType') if obj_type in ('note', 'article', 'comment'): if (not obj.get('content') and not obj.get('summary') and not obj.get('displayName')): return gr_source.creation_result( abort=False, error_plain='Could not find content in %s' % self.fetched.url, error_html='Could not find <a href="http://microformats.org/">content</a> in %s' % self.fetched.url) self.preprocess(obj) include_link = self.include_link(item) if not self.authorize(): return gr_source.creation_result(abort=True) # RIP Facebook comments/likes. https://github.com/snarfed/bridgy/issues/350 if (isinstance(self.source, FacebookPage) and (obj_type == 'comment' or obj.get('verb') == 'like')): return gr_source.creation_result( abort=True, error_plain='Facebook comments and likes are no longer supported. :(', error_html='<a href="https://github.com/snarfed/bridgy/issues/350">' 'Facebook comments and likes are no longer supported.</a> :(') if self.PREVIEW: result = self.source.gr_source.preview_create( obj, include_link=include_link, ignore_formatting=ignore_formatting) self.entity.published = result.content or result.description if not self.entity.published: return result # there was an error state = { 'source_key': self.source.key.urlsafe(), 'source_url': self.source_url(), 'target_url': self.target_url(), 'include_link': include_link, } vars = {'source': self.preprocess_source(self.source), 'preview': result.content, 'description': result.description, 'webmention_endpoint': self.request.host_url + '/publish/webmention', 'state': util.encode_oauth_state(state), } vars.update(state) logging.info('Rendering preview with template vars %s', pprint.pformat(vars)) return gr_source.creation_result( JINJA_ENV.get_template('preview.html').render(**vars)) else: result = self.source.gr_source.create( obj, include_link=include_link, ignore_formatting=ignore_formatting) self.entity.published = result.content if not result.content: return result # there was an error if 'url' not in self.entity.published: self.entity.published['url'] = obj.get('url') self.entity.type = self.entity.published.get('type') or models.get_type(obj) self.entity.type_label = self.source.TYPE_LABELS.get(self.entity.type) self.response.headers['Content-Type'] = 'application/json' logging.info('Returning %s', json.dumps(self.entity.published, indent=2)) self.response.headers['Location'] = self.entity.published['url'].encode('utf-8') self.response.status = 201 return gr_source.creation_result( json.dumps(self.entity.published, indent=2))