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 = self.encode_state_parameter({ '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) path = ('/instagram/callback' if module is indieauth else '/wordpress/add' if module is oauth_wordpress_rest else '/%s/delete/finish' % key.get().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) self.redirect(handler.redirect_url(state=state))
def post(self): logging.debug('Params: %s', self.request.params) key = util.get_required_param(self, 'source_key') source = ndb.Key(urlsafe=key).get() if not source or source.status == 'disabled' or 'listen' not in source.features: logging.error('Source not found or disabled. Dropping task.') return logging.info('Source: %s %s, %s', source.label(), source.key.string_id(), source.bridgy_url(self)) post_id = util.get_required_param(self, 'post_id') source.updates = {} try: activities = source.get_activities( fetch_replies=True, fetch_likes=True, fetch_shares=True, activity_id=post_id, user_id=source.key.id()) if not activities: logging.info('Post %s not found.', post_id) return assert len(activities) == 1 self.backfeed(source, activities={activities[0]['id']: activities[0]}) except Exception, e: code, body = util.interpret_http_exception(e) if (code and (code in util.HTTP_RATE_LIMIT_CODES or code == '400' or int(code) / 100 == 5) or util.is_connection_failure(e)): logging.error('API call failed; giving up. %s: %s\n%s', code, body, e) self.abort(util.ERROR_HTTP_RETURN_CODE) else: raise
def get(self): if self.request.get('declined'): self.messages.add("OK, you're still signed up.") self.redirect('/') return parts = util.get_required_param(self, 'state').split('-', 1) feature = parts[0] if len(parts) != 2 or feature not in (Source.FEATURES): self.abort(400, 'state query parameter must be [FEATURE]-[SOURCE KEY]') logged_in_as = util.get_required_param(self, 'auth_entity') source = ndb.Key(urlsafe=parts[1]).get() if logged_in_as == source.auth_entity.urlsafe(): # TODO: remove credentials if feature in source.features: source.features.remove(feature) source.put() noun = 'webmentions' if feature == 'webmention' else feature + 'ing' self.messages.add('Disabled %s for %s. Sorry to see you go!' % (noun, source.label())) # util.email_me(subject='Deleted Bridgy %s user: %s %s' % # (feature, source.label(), source.key.string_id()), # body=source.bridgy_url(self)) else: self.messages.add('Please log into %s as %s to disable it here.' % (source.AS_CLASS.NAME, source.name)) self.redirect(source.bridgy_url(self))
def get(self): if self.request.get('declined'): self.messages.add('If you want to disable, please approve the prompt.') self.redirect('/') return parts = self.decode_state_parameter(util.get_required_param(self, 'state')) if (not isinstance(parts, dict) or 'feature' not in parts or 'source' not in parts): self.abort(400, 'state query parameter must include "feature" and "source"') feature = parts['feature'] if feature not in (Source.FEATURES): self.abort(400, 'cannot delete unknown feature %s' % feature) logged_in_as = util.get_required_param(self, 'auth_entity') source = ndb.Key(urlsafe=parts['source']).get() if logged_in_as == source.auth_entity.urlsafe(): # TODO: remove credentials if feature in source.features: source.features.remove(feature) source.put() noun = 'webmentions' if feature == 'webmention' else feature + 'ing' self.messages.add('Disabled %s for %s. Sorry to see you go!' % (noun, source.label())) # util.email_me(subject='Deleted Bridgy %s user: %s %s' % # (feature, source.label(), source.key.string_id()), # body=source.bridgy_url(self)) else: self.messages.add('Please log into %s as %s to disable it here.' % (source.AS_CLASS.NAME, source.name)) self.redirect(source.bridgy_url(self))
def post(self): # load source try: source = ndb.Key(urlsafe=util.get_required_param(self, 'source_key')).get() if not source: self.abort(400, 'Source key not found') except ProtocolBufferDecodeError: logging.exception('Bad value for source_key') self.abort(400, 'Bad value for source_key') # validate URL, find silo post url = util.get_required_param(self, 'url') domain = util.domain_from_link(url) msg = 'Discovering now. Refresh in a minute to see the results!' if domain == source.GR_CLASS.DOMAIN: post_id = source.GR_CLASS.post_id(url) util.add_discover_task(source, post_id) elif util.domain_or_parent_in(domain, source.domains): synd_links = original_post_discovery.process_entry(source, url, {}, False, []) if synd_links: for link in synd_links: util.add_discover_task(source, source.GR_CLASS.post_id(link)) else: msg = 'Failed to fetch %s or find a %s syndication link.' % ( util.pretty_link(url), source.GR_CLASS.NAME) else: msg = 'Please enter a URL on either your web site or %s.' % source.GR_CLASS.NAME self.messages.add(msg) self.redirect(source.bridgy_url(self))
def test_get_required_param(self): handler = webapp2.RequestHandler(webapp2.Request.blank('/?a=b'), None) self.assertEqual('b', util.get_required_param(handler, 'a')) try: util.get_required_param(handler, 'c') self.fail('Expected HTTPException') except exc.HTTPException, e: self.assertEqual(400, e.status_int)
def post(self): auth_entity_key = util.get_required_param(self, "auth_entity_key") self.maybe_add_or_delete_source( Tumblr, ndb.Key(urlsafe=auth_entity_key).get(), util.get_required_param(self, "state"), blog_name=util.get_required_param(self, "blog"), )
def post(self): auth_entity_key = util.get_required_param(self, 'auth_entity_key') self.maybe_add_or_delete_source( Blogger, ndb.Key(urlsafe=auth_entity_key).get(), util.get_required_param(self, 'state'), blog_id=util.get_required_param(self, 'blog'), )
def get(self): """URL parameters: start_time: float, seconds since the epoch key: string that should appear in the first app log """ start_time = util.get_required_param(self, 'start_time') if not util.is_float(start_time): self.abort(400, "Couldn't convert start_time to float: %r" % start_time) start_time = float(start_time) key = util.get_required_param(self, 'key') if not util.is_base64(key): self.abort(400, 'key is not base64: %r' % key) key = urllib.unquote(key) # the propagate task logs the poll task's URL, which includes the source # entity key as a query param. exclude that with this heuristic. key_re = re.compile('[^=]' + key) self.response.headers['Content-Type'] = 'text/html; charset=utf-8' offset = None for log in logservice.fetch(start_time=start_time, end_time=start_time + 120, offset=offset, include_app_logs=True, version_ids=['2', '3', '4', '5', '6', '7']): first_lines = '\n'.join([ line.message.decode('utf-8') for line in log.app_logs[:min(10, len(log.app_logs))] ]) if log.app_logs and key_re.search(first_lines): # found it! render and return self.response.out.write("""\ <html> <body style="font-family: monospace; white-space: pre"> """) self.response.out.write(sanitize(log.combined)) self.response.out.write('<br /><br />') for a in log.app_logs: msg = a.message.decode('utf-8') # don't sanitize poll task URLs since they have a key= query param msg = linkify_datastore_keys( util.linkify( cgi.escape(msg if msg.startswith( 'Created by this poll:') else sanitize(msg)))) self.response.out.write( '%s %s %s<br />' % (datetime.datetime.utcfromtimestamp(a.time), LEVELS[a.level], msg.replace('\n', '<br />'))) self.response.out.write('</body>\n</html>') return offset = log.offset self.response.out.write('No log found!')
def post(self): features = self.request.get('feature') features = features.split(',') if features else [] callback = util.get_required_param(self, 'callback') ia_start = util.oauth_starter( indieauth.StartHandler).to('/instagram/callback')(self.request, self.response) self.redirect( ia_start.redirect_url( me=util.get_required_param(self, 'user_url')))
def post(self): state = util.get_required_param(self, 'state') id = util.get_required_param(self, 'id') auth_entity_key = util.get_required_param(self, 'auth_entity_key') auth_entity = ndb.Key(urlsafe=auth_entity_key).get() if id != auth_entity.key.id(): auth_entity = auth_entity.for_page(id) auth_entity.put() self.maybe_add_or_delete_source(FacebookPage, auth_entity, state)
def get(self): parts = self.decode_state_parameter(util.get_required_param(self, 'state')) callback = parts and parts.get('callback') if self.request.get('declined'): if callback: # disable declined means no change took place callback = util.add_query_params(callback, {'result': 'declined'}) else: self.messages.add('If you want to disable, please approve the prompt.') self.redirect(callback.encode('utf-8') if callback else '/') return if (not parts or 'feature' not in parts or 'source' not in parts): self.abort(400, 'state query parameter must include "feature" and "source"') feature = parts['feature'] if feature not in (Source.FEATURES): self.abort(400, 'cannot delete unknown feature %s' % feature) logged_in_as = ndb.Key( urlsafe=util.get_required_param(self, 'auth_entity')).get() source = ndb.Key(urlsafe=parts['source']).get() if logged_in_as and logged_in_as.is_authority_for(source.auth_entity): # TODO: remove credentials if feature in source.features: source.features.remove(feature) source.put() noun = 'webmentions' if feature == 'webmention' else feature + 'ing' if callback: callback = util.add_query_params(callback, { 'result': 'success', 'user': source.bridgy_url(self), 'key': source.key.urlsafe(), }) else: self.messages.add('Disabled %s for %s. Sorry to see you go!' % (noun, source.label())) # util.email_me(subject='Deleted Bridgy %s user: %s %s' % # (feature, source.label(), source.key.string_id()), # body=source.bridgy_url(self)) else: if callback: callback = util.add_query_params(callback, {'result': 'failure'}) else: self.messages.add('Please log into %s as %s to disable it here.' % (source.GR_CLASS.NAME, source.name)) self.redirect(callback.encode('utf-8') if callback else source.bridgy_url(self) if source.features else '/')
def get(self): """URL parameters: start_time: float, seconds since the epoch key: string that should appear in the first app log """ start_time = util.get_required_param(self, 'start_time') if not util.is_float(start_time): self.abort(400, "Couldn't convert start_time to float: %r" % start_time) start_time = float(start_time) key = util.get_required_param(self, 'key') if not util.is_base64(key): self.abort(400, 'key is not base64: %r' % key) key = urllib.unquote(key) # the propagate task logs the poll task's URL, which includes the source # entity key as a query param. exclude that with this heuristic. key_re = re.compile('[^=]' + key) self.response.headers['Content-Type'] = 'text/html; charset=utf-8' offset = None for log in logservice.fetch(start_time=start_time, end_time=start_time + 120, offset=offset, include_app_logs=True, version_ids=['2', '3', '4', '5', '6', '7']): first_lines = '\n'.join([line.message.decode('utf-8') for line in log.app_logs[:min(10, len(log.app_logs))]]) if log.app_logs and key_re.search(first_lines): # found it! render and return self.response.out.write("""\ <html> <body style="font-family: monospace; white-space: pre"> """) self.response.out.write(sanitize(log.combined)) self.response.out.write('<br /><br />') for a in log.app_logs: msg = a.message.decode('utf-8') # don't sanitize poll task URLs since they have a key= query param msg = linkify_datastore_keys(util.linkify(cgi.escape( msg if msg.startswith('Created by this poll:') else sanitize(msg)))) self.response.out.write('%s %s %s<br />' % (datetime.datetime.utcfromtimestamp(a.time), LEVELS[a.level], msg.replace('\n', '<br />'))) self.response.out.write('</body>\n</html>') return offset = log.offset self.response.out.write('No log found!')
def post(self): features = self.request.get('feature') features = features.split(',') if features else [] callback = util.get_required_param(self, 'callback') ia_start = util.oauth_starter(indieauth.StartHandler).to('/instagram/callback')( self.request, self.response) try: self.redirect(ia_start.redirect_url(me=util.get_required_param(self, 'user_url'))) except Exception as e: if util.is_connection_failure(e) or util.interpret_http_exception(e)[0]: self.messages.add("Couldn't fetch your web site: %s" % e) return self.redirect('/') raise
def post(self): logging.debug('Params: %s', self.request.params) type = self.request.get('type') if type: assert type in ('event',) key = util.get_required_param(self, 'source_key') source = ndb.Key(urlsafe=key).get() if not source or source.status == 'disabled' or 'listen' not in source.features: logging.error('Source not found or disabled. Dropping task.') return logging.info('Source: %s %s, %s', source.label(), source.key.string_id(), source.bridgy_url(self)) post_id = util.get_required_param(self, 'post_id') source.updates = {} try: if type == 'event': activities = [source.gr_source.get_event(post_id)] else: activities = source.get_activities( fetch_replies=True, fetch_likes=True, fetch_shares=True, activity_id=post_id, user_id=source.key.id()) if not activities or not activities[0]: logging.info('Post %s not found.', post_id) return assert len(activities) == 1, activities self.backfeed(source, activities={activities[0]['id']: activities[0]}) in_reply_to = util.get_first(activities[0]['object'], 'inReplyTo') if in_reply_to: parsed = util.parse_tag_uri(in_reply_to.get('id', '')) # TODO: fall back to url if parsed: util.add_discover_task(source, parsed[1]) except Exception, e: code, body = util.interpret_http_exception(e) if (code and (code in util.HTTP_RATE_LIMIT_CODES or code in ('400', '404') or int(code) / 100 == 5) or util.is_connection_failure(e)): logging.error('API call failed; giving up. %s: %s\n%s', code, body, e) self.abort(util.ERROR_HTTP_RETURN_CODE) else: raise
def check_token_for_actor(self, actor): """Checks that the given actor is public and matches the request's token. Raises: :class:`HTTPException` with HTTP 400 """ if not actor: self.abort(400, f'Missing actor!') if not gr_source.Source.is_public(actor): self.abort( 400, f'Your {self.gr_source().NAME} account is private. Bridgy only supports public accounts.' ) token = util.get_required_param(self, 'token') domains = set( util.domain_from_link(util.replace_test_domains_with_localhost(u)) for u in microformats2.object_urls(actor)) domains.discard(self.source_class().GR_CLASS.DOMAIN) logging.info(f'Checking token against domains {domains}') for domain in ndb.get_multi(ndb.Key(Domain, d) for d in domains): if domain and token in domain.tokens: return self.abort(403, f'Token {token} is not authorized for any of: {domains}')
def post(self): key = ndb.Key(urlsafe=util.get_required_param(self, 'key')) module = self.OAUTH_MODULES[key.kind()] state = '%s-%s' % (util.get_required_param(self, 'feature'), key.urlsafe()) # Google+ and Blogger don't support redirect_url() yet if module is oauth_googleplus: self.redirect('/googleplus/delete/start?state=%s' % state) elif module is oauth_blogger_v2: self.redirect('/blogger/delete/start?state=%s' % state) else: path = ('/instagram/oauth_callback' if module is oauth_instagram else '/wordpress/add' if module is oauth_wordpress_rest else '/%s/delete/finish' % key.get().SHORT_NAME) handler = module.StartHandler.to(path)(self.request, self.response) self.redirect(handler.redirect_url(state=state))
def auth(self): """Loads the source and token and checks that they're valid. Expects token in the `token` query param, source in `key` or `username`. Raises: :class:`HTTPException` with HTTP 400 if the token or source are missing or invalid Returns: BrowserSource or None """ # Load source source = util.load_source(self, param='key') if not source: self.abort( 404, f'No account found for {self.gr_source().NAME} user {key or username}' ) # Load and check token token = util.get_required_param(self, 'token') for domain in Domain.query(Domain.tokens == token): if domain.key.id() in source.domains: return source self.abort( 403, f'Token {token} is not authorized for any of: {source.domains}')
def post(self): source = self.load_source() # validate URL, find silo post url = util.get_required_param(self, 'url') domain = util.domain_from_link(url) path = urlparse.urlparse(url).path msg = 'Discovering now. Refresh in a minute to see the results!' if domain == source.GR_CLASS.DOMAIN: post_id = source.GR_CLASS.post_id(url) if post_id: type = 'event' if path.startswith('/events/') else None util.add_discover_task(source, post_id, type=type) else: msg = "Sorry, that doesn't look like a %s post URL." % source.GR_CLASS.NAME elif util.domain_or_parent_in(domain, source.domains): synd_links = original_post_discovery.process_entry(source, url, {}, False, []) if synd_links: for link in synd_links: util.add_discover_task(source, source.GR_CLASS.post_id(link)) source.updates = {'last_syndication_url': util.now_fn()} models.Source.put_updates(source) else: msg = 'Failed to fetch %s or find a %s syndication link.' % ( util.pretty_link(url), source.GR_CLASS.NAME) else: msg = 'Please enter a URL on either your web site or %s.' % source.GR_CLASS.NAME self.messages.add(msg) self.redirect(source.bridgy_url(self))
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 finish_oauth_flow(self, auth_entity, state): """Adds or deletes a FacebookPage, or restarts OAuth to get publish permissions. Args: auth_entity: FacebookAuth state: encoded state string """ if auth_entity is None: auth_entity_key = util.get_required_param(self, "auth_entity_key") auth_entity = ndb.Key(urlsafe=auth_entity_key).get() if state is None: state = self.request.get("state") state_obj = self.decode_state_parameter(state) id = state_obj.get("id") or self.request.get("id") if id and id != auth_entity.key.id(): auth_entity = auth_entity.for_page(id) auth_entity.put() source = self.maybe_add_or_delete_source(FacebookPage, auth_entity, state) # If we were already signed up for publish, we had an access token with publish # permissions. If we then go through the listen signup flow, we'll get a token # with just the listen permissions. In that case, do the whole OAuth flow again # to get a token with publish permissions again. feature = state_obj.get("feature") if source is not None and feature == "listen" and "publish" in source.features: logging.info("Restarting OAuth flow to get publish permissions.") source.features.remove("publish") source.put() start = util.oauth_starter(oauth_facebook.StartHandler, feature="publish", id=id) restart = start.to("/facebook/oauth_handler", scopes=PUBLISH_SCOPES) restart(self.request, self.response).post()
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 get_source(self): if self.source: return self.source self.source = ndb.Key(urlsafe=util.get_required_param(self, 'key')).get() if not self.source: self.abort(400, 'source not found')
def post(self): source = self.load_source() # validate URL, find silo post url = util.get_required_param(self, 'url') domain = util.domain_from_link(url) path = urllib.parse.urlparse(url).path msg = 'Discovering now. Refresh in a minute to see the results!' if domain == source.GR_CLASS.DOMAIN: post_id = source.GR_CLASS.post_id(url) if post_id: type = 'event' if path.startswith('/events/') else None util.add_discover_task(source, post_id, type=type) else: msg = "Sorry, that doesn't look like a %s post URL." % source.GR_CLASS.NAME elif util.domain_or_parent_in(domain, source.domains): synd_links = original_post_discovery.process_entry( source, url, {}, False, []) if synd_links: for link in synd_links: util.add_discover_task(source, source.GR_CLASS.post_id(link)) source.updates = {'last_syndication_url': util.now_fn()} models.Source.put_updates(source) else: msg = 'Failed to fetch %s or find a %s syndication link.' % ( util.pretty_link(url), source.GR_CLASS.NAME) else: msg = 'Please enter a URL on either your web site or %s.' % source.GR_CLASS.NAME self.messages.add(msg) self.redirect(source.bridgy_url(self))
def post(self): features = util.get_required_param(self, 'feature') scopes = PUBLISH_SCOPES if 'publish' in features else LISTEN_SCOPES starter = util.oauth_starter(oauth_github.StartHandler, feature=features).to('/github/add', scopes=scopes) return starter(self.request, self.response).post()
def post(self): entity = ndb.Key(urlsafe=util.get_required_param(self, 'key')).get() if not entity: self.abort(400, 'key not found') elif not isinstance(entity, Webmentions): self.abort(400, 'Unexpected key kind %s', entity.key.kind()) # run OPD to pick up any new SyndicatedPosts. note that we don't refetch # their h-feed, so if they've added a syndication URL since we last crawled, # retry won't make us pick it up. background in #524. if entity.key.kind() == 'Response': source = entity.source.get() for activity in [json.loads(a) for a in entity.activities_json]: originals, mentions = original_post_discovery.discover( source, activity, fetch_hfeed=False, include_redirect_sources=False) entity.unsent += original_post_discovery.targets_for_response( json.loads(entity.response_json), originals=originals, mentions=mentions) entity.restart() self.messages.add('Retrying. Refresh in a minute to see the results!') self.redirect( self.request.get('redirect_to').encode('utf-8') or entity.source.get().bridgy_url(self))
def authorize(self): from_source = ndb.Key(urlsafe=util.get_required_param(self, 'source_key')) if from_source != self.source.key: self.error('Try publishing that page from <a href="%s">%s</a> instead.' % (self.source.bridgy_path(), self.source.label())) return False return True
def post(self): try: self.redirect(self.redirect_url(state=util.get_required_param(self, 'token'))) except Exception as e: if util.is_connection_failure(e) or util.interpret_http_exception(e)[0]: self.messages.add("Couldn't fetch your web site: %s" % e) return self.redirect('/') raise
def post(self): source = ndb.Key(urlsafe=util.get_required_param(self, 'key')).get() if not source: self.abort(400, 'source not found') util.add_poll_task(source, now=True) self.messages.add("Polling now. Refresh in a minute to see what's new!") self.redirect(source.bridgy_url(self))
def post(self): token = util.get_required_param(self, 'token') domains = [d.key.id() for d in Domain.query(Domain.tokens == token)] if not domains: self.abort(404, f'No registered domains for token {token}') self.output(domains)
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 post(self): # pass explicit 'write' instead of None for publish so that oauth-dropins # (and tweepy) don't use signin_with_twitter ie /authorize. this works # around a twitter API bug: https://dev.twitter.com/discussions/21281 access_type = ('read' if util.get_required_param(self, 'feature') == 'listen' else 'write') handler = util.oauth_starter(oauth_twitter.StartHandler).to( '/twitter/add', access_type=access_type)(self.request, self.response) return handler.post()
def get(self): auth_entity_str_key = util.get_required_param(self, 'auth_entity') state = self.request.get('state') if not state: # state doesn't currently come through for G+. not sure why. doesn't # matter for now since we don't plan to implement publish for G+. state = self.construct_state_param_for_add(feature='listen') auth_entity = ndb.Key(urlsafe=auth_entity_str_key).get() self.maybe_add_or_delete_source(GooglePlusPage, auth_entity, state)
def post(self): features = self.request.get('feature') features = features.split(',') if features else [] callback = util.get_required_param(self, 'callback') ia_start = util.oauth_starter( indieauth.StartHandler).to('/instagram/callback')(self.request, self.response) try: self.redirect( ia_start.redirect_url( me=util.get_required_param(self, 'user_url'))) except Exception as e: if util.is_connection_failure(e) or util.interpret_http_exception( e)[0]: self.messages.add("Couldn't fetch your web site: %s" % e) return self.redirect('/') raise
def post(self): logging.debug('Params: %s', self.request.params) type = self.request.get('type') if type: assert type in ('event', ) source = util.load_source(self) if not source or source.status == 'disabled' or 'listen' not in source.features: logging.error('Source not found or disabled. Dropping task.') return logging.info('Source: %s %s, %s', source.label(), source.key.string_id(), source.bridgy_url(self)) post_id = util.get_required_param(self, 'post_id') source.updates = {} try: if type == 'event': activities = [source.gr_source.get_event(post_id)] else: activities = source.get_activities(fetch_replies=True, fetch_likes=True, fetch_shares=True, activity_id=post_id, user_id=source.key.id()) if not activities or not activities[0]: logging.info('Post %s not found.', post_id) return assert len(activities) == 1, activities self.backfeed(source, activities={activities[0]['id']: activities[0]}) obj = activities[0].get('object') or activities[0] in_reply_to = util.get_first(obj, 'inReplyTo') if in_reply_to: parsed = util.parse_tag_uri(in_reply_to.get( 'id', '')) # TODO: fall back to url if parsed: util.add_discover_task(source, parsed[1]) except Exception, e: code, body = util.interpret_http_exception(e) if (code and (code in source.RATE_LIMIT_HTTP_CODES or code in ('400', '404') or int(code) / 100 == 5) or util.is_connection_failure(e)): logging.error('API call failed; giving up. %s: %s\n%s', code, body, e) self.abort(util.ERROR_HTTP_RETURN_CODE) else: raise
def post(self): feature = self.request.get('feature') start_cls = util.oauth_starter(StartHandler).to('/mastodon/callback', scopes=PUBLISH_SCOPES if feature == 'publish' else LISTEN_SCOPES) start = start_cls(self.request, self.response) instance = util.get_required_param(self, 'instance') try: self.redirect(start.redirect_url(instance=instance)) except ValueError as e: logging.warning('Bad Mastodon instance', exc_info=True) self.messages.add(util.linkify(unicode(e), pretty=True)) return self.redirect(self.request.path)
def post(self): entity = ndb.Key(urlsafe=util.get_required_param(self, 'key')).get() if not entity: self.abort(400, 'key not found') if entity.key.kind() == 'Response': util.add_propagate_task(entity) elif entity.key.kind() == 'BlogPost': util.add_propagate_blogpost_task(entity) else: self.abort(400, 'Unexpected key kind %s', entity.key.kind()) self.messages.add('Retrying. Refresh in a minute to see the results!') self.redirect(entity.source.get().bridgy_url(self))
def post(self): logging.debug('Params: %s', list(self.request.params.items())) type = self.request.get('type') if type: assert type in ('event', ) source = self.source = util.load_source(self) if not source or source.status == 'disabled' or 'listen' not in source.features: logging.error('Source not found or disabled. Dropping task.') return logging.info('Source: %s %s, %s', source.label(), source.key_id(), source.bridgy_url(self)) post_id = util.get_required_param(self, 'post_id') source.updates = {} if type == 'event': activities = [source.gr_source.get_event(post_id)] else: activities = source.get_activities(fetch_replies=True, fetch_likes=True, fetch_shares=True, activity_id=post_id, user_id=source.key_id()) if not activities or not activities[0]: logging.info('Post %s not found.', post_id) return assert len(activities) == 1, activities activity = activities[0] activities = {activity['id']: activity} self.backfeed(source, responses=activities, activities=activities) obj = activity.get('object') or activity in_reply_to = util.get_first(obj, 'inReplyTo') if in_reply_to: parsed = util.parse_tag_uri(in_reply_to.get( 'id', '')) # TODO: fall back to url if parsed: util.add_discover_task(source, parsed[1])
def post(self): entity = ndb.Key(urlsafe=util.get_required_param(self, 'key')).get() if not entity: self.abort(400, 'key not found') # start all target URLs over if entity.status == 'complete': entity.status = 'new' targets = set(entity.unsent + entity.sent + entity.skipped + entity.error + entity.failed) entity.sent = entity.skipped = entity.error = entity.failed = [] # run OPD to pick up any new SyndicatedPosts. note that we don't refetch # their h-feed, so if they've added a syndication URL since we last crawled, # retry won't make us pick it up. background in #524. if entity.key.kind() == 'Response': source = entity.source.get() for activity in [json.loads(a) for a in entity.activities_json]: originals, mentions = original_post_discovery.discover( source, activity, fetch_hfeed=False, include_redirect_sources=False) targets |= original_post_discovery.targets_for_response( json.loads(entity.response_json), originals=originals, mentions=mentions) entity.unsent = targets entity.put() # clear any cached webmention endpoints memcache.delete_multi(util.webmention_endpoint_cache_key(url) for url in targets) if entity.key.kind() == 'Response': util.add_propagate_task(entity) elif entity.key.kind() == 'BlogPost': util.add_propagate_blogpost_task(entity) else: self.abort(400, 'Unexpected key kind %s', entity.key.kind()) self.messages.add('Retrying. Refresh in a minute to see the results!') self.redirect(self.request.get('redirect_to').encode('utf-8') or entity.source.get().bridgy_url(self))
def post(self): entity = ndb.Key(urlsafe=util.get_required_param(self, 'key')).get() if not entity: self.abort(400, 'key not found') elif not isinstance(entity, Webmentions): self.abort(400, 'Unexpected key kind %s', entity.key.kind()) # run OPD to pick up any new SyndicatedPosts. note that we don't refetch # their h-feed, so if they've added a syndication URL since we last crawled, # retry won't make us pick it up. background in #524. if entity.key.kind() == 'Response': source = entity.source.get() for activity in [json.loads(a) for a in entity.activities_json]: originals, mentions = original_post_discovery.discover( source, activity, fetch_hfeed=False, include_redirect_sources=False) entity.unsent += original_post_discovery.targets_for_response( json.loads(entity.response_json), originals=originals, mentions=mentions) entity.restart() self.messages.add('Retrying. Refresh in a minute to see the results!') self.redirect(self.request.get('redirect_to').encode('utf-8') or entity.source.get().bridgy_url(self))
def post(self, *args): source = self.auth() gr_src = self.gr_source() id = util.get_required_param(self, 'id') # validate request parsed_id = util.parse_tag_uri(id) if not parsed_id: self.abort(400, f'Expected id to be tag URI; got {id}') activity = Activity.get_by_id(id) if not activity: self.abort(404, f'No {gr_src.NAME} post found for id {id}') elif activity.source != source.key: self.abort( 403, f'Activity {id} is owned by {activity.source}, not {source.key}' ) activity_data = json_loads(activity.activity_json) # convert new reactions to AS, merge into existing activity try: new_reactions = gr_src.merge_scraped_reactions( self.request.text, activity_data) except ValueError as e: msg = "Couldn't parse scraped reactions: %s" % e logging.error(msg, stack_info=True) self.abort(400, msg) activity.activity_json = json_dumps(activity_data) activity.put() reaction_ids = ' '.join(r['id'] for r in new_reactions) logging.info(f"Stored reactions for activity {id}: {reaction_ids}") self.output(new_reactions)
def post(self): return self.start_oauth_flow(util.get_required_param(self, 'feature'))
def source_url(self): return util.get_required_param(self, 'source')
def target_url(self): return util.get_required_param(self, 'target')
def get(self): parts = self.decode_state_parameter(self.request.get('state') or '') callback = parts and parts.get('callback') if self.request.get('declined'): # disable declined means no change took place if callback: callback = util.add_query_params(callback, {'result': 'declined'}) self.redirect(callback.encode('utf-8')) else: self.messages.add('If you want to disable, please approve the prompt.') self.redirect('/') return if (not parts or 'feature' not in parts or 'source' not in parts): self.abort(400, 'state query parameter must include "feature" and "source"') feature = parts['feature'] if feature not in (Source.FEATURES): self.abort(400, 'cannot delete unknown feature %s' % feature) logged_in_as = ndb.Key( urlsafe=util.get_required_param(self, 'auth_entity')).get() source = ndb.Key(urlsafe=parts['source']).get() if logged_in_as and logged_in_as.is_authority_for(source.auth_entity): # TODO: remove credentials if feature in source.features: source.features.remove(feature) source.put() # remove login cookie logins = self.get_logins() login = util.Login(path=source.bridgy_path(), site=source.SHORT_NAME, name=source.label_name()) if login in logins: logins.remove(login) self.set_logins(logins) noun = 'webmentions' if feature == 'webmention' else feature + 'ing' if callback: callback = util.add_query_params(callback, { 'result': 'success', 'user': source.bridgy_url(self), 'key': source.key.urlsafe(), }) else: self.messages.add('Disabled %s for %s. Sorry to see you go!' % (noun, source.label())) # util.email_me(subject='Deleted Bridgy %s user: %s %s' % # (feature, source.label(), source.key.string_id()), # body=source.bridgy_url(self)) else: if callback: callback = util.add_query_params(callback, {'result': 'failure'}) else: self.messages.add('Please log into %s as %s to disable it here.' % (source.GR_CLASS.NAME, source.name)) self.redirect(callback.encode('utf-8') if callback else source.bridgy_url(self) if source.features else '/')
def post(self): self.maybe_add_or_delete_source( WordPress, ndb.Key(urlsafe=util.get_required_param(self, 'auth_entity_key')).get(), util.get_required_param(self, 'state'))
def post(self): auth_entity = ndb.Key( urlsafe=util.get_required_param(self, 'auth_entity_key')).get() state = util.get_required_param(self, 'state') id = util.get_required_param(self, 'blog') self.maybe_add_or_delete_source(Medium, auth_entity, state, id=id)
def post(self, source_short_name): logging.info('Params: %self', self.request.params.items()) # strip fragments from source and target url self.source_url = urlparse.urldefrag(util.get_required_param(self, 'source'))[0] self.target_url = urlparse.urldefrag(util.get_required_param(self, 'target'))[0] # clean target url (strip utm_* query params) self.target_url = util.clean_webmention_url(self.target_url) # parse and validate target URL domain = util.domain_from_link(self.target_url) if not domain: return self.error(msg, 'Could not parse target URL %s' % self.target_url) # look up source by domain source_cls = SOURCES[source_short_name] domain = domain.lower() self.source = (source_cls.query() .filter(source_cls.domains == domain) .filter(source_cls.features == 'webmention') .filter(source_cls.status == 'enabled') .get()) if not self.source: return self.error( 'Could not find %s account for %s. Is it registered with Bridgy?' % (source_cls.AS_CLASS.NAME, domain), mail=False) # create BlogWebmention entity id = '%s %s' % (self.source_url, self.target_url) self.entity = BlogWebmention.get_or_insert(id, source=self.source.key) if self.entity.status == 'complete': # TODO: response message saying update isn't supported self.response.write(self.entity.published) return logging.debug('BlogWebmention entity: %s', self.entity.key.urlsafe()) # fetch source page resp = self.fetch_mf2(self.source_url) if not resp: return self.fetched, data = resp item = self.find_mention_item(data) if not item: return self.error('Could not find target URL %s in source page %s' % (self.target_url, self.fetched.url), data=data, log_exception=False) # default author to target domain author_name = domain author_url = 'http://%s/' % domain # extract author name and URL from h-card, if any props = item['properties'] author = first_value(props, 'author') if author: if isinstance(author, basestring): author_name = author else: author_props = author.get('properties', {}) author_name = first_value(author_props, 'name') author_url = first_value(author_props, 'url') # if present, u-url overrides source url u_url = first_value(props, 'url') if u_url: self.entity.u_url = u_url # generate content content = props['content'][0] # find_mention_item() guaranteed this is here text = (content.get('html') or content.get('value')).strip() text += ' <br /> <a href="%s">via %s</a>' % ( self.entity.source_url(), util.domain_from_link(self.entity.source_url())) # write comment try: self.entity.published = self.source.create_comment( self.target_url, author_name, author_url, text) except urllib2.HTTPError, e: body = e.read() logging.error('Error response body: %r', body) return self.error('Error: %s; %s' % (e, body), status=e.code)
def post(self, source_short_name): logging.info('Params: %self', self.request.params.items()) # strip fragments from source and target url self.source_url = urlparse.urldefrag(util.get_required_param(self, 'source'))[0] self.target_url = urlparse.urldefrag(util.get_required_param(self, 'target'))[0] # follow target url through any redirects, strip utm_* query params resp = util.follow_redirects(self.target_url) redirected_target_urls = [r.url for r in resp.history] self.target_url = util.clean_url(resp.url) # parse and validate target URL domain = util.domain_from_link(self.target_url) if not domain: return self.error('Could not parse target URL %s' % self.target_url) # look up source by domain source_cls = models.sources[source_short_name] domain = domain.lower() self.source = (source_cls.query() .filter(source_cls.domains == domain) .filter(source_cls.features == 'webmention') .filter(source_cls.status == 'enabled') .get()) if not self.source: return self.error( 'Could not find %s account for %s. Is it registered with Bridgy?' % (source_cls.GR_CLASS.NAME, domain)) if urlparse.urlparse(self.target_url).path in ('', '/'): return self.error('Home page webmentions are not currently supported.') # create BlogWebmention entity id = u'%s %s' % (self.source_url, self.target_url) self.entity = BlogWebmention.get_or_insert( id, source=self.source.key, redirected_target_urls=redirected_target_urls) if self.entity.status == 'complete': # TODO: response message saying update isn't supported self.response.write(self.entity.published) return logging.debug('BlogWebmention entity: %s', self.entity.key.urlsafe()) # fetch source page resp = self.fetch_mf2(self.source_url) if not resp: return self.fetched, data = resp item = self.find_mention_item(data) if not item: return self.error('Could not find target URL %s in source page %s' % (self.target_url, self.fetched.url), data=data, log_exception=False) # default author to target domain author_name = domain author_url = 'http://%s/' % domain # extract author name and URL from h-card, if any props = item['properties'] author = first_value(props, 'author') if author: if isinstance(author, basestring): author_name = author else: author_props = author.get('properties', {}) author_name = first_value(author_props, 'name') author_url = first_value(author_props, 'url') # if present, u-url overrides source url u_url = first_value(props, 'url') if u_url: self.entity.u_url = u_url # generate content content = props['content'][0] # find_mention_item() guaranteed this is here text = (content.get('html') or content.get('value')).strip() source_url = self.entity.source_url() text += ' <br /> <a href="%s">via %s</a>' % ( source_url, util.domain_from_link(source_url)) # write comment try: self.entity.published = self.source.create_comment( self.target_url, author_name, author_url, text) except Exception, e: code, body = util.interpret_http_exception(e) msg = 'Error: %s %s; %s' % (code, e, body) if code == '401': logging.warning('Disabling source!') self.source.status = 'disabled' self.source.put() return self.error(msg, status=code, mail=False) elif code == '404': # post is gone return self.error(msg, status=code, mail=False) elif code or body: return self.error(msg, status=code, mail=True) else: raise
def post(self, source_short_name): logging.info('Params: %s', list(self.request.params.items())) # strip fragments from source and target url self.source_url = urllib.parse.urldefrag( util.get_required_param(self, 'source'))[0] self.target_url = urllib.parse.urldefrag( util.get_required_param(self, 'target'))[0] # follow target url through any redirects, strip utm_* query params resp = util.follow_redirects(self.target_url) redirected_target_urls = [r.url for r in resp.history] self.target_url = util.clean_url(resp.url) # parse and validate target URL domain = util.domain_from_link(self.target_url) if not domain: return self.error('Could not parse target URL %s' % self.target_url) # look up source by domain source_cls = models.sources[source_short_name] domain = domain.lower() self.source = (source_cls.query().filter( source_cls.domains == domain).filter( source_cls.features == 'webmention').filter( source_cls.status == 'enabled').get()) if not self.source: # check for a rel-canonical link. Blogger uses these when it serves a post # from multiple domains, e.g country TLDs like epeus.blogspot.co.uk vs # epeus.blogspot.com. # https://github.com/snarfed/bridgy/issues/805 mf2 = self.fetch_mf2(self.target_url, require_mf2=False) if not mf2: # fetch_mf2() already wrote the error response return domains = util.dedupe_urls( util.domain_from_link(url) for url in mf2[1]['rels'].get('canonical', [])) if domains: self.source = (source_cls.query().filter( source_cls.domains.IN(domains)).filter( source_cls.features == 'webmention').filter( source_cls.status == 'enabled').get()) if not self.source: return self.error( 'Could not find %s account for %s. Is it registered with Bridgy?' % (source_cls.GR_CLASS.NAME, domain)) # check that the target URL path is supported target_path = urllib.parse.urlparse(self.target_url).path if target_path in ('', '/'): return self.error( 'Home page webmentions are not currently supported.', status=202) for pattern in self.source.PATH_BLOCKLIST: if pattern.match(target_path): return self.error( '%s webmentions are not supported for URL path: %s' % (self.source.GR_CLASS.NAME, target_path), status=202) # create BlogWebmention entity id = '%s %s' % (self.source_url, self.target_url) self.entity = BlogWebmention.get_or_insert( id, source=self.source.key, redirected_target_urls=redirected_target_urls) if self.entity.status == 'complete': # TODO: response message saying update isn't supported self.response.write(self.entity.published) return logging.debug("BlogWebmention entity: '%s'", self.entity.key.urlsafe().decode()) # fetch source page fetched = self.fetch_mf2(self.source_url) if not fetched: return resp, mf2 = fetched item = self.find_mention_item(mf2.get('items', [])) if not item: return self.error( 'Could not find target URL %s in source page %s' % (self.target_url, resp.url), data=mf2, log_exception=False) # default author to target domain author_name = domain author_url = 'http://%s/' % domain # extract author name and URL from h-card, if any props = item['properties'] author = first_value(props, 'author') if author: if isinstance(author, str): author_name = author else: author_props = author.get('properties', {}) author_name = first_value(author_props, 'name') author_url = first_value(author_props, 'url') # if present, u-url overrides source url u_url = first_value(props, 'url') if u_url: self.entity.u_url = u_url # generate content content = props['content'][ 0] # find_mention_item() guaranteed this is here text = (content.get('html') or content.get('value')).strip() source_url = self.entity.source_url() text += ' <br /> <a href="%s">via %s</a>' % ( source_url, util.domain_from_link(source_url)) # write comment try: self.entity.published = self.source.create_comment( self.target_url, author_name, author_url, text) except Exception as e: code, body = util.interpret_http_exception(e) msg = 'Error: %s %s; %s' % (code, e, body) if code == '401': logging.warning('Disabling source due to: %s' % e, stack_info=True) self.source.status = 'disabled' self.source.put() return self.error(msg, status=code, report=self.source.is_beta_user()) elif code == '404': # post is gone return self.error(msg, status=code, report=False) elif util.is_connection_failure(e) or (code and int(code) // 100 == 5): return self.error(msg, status=util.ERROR_HTTP_RETURN_CODE, report=False) elif code or body: return self.error(msg, status=code, report=True) else: raise # write results to datastore self.entity.status = 'complete' self.entity.put() self.response.write(json_dumps(self.entity.published))