Merge lp:~matthew-j-singleton/gwibber/new-retweets into lp:gwibber

Proposed by Greg Grossmeier
Status: Rejected
Rejected by: Ken VanDine
Proposed branch: lp:~matthew-j-singleton/gwibber/new-retweets
Merge into: lp:gwibber
Diff against target: 523 lines (+460/-0) (has conflicts)
3 files modified
gwibber/microblog/identica.py.OTHER (+211/-0)
gwibber/microblog/twitter.py.OTHER (+210/-0)
ui/templates/base.mako (+39/-0)
Contents conflict in gwibber/microblog/identica.py
Contents conflict in gwibber/microblog/twitter.py
Text conflict in ui/templates/base.mako
To merge this branch: bzr merge lp:~matthew-j-singleton/gwibber/new-retweets
Reviewer Review Type Date Requested Status
Ken VanDine Disapprove
Review via email: mp+65089@code.launchpad.net

Description of the change

Support for native retweets.

To post a comment you must log in.
Revision history for this message
Ken VanDine (ken-vandine) wrote :

Thanks for the branch, I manually merged some of your logic into the backend. Your branch was full of conflicts and gwibber has changed quite a bit. Trunk now has native retweet support as well as displaying of retweets natively, Thanks!

review: Disapprove

Unmerged revisions

706. By Matt Singleton

fix a dumb bug that breaks services that do not have retweets

705. By Matt Singleton

preliminary identica retweet support (naive port from twitter)

704. By Matt Singleton <msingleton@msingleton-wks>

merged from upstream

703. By Matt Singleton <msingleton@jayne>

display retweeter info in timeline

702. By Matt Singleton <msingleton@jayne>

refactor to cut down on some duplication around retweets in the twitter api

701. By Matt Singleton <msingleton@jayne>

merging from upstream

700. By Matt Singleton <msingleton@jayne>

adding vim modeline.

699. By Matt Singleton <msingleton@jayne>

merging from upstream

698. By Matt Singleton <msingleton@jayne>

inital support for retweets in home timeline. needs a quick refactor to stomp out some duplication.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'gwibber/microblog/identica.py.OTHER'
2--- gwibber/microblog/identica.py.OTHER 1970-01-01 00:00:00 +0000
3+++ gwibber/microblog/identica.py.OTHER 2011-06-18 11:06:46 +0000
4@@ -0,0 +1,211 @@
5+# vim: set ts=2 sts=2 sw=2 expandtab:
6+import re, network, util
7+from util import log
8+from util import exceptions
9+from gettext import lgettext as _
10+log.logger.name = "Identi.ca"
11+
12+PROTOCOL_INFO = {
13+ "name": "Identi.ca",
14+ "version": 1.0,
15+
16+ "config": [
17+ "private:password",
18+ "username",
19+ "color",
20+ "receive_enabled",
21+ "send_enabled",
22+ ],
23+
24+ "authtype": "login",
25+ "color": "#4E9A06",
26+
27+ "features": [
28+ "send",
29+ "receive",
30+ "search",
31+ "tag",
32+ "reply",
33+ "responses",
34+ "private",
35+ "public",
36+ "delete",
37+ "retweet",
38+ "like",
39+ "send_thread",
40+ "user_messages",
41+ "sinceid",
42+ ],
43+
44+ "default_streams": [
45+ "receive",
46+ "responses",
47+ "private",
48+ ],
49+}
50+
51+URL_PREFIX = "https://identi.ca"
52+
53+# Whether to treat the original sender or the
54+# retweeter as the actual sender
55+# TODO: make this a user configurable value
56+USE_ORIGINAL_SENDER = True
57+
58+def get_html_field(data):
59+ return util.linkify(data["text"],
60+ ((util.PARSE_HASH, '#<a class="hash" href="%s#search?q=\\1">\\1</a>' % URL_PREFIX),
61+ (util.PARSE_NICK, '@<a class="nick" href="%s/\\1">\\1</a>' % URL_PREFIX)),
62+ escape=False)
63+
64+def get_content_field(data, account_id):
65+ return util.linkify(data["text"],
66+ ((util.PARSE_HASH, '#<a class="hash" href="gwibber:/tag?acct=%s&query=\\1">\\1</a>' % account_id),
67+ (util.PARSE_NICK, '@<a class="nick" href="gwibber:/user?acct=%s&name=\\1">\\1</a>' % account_id)),
68+ escape=False)
69+
70+def get_user_data(data, account):
71+ user = data["user"] if "user" in data else data["sender"]
72+ ud = {}
73+ ud["name"] = user["name"]
74+ ud["nick"] = user["screen_name"]
75+ ud["id"] = user["id"]
76+ ud["location"] = user["location"]
77+ ud["followers"] = user["followers_count"]
78+ ud["image"] = user["profile_image_url"]
79+ ud["url"] = "/".join((URL_PREFIX, ud["nick"]))
80+ ud["is_me"] = ud["nick"] == account["username"]
81+ return ud
82+
83+class Client:
84+ def __init__(self, acct):
85+ self.account = acct
86+
87+ def _common(self, data):
88+ m = {}
89+ m["protocol"] = "identica"
90+ m["account"] = self.account["_id"]
91+ m["time"] = util.parsetime(data["created_at"])
92+ m["source"] = data.get("source", False)
93+ m["to_me"] = ("@%s" % self.account["username"]) in data["text"]
94+
95+ if "retweeted_status" in data and (USE_ORIGINAL_SENDER):
96+ m["id"] = str(data["retweeted_status"]["id"])
97+ m["text"] = data["retweeted_status"]["text"]
98+ m["html"] = get_html_field(data["retweeted_status"])
99+ m["content"] = get_content_field(data["retweeted_status"], m["account"])
100+ else:
101+ m["id"] = str(data["id"])
102+ m["text"] = data["text"]
103+ m["html"] = get_html_field(data)
104+ m["content"] = get_content_field(data, m["account"])
105+
106+ if data.get("attachments", 0):
107+ m["images"] = []
108+ for a in data["attachments"]:
109+ mime = a.get("mimetype", "")
110+ if mime and mime.startswith("image") and a.get("url", 0):
111+ m["images"].append({"src": a["url"], "url": a["url"]})
112+
113+ return m
114+
115+ def _message(self, data):
116+ m = self._common(data)
117+
118+ if data.get("in_reply_to_status_id", 0) and data.get("in_reply_to_screen_name", 0):
119+ m["reply"] = {}
120+ m["reply"]["id"] = data["in_reply_to_status_id"]
121+ m["reply"]["nick"] = data["in_reply_to_screen_name"]
122+ m["reply"]["url"] = "/".join((URL_PREFIX, "notice", str(m["reply"]["id"])))
123+
124+ if ("retweeted_status" in data) and (USE_ORIGINAL_SENDER):
125+ m["sender"] = get_user_data(data["retweeted_status"], self.account)
126+ m["retweeter"] = get_user_data(data, self.account)
127+ m["retweet"] = True
128+ else:
129+ m["sender"] = get_user_data(data, self.account)
130+ m["retweet"] = False
131+
132+ m["url"] = "/".join((URL_PREFIX, "notice", m["id"]))
133+
134+ return m
135+
136+ def _private(self, data):
137+ m = self._message(data)
138+ m["private"] = True
139+ return m
140+
141+ def _result(self, data):
142+ m = self._common(data)
143+
144+ if data["to_user_id"]:
145+ m["reply"] = {}
146+ m["reply"]["id"] = data["to_user_id"]
147+ m["reply"]["nick"] = data["to_user"]
148+
149+ m["sender"] = {}
150+ m["sender"]["nick"] = data["from_user"]
151+ m["sender"]["id"] = data["from_user_id"]
152+ m["sender"]["image"] = data["profile_image_url"]
153+ m["sender"]["url"] = "/".join((URL_PREFIX, m["sender"]["nick"]))
154+ m["url"] = "/".join((m["sender"]["url"], "statuses", m["id"]))
155+ return m
156+
157+ def _get(self, path, parse="message", post=False, single=False, **args):
158+ url = "/".join((URL_PREFIX, "api", path))
159+ data = network.Download(url, util.compact(args), post,
160+ self.account["username"], self.account["password"]).get_json()
161+
162+ # error is "Could not authenticate you" for failed auth
163+ try:
164+ if single: return [getattr(self, "_%s" % parse)(data)]
165+ if parse: return [getattr(self, "_%s" % parse)(m) for m in data]
166+ else: return []
167+ except:
168+ if data.has_key("error"):
169+ if "authenticate" in data["error"]:
170+ raise exceptions.GwibberProtocolError("auth", self.account["protocol"], self.account["username"], data["error"])
171+
172+ def _search(self, **args):
173+ data = network.Download("%s/api/search.json" % URL_PREFIX, util.compact(args))
174+ data = data.get_json()["results"]
175+ return [self._result(m) for m in data]
176+
177+ def __call__(self, opname, **args):
178+ return getattr(self, opname)(**args)
179+
180+ def receive(self, count=util.COUNT, since=None):
181+ return self._get("statuses/friends_timeline.json", count=count, since_id=since)
182+
183+ def user_messages(self, id=None, count=util.COUNT, since=None):
184+ return self._get("statuses/user_timeline.json", id=id, count=count, since_id=since)
185+
186+ def responses(self, count=util.COUNT, since=None):
187+ return self._get("statuses/mentions.json", count=count, since_id=since)
188+
189+ def private(self, count=util.COUNT, since=None):
190+ return self._get("direct_messages.json", "private", count=count, since_id=since)
191+
192+ def public(self):
193+ return self._get("statuses/public_timeline.json")
194+
195+ def search(self, query, count=util.COUNT, since=None):
196+ return self._search(q=query, rpp=count, since_id=since)
197+
198+ def tag(self, query, count=util.COUNT, since=None):
199+ return self._search(q="#%s" % query, count=count, since_id=since)
200+
201+ def delete(self, message):
202+ self._get("statuses/destroy/%s.json" % message["id"], None, post=True, do=1)
203+ return []
204+
205+ def like(self, message):
206+ self._get("favorites/create/%s.json" % message["id"], None, post=True, do=1)
207+ return []
208+
209+ def send(self, message):
210+ return self._get("statuses/update.json", post=True, single=True,
211+ status=message, source="Gwibber")
212+
213+ def send_thread(self, message, target):
214+ return self._get("statuses/update.json", post=True, single=True,
215+ status=message, source="gwibber", in_reply_to_status_id=target["id"])
216
217=== added file 'gwibber/microblog/twitter.py.OTHER'
218--- gwibber/microblog/twitter.py.OTHER 1970-01-01 00:00:00 +0000
219+++ gwibber/microblog/twitter.py.OTHER 2011-06-18 11:06:46 +0000
220@@ -0,0 +1,210 @@
221+# vim: set ts=2 sts=2 sw=2 expandtab:
222+import network, util, htmllib
223+from util import log
224+from util import exceptions
225+from gettext import lgettext as _
226+log.logger.name = "Twitter"
227+
228+PROTOCOL_INFO = {
229+ "name": "Twitter",
230+ "version": "1.0",
231+
232+ "config": [
233+ "private:password",
234+ "username",
235+ "color",
236+ "receive_enabled",
237+ "send_enabled",
238+ ],
239+
240+ "authtype": "login",
241+ "color": "#729FCF",
242+
243+ "features": [
244+ "send",
245+ "receive",
246+ "search",
247+ "tag",
248+ "reply",
249+ "responses",
250+ "private",
251+ "public",
252+ "delete",
253+ "retweet",
254+ "like",
255+ "send_thread",
256+ "user_messages",
257+ "sinceid",
258+ ],
259+
260+ "default_streams": [
261+ "receive",
262+ "responses",
263+ "private",
264+ ],
265+}
266+
267+URL_PREFIX = "https://twitter.com"
268+
269+# Whether to treat the original sender or the
270+# retweeter as the actual sender
271+# TODO: make this a user configurable value
272+USE_ORIGINAL_SENDER = True
273+
274+def unescape(s):
275+ p = htmllib.HTMLParser(None)
276+ p.save_bgn()
277+ p.feed(s)
278+ return p.save_end()
279+
280+def get_html_field(data):
281+ return util.linkify(data["text"],
282+ ((util.PARSE_HASH, '#<a class="hash" href="%s#search?q=\\1">\\1</a>' % URL_PREFIX),
283+ (util.PARSE_NICK, '@<a class="nick" href="%s/\\1">\\1</a>' % URL_PREFIX)),
284+ escape=False)
285+
286+def get_content_field(data, account_id):
287+ return util.linkify(data["text"],
288+ ((util.PARSE_HASH, '#<a class="hash" href="gwibber:/tag?acct=%s&query=\\1">\\1</a>' % account_id),
289+ (util.PARSE_NICK, '@<a class="nick" href="gwibber:/user?acct=%s&name=\\1">\\1</a>' % account_id)),
290+ escape=False)
291+
292+def get_user_data(data, account):
293+ user = data["user"] if "user" in data else data["sender"]
294+ ud = {}
295+ ud["name"] = user["name"]
296+ ud["nick"] = user["screen_name"]
297+ ud["id"] = user["id"]
298+ ud["location"] = user["location"]
299+ ud["followers"] = user["followers_count"]
300+ ud["image"] = user["profile_image_url"]
301+ ud["url"] = "/".join((URL_PREFIX, ud["nick"]))
302+ ud["is_me"] = ud["nick"] == account["username"]
303+ return ud
304+
305+class Client:
306+ def __init__(self, acct):
307+ self.account = acct
308+
309+ def _common(self, data):
310+ m = {}
311+ m["protocol"] = "twitter"
312+ m["account"] = self.account["_id"]
313+ m["time"] = util.parsetime(data["created_at"])
314+ m["to_me"] = ("@%s" % self.account["username"]) in data["text"]
315+
316+ if "retweeted_status" in data and (USE_ORIGINAL_SENDER):
317+ m["id"] = str(data["retweeted_status"]["id"])
318+ m["text"] = unescape(data["retweeted_status"]["text"])
319+ m["html"] = get_html_field(data["retweeted_status"])
320+ m["content"] = get_content_field(data["retweeted_status"], m["account"])
321+ else:
322+ m["id"] = str(data["id"])
323+ m["text"] = unescape(data["text"])
324+ m["html"] = get_html_field(data)
325+ m["content"] = get_content_field(data, m["account"])
326+
327+ return m
328+
329+ def _message(self, data):
330+ m = self._common(data)
331+ m["source"] = data.get("source", False)
332+
333+ if "in_reply_to_status_id" in data and data["in_reply_to_status_id"]:
334+ m["reply"] = {}
335+ m["reply"]["id"] = data["in_reply_to_status_id"]
336+ m["reply"]["nick"] = data["in_reply_to_screen_name"]
337+ m["reply"]["url"] = "/".join((URL_PREFIX, m["reply"]["nick"], "statuses", str(m["reply"]["id"])))
338+
339+ if ("retweeted_status" in data) and (USE_ORIGINAL_SENDER):
340+ m["sender"] = get_user_data(data["retweeted_status"], self.account)
341+ m["retweeter"] = get_user_data(data, self.account)
342+ m["retweet"] = True
343+ else:
344+ m["sender"] = get_user_data(data, self.account)
345+ m["retweet"] = False
346+
347+ m["url"] = "/".join((m["sender"]["url"], "statuses", str(m["id"])))
348+
349+ return m
350+
351+ def _private(self, data):
352+ m = self._message(data)
353+ m["private"] = True
354+ return m
355+
356+ def _result(self, data):
357+ m = self._common(data)
358+
359+ if data["to_user_id"]:
360+ m["reply"] = {}
361+ m["reply"]["id"] = data["to_user_id"]
362+ m["reply"]["nick"] = data["to_user"]
363+
364+ m["sender"] = {}
365+ m["sender"]["nick"] = data["from_user"]
366+ m["sender"]["id"] = data["from_user_id"]
367+ m["sender"]["image"] = data["profile_image_url"]
368+ m["sender"]["url"] = "/".join((URL_PREFIX, m["sender"]["nick"]))
369+ m["sender"]["is_me"] = m["sender"]["nick"] == self.account["username"]
370+ m["url"] = "/".join((m["sender"]["url"], "statuses", str(m["id"])))
371+ return m
372+
373+ def _get(self, path, parse="message", post=False, single=False, **args):
374+ url = "/".join((URL_PREFIX, path))
375+ data = network.Download(url, util.compact(args) or None, post,
376+ self.account["username"], self.account["password"]).get_json()
377+
378+ # error is "Could not authenticate you" for failed auth
379+ try:
380+ if single: return [getattr(self, "_%s" % parse)(data)]
381+ if parse: return [getattr(self, "_%s" % parse)(m) for m in data]
382+ else: return []
383+ except:
384+ if data.has_key("error"):
385+ if "authenticate" in data["error"]:
386+ raise exceptions.GwibberProtocolError("auth", self.account["protocol"], self.account["username"], data["error"])
387+
388+ def _search(self, **args):
389+ data = network.Download("http://search.twitter.com/search.json", util.compact(args))
390+ data = data.get_json()["results"]
391+ return [self._result(m) for m in data]
392+
393+ def __call__(self, opname, **args):
394+ return getattr(self, opname)(**args)
395+
396+ def receive(self, count=util.COUNT, since=None):
397+ return self._get("statuses/home_timeline.json", count=count, since_id=since)
398+
399+ def user_messages(self, id=None, count=util.COUNT, since=None):
400+ return self._get("statuses/user_timeline.json", id=id, count=count, since_id=since)
401+
402+ def responses(self, count=util.COUNT, since=None):
403+ return self._get("statuses/mentions.json", count=count, since_id=since)
404+
405+ def private(self, count=util.COUNT, since=None):
406+ return self._get("direct_messages.json", "private", count=count, since_id=since)
407+
408+ def public(self):
409+ return self._get("statuses/public_timeline.json")
410+
411+ def search(self, query, count=util.COUNT, since=None):
412+ return self._search(q=query, rpp=count, since_id=since)
413+
414+ def tag(self, query, count=util.COUNT, since=None):
415+ return self._search(q="#%s" % query, count=count, since_id=since)
416+
417+ def delete(self, message):
418+ return self._get("statuses/destroy/%s.json" % message["id"], None, post=True, do=1)
419+
420+ def like(self, message):
421+ return self._get("favorites/create/%s.json" % message["id"], None, post=True, do=1)
422+
423+ def send(self, message):
424+ return self._get("statuses/update.json", post=True, single=True,
425+ status=message, source="gwibbernet")
426+
427+ def send_thread(self, message, target):
428+ return self._get("statuses/update.json", post=True, single=True,
429+ status=message, source="gwibbernet", in_reply_to_status_id=target["id"])
430+
431
432=== modified file 'ui/templates/base.mako'
433--- ui/templates/base.mako 2011-04-13 18:34:14 +0000
434+++ ui/templates/base.mako 2011-06-18 11:06:46 +0000
435@@ -4,6 +4,7 @@
436 </a>
437 </%def>
438
439+<<<<<<< TREE
440 <%def name="profile_url(data)" filter="trim">
441 % if services.has_key(data["service"]):
442 % if "user_messages" in services[data["service"]]["features"]:
443@@ -11,6 +12,13 @@
444 % else:
445 ${data['sender']['url']}
446 % endif
447+=======
448+<%def name="profile_url(data, user)" filter="trim">
449+ % if "user_messages" in services[data["protocol"]]["features"]:
450+ gwibber:/user?acct=${data['account']}&amp;name=${user["nick"]}
451+ % else:
452+ ${user['url']}
453+>>>>>>> MERGE-SOURCE
454 % endif
455 </%def>
456
457@@ -64,6 +72,7 @@
458 </%def>
459
460 <%def name="image(data)">
461+<<<<<<< TREE
462 % if data.get("private", 0):
463 % if "recipient" in data:
464 % if data['recipient'].get("is_me", 0):
465@@ -141,6 +150,11 @@
466 <div class="thumbnails">
467 <a href="${data['photo']['url']}"><img src="${data['photo']['picture']}" /></a>
468 </div>
469+=======
470+ <a href="${profile_url(data, data['sender'])}">
471+ <div class="imgbox" title="${data["sender"].get("nick", "")}" style="background-image: url(${data["sender"]["image"]});"></div>
472+ </a>
473+>>>>>>> MERGE-SOURCE
474 </%def>
475
476 <%def name="images(data)">
477@@ -206,7 +220,16 @@
478 % endif
479 </%def>
480
481+<%def name="username(user)" filter="trim">
482+ % if preferences["show_fullname"]:
483+ ${user.get("name", 0) or user.get("nick", "")}
484+ % else:
485+ ${user.get("nick", 0) or user.get("name", "")}
486+ % endif
487+</%def>
488+
489 <%def name="sender(data)" filter="trim">
490+<<<<<<< TREE
491 % if "sender" in data:
492 % if preferences["show_fullname"]:
493 % if data.get("private", 0):
494@@ -238,6 +261,13 @@
495 % endif
496 % endif
497 % endif
498+=======
499+ ${username(data['sender'])}
500+</%def>
501+
502+<%def name="retweeter(data)" filter="trim">
503+ ${username(data['retweeter'])}
504+>>>>>>> MERGE-SOURCE
505 </%def>
506
507 <%def name="title(data)">
508@@ -276,6 +306,15 @@
509 <span class="text" id="text-${data['id']}">${data.get('content', '')}</span>
510 % endif
511 </p>
512+ % if data.get("retweet") == True:
513+ <p class="rt_attribution">
514+ <span class="text">Retweeted by:
515+ <a href="${profile_url(data, data['retweeter'])}">
516+ ${retweeter(data)}
517+ </a>
518+ </span>
519+ </p>
520+ % endif
521 </%def>
522
523 <%def name="sidebar(data)">