~robru/gwibber/locked-login-refactor

« back to all changes in this revision

Viewing changes to gwibber/microblog/plugins/foursquare/__init__.py

  • Committer: Barry Warsaw
  • Date: 2012-09-20 18:52:35 UTC
  • Revision ID: barry@python.org-20120920185235-terf5t03uzx3zd6l
foursquare plugin is ported.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
from gwibber.microblog import network, util
2
 
from gwibber.microblog.util import resources
3
 
from gwibber.microblog.util.auth import Authentication
4
 
 
5
 
import json, re
6
 
from gettext import lgettext as _
7
 
 
8
 
import logging
9
 
logger = logging.getLogger("FourSquare")
10
 
logger.debug("Initializing.")
11
 
 
12
 
import json, urllib2
13
 
 
14
 
PROTOCOL_INFO = {
15
 
  "name": "Foursquare",
16
 
  "version": "5.0",
17
 
  
18
 
  "config": [
19
 
    "private:secret_token",
20
 
    "access_token",
21
 
    "receive_enabled",
22
 
    "username",
23
 
    "color",
24
 
  ],
25
 
 
26
 
  "authtype": "oauth2",
27
 
  "color": "#990099",
28
 
 
29
 
  "features": [
30
 
    "receive",
31
 
  ],
32
 
 
33
 
  "default_streams": [
34
 
    "receive",
35
 
  ],
36
 
}
37
 
 
38
 
URL_PREFIX = "https://api.foursquare.com/v2"
39
 
 
40
 
class Client:
41
 
  """Query Foursquare and converts data.
42
 
  
43
 
  The Client class is responsible for querying Foursquare and turning the data obtained
44
 
  into data that Gwibber can understand. Foursquare uses a version of OAuth for security.
45
 
  
46
 
  Tokens have already been obtained when the account was set up in Gwibber and are used to 
47
 
  authenticate when getting data.
48
 
  
49
 
  """
50
 
  def __init__(self, acct):
51
 
    self.service = util.getbus("Service")
52
 
    self.account = acct
53
 
    self._loop = None
54
 
 
55
 
  def _retrieve_user_id(self):
56
 
    #Make a request with our new token for the user's own data
57
 
    url = "https://api.foursquare.com/v2/users/self?oauth_token=" + self.account["access_token"]
58
 
    data = json.load(urllib2.urlopen(url))
59
 
    fullname = ""
60
 
    if isinstance(data, dict):
61
 
      if data["response"]["user"].has_key("firstName"):
62
 
         fullname += data["response"]["user"]["firstName"] + " "
63
 
      if data["response"]["user"].has_key("lastName"):
64
 
        fullname += data["response"]["user"]["lastName"]
65
 
      self.account["username"] = fullname.encode("utf-8")
66
 
      self.account["user_id"] = data["response"]["user"]["id"]
67
 
      self.account["uid"] = self.account["user_id"]
68
 
    else:
69
 
      logger.error("Couldn't get user ID")
70
 
 
71
 
  def _login(self):
72
 
    old_token = self.account.get("access_token", None)
73
 
    with self.account.login_lock:
74
 
      # Perform the login only if it wasn't already performed by another thread
75
 
      # while we were waiting to the lock
76
 
      if self.account.get("access_token", None) == old_token:
77
 
        self._locked_login(old_token)
78
 
 
79
 
    return "access_token" in self.account and \
80
 
        self.account["access_token"] != old_token
81
 
 
82
 
  def _locked_login(self, old_token):
83
 
    logger.debug("Re-authenticating" if old_token else "Logging in")
84
 
 
85
 
    auth = Authentication(self.account, logger)
86
 
    reply = auth.login()
87
 
    if reply and reply.has_key("AccessToken"):
88
 
      self.account["access_token"] = reply["AccessToken"]
89
 
      self._retrieve_user_id()
90
 
      logger.debug("User id is: %s" % self.account["uid"])
91
 
    else:
92
 
      logger.error("Didn't find token in session: %s", (reply,))
93
 
 
94
 
  def _message(self, data):
95
 
    """Parses messages into Gwibber compatible forms.
96
 
    
97
 
    Arguments: 
98
 
      data -- A data object obtained from Foursquare containing a complete checkin
99
 
    
100
 
    Returns: 
101
 
      m -- A data object compatible with inserting into the Gwibber database for that checkin
102
 
    
103
 
    """
104
 
    m = {}; 
105
 
    m["mid"] = str(data["id"])
106
 
    m["service"] = "foursquare"
107
 
    m["account"] = self.account["id"]
108
 
    m["time"] = data["createdAt"]
109
 
 
110
 
    shouttext = ""
111
 
    text = ""
112
 
    
113
 
    if data.has_key("shout"):
114
 
      shout = data["shout"]
115
 
      shout = shout.replace('& ', '& ')
116
 
        
117
 
    if data.has_key("venue"):
118
 
      venuename = data["venue"]["name"]
119
 
      venuename = venuename.replace('& ', '& ')
120
 
    
121
 
      if data.has_key("shout"):
122
 
        shouttext += shout + "\n\n"
123
 
        text += shout + "\n"
124
 
      if data["venue"].has_key("id"):
125
 
        m["url"] = "https://foursquare.com/venue/%s" % data["venue"]["id"]
126
 
      else:
127
 
        m["url"] = "https://foursquare.com"
128
 
      shouttext += "Checked in at <a href='" + m["url"] + "'>" + venuename + "</a>"
129
 
      text += "Checked in at " + venuename
130
 
      if data["venue"]["location"].has_key("address"):
131
 
        shouttext += ", " + data["venue"]["location"]["address"]
132
 
        text += ", " + data["venue"]["location"]["address"]
133
 
      if data["venue"]["location"].has_key("crossstreet"):
134
 
        shouttext += " and " + data["venue"]["location"]["crossstreet"]
135
 
        text += " and " + data["venue"]["location"]["crossstreet"]
136
 
      if data["venue"]["location"].has_key("city"):
137
 
        shouttext += ", " + data["venue"]["location"]["city"]
138
 
        text += ", " + data["venue"]["location"]["city"]
139
 
      if data["venue"]["location"].has_key("state"):
140
 
        shouttext += ", " + data["venue"]["location"]["state"]
141
 
        text += ", " + data["venue"]["location"]["state"]
142
 
      if data.has_key("event"):
143
 
        if data["event"].has_key("name"):
144
 
          shouttext += " for " + data["event"]["name"]
145
 
    else:
146
 
      if data.has_key("shout"):
147
 
          shouttext += shout + "\n\n"
148
 
          text += shout + "\n"
149
 
      else:
150
 
          text= "Checked in off the grid"
151
 
          shouttext= "Checked in off the grid"
152
 
    
153
 
    m["text"] = text
154
 
    m["content"] = shouttext
155
 
    m["html"] = shouttext
156
 
    
157
 
    m["sender"] = {}
158
 
    m["sender"]["id"] = data["user"]["id"]
159
 
    m["sender"]["image"] = data["user"]["photo"]
160
 
    m["sender"]["url"] = "https://www.foursquare.com/user/" + data["user"]["id"]
161
 
    if data["user"]["relationship"] == "self": 
162
 
        m["sender"]["is_me"] = True 
163
 
    else: 
164
 
        m["sender"]["is_me"] = False
165
 
    fullname = ""
166
 
    if data["user"].has_key("firstName"):
167
 
        fullname += data["user"]["firstName"] + " "
168
 
    if data["user"].has_key("lastName"):
169
 
        fullname += data["user"]["lastName"]
170
 
 
171
 
    if data.has_key("photos"):
172
 
      if data["photos"]["count"] > 0:
173
 
        m["photo"] = {}
174
 
        m["photo"]["url"] = ""
175
 
        m["photo"]["picture"] = data["photos"]["items"][0]["url"] 
176
 
        m["photo"]["name"] = ""
177
 
        m["type"] = "photo"
178
 
 
179
 
    if data.has_key("likes"):
180
 
      if data["likes"]["count"] > 0:
181
 
        m["likes"]["count"] = data["likes"]["count"]
182
 
 
183
 
    m["sender"]["name"] = fullname
184
 
    m["sender"]["nick"] = fullname
185
 
 
186
 
    if data.has_key("source"):
187
 
        m["source"] = "<a href='" + data["source"]["url"] + "'>" + data["source"]["name"] + "</a>"
188
 
    else:
189
 
        m["source"] = "<a href='https://foursquare.com/'>Foursquare</a>"
190
 
    
191
 
    if data.has_key("comments"):
192
 
      if data["comments"]["count"] > 0:
193
 
 
194
 
        m["comments"] = []
195
 
        comments = self._get_comments(data["id"])
196
 
        for comment in comments:
197
 
          # Get the commenter's name
198
 
          fullname = ""
199
 
          if comment["user"].has_key("firstName"):
200
 
            fullname += comment["user"]["firstName"] + " "
201
 
          if comment["user"].has_key("lastName"):
202
 
            fullname += comment["user"]["lastName"]
203
 
         
204
 
          # Create a sender
205
 
          sender = {
206
 
            "name" : fullname,
207
 
            "id"   : comment["user"]["id"],
208
 
            "is_me": False,
209
 
            "image": comment["user"]["photo"],
210
 
            "url"  : "https://www.foursquare.com/user/" + comment["user"]["id"]
211
 
          }
212
 
          # Create a comment
213
 
          m["comments"].append({
214
 
              "text"  : comment["text"],
215
 
              "time"  : comment["createdAt"],
216
 
              "sender": sender,
217
 
          })
218
 
 
219
 
    return m
220
 
 
221
 
  def _check_error(self, data):
222
 
    """Checks to ensure the data obtained by Foursquare is in the correct form.
223
 
    
224
 
    If it's not in the correct form, an error is logged in gwibber.log
225
 
    
226
 
    Arguments:
227
 
    data -- A data structure obtained from Foursquare
228
 
    
229
 
    Returns:
230
 
    True if data is valid (is a dictionary and contains a 'recent' parameter).
231
 
    If the data is not valid, then return False.
232
 
    
233
 
    """
234
 
    if isinstance(data, dict) and "recent" in data:
235
 
      return True
236
 
    else:
237
 
      logger.error("Foursquare error %s", data)
238
 
      return False
239
 
  
240
 
  def _get_comments(self, checkin_id):
241
 
    """Gets comments on a particular check in ID.
242
 
    
243
 
    Arguments: 
244
 
    checkin_id -- The checkin id of the checkin
245
 
    
246
 
    Returns: 
247
 
    A comment object
248
 
    
249
 
    """ 
250
 
    url = "/".join((URL_PREFIX, "checkins", checkin_id))
251
 
    url = url + "?oauth_token=" + self.token
252
 
    data = network.Download(url, None, False).get_json()["response"]
253
 
    return data["checkin"]["comments"]["items"]
254
 
 
255
 
  def _get(self, path, parse="message", post=False, single=False, **args):
256
 
    """Establishes a connection with Foursquare and gets the data requested.
257
 
    
258
 
    Arguments: 
259
 
      path -- The end of the URL to look up on Foursquare
260
 
      parse -- The function to use to parse the data returned (message by default)
261
 
      post -- True if using POST, for example the send operation. False if using GET, most operations other than send. (False by default)
262
 
      single -- True if a single checkin is requested, False if multiple (False by default)
263
 
      **args -- Arguments to be added to the URL when accessed
264
 
    
265
 
    Returns:
266
 
    A list of Gwibber compatible objects which have been parsed by the parse function.
267
 
    
268
 
    """
269
 
    if "access_token" not in self.account and not self._login():
270
 
      logstr = """%s: %s - %s""" % (PROTOCOL_INFO["name"], _("Authentication failed"), "Auth needs updating")
271
 
      logger.error("%s", logstr)
272
 
      return [{"error": {"type": "auth", "account": self.account, "message": _("Authentication failed, please re-authorize")}}]
273
 
 
274
 
    url = "/".join((URL_PREFIX, path))
275
 
    
276
 
    url = url + "?oauth_token=" + self.account["access_token"]
277
 
 
278
 
    data = network.Download(url, None, post).get_json()["response"]
279
 
 
280
 
    resources.dump(self.account["service"], self.account["id"], data)
281
 
 
282
 
    if isinstance(data, dict) and data.get("errors", 0):
283
 
      if "authenticate" in data["errors"][0]["message"]:
284
 
        # Try again, if we get a new token
285
 
        if self._login():
286
 
          logger.debug("Authentication error, logging in again")
287
 
          return self._get(path, parse, post, single, args)
288
 
        else:
289
 
          logstr = """%s: %s - %s""" % (PROTOCOL_INFO["name"], _("Authentication failed"), data["errors"][0]["message"])
290
 
          logger.error("%s", logstr)
291
 
          return [{"error": {"type": "auth", "account": self.account, "message": data["errors"][0]["message"]}}]
292
 
      else:
293
 
        for error in data["errors"]:
294
 
          logstr = """%s: %s - %s""" % (PROTOCOL_INFO["name"], _("Unknown failure"), error["message"])
295
 
          return [{"error": {"type": "unknown", "account": self.account, "message": error["message"]}}]
296
 
    elif isinstance(data, dict) and data.get("error", 0):
297
 
      if "Incorrect signature" in data["error"]:
298
 
        # Try again, if we get a new token
299
 
        if self._login():
300
 
          logger.debug("Authentication error, logging in again")
301
 
          return self._get(path, parse, post, single, args)
302
 
        else:
303
 
          logstr = """%s: %s - %s""" % (PROTOCOL_INFO["name"], _("Request failed"), data["error"])
304
 
          logger.error("%s", logstr)
305
 
          return [{"error": {"type": "auth", "account": self.account, "message": data["error"]}}]
306
 
    elif isinstance(data, str):
307
 
      logstr = """%s: %s - %s""" % (PROTOCOL_INFO["name"], _("Request failed"), data)
308
 
      logger.error("%s", logstr)
309
 
      return [{"error": {"type": "request", "account": self.account, "message": data}}]
310
 
    if not self._check_error(data):
311
 
      return []
312
 
 
313
 
    checkins = data["recent"]
314
 
    if single: return [getattr(self, "_%s" % parse)(checkins)]
315
 
    if parse: return [getattr(self, "_%s" % parse)(m) for m in checkins]
316
 
    else: return []
317
 
    
318
 
  def __call__(self, opname, **args):
319
 
    return getattr(self, opname)(**args)
320
 
 
321
 
  def receive(self):
322
 
    """Gets a list of each friend's most recent check-ins.
323
 
    
324
 
    The list of each friend's recent check-ins is then
325
 
    saved to the database.
326
 
 
327
 
    Arguments: 
328
 
    None
329
 
    
330
 
    Returns:
331
 
    A list of Gwibber compatible objects which have been parsed by the parse function.
332
 
    
333
 
    """
334
 
    return self._get("checkins/recent", v=20120612)