Merge lp:~seriy-pr/gwibber/vkontakte-ru-plugin into lp:gwibber
- vkontakte-ru-plugin
- Merge into trunk
Status: | Superseded | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Proposed branch: | lp:~seriy-pr/gwibber/vkontakte-ru-plugin | ||||||||||||||||||||
Merge into: | lp:gwibber | ||||||||||||||||||||
Diff against target: |
1108 lines (+1041/-0) 8 files modified
.bzrignore (+3/-0) gwibber/microblog/plugins/vkontakte/__init__.py (+552/-0) gwibber/microblog/plugins/vkontakte/gtk/vkontakte/__init__.py (+162/-0) gwibber/microblog/plugins/vkontakte/ui/gwibber-accounts-vkontakte.ui (+182/-0) gwibber/microblog/plugins/vkontakte/vk_api_wrapper.py (+81/-0) gwibber/microblog/util/const.py (+2/-0) setup.py (+4/-0) ui/icons/breakdance/scalable/vkontakte.svg (+55/-0) |
||||||||||||||||||||
To merge this branch: | bzr merge lp:~seriy-pr/gwibber/vkontakte-ru-plugin | ||||||||||||||||||||
Related bugs: |
|
||||||||||||||||||||
Related blueprints: |
A button to "retweet" a microblog post.
(Undefined)
Vkontakte.ru support
(Undefined)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
gwibber-committers | Pending | ||
Review via email: mp+47651@code.launchpad.net |
This proposal has been superseded by a proposal from 2011-05-15.
Commit message
Description of the change
Added plugin for vkontakte.ru - most popular social network in Russia, Ukraine and many ex-USSR countries.
This branch contains a fix of whishlist bug #572753
Also there is a related blueprint https:/
Some peoples report that plugin works for them: https:/
- 961. By Sergey Prokhorov
-
Fix duplicate browser windows when try re-authorize
- 962. By Sergey Prokhorov
-
Merged with latest trunk
- 963. By Sergey Prokhorov
-
Merged with trunk
- 964. By Sergey Prokhorov
-
Code moved to new authorization method (OAuth2.0)
- 965. By Sergey Prokhorov
-
Fixed error with authorization (appears after migration to new API)
- 966. By Sergey Prokhorov
-
Merged with latest trunk
- 967. By Sergey Prokhorov
-
Add video/photo/links streams support + extra debug messages
- 968. By Sergey Prokhorov
-
Fix bug: "post comments doesn't loaded" + add declarative JSON structure description.
- 969. By Sergey Prokhorov
-
Simplify lazy loading mechanizm
- 970. By Sergey Prokhorov
-
Linkify comments and links description
- 971. By Sergey Prokhorov
-
Add mentions support
- 972. By Sergey Prokhorov
-
Fix notifications
- 973. By Sergey Prokhorov
-
Fix right-aligned text on "\ufeff" char appears on message
- 974. By Sergey Prokhorov
-
Add support for vkontakte hashtags.
- 975. By Sergey Prokhorov
-
Merged with trunk
- 976. By Sergey Prokhorov
-
Merged with head
- 977. By Sergey Prokhorov
-
merged with trunk
- 978. By Sergey Prokhorov
-
Make code less ugly + replace <br> with <br/>
- 979. By Sergey Prokhorov
-
merged with trunk
- 980. By Sergey Prokhorov
-
move icons to plugin directory
- 981. By Sergey Prokhorov
-
setup.py don't used in gwibber now
- 982. By Sergey Prokhorov
-
merged with trunk
- 983. By Sergey Prokhorov
-
Fix logging issue
- 984. By Sergey Prokhorov
-
Fix indentation
- 985. By Sergey Prokhorov
-
Fix logger issue
- 986. By Sergey Prokhorov
-
More logging fixes
- 987. By Sergey Prokhorov
-
fix code conventions
- 988. By Sergey Prokhorov
-
Fix issue of renaming the vk api wrapper
- 989. By Sergey Prokhorov
-
Fix indentation on gui module
- 990. By Sergey Prokhorov
-
Compatible with 3.3.92 on ubuntu 12.04
Unmerged revisions
- 990. By Sergey Prokhorov
-
Compatible with 3.3.92 on ubuntu 12.04
- 989. By Sergey Prokhorov
-
Fix indentation on gui module
- 988. By Sergey Prokhorov
-
Fix issue of renaming the vk api wrapper
- 987. By Sergey Prokhorov
-
fix code conventions
- 986. By Sergey Prokhorov
-
More logging fixes
- 985. By Sergey Prokhorov
-
Fix logger issue
- 984. By Sergey Prokhorov
-
Fix indentation
- 983. By Sergey Prokhorov
-
Fix logging issue
- 982. By Sergey Prokhorov
-
merged with trunk
- 981. By Sergey Prokhorov
-
setup.py don't used in gwibber now
Preview Diff
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2008-12-27 22:52:45 +0000 |
3 | +++ .bzrignore 2011-05-15 21:54:31 +0000 |
4 | @@ -1,3 +1,6 @@ |
5 | gwibber/semantic.cache |
6 | semantic.cache |
7 | *.mo |
8 | +./.project |
9 | +./.pydevproject |
10 | +./.settings |
11 | |
12 | === added directory 'gwibber/microblog/plugins/vkontakte' |
13 | === added file 'gwibber/microblog/plugins/vkontakte/__init__.py' |
14 | --- gwibber/microblog/plugins/vkontakte/__init__.py 1970-01-01 00:00:00 +0000 |
15 | +++ gwibber/microblog/plugins/vkontakte/__init__.py 2011-05-15 21:54:31 +0000 |
16 | @@ -0,0 +1,552 @@ |
17 | +# -*- coding: utf-8 -*- |
18 | +''' |
19 | +Created on 05.12.2010 |
20 | + |
21 | +@author: Sergey Prokhorov <root@seriyps.ru> |
22 | +''' |
23 | +import json |
24 | +import inspect |
25 | +from gwibber.microblog.util.const import * |
26 | +from gwibber.microblog.util import log |
27 | +from gwibber.microblog import util |
28 | +import time |
29 | +import urllib |
30 | +from vk_api_wrapper import vk_api, vkException |
31 | +# Try to import * from custom, install custom.py to include packaging |
32 | +# customizations like distro API keys, etc |
33 | +try: |
34 | + from gwibber.microblog.util.custom import * |
35 | +except: |
36 | + pass |
37 | + |
38 | +log.logger.name="Vkontakte" |
39 | + |
40 | +VK_APP_ID="2036925"#XXX: remove me! see gwibber.microblog.util.const.VK_APP_ID |
41 | + |
42 | +PROTOCOL_INFO={ |
43 | + "name": "Vkontakte", |
44 | + "version": "0.2", |
45 | + |
46 | + "config": [ |
47 | + "color", |
48 | + "receive_enabled", |
49 | + "receive_groups", |
50 | + "send_enabled", |
51 | + "username", |
52 | + "uid", |
53 | + "private:access_token" |
54 | + ], |
55 | + |
56 | + "authtype": "vkontakte", |
57 | + "color": "#45688E", # #6D8FB3 |
58 | + |
59 | + "features": [ |
60 | + "send", |
61 | + "reply", |
62 | + "receive", |
63 | + #"thread", |
64 | + "delete", |
65 | + #"send_thread", |
66 | + "like", |
67 | + "sincetime" |
68 | + ], |
69 | + |
70 | + "default_streams": [ |
71 | + "receive", |
72 | + "images", |
73 | + "links", |
74 | + "videos", |
75 | + ] |
76 | +} |
77 | + |
78 | +URL_PREFIX='http://vkontakte.ru/' |
79 | + |
80 | +class attachment_processor: |
81 | + """Generate attachment fields |
82 | + http://vkontakte.ru/developers.php?o=-1&p=%D0%9E%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5+%D0%BF%D0%BE%D0%BB%D1%8F+attachment |
83 | + """ |
84 | + |
85 | + def __init__(self, client): |
86 | + self.client=client |
87 | + |
88 | + def _process_photo(self, attachment): |
89 | + return ("photo", |
90 | + photoAttachment( |
91 | + url="%sphoto%d_%d"%(URL_PREFIX, attachment["owner_id"], attachment["pid"]), |
92 | + picture=attachment["src"])) |
93 | + |
94 | + _process_posted_photo=_process_photo |
95 | + |
96 | + def _process_graffiti(self, attachment): |
97 | + return ("photo", |
98 | + photoAttachment( |
99 | + url="%sgraffiti%d?from_id=%d"%(URL_PREFIX, attachment["gid"], attachment["owner_id"]), |
100 | + picture=attachment["src"])) |
101 | + |
102 | + def _process_video(self, attachment): |
103 | + lnk="%svideo%d_%d"%(URL_PREFIX, attachment["owner_id"], attachment["vid"]) |
104 | + video_data=self.client.api.video.get(videos="%d_%d"%(attachment["owner_id"], attachment["vid"]))["response"] |
105 | + return ("video", |
106 | + videoAttachment( |
107 | + url=lnk, |
108 | + name=attachment["title"], |
109 | + source=lnk, |
110 | + picture=video_data[1]["image"])) |
111 | + |
112 | + def _process_link(self, attachment): |
113 | + l=linkAttachment( |
114 | + name=attachment["title"], |
115 | + url=attachment["url"], |
116 | + description=attachment["description"]) |
117 | + if attachment.has_key("image_src"): |
118 | + l.picture=attachment["image_src"] |
119 | + return "link", l |
120 | + |
121 | + def process(self, attachment, message): |
122 | + """find is processor has matched handler, and if yes, |
123 | + generate attachment with this handler and merge it with |
124 | + original message""" |
125 | + type_=attachment["type"] |
126 | + processor_name="_process_%s"%type_ |
127 | + if hasattr(self, processor_name): |
128 | + key, val=getattr(self, processor_name)(attachment[type_]) |
129 | + setattr(message, key, val) |
130 | + message.type=key |
131 | + return message |
132 | + |
133 | + |
134 | +########### |
135 | +# MAGICK!!! |
136 | +########### |
137 | +'''vkontakte API don't return comments for wall posts, |
138 | +but return comments count, so, we need download comments |
139 | +for each posts with comments count > 0 by separate API query. |
140 | +But, surprise! API don't return commenters profile data, only |
141 | +commenter ID. That means, that we need download commenter profile |
142 | +for each commenter. Only one good thing is that we can |
143 | +download many profiles by single query. So, when process |
144 | +wall posts, we insert "lasy_mask" fake objects in places, where |
145 | +commenter profile must be. And ALL profiles loads only when |
146 | +some slave object need profile data. |
147 | +''' |
148 | +class lazy_master: |
149 | + '''Dispatcher, that store slaves and start |
150 | + worker when one of slaves need data''' |
151 | + |
152 | + def __init__(self, worker): |
153 | + ''' |
154 | + @param worker: callback function, that do main work (convert |
155 | + masks to results)''' |
156 | + self.masks={} |
157 | + self.results={} |
158 | + self.worker=worker |
159 | + |
160 | + def new_mask(self, id): |
161 | + mask=lazy_mask(self, id) |
162 | + self.masks[mask.id]=mask |
163 | + return mask |
164 | + |
165 | + def run(self): |
166 | + #TODO: implement multiple run |
167 | + self.results=self.worker(self.masks) |
168 | + |
169 | + def get(self, id): |
170 | + if not self.results:#.get(id, False): |
171 | + self.run() |
172 | + return self.results.get(id, None) |
173 | + |
174 | + |
175 | +class lazy_mask(object): |
176 | + '''Fake object, which is substituted in |
177 | + place of masked object''' |
178 | + |
179 | + def __init__(self, master, id): |
180 | + self.master=master |
181 | + self.id=id |
182 | + self._result=None |
183 | + |
184 | + def res(self): |
185 | + '''Run lazy job''' |
186 | + if not self._result: |
187 | + self._result=self.master.get(self.id) |
188 | + return self._result |
189 | + |
190 | +########### |
191 | +# END MAGICK!!! |
192 | +########### |
193 | + |
194 | +########### |
195 | +# declarative description of Gwibber message JSON structure |
196 | +# XXX: this can/must be reused in other Gwibber plugins!!! |
197 | +########### |
198 | + |
199 | +# base class |
200 | +class jsonable(object): |
201 | + """ |
202 | + Allow subclass instances be represented as combination |
203 | + of python std types (list, str, dict, int, float) that |
204 | + can be used by json.dumps() |
205 | + eg |
206 | + ======================== |
207 | + class C2(jsonable): |
208 | + prop0="" |
209 | + |
210 | + class C1(jsonable): |
211 | + prop1="" # str |
212 | + prop2=0 # int |
213 | + prop3=0.0 # float |
214 | + prop4={} # as is |
215 | + prop5=("choice1", "choice2", "choice3") # one of |
216 | + prop6=[C2, ] # list of |
217 | + |
218 | + c1=C1(prop1="p1_val") # pass to constructor |
219 | + c1.prop2=100500 # pass as property assignment |
220 | + ... |
221 | + prop5=[C2(prop0="c2_p0_val1"), C2(prop0="c2_p0_val2")] |
222 | + |
223 | + >>> c1.to_jsonable() # return dict object |
224 | + { |
225 | + "prop1": "p1_val", |
226 | + "prop2": 100500, |
227 | + ... |
228 | + "prop6":[ |
229 | + { |
230 | + "prop0": "c2_p0_val1" |
231 | + }, |
232 | + { |
233 | + "prop0": "c2_p0_val2" |
234 | + } |
235 | + ] |
236 | + } |
237 | + =========================== |
238 | + Idea came from http://code.google.com/p/gdata-python-client/source/browse/src/atom/core.py |
239 | + "XmlElement" class |
240 | + """ |
241 | + _members=None |
242 | + |
243 | + def __init__(self, **kwargs): |
244 | + if self.__class__._members is None: |
245 | + self.__class__._members=tuple(self.__class__._list_members()) |
246 | + for member_name, member_type in self.__class__._members: |
247 | + if member_name in kwargs: |
248 | + setattr(self, member_name, kwargs[member_name]) |
249 | + else: |
250 | + if isinstance(member_type, list): |
251 | + setattr(self, member_name, []) |
252 | + else: |
253 | + setattr(self, member_name, None) |
254 | + |
255 | + @classmethod |
256 | + def _list_members(cls): |
257 | + """Introspect class properties""" |
258 | + members=[] |
259 | + for pair in inspect.getmembers(cls): |
260 | + if not pair[0].startswith('_'): |
261 | + member_type=pair[1] |
262 | + if (isinstance(member_type, (tuple, list, str, unicode, dict, int, float, bool)) |
263 | + or (inspect.isclass(member_type) |
264 | + and issubclass(member_type, jsonable))): |
265 | + members.append(pair) |
266 | + return members |
267 | + |
268 | + def to_jsonable(self): |
269 | + """Recursively transform objects hierarchy to |
270 | + hierarchy of python std types""" |
271 | + res={} |
272 | + for m_name, m_type in self._members: |
273 | + child=getattr(self, m_name) |
274 | + if child is None : |
275 | + continue |
276 | + if isinstance(m_type, (str, unicode)): |
277 | + res[m_name]=unicode(child) |
278 | + elif isinstance(m_type, int): |
279 | + res[m_name]=int(child) |
280 | + elif isinstance(m_type, float): |
281 | + res[m_name]=float(child) |
282 | + elif isinstance(m_type, bool): |
283 | + res[m_name]=bool(child) |
284 | + elif isinstance(m_type, dict): # copy as is |
285 | + res[m_name]=child |
286 | + elif isinstance(m_type, list): |
287 | + res[m_name]=[] |
288 | + for subchild in child: |
289 | + res[m_name].append(subchild.to_jsonable()) |
290 | + elif isinstance(m_type, tuple): |
291 | + if child in m_type: |
292 | + res[m_name]=child |
293 | + elif inspect.isclass(m_type) and issubclass(m_type, jsonable): |
294 | + res[m_name]=child.to_jsonable() # recursively |
295 | + return res |
296 | + |
297 | + def __str__(self): |
298 | + return str(self.to_jsonable()) |
299 | + |
300 | +# Gwibber message structure hierarchy: |
301 | + |
302 | +class errMessage(jsonable): |
303 | + type="" |
304 | + account={} |
305 | + message="" |
306 | + |
307 | +class error(jsonable): |
308 | + error=errMessage |
309 | + |
310 | +class user(jsonable): |
311 | + name="" |
312 | + id="" |
313 | + is_me=False |
314 | + image="" |
315 | + url="" |
316 | + |
317 | +class lazyUserLoader(jsonable): |
318 | + """Loads user profile only when to_json() called""" |
319 | + def __init__(self, callback, **kwargs): |
320 | + super(lazyUserLoader, self).__init__(**kwargs) |
321 | + self._cb=callback |
322 | + |
323 | + def to_jsonable(self): |
324 | + res=self._cb.res() |
325 | + return res.to_jsonable() if isinstance(res, jsonable) else {} |
326 | + |
327 | +class like(jsonable): |
328 | + count=0 |
329 | + |
330 | +class photoAttachment(jsonable): |
331 | + url="" |
332 | + picture="" |
333 | + |
334 | +class videoAttachment(jsonable): |
335 | + url="" |
336 | + name="" |
337 | + source="" |
338 | + picture="" |
339 | + |
340 | +class linkAttachment(jsonable): |
341 | + name="" |
342 | + url="" |
343 | + description="" |
344 | + picture="" |
345 | + |
346 | +class comment(jsonable): |
347 | + text="" |
348 | + time="" |
349 | + sender=lazyUserLoader |
350 | + |
351 | +class message(jsonable): |
352 | + mid="" |
353 | + service="" |
354 | + account="" |
355 | + time=0 |
356 | + sender=user |
357 | + url="" |
358 | + text="" |
359 | + html="" |
360 | + content="" |
361 | + likes=like |
362 | + photo=photoAttachment |
363 | + video=videoAttachment |
364 | + link=linkAttachment |
365 | + type=("photo", "video", "link") |
366 | + comments=[comment, ] |
367 | + |
368 | +########### |
369 | +# END declarative description of Gwibber message JSON structure |
370 | +########### |
371 | + |
372 | +class Client: |
373 | + |
374 | + def __init__(self, acct): |
375 | + self.account=acct |
376 | + self.access_token=acct.get("access_token") |
377 | + self.api=vk_api(access_token=self.access_token) |
378 | + self.attachment_processor=attachment_processor(self) |
379 | + |
380 | + def __call__(self, opname, **args): |
381 | + try: |
382 | + ret=[item for item in getattr(self, opname)(**args) if isinstance(item, jsonable)] # read all yield's |
383 | + #for i in ret: |
384 | + # print str(i) |
385 | + return [item.to_jsonable() for item in ret] |
386 | + except vkException, e: |
387 | + return [self._format_error(e).to_jsonable()] |
388 | + |
389 | + def _format_error(self, e, type=None, message=None): |
390 | + log.logger.error('Vkontakte error #%d: "%s" in url %s', e.code, e.msg, e.url) |
391 | + if e.code in (3, 5, 7): |
392 | + _type="auth" |
393 | + #elif e.code in (2, 4, 100): |
394 | + # _type="internal" |
395 | + else: |
396 | + _type="unknown" |
397 | + return error(error=errMessage(type=type or _type, |
398 | + account=self.account, |
399 | + message=message or e.msg)) |
400 | + |
401 | + def _user(self, acc_data): |
402 | + """Extract single user data from API response""" |
403 | + if acc_data.has_key("gid"): |
404 | + return user(name=acc_data["name"], |
405 | + id=str(acc_data["gid"]*-1), |
406 | + is_me=False, |
407 | + image=acc_data["photo"], |
408 | + url="%sclub%s"%(URL_PREFIX, acc_data["gid"])) |
409 | + else: |
410 | + return user(name="%s %s"%(acc_data["first_name"], acc_data["last_name"]), |
411 | + id=str(acc_data["uid"]), |
412 | + is_me=acc_data["uid"]==int(self.account["uid"]), |
413 | + image=acc_data["photo"], |
414 | + url="%sid%s"%(URL_PREFIX, acc_data["uid"])) |
415 | + |
416 | + def _message(self, item, account, comment_author_factory): |
417 | + """Extract single message data from API response""" |
418 | + m=message() |
419 | + m.mid=str(item["post_id"]) |
420 | + m.service="vkontakte" |
421 | + m.account=self.account["id"] |
422 | + m.time=item["date"]+time.timezone # XXX: WTF?!? why we don't move timezone correction to presentation logic??? We must store to DB absolute UTC timestamp!!! |
423 | + m.sender=self._user(account) |
424 | + m.url="%swall%s_%s"%(URL_PREFIX, m.sender.id, item["post_id"]) |
425 | + m.text=item["text"] |
426 | + m.html=util.linkify(item["text"], escape=False) |
427 | + m.content=m.html |
428 | + |
429 | + if item.get("likes", 0): |
430 | + m.likes=like(count=item["likes"]["count"]) |
431 | + |
432 | + if item.has_key("attachment"): |
433 | + if item["attachment"]["type"]=="audio": |
434 | + return#FIXME: Gwibber don't support audio |
435 | + m=self.attachment_processor.process(item["attachment"], m) |
436 | + |
437 | + if item["comments"]["count"]>0: |
438 | + try: |
439 | + comments=self.api.wall.getComments(owner_id=m.sender.id, |
440 | + post_id=item["post_id"], |
441 | + sort="asc", |
442 | + count=50) |
443 | + except vkException, e: |
444 | + return self._format_error(e) # XXX: may be return "m" ? |
445 | + if comments.has_key("response"): |
446 | + for comm in comments["response"]: |
447 | + if not isinstance(comm, dict):#drop first(?) element, because it is integer count of comments |
448 | + continue |
449 | + mask=comment_author_factory.new_mask(comm["uid"]) |
450 | + m.comments.append( |
451 | + comment( |
452 | + text=comm["text"], |
453 | + time=comm["date"], |
454 | + sender=lazyUserLoader(mask) |
455 | + ) |
456 | + ) |
457 | + return m |
458 | + |
459 | + def _get_lazy_profiles_factory(self): |
460 | + def lazy_comment_profiles_loader(slaves): |
461 | + '''magick (see comments upper) lazy worker (transform map of |
462 | + profile id's to map of profiles)''' |
463 | + ids=[]#slaves.keys() |
464 | + for slave in slaves.itervalues(): |
465 | + ids.append(str(slave.id)) |
466 | + uids=",".join(ids) |
467 | + try: |
468 | + profiles=self.api.getProfiles(uids=uids, fields="uid,photo,first_name,last_name") |
469 | + except vkException, e: |
470 | + self._format_error(e)#log message to console |
471 | + return {} |
472 | + res={} |
473 | + for profile in profiles['response']: |
474 | + res[profile["uid"]]=self._user(profile) |
475 | + return res |
476 | + |
477 | + return lazy_master(worker=lazy_comment_profiles_loader) |
478 | + |
479 | + def receive(self, since=None): |
480 | + '''Retrieve messages from friend's and own walls.''' |
481 | + if not since: |
482 | + since=int(time.time()-60*60*24)#24 hours |
483 | + response=self.api.newsfeed.get(filters="post", start_time=since)["response"] |
484 | + |
485 | + comment_author_factory=self._get_lazy_profiles_factory() |
486 | + |
487 | + profiles_by_id={}#index user and group profiles by user id |
488 | + for _acc in response["profiles"]: |
489 | + profiles_by_id[_acc["uid"]]=_acc |
490 | + if self.account["receive_groups"]: |
491 | + for _acc in response["groups"]: |
492 | + profiles_by_id[_acc["gid"]*-1]=_acc |
493 | + for item in response["items"]: |
494 | + if item["source_id"]<0 and not self.account["receive_groups"]:#source_id > 0 for peoples and < 0 for groups |
495 | + continue#skip groups if "receive_groups" option disabled |
496 | + account=profiles_by_id[item["source_id"]] |
497 | + message=self._message(item, account, comment_author_factory) |
498 | + yield message |
499 | + for msg in self._own_wall_posts(5, comment_author_factory): |
500 | + yield msg |
501 | + |
502 | + def _own_wall_posts(self, count, comment_author_factory): |
503 | + '''Download all posts user post in own wall''' |
504 | + try: |
505 | + response=self.api.wall.get(filter="owner", count=count)["response"] |
506 | + except vkException, e: |
507 | + yield self._format_error(e) |
508 | + return |
509 | + me=None |
510 | + for post in response: |
511 | + if not isinstance(post, dict):#skip first element - number of items |
512 | + continue |
513 | + post["post_id"]=post["id"] |
514 | + post["source_id"]=post["from_id"] |
515 | + if not me: |
516 | + me=self.api.getProfiles(uids=post["source_id"], fields="uid,photo,first_name,last_name")["response"][0] |
517 | + yield self._message(post, me, comment_author_factory) |
518 | + |
519 | + def user_messages(self, id=None, count=util.COUNT, since=None): |
520 | + """Messages posted by some user""" |
521 | + raise NotImplementedError |
522 | + if count>100:#vk limit |
523 | + count=100 |
524 | + comment_author_factory=self._get_lazy_profiles_factory() |
525 | + return self._own_wall_posts(count, comment_author_factory) |
526 | + |
527 | + def send(self, message): |
528 | + """Send new post to own wall""" |
529 | + self.api.wall.post(message=message) |
530 | + return self._own_wall_posts(1, self._get_lazy_profiles_factory()) |
531 | + |
532 | + def send_thread(self, message, target): |
533 | + """Send comment with text @message to post @target |
534 | + @param message: message text |
535 | + @param target: target post""" |
536 | + self.api.wall.addComment(owner_id=target["sender"]["id"], |
537 | + post_id=target["mid"], |
538 | + text=message) |
539 | + # Update post data |
540 | + # XXX: this doesn't update Gwibber UI and this is a Gwibber bug... |
541 | + return self._post_by_id(target["sender"]["id"], target["mid"]) |
542 | + |
543 | + def _post_by_id(self, owner_id, post_id): |
544 | + response=self.api.wall.getById(posts="%s_%s"%(owner_id, post_id))["response"] |
545 | + comment_author_factory=self._get_lazy_profiles_factory() |
546 | + me=None |
547 | + for post in response: |
548 | + if not isinstance(post, dict):#skip first element - number of items |
549 | + continue |
550 | + post["post_id"]=post["id"] |
551 | + post["source_id"]=post["from_id"] |
552 | + if not me: |
553 | + me=self.api.getProfiles(uids=post["source_id"], fields="uid,photo,first_name,last_name")["response"][0] |
554 | + yield self._message(post, me, comment_author_factory) |
555 | + |
556 | + def like(self, message): |
557 | + """Like message |
558 | + @param message: target post you like""" |
559 | + self.api.wall.addLike(owner_id=message["sender"]["id"], |
560 | + post_id=message["mid"]) |
561 | + return [] |
562 | + |
563 | + def delete(self, message): |
564 | + """Remove message from you wall |
565 | + @param message: post you want to delete""" |
566 | + self.api.wall.delete(owner_id=message["sender"]["id"], |
567 | + post_id=message["mid"]) |
568 | + return [] |
569 | |
570 | === added directory 'gwibber/microblog/plugins/vkontakte/gtk' |
571 | === added file 'gwibber/microblog/plugins/vkontakte/gtk/__init__.py' |
572 | === added directory 'gwibber/microblog/plugins/vkontakte/gtk/vkontakte' |
573 | === added file 'gwibber/microblog/plugins/vkontakte/gtk/vkontakte/__init__.py' |
574 | --- gwibber/microblog/plugins/vkontakte/gtk/vkontakte/__init__.py 1970-01-01 00:00:00 +0000 |
575 | +++ gwibber/microblog/plugins/vkontakte/gtk/vkontakte/__init__.py 2011-05-15 21:54:31 +0000 |
576 | @@ -0,0 +1,162 @@ |
577 | + |
578 | +import gtk |
579 | +from gwibber.microblog.util import resources |
580 | +from gwibber.microblog.util.const import * |
581 | +try: |
582 | + from gwibber.microblog.plugins.vkontakte.vk_api_wrapper import vk_api |
583 | +except ImportError: |
584 | + try: |
585 | + from vkontakte.vk_api_wrapper import vk_api |
586 | + except ImportError: |
587 | + vk_api=None |
588 | + |
589 | +import gnomekeyring |
590 | +import webkit |
591 | +import urllib |
592 | +import urlparse |
593 | + |
594 | +import gettext |
595 | +from gettext import gettext as _ |
596 | +if hasattr(gettext, 'bind_textdomain_codeset'): |
597 | + gettext.bind_textdomain_codeset('gwibber', 'UTF-8') |
598 | +gettext.textdomain('gwibber') |
599 | + |
600 | +VK_APP_ID="2036925"#XXX: remove me! see gwibber.microblog.util.const.VK_APP_ID |
601 | + |
602 | +class AccountWidget(gtk.VBox): |
603 | + """AccountWidget: A widget that provides a user interface for configuring |
604 | + Vkontakte accounts in Gwibber""" |
605 | + |
606 | + def __init__(self, account=None, dialog=None): |
607 | + """Creates the account pane for configuring Vkontakte accounts""" |
608 | + gtk.VBox.__init__(self, False, 20) |
609 | + self._state_init() |
610 | + if account: |
611 | + self.account=account |
612 | + else: |
613 | + self.account={} |
614 | + self.dialog=dialog |
615 | + has_access_token=True |
616 | + if self.account.has_key("id"):#check is current account already authorized |
617 | + try: |
618 | + _value=gnomekeyring.find_items_sync(gnomekeyring.ITEM_GENERIC_SECRET, {"id": str("%s/%s"%(self.account["id"], "access_token"))})[0].secret |
619 | + except gnomekeyring.NoMatchError: |
620 | + has_access_token=False |
621 | + try:#if account authorized, don't show "authorize" button |
622 | + if self.account["access_token"] and self.account["username"] and has_access_token and not self.dialog.condition: |
623 | + self._state_authorized() |
624 | + else:#else don't show "authorized" label |
625 | + self._state_not_authorized() |
626 | + except: |
627 | + self._state_not_authorized() |
628 | + |
629 | + |
630 | + def on_vk_auth_clicked(self, widget, data=None): |
631 | + """Start embed browser and show authorization dialog""" |
632 | + web=webkit.WebView() |
633 | + web.get_settings().set_property("enable-plugins", False) |
634 | + web.load_html_string(_("<p>Please wait...</p>"), "file:///") |
635 | + |
636 | + qstring=urllib.urlencode({#call to vkontakte API authorization |
637 | + "client_id": VK_APP_ID, |
638 | + "redirect_uri": "http://vkontakte.ru/api/login_success.html", #FIXME: http://api.vkontakte.ru/blank.html don't fire "title-changed" |
639 | + "response_type": "token", |
640 | + "display": "popup", |
641 | + "scope": ",".join(("video", "offline", "wall")) |
642 | + }) |
643 | + web.set_size_request(550, 440) |
644 | + web.load_uri("http://api.vkontakte.ru/oauth/authorize?"+qstring) |
645 | + #http://api.vkontakte.ru/oauth/authorize?client_id=2036925&response_type=token&scope=video,offline,wal&redirect_uri=http://api.vkontakte.ru/blank.html |
646 | + web.connect("title-changed", self.on_vk_auth_title_change) |
647 | + self._state_show_browser(web) |
648 | + |
649 | + def on_vk_auth_title_change(self, web=None, title=None, data=None): |
650 | + """When user confirm or revoke authorization in embed browser""" |
651 | + saved=False |
652 | + url=web.get_main_frame().get_uri() |
653 | + if "access_token=" in url: |
654 | + """When user successfully authorize our application, extract access_token secret... |
655 | + http://api.vkontakte.ru/blank.html#access_token=55a69...c55&expires_in=0&user_id=535...7""" |
656 | + query_string=url.split("#", 1)[1] |
657 | + data=urlparse.parse_qs(query_string) |
658 | + self.account["access_token"]=str(data["access_token"][0]) |
659 | + self.account["uid"]=data["user_id"][0] |
660 | + if vk_api:#try retrieve user name and last name from API |
661 | + profile=vk_api(access_token=self.account["access_token"]).getProfiles(uids=self.account["uid"], |
662 | + fields="first_name,last_name") |
663 | + self.account["username"]=u"%(first_name)s %(last_name)s"%(profile['response'][0]) |
664 | + else: |
665 | + self.account["username"]="id%s"%self.account["uid"] |
666 | + saved=self.dialog.on_edit_account_save()#store user account data |
667 | + self._state_authorized() |
668 | + self._state_login_success(saved) |
669 | + self._state_hide_browser(web) |
670 | + if "error=" in url: |
671 | + query_string=url.split("?", 1)[1] |
672 | + desc=urlparse.parse_qs(query_string)["error_description"][0] |
673 | + self._state_not_authorized() |
674 | + self._state_login_failed(_("Authentication error: %s")%desc) |
675 | + self._state_hide_browser(web) |
676 | + |
677 | + ### UI-manipulation methods ### |
678 | + |
679 | + def _state_init(self): |
680 | + """create UI from UI file""" |
681 | + self._browser_scroll=None |
682 | + self.ui=gtk.Builder() |
683 | + self.ui.set_translation_domain("gwibber") |
684 | + self.ui.add_from_file(resources.get_ui_asset("gwibber-accounts-vkontakte.ui")) |
685 | + self.ui.connect_signals(self)#attach events (on_vk_auth_clicked) |
686 | + self.vbox_settings=self.ui.get_object("vbox_settings") |
687 | + self.pack_start(self.vbox_settings, False, False) |
688 | + self.show_all() |
689 | + |
690 | + def _state_show_browser(self, web): |
691 | + """Show webkit widget, hide settings etc..""" |
692 | + (self.win_w, self.win_h)=self.window.get_size() |
693 | + if not self._browser_scroll: |
694 | + self._browser_scroll=gtk.ScrolledWindow() |
695 | + self.pack_start(self._browser_scroll, True, True, 0) |
696 | + self._browser_scroll.child=None |
697 | + self._browser_scroll.add(web) |
698 | + self.show_all() |
699 | + self.ui.get_object("vbox1").hide() |
700 | + self.ui.get_object("vbox_advanced").hide() |
701 | + |
702 | + def _state_hide_browser(self, web): |
703 | + """Hide webkit, show advanced settings, etc..""" |
704 | + web.hide() |
705 | + self.window.resize(self.win_w, self.win_h) |
706 | + self.ui.get_object("vbox1").show() |
707 | + self.ui.get_object("vbox_advanced").show() |
708 | + |
709 | + def _state_login_success(self, saved): |
710 | + """Show 'update' or 'create' buttons if not saved automatically""" |
711 | + if self.dialog.ui and self.account.has_key("id") and not saved: |
712 | + self.dialog.ui.get_object("vbox_save").show() |
713 | + elif self.dialog.ui and not saved: |
714 | + self.dialog.ui.get_object("vbox_create").show() |
715 | + |
716 | + def _state_login_failed(self, reason=None): |
717 | + """When user fail or revoke authorization, show popup message""" |
718 | + gtk.gdk.threads_enter() |
719 | + if not reason: |
720 | + reason=_("Vkontakte authorization failed. Please try again.") |
721 | + else: |
722 | + reason=_(reason) |
723 | + d=gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, reason) |
724 | + if d.run(): d.destroy() |
725 | + gtk.gdk.threads_leave() |
726 | + |
727 | + def _state_not_authorized(self): |
728 | + """Show 'Authorize with vkontakte' button""" |
729 | + self.ui.get_object("hbox_vk_auth_done").hide() |
730 | + self.ui.get_object("hbox_vk_auth").show() |
731 | + if self.dialog.ui: |
732 | + self.dialog.ui.get_object('vbox_create').hide() |
733 | + |
734 | + def _state_authorized(self): |
735 | + """Hide 'Authorize with vkontakte' button, show 'Authorized'""" |
736 | + self.ui.get_object("hbox_vk_auth").hide() |
737 | + self.ui.get_object("vk_auth_done_label").set_label(_("%s has been authorized by Vkontakte")%str(self.account["username"])) |
738 | + self.ui.get_object("hbox_vk_auth_done").show() |
739 | |
740 | === added directory 'gwibber/microblog/plugins/vkontakte/ui' |
741 | === added file 'gwibber/microblog/plugins/vkontakte/ui/gwibber-accounts-vkontakte.ui' |
742 | --- gwibber/microblog/plugins/vkontakte/ui/gwibber-accounts-vkontakte.ui 1970-01-01 00:00:00 +0000 |
743 | +++ gwibber/microblog/plugins/vkontakte/ui/gwibber-accounts-vkontakte.ui 2011-05-15 21:54:31 +0000 |
744 | @@ -0,0 +1,182 @@ |
745 | +<?xml version="1.0" encoding="UTF-8"?> |
746 | +<interface> |
747 | + <requires lib="gtk+" version="2.16"/> |
748 | + <!-- interface-naming-policy toplevel-contextual --> |
749 | + <object class="GtkVBox" id="vbox_settings"> |
750 | + <property name="visible">True</property> |
751 | + <property name="spacing">6</property> |
752 | + <child> |
753 | + <object class="GtkVBox" id="vbox1"> |
754 | + <property name="visible">True</property> |
755 | + <child> |
756 | + <object class="GtkHBox" id="hbox_vk_auth"> |
757 | + <property name="visible">True</property> |
758 | + <child> |
759 | + <object class="GtkButton" id="vk_auth_button"> |
760 | + <property name="label" translatable="yes">_Authorize</property> |
761 | + <property name="visible">True</property> |
762 | + <property name="can_focus">True</property> |
763 | + <property name="receives_default">True</property> |
764 | + <property name="use_underline">True</property> |
765 | + <signal name="clicked" handler="on_vk_auth_clicked"/> |
766 | + </object> |
767 | + <packing> |
768 | + <property name="fill">False</property> |
769 | + <property name="position">0</property> |
770 | + </packing> |
771 | + </child> |
772 | + <child> |
773 | + <object class="GtkLabel" id="vk_auth_label"> |
774 | + <property name="visible">True</property> |
775 | + <property name="label" translatable="yes">Authorize with vkontakte</property> |
776 | + </object> |
777 | + <packing> |
778 | + <property name="position">1</property> |
779 | + </packing> |
780 | + </child> |
781 | + </object> |
782 | + <packing> |
783 | + <property name="position">0</property> |
784 | + </packing> |
785 | + </child> |
786 | + <child> |
787 | + <object class="GtkHBox" id="hbox_vk_auth_done"> |
788 | + <property name="visible">True</property> |
789 | + <child> |
790 | + <object class="GtkLabel" id="vk_auth_done_label"> |
791 | + <property name="visible">True</property> |
792 | + <property name="label" translatable="yes">Vkontakte authorized</property> |
793 | + </object> |
794 | + <packing> |
795 | + <property name="position">0</property> |
796 | + </packing> |
797 | + </child> |
798 | + </object> |
799 | + <packing> |
800 | + <property name="position">1</property> |
801 | + </packing> |
802 | + </child> |
803 | + </object> |
804 | + <packing> |
805 | + <property name="position">0</property> |
806 | + </packing> |
807 | + </child> |
808 | + <child> |
809 | + <object class="GtkHSeparator" id="hseparator1"> |
810 | + <property name="visible">True</property> |
811 | + </object> |
812 | + <packing> |
813 | + <property name="expand">False</property> |
814 | + <property name="position">1</property> |
815 | + </packing> |
816 | + </child> |
817 | + <child> |
818 | + <object class="GtkVBox" id="vbox_advanced"> |
819 | + <property name="visible">True</property> |
820 | + <property name="spacing">6</property> |
821 | + <child> |
822 | + <object class="GtkLabel" id="label3"> |
823 | + <property name="visible">True</property> |
824 | + <property name="xalign">0</property> |
825 | + <property name="label" translatable="yes">Account Settings:</property> |
826 | + <attributes> |
827 | + <attribute name="weight" value="bold"/> |
828 | + </attributes> |
829 | + </object> |
830 | + <packing> |
831 | + <property name="position">0</property> |
832 | + </packing> |
833 | + </child> |
834 | + <child> |
835 | + <object class="GtkVBox" id="vbox2"> |
836 | + <property name="visible">True</property> |
837 | + <child> |
838 | + <object class="GtkCheckButton" id="receive_enabled"> |
839 | + <property name="label" translatable="yes">_Receive Messages</property> |
840 | + <property name="visible">True</property> |
841 | + <property name="can_focus">True</property> |
842 | + <property name="receives_default">False</property> |
843 | + <property name="tooltip_text" translatable="yes">Include this account when downloading messages</property> |
844 | + <property name="use_underline">True</property> |
845 | + <property name="active">True</property> |
846 | + <property name="draw_indicator">True</property> |
847 | + </object> |
848 | + <packing> |
849 | + <property name="position">0</property> |
850 | + </packing> |
851 | + </child> |
852 | + <child> |
853 | + <object class="GtkCheckButton" id="send_enabled"> |
854 | + <property name="label" translatable="yes">_Send Messages</property> |
855 | + <property name="visible">True</property> |
856 | + <property name="can_focus">True</property> |
857 | + <property name="receives_default">False</property> |
858 | + <property name="tooltip_text" translatable="yes">Allow sending posts to this account</property> |
859 | + <property name="use_underline">True</property> |
860 | + <property name="active">True</property> |
861 | + <property name="draw_indicator">True</property> |
862 | + </object> |
863 | + <packing> |
864 | + <property name="position">1</property> |
865 | + </packing> |
866 | + </child> |
867 | + <child> |
868 | + <object class="GtkCheckButton" id="receive_groups"> |
869 | + <property name="label" translatable="yes" comments="Receive messages from the groups, in addition to messages from users?">_Receive groups</property> |
870 | + <property name="visible">True</property> |
871 | + <property name="can_focus">True</property> |
872 | + <property name="receives_default">False</property> |
873 | + <property name="tooltip_text" translatable="yes">Receive news from groups</property> |
874 | + <property name="use_underline">True</property> |
875 | + <property name="active">True</property> |
876 | + <property name="draw_indicator">True</property> |
877 | + </object> |
878 | + <packing> |
879 | + <property name="position">2</property> |
880 | + </packing> |
881 | + </child> |
882 | + </object> |
883 | + <packing> |
884 | + <property name="position">1</property> |
885 | + </packing> |
886 | + </child> |
887 | + <child> |
888 | + <object class="GtkHBox" id="hbox1"> |
889 | + <property name="visible">True</property> |
890 | + <property name="homogeneous">True</property> |
891 | + <child> |
892 | + <object class="GtkLabel" id="label4"> |
893 | + <property name="visible">True</property> |
894 | + <property name="tooltip_text" translatable="yes">Color used to help distinguish accounts</property> |
895 | + <property name="xalign">0</property> |
896 | + <property name="label" translatable="yes">Account Color:</property> |
897 | + </object> |
898 | + <packing> |
899 | + <property name="position">0</property> |
900 | + </packing> |
901 | + </child> |
902 | + <child> |
903 | + <object class="GtkColorButton" id="color"> |
904 | + <property name="visible">True</property> |
905 | + <property name="can_focus">True</property> |
906 | + <property name="receives_default">True</property> |
907 | + <property name="tooltip_text" translatable="yes">Color used to help distinguish accounts</property> |
908 | + <property name="color">#000000000000</property> |
909 | + </object> |
910 | + <packing> |
911 | + <property name="expand">False</property> |
912 | + <property name="position">1</property> |
913 | + </packing> |
914 | + </child> |
915 | + </object> |
916 | + <packing> |
917 | + <property name="position">2</property> |
918 | + </packing> |
919 | + </child> |
920 | + </object> |
921 | + <packing> |
922 | + <property name="position">2</property> |
923 | + </packing> |
924 | + </child> |
925 | + </object> |
926 | +</interface> |
927 | |
928 | === added file 'gwibber/microblog/plugins/vkontakte/vk_api_wrapper.py' |
929 | --- gwibber/microblog/plugins/vkontakte/vk_api_wrapper.py 1970-01-01 00:00:00 +0000 |
930 | +++ gwibber/microblog/plugins/vkontakte/vk_api_wrapper.py 2011-05-15 21:54:31 +0000 |
931 | @@ -0,0 +1,81 @@ |
932 | +# -*- coding: utf-8 -*- |
933 | +''' |
934 | +Created on 07.12.2010 |
935 | + |
936 | +@author: Sergey Prokhorov <root@seriyps.ru> |
937 | +''' |
938 | +from gwibber.microblog import network |
939 | +from gwibber.microblog.util import log |
940 | +import urllib |
941 | +import time |
942 | + |
943 | +class vkException(Exception): |
944 | + |
945 | + def __init__(self, code, msg, url, response): |
946 | + self.code=code |
947 | + self.msg=msg |
948 | + self.response=response |
949 | + self.url=url |
950 | + Exception.__init__(self, code, msg, url, response) |
951 | + |
952 | + |
953 | +class vk_api: |
954 | + """Vkontakte.ru (vk.com) API wrapper for Gwibber |
955 | + == Usage == |
956 | + Initialization: |
957 | + api=vk_api(access_token) |
958 | + "access_token" can be retrieved from call to http://api.vkontakte.ru/oauth/authorize |
959 | + (see http://vkontakte.ru/developers.php?o=-1&p=%C0%E2%F2%EE%F0%E8%E7%E0%F6%E8%FF%20%EA%EB%E8%E5%ED%F2%F1%EA%E8%F5%20%EF%F0%E8%EB%EE%E6%E5%ED%E8%E9) |
960 | + Make calls to simple methods (has no dots in name, like "getProfiles", "isAppUser"): |
961 | + res=api.method(arg_name1=arg1, arg_name2=arg2) |
962 | + eg res=api.getProfiles(uids="535397,1", fields="uid,first_name,last_name") |
963 | + Make calls to namespaced methods (has dots in name, like "wall.post", "newsfeed.get"): |
964 | + res=api.namespace.method(arg_name1=arg1, arg_name2=arg2) |
965 | + eg res=api.wall.post(message="New wall post from API") |
966 | + Alternative call syntax (better performance): |
967 | + res=api._load('namespace.method', arg_name1=arg1, arg_name2=arg2) |
968 | + eg res=api._get('wall.post', message="New wall post from API") |
969 | + """ |
970 | + |
971 | + api_url='https://api.vkontakte.ru/method/%s' |
972 | + |
973 | + def __init__(self, access_token): |
974 | + self._access_token=access_token |
975 | + self._prefix=[] |
976 | + |
977 | + def _load(self, method, **params): |
978 | + """Make call by method name and arguments""" |
979 | + params["access_token"]=self._access_token |
980 | + url=self.api_url%method |
981 | + for _i in xrange(3):#this cycle used for API error #6 "Too many requests" |
982 | + log.logger.debug("Perform API request to method %s. URL: %s?%s", method, url, urllib.urlencode(params)) |
983 | + res=network.Download(url, params, False).get_json() |
984 | + if not res.has_key("error"): |
985 | + #log.logger.debug("%s"%res) |
986 | + return res |
987 | + |
988 | + if res["error"]["error_code"]!=6: |
989 | + break |
990 | + log.logging.warning('Error #6 "%s". Wait 0.5s and retry...', res["error"]["error_msg"]) |
991 | + '''if we get error #6 "Too many requests per second" (vk allow |
992 | + max 3rps http://vkontakte.ru/developers.php?o=-1&p=%D0%92%D0%B7%D0%B0%D0%B8%D0%BC%D0%BE%D0%B4%D0%B5%D0%B9%D1%81%D1%82%D0%B2%D0%B8%D0%B5+%D0%BF%D1%80%D0%B8%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F+%D1%81+API ) |
993 | + try to sleep with small delay and retry request not more then 3x times''' |
994 | + time.sleep(0.5)#TODO: play with sleep value |
995 | + raise vkException(res["error"]["error_code"], |
996 | + res["error"]["error_msg"], |
997 | + "%s?%s"%(url, urllib.urlencode(params)), |
998 | + res) |
999 | + |
1000 | + |
1001 | + """Support for api.namespace.method(**kwargs) calls below""" |
1002 | + def __getattr__(self, name): |
1003 | + self._prefix.append(name) |
1004 | + return self |
1005 | + |
1006 | + def __call__(self, **kwargs): |
1007 | + if self._prefix: |
1008 | + method=".".join(self._prefix) |
1009 | + self._prefix=[] |
1010 | + else: |
1011 | + method=kwargs.pop("method") |
1012 | + return self._load(method, **kwargs) |
1013 | |
1014 | === modified file 'gwibber/microblog/util/const.py' |
1015 | --- gwibber/microblog/util/const.py 2011-04-22 15:53:40 +0000 |
1016 | +++ gwibber/microblog/util/const.py 2011-05-15 21:54:31 +0000 |
1017 | @@ -15,6 +15,8 @@ |
1018 | TWITTER_OAUTH_KEY = "VDOuA5qCJ1XhjaSa4pl76g" |
1019 | TWITTER_OAUTH_SECRET = "BqHlB8sMz5FhZmmFimwgiIdB0RiBr72Y0bio49IVJM" |
1020 | |
1021 | +VK_APP_ID="2036925" |
1022 | + |
1023 | # Gwibber |
1024 | MAX_MESSAGE_LENGTH = 140 |
1025 | MAX_MESSAGE_COUNT = 20000 |
1026 | |
1027 | === modified file 'setup.py' |
1028 | --- setup.py 2011-04-22 16:12:39 +0000 |
1029 | +++ setup.py 2011-05-15 21:54:31 +0000 |
1030 | @@ -34,6 +34,10 @@ |
1031 | ('share/gwibber/plugins/facebook/gtk', glob("gwibber/microblog/plugins/facebook/gtk/*.*")), |
1032 | ('share/gwibber/plugins/facebook/gtk/facebook', glob("gwibber/microblog/plugins/facebook/gtk/facebook/*.*")), |
1033 | ('share/gwibber/plugins/facebook/ui', glob("gwibber/microblog/plugins/facebook/ui/*.*")), |
1034 | + ('share/gwibber/plugins/vkontakte', glob("gwibber/microblog/plugins/vkontakte/*.*")), |
1035 | + ('share/gwibber/plugins/vkontakte/gtk', glob("gwibber/microblog/plugins/vkontakte/gtk/*.*")), |
1036 | + ('share/gwibber/plugins/vkontakte/gtk/vkontakte', glob("gwibber/microblog/plugins/vkontakte/gtk/vkontakte/*.*")), |
1037 | + ('share/gwibber/plugins/vkontakte/ui', glob("gwibber/microblog/plugins/vkontakte/ui/*.*")), |
1038 | ('share/gwibber/plugins/flickr', glob("gwibber/microblog/plugins/flickr/*.*")), |
1039 | ('share/gwibber/plugins/flickr/gtk', glob("gwibber/microblog/plugins/flickr/gtk/*.*")), |
1040 | ('share/gwibber/plugins/flickr/gtk/flickr', glob("gwibber/microblog/plugins/flickr/gtk/flickr/*.*")), |
1041 | |
1042 | === added file 'ui/icons/breakdance/16x16/vkontakte.png' |
1043 | Binary files ui/icons/breakdance/16x16/vkontakte.png 1970-01-01 00:00:00 +0000 and ui/icons/breakdance/16x16/vkontakte.png 2011-05-15 21:54:31 +0000 differ |
1044 | === added file 'ui/icons/breakdance/22x22/vkontakte.png' |
1045 | Binary files ui/icons/breakdance/22x22/vkontakte.png 1970-01-01 00:00:00 +0000 and ui/icons/breakdance/22x22/vkontakte.png 2011-05-15 21:54:31 +0000 differ |
1046 | === added file 'ui/icons/breakdance/32x32/vkontakte.png' |
1047 | Binary files ui/icons/breakdance/32x32/vkontakte.png 1970-01-01 00:00:00 +0000 and ui/icons/breakdance/32x32/vkontakte.png 2011-05-15 21:54:31 +0000 differ |
1048 | === added file 'ui/icons/breakdance/scalable/vkontakte.png' |
1049 | Binary files ui/icons/breakdance/scalable/vkontakte.png 1970-01-01 00:00:00 +0000 and ui/icons/breakdance/scalable/vkontakte.png 2011-05-15 21:54:31 +0000 differ |
1050 | === added file 'ui/icons/breakdance/scalable/vkontakte.svg' |
1051 | --- ui/icons/breakdance/scalable/vkontakte.svg 1970-01-01 00:00:00 +0000 |
1052 | +++ ui/icons/breakdance/scalable/vkontakte.svg 2011-05-15 21:54:31 +0000 |
1053 | @@ -0,0 +1,55 @@ |
1054 | +<?xml version="1.0" encoding="UTF-8" standalone="no"?> |
1055 | +<!-- Created with Inkscape (http://www.inkscape.org/) --> |
1056 | +<!-- SVG Created by Sergey A Prochorov <root@seriyps.ru> (http://seriyps.ru/) --> |
1057 | +<svg |
1058 | + xmlns:dc="http://purl.org/dc/elements/1.1/" |
1059 | + xmlns:cc="http://creativecommons.org/ns#" |
1060 | + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" |
1061 | + xmlns:svg="http://www.w3.org/2000/svg" |
1062 | + xmlns="http://www.w3.org/2000/svg" |
1063 | + version="1.1" |
1064 | + width="285" |
1065 | + height="285" |
1066 | + id="svg3036"> |
1067 | + <defs |
1068 | + id="defs8"> |
1069 | + <filter |
1070 | + color-interpolation-filters="sRGB" |
1071 | + id="filter3773"> |
1072 | + <feGaussianBlur |
1073 | + id="feGaussianBlur3775" |
1074 | + stdDeviation="3.931875" /> |
1075 | + </filter> |
1076 | + </defs> |
1077 | + <metadata |
1078 | + id="metadata3042"> |
1079 | + <rdf:RDF> |
1080 | + <cc:Work |
1081 | + rdf:about=""> |
1082 | + <dc:format>image/svg+xml</dc:format> |
1083 | + <dc:type |
1084 | + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> |
1085 | + <dc:title></dc:title> |
1086 | + </cc:Work> |
1087 | + </rdf:RDF> |
1088 | + </metadata> |
1089 | + <g |
1090 | + transform="translate(-45.0635,-38.936486)" |
1091 | + id="g3796"> |
1092 | + <rect |
1093 | + width="168" |
1094 | + height="197" |
1095 | + x="108" |
1096 | + y="87" |
1097 | + id="rect4024" |
1098 | + style="fill:#4c77a0;fill-opacity:1;stroke:none" /> |
1099 | + <path |
1100 | + d="M 102,52.5 C 78.75,55 57.625,71.25 54.5,98 l 0,170.5 c 2.875,25.25 20,43.625 46.5,46 l 168.5,0 c 22.75,-0.625 43.625,-18.125 47.25,-46.5 l 0,-169.375 C 314.25,73.875 292.375,54.5 270,52.5 l -168,0 z m 34,55.75 64.25,0 c 66,0.5 51.15504,62.271 24,68.5 44.5,3.75 50.25,82.5 -23.75,83.25 L 136.25,260 136,108.25 z m 36,27 0,31.75 18,0 c 22.1875,0 23.0625,-31.75 0,-31.75 l -18,0 z m 0,58 0,37.5 24.25,0 c 26.75,0 28,-37.5 0,-37.5 l -24.25,0 z" |
1101 | + id="path3978" |
1102 | + style="opacity:0.62999998;fill:#000000;fill-opacity:1;stroke:none;filter:url(#filter3773)" /> |
1103 | + <path |
1104 | + d="M 99,50 C 75.75,52.5 54.625,68.75 51.5,95.5 l 0,170.5 c 2.875,25.25 20,43.625 46.5,46 l 168.5,0 c 22.75,-0.625 43.625,-18.125 47.25,-46.5 l 0,-169.375 C 311.25,71.375 289.375,52 267,50 L 99,50 z m 34,55.75 64.25,0 c 66,0.5 51.15504,62.271 24,68.5 44.5,3.75 50.25,82.5 -23.75,83.25 l -64.25,0 L 133,105.75 z m 36,27 0,31.75 18,0 c 22.1875,0 23.0625,-31.75 0,-31.75 l -18,0 z m 0,58 0,37.5 24.25,0 c 26.75,0 28,-37.5 0,-37.5 l -24.25,0 z" |
1105 | + id="path3046" |
1106 | + style="fill:#ffffff;fill-opacity:1;stroke:none" /> |
1107 | + </g> |
1108 | +</svg> |