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
=== added file 'gwibber/microblog/identica.py.OTHER'
--- gwibber/microblog/identica.py.OTHER 1970-01-01 00:00:00 +0000
+++ gwibber/microblog/identica.py.OTHER 2011-06-18 11:06:46 +0000
@@ -0,0 +1,211 @@
1# vim: set ts=2 sts=2 sw=2 expandtab:
2import re, network, util
3from util import log
4from util import exceptions
5from gettext import lgettext as _
6log.logger.name = "Identi.ca"
7
8PROTOCOL_INFO = {
9 "name": "Identi.ca",
10 "version": 1.0,
11
12 "config": [
13 "private:password",
14 "username",
15 "color",
16 "receive_enabled",
17 "send_enabled",
18 ],
19
20 "authtype": "login",
21 "color": "#4E9A06",
22
23 "features": [
24 "send",
25 "receive",
26 "search",
27 "tag",
28 "reply",
29 "responses",
30 "private",
31 "public",
32 "delete",
33 "retweet",
34 "like",
35 "send_thread",
36 "user_messages",
37 "sinceid",
38 ],
39
40 "default_streams": [
41 "receive",
42 "responses",
43 "private",
44 ],
45}
46
47URL_PREFIX = "https://identi.ca"
48
49# Whether to treat the original sender or the
50# retweeter as the actual sender
51# TODO: make this a user configurable value
52USE_ORIGINAL_SENDER = True
53
54def get_html_field(data):
55 return util.linkify(data["text"],
56 ((util.PARSE_HASH, '#<a class="hash" href="%s#search?q=\\1">\\1</a>' % URL_PREFIX),
57 (util.PARSE_NICK, '@<a class="nick" href="%s/\\1">\\1</a>' % URL_PREFIX)),
58 escape=False)
59
60def get_content_field(data, account_id):
61 return util.linkify(data["text"],
62 ((util.PARSE_HASH, '#<a class="hash" href="gwibber:/tag?acct=%s&query=\\1">\\1</a>' % account_id),
63 (util.PARSE_NICK, '@<a class="nick" href="gwibber:/user?acct=%s&name=\\1">\\1</a>' % account_id)),
64 escape=False)
65
66def get_user_data(data, account):
67 user = data["user"] if "user" in data else data["sender"]
68 ud = {}
69 ud["name"] = user["name"]
70 ud["nick"] = user["screen_name"]
71 ud["id"] = user["id"]
72 ud["location"] = user["location"]
73 ud["followers"] = user["followers_count"]
74 ud["image"] = user["profile_image_url"]
75 ud["url"] = "/".join((URL_PREFIX, ud["nick"]))
76 ud["is_me"] = ud["nick"] == account["username"]
77 return ud
78
79class Client:
80 def __init__(self, acct):
81 self.account = acct
82
83 def _common(self, data):
84 m = {}
85 m["protocol"] = "identica"
86 m["account"] = self.account["_id"]
87 m["time"] = util.parsetime(data["created_at"])
88 m["source"] = data.get("source", False)
89 m["to_me"] = ("@%s" % self.account["username"]) in data["text"]
90
91 if "retweeted_status" in data and (USE_ORIGINAL_SENDER):
92 m["id"] = str(data["retweeted_status"]["id"])
93 m["text"] = data["retweeted_status"]["text"]
94 m["html"] = get_html_field(data["retweeted_status"])
95 m["content"] = get_content_field(data["retweeted_status"], m["account"])
96 else:
97 m["id"] = str(data["id"])
98 m["text"] = data["text"]
99 m["html"] = get_html_field(data)
100 m["content"] = get_content_field(data, m["account"])
101
102 if data.get("attachments", 0):
103 m["images"] = []
104 for a in data["attachments"]:
105 mime = a.get("mimetype", "")
106 if mime and mime.startswith("image") and a.get("url", 0):
107 m["images"].append({"src": a["url"], "url": a["url"]})
108
109 return m
110
111 def _message(self, data):
112 m = self._common(data)
113
114 if data.get("in_reply_to_status_id", 0) and data.get("in_reply_to_screen_name", 0):
115 m["reply"] = {}
116 m["reply"]["id"] = data["in_reply_to_status_id"]
117 m["reply"]["nick"] = data["in_reply_to_screen_name"]
118 m["reply"]["url"] = "/".join((URL_PREFIX, "notice", str(m["reply"]["id"])))
119
120 if ("retweeted_status" in data) and (USE_ORIGINAL_SENDER):
121 m["sender"] = get_user_data(data["retweeted_status"], self.account)
122 m["retweeter"] = get_user_data(data, self.account)
123 m["retweet"] = True
124 else:
125 m["sender"] = get_user_data(data, self.account)
126 m["retweet"] = False
127
128 m["url"] = "/".join((URL_PREFIX, "notice", m["id"]))
129
130 return m
131
132 def _private(self, data):
133 m = self._message(data)
134 m["private"] = True
135 return m
136
137 def _result(self, data):
138 m = self._common(data)
139
140 if data["to_user_id"]:
141 m["reply"] = {}
142 m["reply"]["id"] = data["to_user_id"]
143 m["reply"]["nick"] = data["to_user"]
144
145 m["sender"] = {}
146 m["sender"]["nick"] = data["from_user"]
147 m["sender"]["id"] = data["from_user_id"]
148 m["sender"]["image"] = data["profile_image_url"]
149 m["sender"]["url"] = "/".join((URL_PREFIX, m["sender"]["nick"]))
150 m["url"] = "/".join((m["sender"]["url"], "statuses", m["id"]))
151 return m
152
153 def _get(self, path, parse="message", post=False, single=False, **args):
154 url = "/".join((URL_PREFIX, "api", path))
155 data = network.Download(url, util.compact(args), post,
156 self.account["username"], self.account["password"]).get_json()
157
158 # error is "Could not authenticate you" for failed auth
159 try:
160 if single: return [getattr(self, "_%s" % parse)(data)]
161 if parse: return [getattr(self, "_%s" % parse)(m) for m in data]
162 else: return []
163 except:
164 if data.has_key("error"):
165 if "authenticate" in data["error"]:
166 raise exceptions.GwibberProtocolError("auth", self.account["protocol"], self.account["username"], data["error"])
167
168 def _search(self, **args):
169 data = network.Download("%s/api/search.json" % URL_PREFIX, util.compact(args))
170 data = data.get_json()["results"]
171 return [self._result(m) for m in data]
172
173 def __call__(self, opname, **args):
174 return getattr(self, opname)(**args)
175
176 def receive(self, count=util.COUNT, since=None):
177 return self._get("statuses/friends_timeline.json", count=count, since_id=since)
178
179 def user_messages(self, id=None, count=util.COUNT, since=None):
180 return self._get("statuses/user_timeline.json", id=id, count=count, since_id=since)
181
182 def responses(self, count=util.COUNT, since=None):
183 return self._get("statuses/mentions.json", count=count, since_id=since)
184
185 def private(self, count=util.COUNT, since=None):
186 return self._get("direct_messages.json", "private", count=count, since_id=since)
187
188 def public(self):
189 return self._get("statuses/public_timeline.json")
190
191 def search(self, query, count=util.COUNT, since=None):
192 return self._search(q=query, rpp=count, since_id=since)
193
194 def tag(self, query, count=util.COUNT, since=None):
195 return self._search(q="#%s" % query, count=count, since_id=since)
196
197 def delete(self, message):
198 self._get("statuses/destroy/%s.json" % message["id"], None, post=True, do=1)
199 return []
200
201 def like(self, message):
202 self._get("favorites/create/%s.json" % message["id"], None, post=True, do=1)
203 return []
204
205 def send(self, message):
206 return self._get("statuses/update.json", post=True, single=True,
207 status=message, source="Gwibber")
208
209 def send_thread(self, message, target):
210 return self._get("statuses/update.json", post=True, single=True,
211 status=message, source="gwibber", in_reply_to_status_id=target["id"])
0212
=== added file 'gwibber/microblog/twitter.py.OTHER'
--- gwibber/microblog/twitter.py.OTHER 1970-01-01 00:00:00 +0000
+++ gwibber/microblog/twitter.py.OTHER 2011-06-18 11:06:46 +0000
@@ -0,0 +1,210 @@
1# vim: set ts=2 sts=2 sw=2 expandtab:
2import network, util, htmllib
3from util import log
4from util import exceptions
5from gettext import lgettext as _
6log.logger.name = "Twitter"
7
8PROTOCOL_INFO = {
9 "name": "Twitter",
10 "version": "1.0",
11
12 "config": [
13 "private:password",
14 "username",
15 "color",
16 "receive_enabled",
17 "send_enabled",
18 ],
19
20 "authtype": "login",
21 "color": "#729FCF",
22
23 "features": [
24 "send",
25 "receive",
26 "search",
27 "tag",
28 "reply",
29 "responses",
30 "private",
31 "public",
32 "delete",
33 "retweet",
34 "like",
35 "send_thread",
36 "user_messages",
37 "sinceid",
38 ],
39
40 "default_streams": [
41 "receive",
42 "responses",
43 "private",
44 ],
45}
46
47URL_PREFIX = "https://twitter.com"
48
49# Whether to treat the original sender or the
50# retweeter as the actual sender
51# TODO: make this a user configurable value
52USE_ORIGINAL_SENDER = True
53
54def unescape(s):
55 p = htmllib.HTMLParser(None)
56 p.save_bgn()
57 p.feed(s)
58 return p.save_end()
59
60def get_html_field(data):
61 return util.linkify(data["text"],
62 ((util.PARSE_HASH, '#<a class="hash" href="%s#search?q=\\1">\\1</a>' % URL_PREFIX),
63 (util.PARSE_NICK, '@<a class="nick" href="%s/\\1">\\1</a>' % URL_PREFIX)),
64 escape=False)
65
66def get_content_field(data, account_id):
67 return util.linkify(data["text"],
68 ((util.PARSE_HASH, '#<a class="hash" href="gwibber:/tag?acct=%s&query=\\1">\\1</a>' % account_id),
69 (util.PARSE_NICK, '@<a class="nick" href="gwibber:/user?acct=%s&name=\\1">\\1</a>' % account_id)),
70 escape=False)
71
72def get_user_data(data, account):
73 user = data["user"] if "user" in data else data["sender"]
74 ud = {}
75 ud["name"] = user["name"]
76 ud["nick"] = user["screen_name"]
77 ud["id"] = user["id"]
78 ud["location"] = user["location"]
79 ud["followers"] = user["followers_count"]
80 ud["image"] = user["profile_image_url"]
81 ud["url"] = "/".join((URL_PREFIX, ud["nick"]))
82 ud["is_me"] = ud["nick"] == account["username"]
83 return ud
84
85class Client:
86 def __init__(self, acct):
87 self.account = acct
88
89 def _common(self, data):
90 m = {}
91 m["protocol"] = "twitter"
92 m["account"] = self.account["_id"]
93 m["time"] = util.parsetime(data["created_at"])
94 m["to_me"] = ("@%s" % self.account["username"]) in data["text"]
95
96 if "retweeted_status" in data and (USE_ORIGINAL_SENDER):
97 m["id"] = str(data["retweeted_status"]["id"])
98 m["text"] = unescape(data["retweeted_status"]["text"])
99 m["html"] = get_html_field(data["retweeted_status"])
100 m["content"] = get_content_field(data["retweeted_status"], m["account"])
101 else:
102 m["id"] = str(data["id"])
103 m["text"] = unescape(data["text"])
104 m["html"] = get_html_field(data)
105 m["content"] = get_content_field(data, m["account"])
106
107 return m
108
109 def _message(self, data):
110 m = self._common(data)
111 m["source"] = data.get("source", False)
112
113 if "in_reply_to_status_id" in data and data["in_reply_to_status_id"]:
114 m["reply"] = {}
115 m["reply"]["id"] = data["in_reply_to_status_id"]
116 m["reply"]["nick"] = data["in_reply_to_screen_name"]
117 m["reply"]["url"] = "/".join((URL_PREFIX, m["reply"]["nick"], "statuses", str(m["reply"]["id"])))
118
119 if ("retweeted_status" in data) and (USE_ORIGINAL_SENDER):
120 m["sender"] = get_user_data(data["retweeted_status"], self.account)
121 m["retweeter"] = get_user_data(data, self.account)
122 m["retweet"] = True
123 else:
124 m["sender"] = get_user_data(data, self.account)
125 m["retweet"] = False
126
127 m["url"] = "/".join((m["sender"]["url"], "statuses", str(m["id"])))
128
129 return m
130
131 def _private(self, data):
132 m = self._message(data)
133 m["private"] = True
134 return m
135
136 def _result(self, data):
137 m = self._common(data)
138
139 if data["to_user_id"]:
140 m["reply"] = {}
141 m["reply"]["id"] = data["to_user_id"]
142 m["reply"]["nick"] = data["to_user"]
143
144 m["sender"] = {}
145 m["sender"]["nick"] = data["from_user"]
146 m["sender"]["id"] = data["from_user_id"]
147 m["sender"]["image"] = data["profile_image_url"]
148 m["sender"]["url"] = "/".join((URL_PREFIX, m["sender"]["nick"]))
149 m["sender"]["is_me"] = m["sender"]["nick"] == self.account["username"]
150 m["url"] = "/".join((m["sender"]["url"], "statuses", str(m["id"])))
151 return m
152
153 def _get(self, path, parse="message", post=False, single=False, **args):
154 url = "/".join((URL_PREFIX, path))
155 data = network.Download(url, util.compact(args) or None, post,
156 self.account["username"], self.account["password"]).get_json()
157
158 # error is "Could not authenticate you" for failed auth
159 try:
160 if single: return [getattr(self, "_%s" % parse)(data)]
161 if parse: return [getattr(self, "_%s" % parse)(m) for m in data]
162 else: return []
163 except:
164 if data.has_key("error"):
165 if "authenticate" in data["error"]:
166 raise exceptions.GwibberProtocolError("auth", self.account["protocol"], self.account["username"], data["error"])
167
168 def _search(self, **args):
169 data = network.Download("http://search.twitter.com/search.json", util.compact(args))
170 data = data.get_json()["results"]
171 return [self._result(m) for m in data]
172
173 def __call__(self, opname, **args):
174 return getattr(self, opname)(**args)
175
176 def receive(self, count=util.COUNT, since=None):
177 return self._get("statuses/home_timeline.json", count=count, since_id=since)
178
179 def user_messages(self, id=None, count=util.COUNT, since=None):
180 return self._get("statuses/user_timeline.json", id=id, count=count, since_id=since)
181
182 def responses(self, count=util.COUNT, since=None):
183 return self._get("statuses/mentions.json", count=count, since_id=since)
184
185 def private(self, count=util.COUNT, since=None):
186 return self._get("direct_messages.json", "private", count=count, since_id=since)
187
188 def public(self):
189 return self._get("statuses/public_timeline.json")
190
191 def search(self, query, count=util.COUNT, since=None):
192 return self._search(q=query, rpp=count, since_id=since)
193
194 def tag(self, query, count=util.COUNT, since=None):
195 return self._search(q="#%s" % query, count=count, since_id=since)
196
197 def delete(self, message):
198 return self._get("statuses/destroy/%s.json" % message["id"], None, post=True, do=1)
199
200 def like(self, message):
201 return self._get("favorites/create/%s.json" % message["id"], None, post=True, do=1)
202
203 def send(self, message):
204 return self._get("statuses/update.json", post=True, single=True,
205 status=message, source="gwibbernet")
206
207 def send_thread(self, message, target):
208 return self._get("statuses/update.json", post=True, single=True,
209 status=message, source="gwibbernet", in_reply_to_status_id=target["id"])
210
0211
=== modified file 'ui/templates/base.mako'
--- ui/templates/base.mako 2011-04-13 18:34:14 +0000
+++ ui/templates/base.mako 2011-06-18 11:06:46 +0000
@@ -4,6 +4,7 @@
4 </a>4 </a>
5</%def>5</%def>
66
7<<<<<<< TREE
7<%def name="profile_url(data)" filter="trim">8<%def name="profile_url(data)" filter="trim">
8 % if services.has_key(data["service"]):9 % if services.has_key(data["service"]):
9 % if "user_messages" in services[data["service"]]["features"]:10 % if "user_messages" in services[data["service"]]["features"]:
@@ -11,6 +12,13 @@
11 % else:12 % else:
12 ${data['sender']['url']}13 ${data['sender']['url']}
13 % endif14 % endif
15=======
16<%def name="profile_url(data, user)" filter="trim">
17 % if "user_messages" in services[data["protocol"]]["features"]:
18 gwibber:/user?acct=${data['account']}&amp;name=${user["nick"]}
19 % else:
20 ${user['url']}
21>>>>>>> MERGE-SOURCE
14 % endif22 % endif
15</%def>23</%def>
1624
@@ -64,6 +72,7 @@
64</%def>72</%def>
6573
66<%def name="image(data)">74<%def name="image(data)">
75<<<<<<< TREE
67 % if data.get("private", 0):76 % if data.get("private", 0):
68 % if "recipient" in data:77 % if "recipient" in data:
69 % if data['recipient'].get("is_me", 0):78 % if data['recipient'].get("is_me", 0):
@@ -141,6 +150,11 @@
141 <div class="thumbnails">150 <div class="thumbnails">
142 <a href="${data['photo']['url']}"><img src="${data['photo']['picture']}" /></a>151 <a href="${data['photo']['url']}"><img src="${data['photo']['picture']}" /></a>
143 </div>152 </div>
153=======
154 <a href="${profile_url(data, data['sender'])}">
155 <div class="imgbox" title="${data["sender"].get("nick", "")}" style="background-image: url(${data["sender"]["image"]});"></div>
156 </a>
157>>>>>>> MERGE-SOURCE
144</%def>158</%def>
145159
146<%def name="images(data)">160<%def name="images(data)">
@@ -206,7 +220,16 @@
206 % endif220 % endif
207</%def>221</%def>
208222
223<%def name="username(user)" filter="trim">
224 % if preferences["show_fullname"]:
225 ${user.get("name", 0) or user.get("nick", "")}
226 % else:
227 ${user.get("nick", 0) or user.get("name", "")}
228 % endif
229</%def>
230
209<%def name="sender(data)" filter="trim">231<%def name="sender(data)" filter="trim">
232<<<<<<< TREE
210 % if "sender" in data:233 % if "sender" in data:
211 % if preferences["show_fullname"]:234 % if preferences["show_fullname"]:
212 % if data.get("private", 0):235 % if data.get("private", 0):
@@ -238,6 +261,13 @@
238 % endif261 % endif
239 % endif262 % endif
240 % endif263 % endif
264=======
265 ${username(data['sender'])}
266</%def>
267
268<%def name="retweeter(data)" filter="trim">
269 ${username(data['retweeter'])}
270>>>>>>> MERGE-SOURCE
241</%def>271</%def>
242272
243<%def name="title(data)">273<%def name="title(data)">
@@ -276,6 +306,15 @@
276 <span class="text" id="text-${data['id']}">${data.get('content', '')}</span>306 <span class="text" id="text-${data['id']}">${data.get('content', '')}</span>
277 % endif307 % endif
278 </p>308 </p>
309 % if data.get("retweet") == True:
310 <p class="rt_attribution">
311 <span class="text">Retweeted by:
312 <a href="${profile_url(data, data['retweeter'])}">
313 ${retweeter(data)}
314 </a>
315 </span>
316 </p>
317 % endif
279</%def>318</%def>
280319
281<%def name="sidebar(data)">320<%def name="sidebar(data)">