Update twitter sync code to use requests_oauthlib
authorMagnus Hagander <magnus@hagander.net>
Tue, 5 Feb 2019 09:47:19 +0000 (10:47 +0100)
committerMagnus Hagander <magnus@hagander.net>
Tue, 5 Feb 2019 09:48:23 +0000 (10:48 +0100)
This simplifies the code a lot, and also adds the functionality to not
get stuck in a cron-loop when somebody specifies a non-public twitter
account as their twitter account. Instead when that happens just remove
the twitter account from the feed (storing it in a new field,
oldtwitteraccount, just to be on the safe side in case twitter does evil
things to us at some point)

Code mostly stolen from the pgeu website code.

oauth2/__init__.py [deleted file]
schema.sql
setuptwitter.py
synctwitter.py
twitterclient.py

diff --git a/oauth2/__init__.py b/oauth2/__init__.py
deleted file mode 100644 (file)
index a965fc7..0000000
+++ /dev/null
@@ -1,704 +0,0 @@
-"""
-The MIT License
-
-Copyright (c) 2007 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-"""
-
-import urllib
-import time
-import random
-import urlparse
-import hmac
-import binascii
-import httplib2
-
-try:
-    from urlparse import parse_qs, parse_qsl
-except ImportError:
-    from cgi import parse_qs, parse_qsl
-
-
-VERSION = '1.0' # Hi Blaine!
-HTTP_METHOD = 'GET'
-SIGNATURE_METHOD = 'PLAINTEXT'
-
-
-class Error(RuntimeError):
-    """Generic exception class."""
-
-    def __init__(self, message='OAuth error occured.'):
-        self._message = message
-
-    @property
-    def message(self):
-        """A hack to get around the deprecation errors in 2.6."""
-        return self._message
-
-    def __str__(self):
-        return self._message
-
-class MissingSignature(Error):
-    pass
-
-def build_authenticate_header(realm=''):
-    """Optional WWW-Authenticate header (401 error)"""
-    return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
-
-
-def escape(s):
-    """Escape a URL including any /."""
-    return urllib.quote(s, safe='~')
-
-
-def generate_timestamp():
-    """Get seconds since epoch (UTC)."""
-    return int(time.time())
-
-
-def generate_nonce(length=8):
-    """Generate pseudorandom number."""
-    return ''.join([str(random.randint(0, 9)) for i in range(length)])
-
-
-def generate_verifier(length=8):
-    """Generate pseudorandom number."""
-    return ''.join([str(random.randint(0, 9)) for i in range(length)])
-
-
-class Consumer(object):
-    """A consumer of OAuth-protected services.
-    The OAuth consumer is a "third-party" service that wants to access
-    protected resources from an OAuth service provider on behalf of an end
-    user. It's kind of the OAuth client.
-    Usually a consumer must be registered with the service provider by the
-    developer of the consumer software. As part of that process, the service
-    provider gives the consumer a *key* and a *secret* with which the consumer
-    software can identify itself to the service. The consumer will include its
-    key in each request to identify itself, but will use its secret only when
-    signing requests, to prove that the request is from that particular
-    registered consumer.
-    Once registered, the consumer can then use its consumer credentials to ask
-    the service provider for a request token, kicking off the OAuth
-    authorization process.
-    """
-
-    key = None
-    secret = None
-
-    def __init__(self, key, secret):
-        self.key = key
-        self.secret = secret
-
-        if self.key is None or self.secret is None:
-            raise ValueError("Key and secret must be set.")
-
-    def __str__(self):
-        data = {
-            'oauth_consumer_key': self.key,
-            'oauth_consumer_secret': self.secret
-        }
-
-        return urllib.urlencode(data)
-
-
-class Token(object):
-    """An OAuth credential used to request authorization or a protected
-    resource.
-    Tokens in OAuth comprise a *key* and a *secret*. The key is included in
-    requests to identify the token being used, but the secret is used only in
-    the signature, to prove that the requester is who the server gave the
-    token to.
-    When first negotiating the authorization, the consumer asks for a *request
-    token* that the live user authorizes with the service provider. The
-    consumer then exchanges the request token for an *access token* that can
-    be used to access protected resources.
-    """
-
-    key = None
-    secret = None
-    callback = None
-    callback_confirmed = None
-    verifier = None
-
-    def __init__(self, key, secret):
-        self.key = key
-        self.secret = secret
-
-        if self.key is None or self.secret is None:
-            raise ValueError("Key and secret must be set.")
-
-    def set_callback(self, callback):
-        self.callback = callback
-        self.callback_confirmed = 'true'
-
-    def set_verifier(self, verifier=None):
-        if verifier is not None:
-            self.verifier = verifier
-        else:
-            self.verifier = generate_verifier()
-
-    def get_callback_url(self):
-        if self.callback and self.verifier:
-            # Append the oauth_verifier.
-            parts = urlparse.urlparse(self.callback)
-            scheme, netloc, path, params, query, fragment = parts[:6]
-            if query:
-                query = '%s&oauth_verifier=%s' % (query, self.verifier)
-            else:
-                query = 'oauth_verifier=%s' % self.verifier
-            return urlparse.urlunparse((scheme, netloc, path, params,
-                query, fragment))
-        return self.callback
-
-    def to_string(self):
-        """Returns this token as a plain string, suitable for storage.
-        The resulting string includes the token's secret, so you should never
-        send or store this string where a third party can read it.
-        """
-
-        data = {
-            'oauth_token': self.key,
-            'oauth_token_secret': self.secret,
-        }
-
-        if self.callback_confirmed is not None:
-            data['oauth_callback_confirmed'] = self.callback_confirmed
-        return urllib.urlencode(data)
-    @staticmethod
-    def from_string(s):
-        """Deserializes a token from a string like one returned by
-        `to_string()`."""
-
-        if not len(s):
-            raise ValueError("Invalid parameter string.")
-
-        params = parse_qs(s, keep_blank_values=False)
-        if not len(params):
-            raise ValueError("Invalid parameter string.")
-
-        try:
-            key = params['oauth_token'][0]
-        except Exception:
-            raise ValueError("'oauth_token' not found in OAuth request.")
-
-        try:
-            secret = params['oauth_token_secret'][0]
-        except Exception:
-            raise ValueError("'oauth_token_secret' not found in " 
-                "OAuth request.")
-
-        token = Token(key, secret)
-        try:
-            token.callback_confirmed = params['oauth_callback_confirmed'][0]
-        except KeyError:
-            pass # 1.0, no callback confirmed.
-        return token
-
-    def __str__(self):
-        return self.to_string()
-
-
-def setter(attr):
-    name = attr.__name__
-    def getter(self):
-        try:
-            return self.__dict__[name]
-        except KeyError:
-            raise AttributeError(name)
-    def deleter(self):
-        del self.__dict__[name]
-    return property(getter, attr, deleter)
-
-
-class Request(dict):
-    """The parameters and information for an HTTP request, suitable for
-    authorizing with OAuth credentials.
-    When a consumer wants to access a service's protected resources, it does
-    so using a signed HTTP request identifying itself (the consumer) with its
-    key, and providing an access token authorized by the end user to access
-    those resources.
-    """
-    http_method = HTTP_METHOD
-    http_url = None
-    version = VERSION
-    def __init__(self, method=HTTP_METHOD, url=None, parameters=None):
-        if method is not None:
-            self.method = method
-        if url is not None:
-            self.url = url
-        if parameters is not None:
-            self.update(parameters)
-    @setter
-    def url(self, value):
-        parts = urlparse.urlparse(value)
-        scheme, netloc, path = parts[:3]
-
-        # Exclude default port numbers.
-        if scheme == 'http' and netloc[-3:] == ':80':
-            netloc = netloc[:-3]
-        elif scheme == 'https' and netloc[-4:] == ':443':
-            netloc = netloc[:-4]
-
-        if scheme != 'http' and scheme != 'https':
-            raise ValueError("Unsupported URL %s (%s)." % (value, scheme))
-
-        value = '%s://%s%s' % (scheme, netloc, path)
-        self.__dict__['url'] = value
-    @setter
-    def method(self, value):
-        self.__dict__['method'] = value.upper()
-    def _get_timestamp_nonce(self):
-        return self['oauth_timestamp'], self['oauth_nonce']
-    def get_nonoauth_parameters(self):
-        """Get any non-OAuth parameters."""
-        return dict([(k, v) for k, v in self.iteritems() 
-                    if not k.startswith('oauth_')])
-    def to_header(self, realm=''):
-        """Serialize as a header for an HTTPAuth request."""
-        oauth_params = ((k, v) for k, v in self.items() 
-                            if k.startswith('oauth_'))
-        stringy_params = ((k, escape(str(v))) for k, v in oauth_params)
-        header_params = ('%s="%s"' % (k, v) for k, v in stringy_params)
-        params_header = ', '.join(header_params)
-        auth_header = 'OAuth realm="%s"' % realm
-        if params_header:
-            auth_header = "%s, %s" % (auth_header, params_header)
-        return {'Authorization': auth_header}
-    def to_postdata(self):
-        """Serialize as post data for a POST request."""
-        return self.encode_postdata(self)
-
-    def encode_postdata(self, data):
-        # tell urlencode to deal with sequence values and map them correctly
-        # to resulting querystring. for example self["k"] = ["v1", "v2"] will
-        # result in 'k=v1&k=v2' and not k=%5B%27v1%27%2C+%27v2%27%5D
-        return urllib.urlencode(data, True)
-
-    def to_url(self):
-        """Serialize as a URL for a GET request."""
-        return '%s?%s' % (self.url, self.to_postdata())
-
-    def get_parameter(self, parameter):
-        ret = self.get(parameter)
-        if ret is None:
-            raise Error('Parameter not found: %s' % parameter)
-
-        return ret
-    def get_normalized_parameters(self):
-        """Return a string that contains the parameters that must be signed."""
-        items = [(k, v) for k, v in self.items() if k != 'oauth_signature']
-        encoded_str = urllib.urlencode(sorted(items), True)
-        # Encode signature parameters per Oauth Core 1.0 protocol
-        # spec draft 7, section 3.6
-        # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6)
-        # Spaces must be encoded with "%20" instead of "+"
-        return encoded_str.replace('+', '%20')
-    def sign_request(self, signature_method, consumer, token):
-        """Set the signature parameter to the result of sign."""
-
-        if 'oauth_consumer_key' not in self:
-            self['oauth_consumer_key'] = consumer.key
-
-        if token and 'oauth_token' not in self:
-            self['oauth_token'] = token.key
-
-        self['oauth_signature_method'] = signature_method.name
-        self['oauth_signature'] = signature_method.sign(self, consumer, token)
-    @classmethod
-    def make_timestamp(cls):
-        """Get seconds since epoch (UTC)."""
-        return str(int(time.time()))
-    @classmethod
-    def make_nonce(cls):
-        """Generate pseudorandom number."""
-        return str(random.randint(0, 100000000))
-    @classmethod
-    def from_request(cls, http_method, http_url, headers=None, parameters=None,
-            query_string=None):
-        """Combines multiple parameter sources."""
-        if parameters is None:
-            parameters = {}
-        # Headers
-        if headers and 'Authorization' in headers:
-            auth_header = headers['Authorization']
-            # Check that the authorization header is OAuth.
-            if auth_header[:6] == 'OAuth ':
-                auth_header = auth_header[6:]
-                try:
-                    # Get the parameters from the header.
-                    header_params = cls._split_header(auth_header)
-                    parameters.update(header_params)
-                except:
-                    raise Error('Unable to parse OAuth parameters from '
-                        'Authorization header.')
-        # GET or POST query string.
-        if query_string:
-            query_params = cls._split_url_string(query_string)
-            parameters.update(query_params)
-        # URL parameters.
-        param_str = urlparse.urlparse(http_url)[4] # query
-        url_params = cls._split_url_string(param_str)
-        parameters.update(url_params)
-        if parameters:
-            return cls(http_method, http_url, parameters)
-        return None
-    @classmethod
-    def from_consumer_and_token(cls, consumer, token=None,
-            http_method=HTTP_METHOD, http_url=None, parameters=None):
-        if not parameters:
-            parameters = {}
-        defaults = {
-            'oauth_consumer_key': consumer.key,
-            'oauth_timestamp': cls.make_timestamp(),
-            'oauth_nonce': cls.make_nonce(),
-            'oauth_version': cls.version,
-        }
-        defaults.update(parameters)
-        parameters = defaults
-        if token:
-            parameters['oauth_token'] = token.key
-        return Request(http_method, http_url, parameters)
-    @classmethod
-    def from_token_and_callback(cls, token, callback=None, 
-        http_method=HTTP_METHOD, http_url=None, parameters=None):
-
-        if not parameters:
-            parameters = {}
-        parameters['oauth_token'] = token.key
-        if callback:
-            parameters['oauth_callback'] = callback
-        return cls(http_method, http_url, parameters)
-    @staticmethod
-    def _split_header(header):
-        """Turn Authorization: header into parameters."""
-        params = {}
-        parts = header.split(',')
-        for param in parts:
-            # Ignore realm parameter.
-            if param.find('realm') > -1:
-                continue
-            # Remove whitespace.
-            param = param.strip()
-            # Split key-value.
-            param_parts = param.split('=', 1)
-            # Remove quotes and unescape the value.
-            params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
-        return params
-    @staticmethod
-    def _split_url_string(param_str):
-        """Turn URL string into parameters."""
-        parameters = parse_qs(param_str, keep_blank_values=False)
-        for k, v in parameters.iteritems():
-            parameters[k] = urllib.unquote(v[0])
-        return parameters
-
-
-class Server(object):
-    """A skeletal implementation of a service provider, providing protected
-    resources to requests from authorized consumers.
-    This class implements the logic to check requests for authorization. You
-    can use it with your web server or web framework to protect certain
-    resources with OAuth.
-    """
-
-    timestamp_threshold = 300 # In seconds, five minutes.
-    version = VERSION
-    signature_methods = None
-
-    def __init__(self, signature_methods=None):
-        self.signature_methods = signature_methods or {}
-
-    def add_signature_method(self, signature_method):
-        self.signature_methods[signature_method.name] = signature_method
-        return self.signature_methods
-
-    def verify_request(self, request, consumer, token):
-        """Verifies an api call and checks all the parameters."""
-
-        version = self._get_version(request)
-        self._check_signature(request, consumer, token)
-        parameters = request.get_nonoauth_parameters()
-        return parameters
-
-    def build_authenticate_header(self, realm=''):
-        """Optional support for the authenticate header."""
-        return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
-
-    def _get_version(self, request):
-        """Verify the correct version request for this server."""
-        try:
-            version = request.get_parameter('oauth_version')
-        except:
-            version = VERSION
-
-        if version and version != self.version:
-            raise Error('OAuth version %s not supported.' % str(version))
-
-        return version
-
-    def _get_signature_method(self, request):
-        """Figure out the signature with some defaults."""
-        try:
-            signature_method = request.get_parameter('oauth_signature_method')
-        except:
-            signature_method = SIGNATURE_METHOD
-
-        try:
-            # Get the signature method object.
-            signature_method = self.signature_methods[signature_method]
-        except:
-            signature_method_names = ', '.join(self.signature_methods.keys())
-            raise Error('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
-
-        return signature_method
-
-    def _get_verifier(self, request):
-        return request.get_parameter('oauth_verifier')
-
-    def _check_signature(self, request, consumer, token):
-        timestamp, nonce = request._get_timestamp_nonce()
-        self._check_timestamp(timestamp)
-        signature_method = self._get_signature_method(request)
-
-        try:
-            signature = request.get_parameter('oauth_signature')
-        except:
-            raise MissingSignature('Missing oauth_signature.')
-
-        # Validate the signature.
-        valid = signature_method.check(request, consumer, token, signature)
-
-        if not valid:
-            key, base = signature_method.signing_base(request, consumer, token)
-
-            raise Error('Invalid signature. Expected signature base ' 
-                'string: %s' % base)
-
-        built = signature_method.sign(request, consumer, token)
-
-    def _check_timestamp(self, timestamp):
-        """Verify that timestamp is recentish."""
-        timestamp = int(timestamp)
-        now = int(time.time())
-        lapsed = now - timestamp
-        if lapsed > self.timestamp_threshold:
-            raise Error('Expired timestamp: given %d and now %s has a '
-                'greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold))
-
-
-class Client(httplib2.Http):
-    """OAuthClient is a worker to attempt to execute a request."""
-
-    def __init__(self, consumer, token=None, cache=None, timeout=None,
-        proxy_info=None):
-
-        if consumer is not None and not isinstance(consumer, Consumer):
-            raise ValueError("Invalid consumer.")
-
-        if token is not None and not isinstance(token, Token):
-            raise ValueError("Invalid token.")
-
-        self.consumer = consumer
-        self.token = token
-        self.method = SignatureMethod_HMAC_SHA1()
-
-        httplib2.Http.__init__(self, cache=cache, timeout=timeout, 
-            proxy_info=proxy_info)
-
-    def set_signature_method(self, method):
-        if not isinstance(method, SignatureMethod):
-            raise ValueError("Invalid signature method.")
-
-        self.method = method
-
-    def request(self, uri, method="GET", body=None, headers=None, 
-        redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None,
-        force_auth_header=False):
-        
-        if not isinstance(headers, dict):
-            headers = {}
-
-        if body and method == "POST":
-            parameters = dict(parse_qsl(body))
-        elif method == "GET":
-            parsed = urlparse.urlparse(uri)
-            parameters = parse_qs(parsed.query)     
-        else:
-            parameters = None
-
-        req = Request.from_consumer_and_token(self.consumer, token=self.token,
-            http_method=method, http_url=uri, parameters=parameters)
-
-        req.sign_request(self.method, self.consumer, self.token)
-
-        if force_auth_header:
-            # ensure we always send Authorization
-            headers.update(req.to_header())
-
-        if method == "POST":
-            if not force_auth_header:
-                body = req.to_postdata()
-            else:
-                body = req.encode_postdata(req.get_nonoauth_parameters())
-            headers['Content-Type'] = 'application/x-www-form-urlencoded'
-        elif method == "GET":
-            if not force_auth_header:
-                uri = req.to_url()
-        else:
-            if not force_auth_header:
-                # don't call update twice.
-                headers.update(req.to_header())
-
-        return httplib2.Http.request(self, uri, method=method, body=body, 
-            headers=headers, redirections=redirections, 
-            connection_type=connection_type)
-
-
-class SignatureMethod(object):
-    """A way of signing requests.
-    The OAuth protocol lets consumers and service providers pick a way to sign
-    requests. This interface shows the methods expected by the other `oauth`
-    modules for signing requests. Subclass it and implement its methods to
-    provide a new way to sign requests.
-    """
-
-    def signing_base(self, request, consumer, token):
-        """Calculates the string that needs to be signed.
-
-        This method returns a 2-tuple containing the starting key for the
-        signing and the message to be signed. The latter may be used in error
-        messages to help clients debug their software.
-
-        """
-        raise NotImplementedError
-
-    def sign(self, request, consumer, token):
-        """Returns the signature for the given request, based on the consumer
-        and token also provided.
-
-        You should use your implementation of `signing_base()` to build the
-        message to sign. Otherwise it may be less useful for debugging.
-
-        """
-        raise NotImplementedError
-
-    def check(self, request, consumer, token, signature):
-        """Returns whether the given signature is the correct signature for
-        the given consumer and token signing the given request."""
-        built = self.sign(request, consumer, token)
-        return built == signature
-
-
-class SignatureMethod_HMAC_SHA1(SignatureMethod):
-    name = 'HMAC-SHA1'
-        
-    def signing_base(self, request, consumer, token):
-        sig = (
-            escape(request.method),
-            escape(request.url),
-            escape(request.get_normalized_parameters()),
-        )
-
-        key = '%s&' % escape(consumer.secret)
-        if token:
-            key += escape(token.secret)
-        raw = '&'.join(sig)
-        return key, raw
-
-    def sign(self, request, consumer, token):
-        """Builds the base signature string."""
-        key, raw = self.signing_base(request, consumer, token)
-
-        # HMAC object.
-        try:
-            import hashlib # 2.5
-            hashed = hmac.new(key, raw, hashlib.sha1)
-        except ImportError:
-            import sha # Deprecated
-            hashed = hmac.new(key, raw, sha)
-
-        # Calculate the digest base 64.
-        return binascii.b2a_base64(hashed.digest())[:-1]
-
-class SignatureMethod_PLAINTEXT(SignatureMethod):
-
-    name = 'PLAINTEXT'
-
-    def signing_base(self, request, consumer, token):
-        """Concatenates the consumer key and secret with the token's
-        secret."""
-        sig = '%s&' % escape(consumer.secret)
-        if token:
-            sig = sig + escape(token.secret)
-        return sig, sig
-
-    def sign(self, request, consumer, token):
-        key, raw = self.signing_base(request, consumer, token)
-        return raw
-
index cb011bde5a5acb80476cff5fd298dc7940210c7c..6e8976264584681e54b4a24d4913c9b2c3d79cfd 100644 (file)
@@ -44,7 +44,8 @@ CREATE TABLE feeds (
     userid text,
     approved boolean DEFAULT false NOT NULL,
     authorfilter text DEFAULT ''::text NOT NULL,
-    twitteruser text DEFAULT ''::text NOT NULL
+    twitteruser text DEFAULT ''::text NOT NULL,
+    oldtwitteruser text DEFAULT ''::text NOT NULL
 );
 
 
index a1267a58c379e5710c67eaf2fb69d1c7f3d401fa..27787be8de84ca859eade9cdd5b74528f525b1fc 100755 (executable)
@@ -9,9 +9,9 @@ Copyright (C) 2010 PostgreSQL Global Development Group
 """
 
 import sys
-import oauth2 as oauth
 import cgi
 import ConfigParser
+import requests_oauthlib
 
 cfg = ConfigParser.ConfigParser()
 cfg.read('planet.ini')
@@ -22,39 +22,25 @@ if not cfg.has_option('twitter', 'consumer') or not cfg.has_option('twitter', 'c
        print "in the [twitter] section of planet.ini."
        sys.exit(1)
 
-consumer = oauth.Consumer(cfg.get('twitter', 'consumer'), cfg.get('twitter', 'consumersecret'))
-client = oauth.Client(consumer)
-resp, content = client.request("https://api.twitter.com/oauth/request_token", "GET")
-if resp['status'] != '200':
-       print "request_token call failed!"
-       print resp
-       sys.exit(1)
-req_token_cred = dict(cgi.parse_qsl(content))
-
-print "Received request token."
-print "Token secret: %s" % req_token_cred['oauth_token_secret']
-print ""
-print "Now, go to the following URL:"
-print "https://api.twitter.com/oauth/authorize?oauth_token=%s" % req_token_cred['oauth_token']
-print ""
-
-pin = raw_input('Enter the PIN here:')
-
-token = oauth.Token(req_token_cred['oauth_token'], req_token_cred['oauth_token_secret'])
-client = oauth.Client(consumer, token)
-# Put the PIN on the URL, because it seems to not work to use token.set_verifier()
-resp, content = client.request('https://api.twitter.com/oauth/access_token?oauth_verifier=%s' % pin, "POST")
-if resp['status'] != '200':
-       print "access_token failed!"
-       print resp
-       print content
-       sys.exit(1)
+oauth = requests_oauthlib.OAuth1Session(cfg.get('twitter', 'consumer'), cfg.get('twitter', 'consumersecret'))
+fetch_response = oauth.fetch_request_token('https://api.twitter.com/oauth/request_token')
+auth_url = oauth.authorization_url('https://api.twitter.com/oauth/authorize')
+
+print "Please go to {0} and log in".format(auth_url)
+pin = raw_input('Enter the PIN received here:')
+
+oauth2 = requests_oauthlib.OAuth1Session(cfg.get('twitter', 'consumer'),
+                                                                                cfg.get('twitter', 'consumersecret'),
+                                                                                fetch_response.get('oauth_token'),
+                                                                                fetch_response.get('oauth_token_secret'),
+                                                                                verifier=pin)
+tokens = oauth2.fetch_access_token('https://api.twitter.com/oauth/access_token')
+
 
-r = dict(cgi.parse_qsl(content))
 print "Access token received."
-print "Token: %s" % r['oauth_token']
-print "Secret: %s" % r['oauth_token_secret']
-print "Record these two values in planet.ini, and you're good to go!"
+print "Register the following two valuesi n planet.ini under [twitter]:"
+print "token={0}".format(tokens.get('oauth_token'))
+print "secret={0}".format(tokens.get('oauth_token_secret'))
 
 
 
index c840e3dd8cc76d2164c6af71414b1d814852fc8f..198c0c465b05e4276b2bc51d1717fe372e808a31 100755 (executable)
@@ -5,7 +5,7 @@
 This file contains the functions to synchronize the list
 of twitter handles with a list on the twitter account.
 
-Copyright (C) 2009-2010 PostgreSQL Global Development Group
+Copyright (C) 2009-2019 PostgreSQL Global Development Group
 """
 
 import psycopg2
@@ -30,8 +30,23 @@ class SyncTwitter(TwitterClient):
                current = set(self.list_subscribers())
 
                # Start by deleting, then adding the new ones
-               map(self.remove_subscriber, current.difference(expected))
-               map(self.add_subscriber, expected.difference(current))
+               for s in current.difference(expected):
+                       # We don't care about the return code and just keep running if it
+                       # fails, since we will try again later.
+                       self.remove_subscriber(s)
+               for s in expected.difference(current):
+                       # If we fail to add a subscriber, stop trying
+                       if not self.add_subscriber(s):
+                               # Most likely it's things like it doesn't exist or we don't have permissions
+                               # to follow it.
+                               print "Failed to add twitter subscriber {0}, removing from feed record".format(s)
+
+                               # To be on the safe side, store the old twitter username. In case the twitter APIs
+                               # go bonkers on us and we end up removing too much.
+                               curs.execute("UPDATE feeds SET oldtwitteruser=twitteruser, twitteruser='' WHERE lower(twitteruser)=%(twitter)s", {
+                                       'twitter': s,
+                               })
+               self.db.commit()
 
 
 if __name__=="__main__":
index 418d99a793b0789647526d9bf17e659727a447a9..76a101fc98c4613e46be7487a77632efde11ce1c 100644 (file)
@@ -5,12 +5,9 @@
 This file contains a base class for twitter integration
 scripts.
 
-Copyright (C) 2009-2010 PostgreSQL Global Development Group
+Copyright (C) 2009-2019 PostgreSQL Global Development Group
 """
-import oauth2 as oauth
-import simplejson as json
-import time
-import urllib
+import requests_oauthlib
 
 class TwitterClient(object):
        """
@@ -27,40 +24,13 @@ class TwitterClient(object):
                """
                self.twittername = cfg.get('twitter', 'account')
                self.twitterlist = cfg.get('twitter', 'listname')
-               self.oauth_token = oauth.Token(cfg.get('twitter', 'token'), cfg.get('twitter', 'secret'))
-               self.oauth_consumer = oauth.Consumer(cfg.get('twitter', 'consumer'), cfg.get('twitter', 'consumersecret'))
 
-       def twitter_request(self, apicall, method='GET', ext_params=None):
-               params = {
-                       'oauth_version': "1.0",
-                       'oauth_nonce': oauth.generate_nonce(),
-                       'oauth_timestamp': int(time.time()),
-                       'oauth_token': self.oauth_token.key,
-                       'oauth_consumer_key': self.oauth_consumer.key,
-                       }
-               if ext_params:
-                       params.update(ext_params)
+               self.tw = requests_oauthlib.OAuth1Session(cfg.get('twitter', 'consumer'),
+                                                                                                 cfg.get('twitter', 'consumersecret'),
+                                                                                                 cfg.get('twitter', 'token'),
+                                                                                                 cfg.get('twitter', 'secret'))
 
-               url = "https://api.twitter.com/1.1/%s" % apicall
-
-               req = oauth.Request(method=method,
-                                                       url=url,
-                                                       parameters=params)
-               req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), self.oauth_consumer, self.oauth_token)
-               if method=='GET':
-                       instream = urllib.urlopen(req.to_url())
-               else:
-                       instream=urllib.urlopen(url, req.to_postdata())
-
-               # Make the actual call to twitter
-               ret=instream.read()
-               instream.close()
-               try:
-                       return json.loads(ret)
-               except json.decoder.JSONDecodeError:
-                       print "Received non-JSON response to a JSON request!"
-                       print ret
-                       raise
+               self.twitter_api = 'https://api.twitter.com/1.1/'
 
        def list_subscribers(self):
                # Eek. It seems subscribers are paged even if we don't ask for it
@@ -68,28 +38,48 @@ class TwitterClient(object):
                cursor=-1
                handles = []
                while cursor != 0:
-                       response = self.twitter_request('lists/members.json', 'GET', {
+                       response = self.tw.get('{0}lists/members.json'.format(self.twitter_api), params={
                                'owner_screen_name': self.twittername,
                                'slug': self.twitterlist,
                                'cursor': cursor,
                                })
-                       handles.extend([x['screen_name'].lower() for x in response['users']])
-                       cursor = response['next_cursor']
+                       if response.status_code != 200:
+                               print response.json()
+                               raise Exception("Received status {0} when listing users".format(response.status_code))
+                       j = response.json()
+                       handles.extend([x['screen_name'].lower() for x in j['users']])
+                       cursor = j['next_cursor']
 
                return handles
 
        def remove_subscriber(self, name):
                print "Removing twitter user %s from list." % name
-               self.twitter_request('lists/members/destroy.json', 'POST', {
+               r = self.tw.post('{0}lists/members/destroy.json'.format(self.twitter_api), data={
                        'owner_screen_name': self.twittername,
                        'slug': self.twitterlist,
                        'screen_name': name,
-                       })
+               })
+               if r.status_code != 200:
+                       try:
+                               err = r.json()['errors'][0]['message']
+                       except:
+                               err = 'Response does not contain error messages with json'
+                       print "Failed to remove subscriber {0}: {1}".format(name, err)
+                       return False
+               return True
 
        def add_subscriber(self, name):
                print "Adding twitter user %s to list." % name
-               self.twitter_request('lists/members/create.json', 'POST', {
+               r = self.tw.post('{0}lists/members/create.json'.format(self.twitter_api), data={
                        'owner_screen_name': self.twittername,
                        'slug': self.twitterlist,
                        'screen_name': name,
-                       })
+               })
+               if r.status_code != 200:
+                       try:
+                               err = r.json()['errors'][0]['message']
+                       except:
+                               err = 'Response does not contain error messages with json'
+                       print "Failed to add subscriber {0}: {1}".format(name, err)
+                       return False
+               return True