~bigwhale/gwibber/follow-unfollow

« back to all changes in this revision

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

  • Committer: Ken VanDine
  • Date: 2010-10-28 03:36:04 UTC
  • mfrom: (901.1.48 gwibber-error-vbox)
  • Revision ID: ken.vandine@canonical.com-20101028033604-rq53t4x2sbbi5k7b
* service: re-factoring splitting the plugins up and making them more dynamic.  Gwibber will now look for plugins in $GWIBBER_PLUGIN_DIR, ~/.local/share/gwibber/plugins, /usr/local/share/gwibber/plugins, /usr/share/gwibber/plugins and /usr/lib/python2.6/dist-packages/gwibber/microblog/plugins
* service: return errors the same way we return messages, and send a "Error" signal which the client can connect to
* client: connect to "Error" signal and display an infobar for each message
* gwibber-accounts: display infobar reporting errors passed in from the client

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
 
 
3
from gwibber.microblog import network, util
 
4
from gwibber.microblog.util import exceptions, log
 
5
import hashlib, mx.DateTime, time
 
6
from os.path import join, getmtime, exists
 
7
from gettext import lgettext as _
 
8
from gwibber.microblog.util.const import *
 
9
# Try to import * from custom, install custom.py to include packaging 
 
10
# customizations like distro API keys, etc
 
11
try:
 
12
  from gwibber.microblog.util.custom import *
 
13
except:
 
14
  pass
 
15
 
 
16
log.logger.name = "Facebook"
 
17
 
 
18
PROTOCOL_INFO = {
 
19
  "name": "Facebook",
 
20
  "version": "1.0",
 
21
  
 
22
  "config": [
 
23
    "color",
 
24
    "receive_enabled",
 
25
    "send_enabled",
 
26
    "username",
 
27
    "session_key",
 
28
    "private:secret_key"
 
29
  ],
 
30
 
 
31
  "authtype": "facebook",
 
32
  "color": "#64006C",
 
33
 
 
34
  "features": [
 
35
    "send",
 
36
    "reply",
 
37
    "receive",
 
38
    "responses",
 
39
    "thread",
 
40
    "delete",
 
41
    "send_thread",
 
42
    "like",
 
43
    "lists",
 
44
    "list",
 
45
    "images",
 
46
    "sincetime"
 
47
  ],
 
48
 
 
49
  "default_streams": [
 
50
    "receive",
 
51
    "responses",
 
52
    "images",
 
53
    "lists",
 
54
  ]
 
55
}
 
56
 
 
57
URL_PREFIX = "https://api.facebook.com/restserver.php"
 
58
POST_URL = "http://www.facebook.com/profile.php?id=%s&v=feed&story_fbid=%s&ref=mf"
 
59
 
 
60
class Client:
 
61
  def __init__(self, acct):
 
62
    self.account = acct
 
63
    self.user_id = acct["session_key"].split("-")[1]
 
64
 
 
65
  def _check_error(self, data):
 
66
    if isinstance(data, dict) and "error_code" in data:
 
67
      log.logger.info("Facebook error %s - %s", data["error_code"], data["error_msg"])
 
68
      return True
 
69
    else: 
 
70
      return False
 
71
    
 
72
  def _get(self, operation, post=False, single=False, **args):
 
73
    args.update({
 
74
      "v": "1.0",
 
75
      "format": "json",
 
76
      "method": "facebook." + operation,
 
77
      "api_key": FB_APP_KEY,
 
78
      "session_key": self.account["session_key"],
 
79
      "call_id": str(int(time.time()) * 1000),
 
80
    })
 
81
 
 
82
    sig = "".join("%s=%s" % (k, v) for k, v in sorted(args.items()))
 
83
    args["sig"] = hashlib.md5(sig + self.account["secret_key"]).hexdigest()
 
84
    data = network.Download(URL_PREFIX, args, post).get_json()
 
85
 
 
86
    if isinstance(data, dict) and data.get("error_msg", 0):
 
87
      if "permission" in data["error_msg"]:
 
88
        logstr = """%s: %s - %s""" % (PROTOCOL_INFO["name"], _("Authentication failed"), data["error_msg"])
 
89
        log.logger.error("%s", logstr)
 
90
        return [{"error": {"type": "auth", "account": self.account, "message": data["error_msg"]}}]
 
91
      else:
 
92
        logstr = """%s: %s - %s""" % (PROTOCOL_INFO["name"], _("Unknown failure"), data["error_msg"])
 
93
        log.logger.error("%s", logstr)
 
94
        return [{"error": {"type": "unknown", "account": self.account, "message": data["error_msg"]}}]
 
95
    """
 
96
    if not data:
 
97
      log.logger.error("%s unexpected result - %s", PROTOCOL_INFO["name"], _("Unknown failure"))
 
98
      return [{"error": {"type": "unknown", "account": self.account, "message": _("Unknown failure")}}]
 
99
    """
 
100
 
 
101
    return data
 
102
 
 
103
  def _sender(self, user):
 
104
    sender = {
 
105
      "nick": user.get("username", str(user.get("id", user.get("uid", "")))),
 
106
      "name": user["name"],
 
107
      "id": str(user.get("id", user.get("uid", ''))),
 
108
      "is_me": str(user.get("id", user.get("uid", ''))) == self.user_id,
 
109
      "url": user.get("url", user.get("profile_url", "")),
 
110
      "image": user["pic_square"],
 
111
    }
 
112
    
 
113
    if user.has_key("username"):
 
114
      sender["nick"] = user["username"]
 
115
    elif sender["url"] and not "?" in sender["url"]:
 
116
      sender["nick"] = sender["url"].rsplit("/", 1)[-1]
 
117
    elif user.has_key("id"):
 
118
      sender["nick"] = str(user["id"])
 
119
    else:
 
120
      sender["nick"] = str(user.get("uid", ""))
 
121
    return sender
 
122
  
 
123
  def _message(self, data, profiles):
 
124
    m = {}
 
125
    m["mid"] = str(data["post_id"])
 
126
    m["service"] = "facebook"
 
127
    m["account"] = self.account["id"]
 
128
    m["time"] = int(mx.DateTime.DateTimeFrom(int(data.get("updated_time", data["created_time"]))).gmtime())
 
129
    m["url"] = data["permalink"]
 
130
    m["to_me"] = ("@%s" % self.account["username"]) in data["message"]
 
131
 
 
132
    if data.get("attribution", 0):
 
133
      m["source"] = util.strip_urls(data["attribution"]).replace("via ", "")
 
134
    
 
135
    if data.get("message", "").strip():
 
136
      m["text"] = data["message"]
 
137
      m["html"] = util.linkify(data["message"])
 
138
      m["content"] = m["html"]
 
139
    else:
 
140
      m["text"] = ""
 
141
      m["html"] = ""
 
142
      m["content"] = ""
 
143
 
 
144
    if data.get("actor_id", 0) in profiles:
 
145
      m["sender"] = self._sender(profiles[data["actor_id"]])
 
146
 
 
147
    # Handle target for wall posts with a specific recipient
 
148
    if data.get("target_id", 0) in profiles:
 
149
      m["sender"]["name"] += u" \u25b8 %s"%(profiles[data["target_id"]]['name'])
 
150
 
 
151
    if data.get("likes", {}).get("count", None):
 
152
      m["likes"] = {
 
153
        "count": data["likes"]["count"],
 
154
        "url": data["likes"]["href"],
 
155
      }
 
156
 
 
157
    if data.get("comments", 0):
 
158
      m["comments"] = []
 
159
      for item in data["comments"]["comment_list"]:
 
160
        if item["fromid"] in profiles:
 
161
          m["comments"].append({
 
162
            "text": item["text"],
 
163
            "time": int(mx.DateTime.DateTimeFrom(int(item["time"])).gmtime()),
 
164
            "sender": self._sender(profiles[item["fromid"]]),
 
165
          })
 
166
 
 
167
    if data.get("attachment", 0):
 
168
      if data["attachment"].get("name", 0):
 
169
        m["content"] += "<p><b>%s</b></p>" % data["attachment"]["name"]
 
170
 
 
171
      if data["attachment"].get("description", 0):
 
172
        m["content"] += "<p>%s</p>" % data["attachment"]["description"]
 
173
 
 
174
      m["images"] = []
 
175
      for a in data["attachment"].get("media", []):
 
176
        if a["type"] in ["photo", "video", "link"]:
 
177
          if a.get("src", 0):
 
178
            if a["src"].startswith("/"):
 
179
              a["src"] = "http://facebook.com" + a["src"]
 
180
            m["images"].append({"src": a["src"], "url": a["href"]})
 
181
 
 
182
    return m
 
183
 
 
184
  def _comment(self, data, profiles):
 
185
    if not data.has_key("fromid"):
 
186
      return []
 
187
    user = profiles[int(data["fromid"])]
 
188
    m = {
 
189
      "mid": str(data["id"]),
 
190
      "service": "facebook",
 
191
      "account": self.account["id"],
 
192
      "time": int(mx.DateTime.DateTimeFrom(int(data['time'])).gmtime()),
 
193
      "text": "@%s: %s" % (self.account["username"], data["text"]),
 
194
      "content": "@%s: %s" % (self.account["username"], data["text"]),
 
195
      "html": "@%s: %s" % (self.account["username"], data["text"]),
 
196
      "to_me": True,
 
197
      "reply": {
 
198
        "id": data["post_id"],
 
199
        "nick": self.account["username"],
 
200
        "url": POST_URL % (self.user_id, data["object_id"]),
 
201
      },
 
202
      "sender": self._sender(user),
 
203
    }
 
204
 
 
205
    return m
 
206
 
 
207
  def _image(self, data, profiles):
 
208
    user = profiles[int(data["owner"])]
 
209
    return {
 
210
      "mid": str(data["object_id"]),
 
211
      "service": "facebook",
 
212
      "account": self.account["id"],
 
213
      "time": int(mx.DateTime.DateTimeFrom(int(data['created'])).gmtime()),
 
214
      "content": data["caption"],
 
215
      "text": data["caption"],
 
216
      "html": data["caption"],
 
217
      "images": [{
 
218
        "full": data["src_big"],
 
219
        "src": data["src_big"],
 
220
        "thumb": data["src_small"],
 
221
        "url": data["link"],
 
222
      }],
 
223
      "sender": {
 
224
        "nick": user["username"] or str(user["uid"]),
 
225
        "name": user["name"],
 
226
        "id": str(user["uid"]),
 
227
        "url": user["profile_url"],
 
228
        "image": user["pic_square"],
 
229
      }
 
230
    }
 
231
 
 
232
  def _list(self, data, user):
 
233
    return {
 
234
        "mid": data["value"],
 
235
        "service": "facebook",
 
236
        "account": self.account["id"],
 
237
        "time": 0,
 
238
        "text": "",
 
239
        "html": "",
 
240
        "content": "",
 
241
        "url": "#", # http://www.facebook.com/friends/?filter=flp_%s" % data["flid"],
 
242
        "name": data["name"],
 
243
        "nick": data["name"],
 
244
        "key": data["filter_key"],
 
245
        "full": "",
 
246
        "type": data["type"],
 
247
        "kind": "list",
 
248
        "fbstreamicon": data["icon_url"],
 
249
        "sender": {
 
250
          "nick": user["username"] or str(user["uid"]),
 
251
          "name": user["name"],
 
252
          "id": str(user["uid"]),
 
253
          "url": user["profile_url"],
 
254
          "image": user["pic_square"],
 
255
        }
 
256
    }
 
257
 
 
258
  def _friends(self):
 
259
    friends_cache_file = join(CACHE_DIR, ("%s_friends.cache" % self.account["id"]))
 
260
    if not exists(friends_cache_file):
 
261
      f = file(friends_cache_file, "w")
 
262
      f.close()
 
263
    f = open(friends_cache_file, "r")
 
264
    try:
 
265
      friends = eval(f.read())
 
266
    except SyntaxError:
 
267
      friends = ""
 
268
    if (int(getmtime(friends_cache_file)) < int(mx.DateTime.DateTimeFromTicks(mx.DateTime.localtime()) - mx.DateTime.TimeDelta(hours=4.0))) \
 
269
      or not isinstance(friends, list):
 
270
      log.logger.debug("facebook:friends is refreshing at %s", mx.DateTime.localtime())
 
271
      
 
272
      f.close()
 
273
      f = open(friends_cache_file, "r+")
 
274
      friends = self._get("fql.query", query="""
 
275
        SELECT name, profile_url, pic_square, username, uid
 
276
          FROM user WHERE uid in (SELECT uid2 FROM friend WHERE uid1=%s)
 
277
        """ % self.user_id)
 
278
      f.write(str(friends))
 
279
      f.close()
 
280
      log.logger.debug("<STATS> facebook:friends account:%s size:%s", self.account["id"], str(friends).__len__())
 
281
 
 
282
    if not self._check_error(friends):
 
283
      try:
 
284
        return dict((p["uid"], p) for p in friends)
 
285
      except TypeError: 
 
286
        log.logger.error("<facebook:_friends> failed to parse friend data")
 
287
        return
 
288
    else:
 
289
      return
 
290
 
 
291
  def __call__(self, opname, **args):
 
292
    return getattr(self, opname)(**args)
 
293
 
 
294
  def thread(self, id):
 
295
    query = "SELECT name, profile_url, pic_square, username, uid FROM user WHERE uid in \
 
296
      (SELECT fromid FROM comment WHERE post_id = '%s')" % id
 
297
    
 
298
    profiles = dict((p["uid"], p) for p in self._get("fql.query", query=query))
 
299
    comments = self._get("stream.getComments", post_id=id)
 
300
    try:
 
301
      return [self._comment(comment, profiles) for comment in comments]
 
302
    except TypeError:
 
303
      log.logger.error("<facebook:thread> failed to parse comments for post_id %s", id)
 
304
      return
 
305
 
 
306
  def receive(self, since=None):
 
307
    if not since:
 
308
      since = int(mx.DateTime.DateTimeFromTicks(mx.DateTime.localtime()) - mx.DateTime.TimeDelta(hours=240.0))
 
309
    else:
 
310
      since = int(mx.DateTime.DateTimeFromTicks(since).localtime())
 
311
 
 
312
    data = self._get("stream.get", viewer_id=self.user_id, start_time=since, limit=80)
 
313
    
 
314
    log.logger.debug("<STATS> facebook:receive account:%s since:%s size:%s",
 
315
        self.account["id"], mx.DateTime.DateTimeFromTicks(since), len(str(data)))
 
316
    
 
317
    if not self._check_error(data):
 
318
      try:
 
319
        profiles = dict((p["id"], p) for p in data["profiles"])
 
320
        return [self._message(post, profiles) for post in data["posts"]]
 
321
      except TypeError:
 
322
        log.logger.error("<facebook:receive> failed to parse message data")
 
323
        return
 
324
    else: return
 
325
 
 
326
  def responses(self, since=None, limit=100):
 
327
    if not since:
 
328
      since = int(mx.DateTime.DateTimeFromTicks(mx.DateTime.localtime()) - mx.DateTime.TimeDelta(hours=240.0))
 
329
    else:
 
330
      since = int(mx.DateTime.DateTimeFromTicks(since).localtime())
 
331
 
 
332
    data = self._get("fql.query", query="""
 
333
      SELECT id, post_id, time, fromid, text, object_id FROM comment WHERE post_id IN
 
334
        (SELECT post_id FROM stream WHERE source_id = %s) AND
 
335
        fromid <> %s 
 
336
        AND time > %s
 
337
        ORDER BY time DESC LIMIT %s
 
338
      """ % (self.user_id, self.user_id, since, limit))
 
339
 
 
340
    log.logger.debug("<STATS> facebook:responses account:%s since:%s size:%s",
 
341
        self.account["id"], mx.DateTime.DateTimeFromTicks(since), len(str(data)))
 
342
 
 
343
    if not self._check_error(data):
 
344
      profiles = self._friends()
 
345
      try:
 
346
        return [self._comment(comment, profiles) for comment in data]
 
347
      except TypeError:
 
348
        log.logger.error("<facebook:responses> failed to parse response data")
 
349
        log.logger.debug("<facebook:responses> %s", data)
 
350
        return
 
351
    else: return 
 
352
 
 
353
  def lists(self, **args):
 
354
    data = self._get("fql.query", query="""
 
355
      SELECT name, profile_url, pic_square, username, uid
 
356
        FROM user WHERE uid=%s""" % self.user_id)
 
357
    if not self._check_error(data):
 
358
      user = data[0]
 
359
    else: return
 
360
 
 
361
    return [self._list(l, user) for l in self._get("stream.getFilters")]
 
362
 
 
363
  def list(self, user, id, count=util.COUNT, since=None):
 
364
    if not since:
 
365
      since = int(mx.DateTime.DateTimeFromTicks(mx.DateTime.localtime()) - mx.DateTime.TimeDelta(hours=240.0))
 
366
    else:
 
367
      since = int(mx.DateTime.DateTimeFromTicks(since).localtime())
 
368
 
 
369
    data = self._get("stream.get", viewer_id=self.user_id, start_time=since, limit=80, filter_key=id)
 
370
    profiles = dict((p["id"], p) for p in data["profiles"])
 
371
    return [self._message(post, profiles) for post in data["posts"]]
 
372
 
 
373
  def images(self, limit=100, since=None):
 
374
    if not since:
 
375
      since = int(mx.DateTime.DateTimeFromTicks(mx.DateTime.localtime()) - mx.DateTime.TimeDelta(hours=240.0))
 
376
    else:
 
377
      since = int(mx.DateTime.DateTimeFromTicks(since).localtime())
 
378
 
 
379
    data = self._get("fql.query", query="""
 
380
      SELECT owner, object_id, created, src_small, src_big, link, caption
 
381
        FROM photo WHERE aid in
 
382
        (SELECT aid FROM album WHERE owner IN
 
383
        (SELECT uid2 FROM friend WHERE uid1=%s))
 
384
        AND created > %s
 
385
        ORDER BY created DESC LIMIT %s
 
386
      """ % (self.user_id, since, limit))
 
387
 
 
388
    log.logger.debug("<STATS> facebook:images account:%s since:%s size:%s",
 
389
        self.account["id"], mx.DateTime.DateTimeFromTicks(since), len(str(data)))
 
390
 
 
391
    if not self._check_error(data):
 
392
      profiles = self._friends()
 
393
      try:
 
394
        return [self._image(post, profiles) for post in data if int(post.get("owner", 0)) in profiles]
 
395
      except TypeError:
 
396
        log.logger.error("<facebook:images> failed to parse image data")
 
397
        log.logger.debug("<facebook:images> %s", data)
 
398
        return []
 
399
    else: return []
 
400
 
 
401
  def delete(self, message):
 
402
    result = self._get("stream.remove", post_id=message["mid"])
 
403
    if not result:
 
404
      log.logger.error("<facebook:delete> failed")
 
405
      return
 
406
    return []
 
407
 
 
408
  def like(self, message):
 
409
    result = self._get("stream.addLike", post_id=message["mid"])
 
410
    if not result:
 
411
      log.logger.error("<facebook:like> failed")
 
412
      return
 
413
    return []
 
414
 
 
415
  def send(self, message):
 
416
    result = self._get("users.setStatus", status=message, status_includes_verb=False)
 
417
    if not result:
 
418
      log.logger.error("<facebook:send> failed")
 
419
      return
 
420
    return []
 
421
 
 
422
  def send_thread(self, message, target):
 
423
    result = self._get("stream.addComment", post_id=target["mid"], comment=message)
 
424
    if not result:
 
425
      log.logger.error("<facebook:send_thread> failed")
 
426
      return
 
427
    return []