Merge lp:~bigwhale/gwibber/follow-unfollow into lp:gwibber

Proposed by David Klasinc
Status: Rejected
Rejected by: Robert Bruce Park
Proposed branch: lp:~bigwhale/gwibber/follow-unfollow
Merge into: lp:gwibber
Diff against target: 751 lines (+412/-42)
9 files modified
gwibber/actions.py (+49/-2)
gwibber/client.py (+1/-4)
gwibber/gwui.py (+17/-10)
gwibber/microblog/dispatcher.py (+71/-21)
gwibber/microblog/plugins/twitter/__init__.py (+68/-3)
gwibber/microblog/storage.py (+144/-1)
gwibber/microblog/util/const.py (+50/-0)
ui/templates/base.mako (+10/-1)
ui/themes/ubuntu/main.css (+2/-0)
To merge this branch: bzr merge lp:~bigwhale/gwibber/follow-unfollow
Reviewer Review Type Date Requested Status
Robert Bruce Park Disapprove
gwibber-committers Pending
Review via email: mp+42346@code.launchpad.net

Description of the change

See branch details for info.

Review and 'external' testing is wanted. :)

To post a comment you must log in.
lp:~bigwhale/gwibber/follow-unfollow updated
929. By David Klasinc

Merged with the latest trunk.

Revision history for this message
Robert Bruce Park (robru) wrote :

Thanks for taking the time to submit this patch, unfortunately Gwibber has gone through extensive changes recently and your patch no longer applies to the latest codebase.

Unfortunately, the feature you were attempting to add is currently only half-implemented. We have follow/unfollow support in the Twitter backend but there is not any UI for it.

I invite you to take a look at the latest lp:gwibber and see if you might be interested in hooking up the UI for this ;-)

review: Disapprove
Revision history for this message
David Klasinc (bigwhale) wrote :

Yeah, this was already in the old Gwibber. ;) Not sure why this request was left here dangling in the limbo. :)

Revision history for this message
Robert Bruce Park (robru) wrote :

Oh great, it seemed like a large diff, I was worried that you'd be upset about the merge being forgotten about for so long ;-)

Unmerged revisions

929. By David Klasinc

Merged with the latest trunk.

928. By David Klasinc

Fixed follow/unfollow for search results, other minor cleanups.

927. By David Klasinc

Removed unsed list.

926. By David Klasinc

Fetching of user data, friends/followers data is now inserted into database with new messages, direct messages can only be sent to those who follow you.

925. By David Klasinc

Fixed broken if sentence.

924. By David Klasinc

Removed debug messages and fixed an issue with friends/followers.

923. By David Klasinc

Relevant comment changed.

922. By David Klasinc

Cleaned up the code a little, implemented recursive fetching of friend/follower ids.

921. By David Klasinc

Removed friends/followers stream buttons.

920. By David Klasinc

Follow/Unfollow menu actions are now implemented.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'gwibber/actions.py'
2--- gwibber/actions.py 2010-10-19 18:28:11 +0000
3+++ gwibber/actions.py 2011-02-16 07:38:14 +0000
4@@ -86,6 +86,53 @@
5 if not msg.get("sender", {}).get("is_me", 0) and not msg.get("private", 0):
6 return True
7
8+class follow(MessageAction):
9+ icon = "bookmark_add"
10+ label = _("_Follow")
11+
12+ @classmethod
13+ def action(self, w, client, msg):
14+ client.service.PerformOp(json.dumps({
15+ "account": msg["account"],
16+ "operation": "follow",
17+ "args": {"screen_name": msg["sender"]["nick"]},
18+ "transient": False,
19+ }))
20+
21+ image = resources.get_ui_asset("gwibber.svg")
22+ expire_timeout = 5000
23+ gwibber.microblog.util.notify(_("Follow"), _("Trying to follow %s" % msg["sender"].get("nick")), image, expire_timeout)
24+
25+ @classmethod
26+ def include(self, client, msg):
27+ if "follow" in client.model.services[msg["service"]]["features"]:
28+ if not msg.get("sender", {}).get("is_friend", 0) and not msg.get("sender", {}).get("is_me", 0):
29+ return True
30+
31+
32+class unfollow(MessageAction):
33+ icon = "bookmark_add"
34+ label = _("_Unfollow")
35+
36+ @classmethod
37+ def action(self, w, client, msg):
38+ client.service.PerformOp(json.dumps({
39+ "account": msg["account"],
40+ "operation": "unfollow",
41+ "args": {"screen_name": msg["sender"]["nick"]},
42+ "transient": False,
43+ }))
44+
45+ image = resources.get_ui_asset("gwibber.svg")
46+ expire_timeout = 5000
47+ gwibber.microblog.util.notify(_("Unfollow"), _("Trying to unfollow %s" % msg["sender"].get("nick")), image, expire_timeout)
48+
49+ @classmethod
50+ def include(self, client, msg):
51+ if "unfollow" in client.model.services[msg["service"]]["features"]:
52+ if msg.get("sender", {}).get("is_friend", 0) and not msg.get("sender", {}).get("is_me", 0):
53+ return True
54+
55 class private(MessageAction):
56 icon = "mail-reply-sender"
57 label = _("_Direct Message")
58@@ -97,7 +144,7 @@
59 @classmethod
60 def include(self, client, msg):
61 if "private" in client.model.services[msg["service"]]["features"]:
62- if not msg.get("sender", {}).get("is_me", 0) and not msg.get("private", 0):
63+ if not msg.get("sender", {}).get("is_me", 0) and msg.get("sender", {}).get("is_follower", 0) and not msg.get("private", 0):
64 return True
65
66 class like(MessageAction):
67@@ -250,4 +297,4 @@
68 def include(self, client, msg):
69 return gwibber.microblog.util.service_is_running("org.gnome.Tomboy")
70
71-MENU_ITEMS = [reply, retweet, private, read, user, like, delete, tomboy, translate]
72+MENU_ITEMS = [reply, retweet, follow, unfollow, private, read, user, like, delete, tomboy, translate]
73
74=== modified file 'gwibber/client.py'
75--- gwibber/client.py 2010-12-11 04:57:03 +0000
76+++ gwibber/client.py 2011-02-16 07:38:14 +0000
77@@ -196,7 +196,7 @@
78 # Load the user's saved streams
79 streams = json.loads(self.model.settings["streams"])
80 streams = [dict((str(k), v) for k, v in s.items()) for s in streams]
81-
82+
83 # Use the multicolumn mode if there are multiple saved streams
84 view_class = getattr(gwui, "MultiStreamUi" if len(streams) > 1 else "SingleStreamUi")
85
86@@ -522,9 +522,6 @@
87 def perform_action(mi, act, widget, msg): act(widget, self, msg)
88
89 for a in actions.MENU_ITEMS:
90- if a.action.__self__.__name__ == "private" and msg["sender"].get("is_me", 0):
91- continue
92-
93 if a.include(self, msg):
94 image = gtk.image_new_from_icon_name(a.icon, gtk.ICON_SIZE_MENU)
95 mi = gtk.ImageMenuItem()
96
97=== modified file 'gwibber/gwui.py'
98--- gwibber/gwui.py 2010-11-16 18:55:08 +0000
99+++ gwibber/gwui.py 2011-02-16 07:38:14 +0000
100@@ -141,15 +141,22 @@
101
102 if len(default_streams) > 1:
103 for feature in default_streams:
104- aname = self.features[feature]["stream"]
105- item["items"].append({
106- "name": _(aname.capitalize()),
107- "account": aId,
108- "stream": aname,
109- "transient": False,
110- "color": util.Color(account["color"]),
111- "service": account["service"],
112- })
113+ #
114+ # Ugly hack that will go away when I implement friends / followers
115+ # lists. For now it prevents displaying an icon for those streams.
116+ #
117+ # -- BigW 2010/11/15
118+ #
119+ if not (feature == "friends" or feature == "followers"):
120+ aname = self.features[feature]["stream"]
121+ item["items"].append({
122+ "name": _(aname.capitalize()),
123+ "account": aId,
124+ "stream": aname,
125+ "transient": False,
126+ "color": util.Color(account["color"]),
127+ "service": account["service"],
128+ })
129
130 item["items"].append({
131 "name": _("Sent"),
132@@ -772,7 +779,7 @@
133 # get message data for selected stream
134 # stream, account, time, transient, recipient, orderby, order, limit
135 msgs = json.loads(self.model.streams.Messages(streams[0]["stream"] or "all", streams[0]["account"] or "all", time, streams[0]["transient"] or "0", streams[0].has_key("recipient") and streams[0]["recipient"] or "0", orderby, order, limit))
136-
137+
138 for item in msgs:
139 message = item
140 message["dupes"] = []
141
142=== modified file 'gwibber/microblog/dispatcher.py'
143--- gwibber/microblog/dispatcher.py 2010-12-08 19:49:04 +0000
144+++ gwibber/microblog/dispatcher.py 2011-02-16 07:38:14 +0000
145@@ -54,7 +54,16 @@
146 text_cleaner = re.compile(u"[: \n\t\r♻♺]+|@[^ ]+|![^ ]+|#[^ ]+") # signs, @nickname, !group, #tag
147 new_messages = []
148
149- if message_data is not None:
150+ # To avoid doubling of log and return calls
151+ if opname in ["friends", "followers", "follow", "unfollow"] and message_data is not None:
152+ if isinstance(message_data[0], dict) and message_data[0].has_key("error"):
153+ new_messages.insert(0, (
154+ "error",
155+ json.dumps(message_data[0])
156+ ))
157+ else:
158+ new_messages = message_data
159+ elif message_data is not None:
160 for m in message_data:
161 try:
162 if isinstance(m, dict) and m.has_key("mid"):
163@@ -145,7 +154,7 @@
164 # if account doesn't have the required feature or is disabled, return
165 if enabled in acct:
166 if not acct[enabled]: return
167- else:
168+ else:
169 return
170 # if there is an account for a service that gwibber doesn't no about, return
171 if not acct["service"] in SERVICES: return
172@@ -243,6 +252,7 @@
173 self.searches = storage.SearchManager(self.db)
174 self.streams = storage.StreamManager(self.db)
175 self.messages = storage.MessageManager(self.db)
176+ self.friends = storage.FriendsManager(self.db)
177 self.collector = OperationCollector(self)
178
179 # Monitor the connection
180@@ -332,16 +342,15 @@
181 def PerformOp(self, opdata):
182 try: o = json.loads(opdata)
183 except: return
184-
185 log.logger.debug("** Starting Single Operation **")
186 self.LoadingStarted()
187
188 params = ["account", "operation", "args", "transient"]
189 operation = None
190-
191+
192 if "account" in o and self.collector.get_account(o["account"]):
193 account = self.collector.get_account(o["account"])
194-
195+
196 if "id" in o:
197 operation = self.collector.get_operation_by_id(o["id"])
198 elif "operation" in o and self.collector.validate_operation(account, o["operation"]):
199@@ -495,6 +504,7 @@
200 icon = util.resources.get_ui_asset("gwibber.svg")
201 util.notify(error["account"]["service"], error["message"], icon, 2000)
202 self.notified_errors[error["account"]["service"]] = error["message"]
203+ print self.notified_errors
204
205 def perform_async_operation(self, iterable):
206 t = MapAsync(perform_operation, iterable, self.loading_complete, self.loading_failed, self.workerpool)
207@@ -502,15 +512,36 @@
208
209 def loading_complete(self, output):
210 self.refresh_count += 1
211-
212+
213 items = []
214 errors = []
215 for o in output:
216 for o2 in o[1]:
217 if len(o2) > 1:
218- if o2[0] != "error":
219- with sqlite3.connect(SQLITE_DB_FILENAME) as db:
220- if len(db.execute("""select * from messages where mid = '%s' and account = '%s' and stream = '%s'""" % (o2[1], o2[2], o2[6])).fetchall()) > 0:
221+ if o2[0] == "friendships": # Deal with follow/unfollow operations
222+ if o2[1]["type"] == "follow":
223+ self.friends.Follow(o2[1]["account"], o2[1]["service"], o2[1]["user_id"], o2[1]["nick"])
224+ elif o2[1]["type"] == "unfollow":
225+ self.friends.Unfollow(o2[1]["account"], o2[1]["service"], o2[1]["user_id"], o2[1]["nick"])
226+ elif o2[0] == "friends": # Operation friends will grab all the friend IDs
227+ with sqlite3.connect(SQLITE_DB_FILENAME) as db:
228+ output = db.execute("UPDATE friends SET friend = 0 WHERE account = '%s' AND service = '%s'" % (o2[1]["account"], o2[1]["service"]))
229+ for id in o2[1]["ids"]:
230+ output = db.execute("UPDATE friends SET friend = 1 WHERE account = '%s' AND service = '%s' AND user_id = '%s'" % (o2[1]["account"], o2[1]["service"], id))
231+ if not output.rowcount:
232+ output = db.execute("INSERT INTO friends (account, service, user_id, friend) VALUES (?, ?, ?, 1)", (o2[1]["account"], o2[1]["service"], id))
233+
234+ elif o2[0] == "followers": # Operation followers will grab all the followers IDs
235+ with sqlite3.connect(SQLITE_DB_FILENAME) as db:
236+ output = db.execute("UPDATE friends SET follower = 0 WHERE account = '%s' AND service = '%s'" % (o2[1]["account"], o2[1]["service"]))
237+ for id in o2[1]["ids"]:
238+ output = db.execute("UPDATE friends SET follower = 1 WHERE account = '%s' AND service = '%s' AND user_id = '%s'" % (o2[1]["account"], o2[1]["service"], id))
239+ if not output.rowcount:
240+ output = db.execute("INSERT INTO friends (account, service, user_id, follower) VALUES (?, ?, ?, 1)", (o2[1]["account"], o2[1]["service"], id))
241+
242+ elif o2[0] != "error":
243+ with sqlite3.connect(SQLITE_DB_FILENAME) as db:
244+ if len(db.execute("""SELECT * FROM MESSAGES WHERE mid = '%s' AND account = '%s' AND stream = '%s'""" % (o2[1], o2[2], o2[6])).fetchall()) > 0:
245 self.messages.Message("update", o2[-1])
246 else:
247 self.messages.Message("new", o2[-1])
248@@ -518,24 +549,46 @@
249 else:
250 errors.append(o2)
251 with sqlite3.connect(SQLITE_DB_FILENAME) as db:
252- oldid = db.execute("select max(ROWID) from messages").fetchone()[0] or 0
253+ oldid = db.execute("SELECT MAX(ROWID) FROM messages").fetchone()[0] or 0
254
255 output = db.executemany("INSERT OR REPLACE INTO messages (%s) VALUES (%s)" % (
256 ",".join(self.messages.columns),
257 ",".join("?" * len(self.messages.columns))), items)
258+ #
259+ # For every message, update or insert new data into friends table
260+ # Skip searches for now, because of a twitter bug.
261+ #
262+ #
263+ for item in items:
264+ data = json.loads(item[13])
265+ if not data["operation"] == "search":
266+ if not data["sender"]["is_me"]:
267+ output = db.execute("""
268+ UPDATE friends SET name = ?, url = ?, image = ?, nick = ?, location = ?
269+ WHERE account = '%s' AND service = '%s' AND user_id = '%s'""" % (data["account"], data["service"], data["sender"]["id"]),
270+ (data["sender"]["name"], data["sender"]["url"], data["sender"]["image"],
271+ data["sender"]["nick"], data["sender"]["location"]))
272+
273+ if not output.rowcount:
274+ output = db.execute("""
275+ INSERT INTO friends (account, service, user_id, name, url, image, nick, location, friend, follower)
276+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 0)""",
277+ (data["account"], data["service"], data["sender"]["id"],
278+ data["sender"]["name"], data["sender"]["url"], data["sender"]["image"],
279+ data["sender"]["nick"], data["sender"]["location"]))
280
281 for s in "messages", "replies", "private":
282- if s == "messages":
283- to_me = 0
284+ if s == "messages":
285+ to_me = 0
286 else: to_me = 1
287 count = db.execute("SELECT count(mid) FROM messages WHERE stream = ? AND from_me = 0 and to_me = ? AND ROWID > ?", (s,to_me,oldid)).fetchone()[0] or 0
288 self.unseen_counts[s] = self.unseen_counts[s] + int(count)
289-
290+
291 self.update_indicators(self.unseen_counts)
292
293 new_items = db.execute("""
294 select * from (select * from messages where operation == "receive" and ROWID > %s and to_me = 0 ORDER BY time DESC LIMIT 10) as a union
295- select * from (select * from messages where operation IN ("receive","private") and ROWID > %s and to_me != 0 ORDER BY time DESC LIMIT 10) as b
296+ select * from (select * from messages where operation == "receive" and ROWID > %s and to_me != 0 ORDER BY time DESC LIMIT 10) as b
297 ORDER BY time ASC""" % (oldid, oldid)).fetchall()
298
299 for i in new_items:
300@@ -592,7 +645,7 @@
301 def on_indicator_interest_removed(self, server, interest):
302 self.IndicatorInterestRemoved()
303
304- def on_indicator_server_activate(self, indicator, timestamp=None):
305+ def on_indicator_server_activate(self, indicator, timestamp):
306 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
307 client_bus = dbus.SessionBus()
308 log.logger.debug("Raising gwibber client")
309@@ -606,7 +659,7 @@
310 except dbus.DBusException:
311 log.logger.error("Indicator activate failed:\n%s", traceback.format_exc())
312
313- def on_indicator_activate(self, indicator, timestamp=None):
314+ def on_indicator_activate(self, indicator, timestamp):
315 if not indicate: return
316 stream = indicator.get_property("stream")
317 log.logger.debug("Raising gwibber client, focusing %s stream", stream)
318@@ -659,10 +712,7 @@
319 sender_name = message["sender"].get("nick", message["sender"].get("name", ""))
320
321 #image = util.resources.get_ui_asset("icons/breakdance/scalable/%s.svg" % message["service"])
322- if message["sender"].has_key("image"):
323- image = util.resources.get_avatar_path(message["sender"]["image"])
324- else:
325- image = util.resources.get_ui_asset("icons/breakdance/scalable/%s.svg" % message["service"])
326+ image = util.resources.get_avatar_path(message["sender"]["image"])
327 if not image:
328 image = util.resources.get_ui_asset("icons/breakdance/scalable/%s.svg" % message["service"])
329 util.notify(sender_name, message["text"], image, 2000)
330@@ -685,7 +735,7 @@
331 gobject.source_remove(self.refresh_timer_id)
332 log.logger.debug("Refresh interval is set to %s", SETTINGS["interval"])
333 operations = []
334-
335+
336 for o in self.collector.get_operations():
337 interval = FEATURES[o[1]].get("interval", 1)
338 if self.refresh_count % interval == 0:
339
340=== modified file 'gwibber/microblog/plugins/twitter/__init__.py'
341--- gwibber/microblog/plugins/twitter/__init__.py 2010-12-14 20:08:02 +0000
342+++ gwibber/microblog/plugins/twitter/__init__.py 2011-02-16 07:38:14 +0000
343@@ -33,6 +33,8 @@
344 "private",
345 "public",
346 "delete",
347+ "follow",
348+ "unfollow",
349 "retweet",
350 "like",
351 "send_thread",
352@@ -41,6 +43,8 @@
353 "sinceid",
354 "lists",
355 "list",
356+ "friends",
357+ "followers",
358 ],
359
360 "default_streams": [
361@@ -48,6 +52,8 @@
362 "responses",
363 "private",
364 "lists",
365+ "friends",
366+ "followers",
367 ],
368 }
369
370@@ -109,7 +115,7 @@
371 "url": "/".join((URL_PREFIX, user["screen_name"])),
372 "is_me": user["screen_name"] == self.account["username"],
373 }
374-
375+
376 def _message(self, data):
377 if type(data) == type(None):
378 return []
379@@ -189,7 +195,7 @@
380 request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, self.token,
381 http_method=post and "POST" or "GET", http_url=url, parameters=util.compact(args))
382 request.sign_request(self.sigmethod, self.consumer, self.token)
383-
384+
385 if post:
386 data = network.Download(request.to_url(), util.compact(args), post).get_json()
387 else:
388@@ -215,9 +221,21 @@
389 logstr = """%s: %s - %s""" % (PROTOCOL_INFO["name"], _("Request failed"), data)
390 log.logger.error("%s", logstr)
391 return [{"error": {"type": "request", "account": self.account, "message": data}}]
392-
393+
394+ if parse == "follow" or parse == "unfollow":
395+ if isinstance(data, dict) and data.get("error", 0):
396+ logstr = """%s: %s - %s""" % (PROTOCOL_INFO["name"], _("Unble to %s user" % parse), data["error"])
397+ log.logger.error("%s", logstr)
398+ return [{"error": {"type": "auth", "account": self.account, "message": data["error"]}}]
399+ else:
400+ return [["friendships", {"type": parse, "account": self.account["id"], "service": self.account["service"],"user_id": data["id"], "nick": data["screen_name"]}]]
401+
402 if parse == "list":
403 return [self._list(l) for l in data["lists"]]
404+
405+ if parse == "friends" or parse == "followers":
406+ return data
407+
408 if single: return [getattr(self, "_%s" % parse)(data)]
409 if parse: return [getattr(self, "_%s" % parse)(m) for m in data]
410 else: return []
411@@ -268,6 +286,12 @@
412 def like(self, message):
413 return self._get("favorites/create/%s.json" % message["mid"], None, post=True, do=1)
414
415+ def follow(self, screen_name):
416+ return self._get("friendships/create.json", screen_name=screen_name, post=True, parse="follow")
417+
418+ def unfollow(self, screen_name):
419+ return self._get("friendships/destroy.json", screen_name=screen_name, post=True, parse="unfollow")
420+
421 def send(self, message):
422 return self._get("statuses/update.json", post=True, single=True,
423 status=message)
424@@ -279,3 +303,44 @@
425 def send_thread(self, message, target):
426 return self._get("statuses/update.json", post=True, single=True,
427 status=message, in_reply_to_status_id=target["mid"])
428+
429+#
430+# If there is a nicer way of doing this, I'll be happy to implement it :)
431+# In case you are Justin Bieber or Lady Gaga, you will have to wait a long time
432+# in order for your followers to load.
433+#
434+# -- BigW 2010/11/17
435+
436+ def friends(self):
437+ data = self._get("friends/ids/%s.json" % self.account["username"], cursor=-1, parse="followers")
438+ ids = data["ids"]
439+ cursor = data["next_cursor"]
440+ while cursor > 0:
441+ data = self._get("friends/ids/%s.json" % self.account["username"], cursor=cursor, parse="followers")
442+ ids = ids + data["ids"]
443+ cursor = data["next_cursor"]
444+
445+ f = [(
446+ "friends",
447+ {"service": "twitter",
448+ "account": self.account["id"],
449+ "ids": ids,}
450+ )]
451+ return f
452+
453+ def followers(self):
454+ data = self._get("followers/ids/%s.json" % self.account["username"], cursor=-1, parse="followers")
455+ ids = data["ids"]
456+ cursor = data["next_cursor"]
457+ while cursor > 0:
458+ data = self._get("followers/ids/%s.json" % self.account["username"], cursor=cursor, parse="followers")
459+ ids = ids + data["ids"]
460+ cursor = data["next_cursor"]
461+
462+ f = [(
463+ "followers",
464+ {"service": "twitter",
465+ "account": self.account["id"],
466+ "ids": ids,}
467+ )]
468+ return f
469
470=== modified file 'gwibber/microblog/storage.py'
471--- gwibber/microblog/storage.py 2010-10-15 03:55:43 +0000
472+++ gwibber/microblog/storage.py 2011-02-16 07:38:14 +0000
473@@ -4,6 +4,7 @@
474 import gtk, gobject, dbus, dbus.service
475 import util, util.keyring, atexit
476 from util import log
477+from util.const import *
478 from dbus.mainloop.glib import DBusGMainLoop
479
480 DBusGMainLoop(set_as_default=True)
481@@ -50,7 +51,32 @@
482 @dbus.service.method("com.Gwibber.Messages", in_signature="s", out_signature="s")
483 def Get(self, id):
484 results = self.db.execute("SELECT data FROM messages WHERE id=?", (id,)).fetchone()
485- if results: return results[0]
486+ #
487+ # Inject is_friend and is_follower in the message data.
488+ #
489+ if results:
490+ data = json.loads(results[0])
491+ #
492+ # Messages from search results have different user_id's, see twitterAPI
493+ # bug: http://code.google.com/p/twitter-api/issues/detail?id=214
494+ # So we try to find a match based on screen_name/nick OR user_id.
495+ #
496+ tmpres = self.db.execute("""
497+ SELECT user_id, friend, follower FROM friends WHERE
498+ account = '%s' AND service = '%s' AND (user_id = %s OR nick = '%s')""" %
499+ (data["account"], data["service"], data["sender"]["id"], data["sender"]["nick"])).fetchone()
500+ if tmpres:
501+ data["sender"]["is_friend"] = tmpres[1]
502+ data["sender"]["is_follower"] = tmpres[2]
503+ else:
504+ #
505+ # For searches: this is also set if we never reveived any messages
506+ # from a friend or follower and we don't have their nickname in the DB.
507+ #
508+ data["sender"]["is_friend"] = 0
509+ data["sender"]["is_follower"] = 0
510+ results = json.dumps(data)
511+ return results
512 return ""
513
514 class SearchManager(dbus.service.Object):
515@@ -201,6 +227,31 @@
516 results = self.db.execute(query, (time, transient))
517 return "[%s]" % ", ".join([i[0] for i in results.fetchall()])
518
519+ #
520+ # Nasty little hack that will inject is_friend and is_follower in the
521+ # sender data. This probably needs some serious rewriting.
522+ #
523+ # I am keeping this here as a placeholder if we want to distinguish
524+ # friends/followers while rendering streams.
525+ #
526+ # -- BigW
527+ #
528+ #tmp = []
529+ #for i in results.fetchall():
530+ # data = json.loads(i[0])
531+ # tmpres = self.db.execute("""
532+ # SELECT user_id, friend, follower FROM friends WHERE
533+ # account = '%s' AND service = '%s' AND user_id = %s""" %
534+ # (data["account"], data["service"], data["sender"]["id"])).fetchone()
535+ # if tmpres:
536+ # data["sender"]["is_friend"] = tmpres[1]
537+ # data["sender"]["is_follower"] = tmpres[2]
538+ # else:
539+ # data["sender"]["is_friend"] = 0
540+ # data["sender"]["is_follower"] = 0
541+ # tmp.append(json.dumps(data))
542+ #return "[%s]" % ", ".join([i for i in tmp])
543+
544 @dbus.service.method("com.Gwibber.Streams", in_signature="s", out_signature="s")
545 def Create(self, search):
546 with self.db:
547@@ -340,3 +391,95 @@
548 data["send_enabled"] = True
549 if data:
550 self.Update(json.dumps(data))
551+
552+class FriendsManager(dbus.service.Object):
553+ __dbus_object_path__ = "/com/gwibber/Friends"
554+
555+ def __init__(self, db):
556+ self.bus = dbus.SessionBus()
557+ bus_name = dbus.service.BusName("com.Gwibber.Friends", bus=self.bus)
558+ dbus.service.Object.__init__(self, bus_name, self.__dbus_object_path__)
559+
560+ self.db = db
561+
562+ if not self.db.execute("PRAGMA table_info(friends)").fetchall():
563+ log.logger.debug("Friends table not found.")
564+ self.setup_table()
565+
566+ def setup_table(self):
567+ log.logger.debug("Creating new friends table.")
568+ with self.db:
569+ self.db.execute("""
570+ CREATE TABLE friends (
571+ account text,
572+ service text,
573+ user_id int,
574+ friend int,
575+ follower int,
576+ name text,
577+ url text,
578+ image text,
579+ nick text,
580+ location text)""")
581+ self.db.execute("CREATE UNIQUE INDEX friends_idx ON friends (account, service, user_id)")
582+
583+ @dbus.service.signal("com.Gwibber.Friends", signature="ss")
584+ def Friend(self, change, data): pass
585+
586+ @dbus.service.signal("com.Gwibber.Friends", signature="ssds")
587+ def Follow(self, account, service, user_id, nick = None):
588+ with sqlite3.connect(SQLITE_DB_FILENAME) as db:
589+ if nick:
590+ query = """UPDATE friends SET friend = 1, nick = '%s'
591+ WHERE account = '%s' AND service = '%s'
592+ AND user_id = '%s'""" % (nick, account, service, user_id)
593+ else:
594+ query = """UPDATE friends SET friend = 1
595+ WHERE account = '%s' AND service = '%s'
596+ AND user_id = '%s'""" % (account, service, user_id)
597+
598+ result = db.execute(query)
599+ if not result.rowcount:
600+ if nick:
601+ result = db.execute("INSERT INTO friends (account, service, user_id, nick, friend) VALUES (?, ?, ?, ?, 1)", (account, service, user_id, nick))
602+ else:
603+ result = db.execute("INSERT INTO friends (account, service, user_id, friend) VALUES (?, ?, ?, 1)", (account, service, user_id))
604+
605+ if result: return result
606+ return ""
607+
608+ @dbus.service.signal("com.Gwibber.Friends", signature="ssds")
609+ def Unfollow(self, account, service, user_id, nick = None):
610+ with sqlite3.connect(SQLITE_DB_FILENAME) as db:
611+ if nick:
612+ query = """UPDATE friends SET friend = 0, nick = '%s'
613+ WHERE account = '%s' AND service = '%s'
614+ AND user_id = '%s'""" % (nick, account, service, user_id)
615+ else:
616+ query = """UPDATE friends SET friend = 0
617+ WHERE account = '%s' AND service = '%s'
618+ AND user_id = '%s'""" % (account, service, user_id)
619+
620+ result = db.execute(query)
621+ if not result.rowcount:
622+ if nick:
623+ result = db.execute("INSERT INTO friends (account, service, user_id, nick, friend) VALUES (?, ?, ?, ?, 0)", (account, service, user_id, nick))
624+ else:
625+ result = db.execute("INSERT INTO friends (account, service, user_id, friend) VALUES (?, ?, ?, 0)", (account, service, user_id))
626+
627+ if result: return result
628+ return ""
629+
630+ @dbus.service.method("com.Gwibber.Friends", in_signature="s", out_signature="s")
631+ def IsFriend(self, id):
632+ with sqlite3.connect(SQLITE_DB_FILENAME) as db:
633+ results = db.execute("SELECT user_id FROM friends WHERE user_id=? AND friend=1", (id,)).fetchone()
634+ if results: return results[0]
635+ return ""
636+
637+ @dbus.service.method("com.Gwibber.Friends", in_signature="s", out_signature="s")
638+ def IsFollower(self, id):
639+ with sqlite3.connect(SQLITE_DB_FILENAME) as db:
640+ results = db.execute("SELECT user_id FROM friends WHERE user_id=? AND follower=1", (id,)).fetchone()
641+ if results: return results[0]
642+ return ""
643
644=== modified file 'gwibber/microblog/util/const.py'
645--- gwibber/microblog/util/const.py 2010-12-14 20:33:03 +0000
646+++ gwibber/microblog/util/const.py 2011-02-16 07:38:14 +0000
647@@ -238,6 +238,30 @@
648 "stream": null,
649 "transient": false
650 },
651+
652+ "follow": {
653+ "account_tree": false,
654+ "dynamic": false,
655+ "enabled": null,
656+ "first_only": false,
657+ "function": null,
658+ "return_value": false,
659+ "search": false,
660+ "stream": null,
661+ "transient": false
662+ },
663+
664+ "unfollow": {
665+ "account_tree": false,
666+ "dynamic": false,
667+ "enabled": null,
668+ "first_only": false,
669+ "function": null,
670+ "return_value": false,
671+ "search": false,
672+ "stream": null,
673+ "transient": false
674+ },
675
676 "search": {
677 "account_tree": false,
678@@ -333,6 +357,32 @@
679 "search": false,
680 "stream": "user",
681 "transient": true
682+ },
683+
684+ "friends": {
685+ "account_tree": false,
686+ "dynamic": false,
687+ "enabled": "recieve",
688+ "first_only": false,
689+ "function": null,
690+ "return_value": true,
691+ "search": false,
692+ "stream": null,
693+ "transient": false,
694+ "interval": 50
695+ },
696+
697+ "followers": {
698+ "account_tree": false,
699+ "dynamic": false,
700+ "enabled": "recieve",
701+ "first_only": false,
702+ "function": null,
703+ "return_value": true,
704+ "search": false,
705+ "stream": null,
706+ "transient": false,
707+ "interval": 50
708 }
709 }
710 """
711
712=== modified file 'ui/templates/base.mako'
713--- ui/templates/base.mako 2011-02-15 14:02:42 +0000
714+++ ui/templates/base.mako 2011-02-16 07:38:14 +0000
715@@ -12,6 +12,15 @@
716 % endif
717 </%def>
718
719+<%def name="recipient_profile_url(data)" filter="trim">
720+ % if "user_messages" in services[data["service"]]["features"]:
721+ gwibber:/user?acct=${data["account"]}&amp;name=${data["recipient"]["nick"]}
722+ % else:
723+ ${data['recipient']['url']}
724+ % endif
725+</%def>
726+
727+
728 <%def name="comment(data)">
729 % if "sender" in data:
730 % if "name" in data["sender"]:
731@@ -75,7 +84,7 @@
732 % endif
733 % else:
734 % if "image" in data["recipient"]:
735- <a href="${profile_url(data)}">
736+ <a href="${recipient_profile_url(data)}">
737 <div class="imgbox" title="${data["sender"].get("nick", "")}" style="background-image: url(${data["sender"]["image"]});">
738 <div class="imgbox" title="${data["recipient"].get("nick", "")}" style="margin: 25px 0px 0px 25px; width: 25px; height: 25px; background-image: url(${data["recipient"]["image"]});"></div></div>
739 </a>
740
741=== modified file 'ui/themes/ubuntu/main.css'
742--- ui/themes/ubuntu/main.css 2010-02-10 22:33:02 +0000
743+++ ui/themes/ubuntu/main.css 2011-02-16 07:38:14 +0000
744@@ -1,5 +1,7 @@
745 * {
746 color: black;
747+ font-family: ubuntu;
748+ font-size: 14px;
749 }
750
751 .imgbox {