Merge lp:~seriy-pr/gwibber/vkontakte-ru-plugin into lp:gwibber

Proposed by Sergey Prokhorov
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
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.

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://blueprints.edge.launchpad.net/gwibber/+spec/vkontakte.ru

Some peoples report that plugin works for them: https://bugs.edge.launchpad.net/gwibber/+bug/572753/comments/18 and http://forum.ubuntu.ru/index.php?topic=65621.msg1004248#msg1004248 + there is non-oficial DEB build for it: http://www.mediafire.com/?5ct1wzc2gkajxp4

To post a comment you must log in.
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '.bzrignore'
--- .bzrignore 2008-12-27 22:52:45 +0000
+++ .bzrignore 2011-05-15 21:54:31 +0000
@@ -1,3 +1,6 @@
1gwibber/semantic.cache1gwibber/semantic.cache
2semantic.cache2semantic.cache
3*.mo3*.mo
4./.project
5./.pydevproject
6./.settings
47
=== added directory 'gwibber/microblog/plugins/vkontakte'
=== added file 'gwibber/microblog/plugins/vkontakte/__init__.py'
--- gwibber/microblog/plugins/vkontakte/__init__.py 1970-01-01 00:00:00 +0000
+++ gwibber/microblog/plugins/vkontakte/__init__.py 2011-05-15 21:54:31 +0000
@@ -0,0 +1,552 @@
1# -*- coding: utf-8 -*-
2'''
3Created on 05.12.2010
4
5@author: Sergey Prokhorov <root@seriyps.ru>
6'''
7import json
8import inspect
9from gwibber.microblog.util.const import *
10from gwibber.microblog.util import log
11from gwibber.microblog import util
12import time
13import urllib
14from vk_api_wrapper import vk_api, vkException
15# Try to import * from custom, install custom.py to include packaging
16# customizations like distro API keys, etc
17try:
18 from gwibber.microblog.util.custom import *
19except:
20 pass
21
22log.logger.name="Vkontakte"
23
24VK_APP_ID="2036925"#XXX: remove me! see gwibber.microblog.util.const.VK_APP_ID
25
26PROTOCOL_INFO={
27 "name": "Vkontakte",
28 "version": "0.2",
29
30 "config": [
31 "color",
32 "receive_enabled",
33 "receive_groups",
34 "send_enabled",
35 "username",
36 "uid",
37 "private:access_token"
38 ],
39
40 "authtype": "vkontakte",
41 "color": "#45688E", # #6D8FB3
42
43 "features": [
44 "send",
45 "reply",
46 "receive",
47 #"thread",
48 "delete",
49 #"send_thread",
50 "like",
51 "sincetime"
52 ],
53
54 "default_streams": [
55 "receive",
56 "images",
57 "links",
58 "videos",
59 ]
60}
61
62URL_PREFIX='http://vkontakte.ru/'
63
64class attachment_processor:
65 """Generate attachment fields
66 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
67 """
68
69 def __init__(self, client):
70 self.client=client
71
72 def _process_photo(self, attachment):
73 return ("photo",
74 photoAttachment(
75 url="%sphoto%d_%d"%(URL_PREFIX, attachment["owner_id"], attachment["pid"]),
76 picture=attachment["src"]))
77
78 _process_posted_photo=_process_photo
79
80 def _process_graffiti(self, attachment):
81 return ("photo",
82 photoAttachment(
83 url="%sgraffiti%d?from_id=%d"%(URL_PREFIX, attachment["gid"], attachment["owner_id"]),
84 picture=attachment["src"]))
85
86 def _process_video(self, attachment):
87 lnk="%svideo%d_%d"%(URL_PREFIX, attachment["owner_id"], attachment["vid"])
88 video_data=self.client.api.video.get(videos="%d_%d"%(attachment["owner_id"], attachment["vid"]))["response"]
89 return ("video",
90 videoAttachment(
91 url=lnk,
92 name=attachment["title"],
93 source=lnk,
94 picture=video_data[1]["image"]))
95
96 def _process_link(self, attachment):
97 l=linkAttachment(
98 name=attachment["title"],
99 url=attachment["url"],
100 description=attachment["description"])
101 if attachment.has_key("image_src"):
102 l.picture=attachment["image_src"]
103 return "link", l
104
105 def process(self, attachment, message):
106 """find is processor has matched handler, and if yes,
107 generate attachment with this handler and merge it with
108 original message"""
109 type_=attachment["type"]
110 processor_name="_process_%s"%type_
111 if hasattr(self, processor_name):
112 key, val=getattr(self, processor_name)(attachment[type_])
113 setattr(message, key, val)
114 message.type=key
115 return message
116
117
118###########
119# MAGICK!!!
120###########
121'''vkontakte API don't return comments for wall posts,
122but return comments count, so, we need download comments
123for each posts with comments count > 0 by separate API query.
124But, surprise! API don't return commenters profile data, only
125commenter ID. That means, that we need download commenter profile
126for each commenter. Only one good thing is that we can
127download many profiles by single query. So, when process
128wall posts, we insert "lasy_mask" fake objects in places, where
129commenter profile must be. And ALL profiles loads only when
130some slave object need profile data.
131'''
132class lazy_master:
133 '''Dispatcher, that store slaves and start
134 worker when one of slaves need data'''
135
136 def __init__(self, worker):
137 '''
138 @param worker: callback function, that do main work (convert
139 masks to results)'''
140 self.masks={}
141 self.results={}
142 self.worker=worker
143
144 def new_mask(self, id):
145 mask=lazy_mask(self, id)
146 self.masks[mask.id]=mask
147 return mask
148
149 def run(self):
150 #TODO: implement multiple run
151 self.results=self.worker(self.masks)
152
153 def get(self, id):
154 if not self.results:#.get(id, False):
155 self.run()
156 return self.results.get(id, None)
157
158
159class lazy_mask(object):
160 '''Fake object, which is substituted in
161 place of masked object'''
162
163 def __init__(self, master, id):
164 self.master=master
165 self.id=id
166 self._result=None
167
168 def res(self):
169 '''Run lazy job'''
170 if not self._result:
171 self._result=self.master.get(self.id)
172 return self._result
173
174###########
175# END MAGICK!!!
176###########
177
178###########
179# declarative description of Gwibber message JSON structure
180# XXX: this can/must be reused in other Gwibber plugins!!!
181###########
182
183# base class
184class jsonable(object):
185 """
186 Allow subclass instances be represented as combination
187 of python std types (list, str, dict, int, float) that
188 can be used by json.dumps()
189 eg
190 ========================
191 class C2(jsonable):
192 prop0=""
193
194 class C1(jsonable):
195 prop1="" # str
196 prop2=0 # int
197 prop3=0.0 # float
198 prop4={} # as is
199 prop5=("choice1", "choice2", "choice3") # one of
200 prop6=[C2, ] # list of
201
202 c1=C1(prop1="p1_val") # pass to constructor
203 c1.prop2=100500 # pass as property assignment
204 ...
205 prop5=[C2(prop0="c2_p0_val1"), C2(prop0="c2_p0_val2")]
206
207 >>> c1.to_jsonable() # return dict object
208 {
209 "prop1": "p1_val",
210 "prop2": 100500,
211 ...
212 "prop6":[
213 {
214 "prop0": "c2_p0_val1"
215 },
216 {
217 "prop0": "c2_p0_val2"
218 }
219 ]
220 }
221 ===========================
222 Idea came from http://code.google.com/p/gdata-python-client/source/browse/src/atom/core.py
223 "XmlElement" class
224 """
225 _members=None
226
227 def __init__(self, **kwargs):
228 if self.__class__._members is None:
229 self.__class__._members=tuple(self.__class__._list_members())
230 for member_name, member_type in self.__class__._members:
231 if member_name in kwargs:
232 setattr(self, member_name, kwargs[member_name])
233 else:
234 if isinstance(member_type, list):
235 setattr(self, member_name, [])
236 else:
237 setattr(self, member_name, None)
238
239 @classmethod
240 def _list_members(cls):
241 """Introspect class properties"""
242 members=[]
243 for pair in inspect.getmembers(cls):
244 if not pair[0].startswith('_'):
245 member_type=pair[1]
246 if (isinstance(member_type, (tuple, list, str, unicode, dict, int, float, bool))
247 or (inspect.isclass(member_type)
248 and issubclass(member_type, jsonable))):
249 members.append(pair)
250 return members
251
252 def to_jsonable(self):
253 """Recursively transform objects hierarchy to
254 hierarchy of python std types"""
255 res={}
256 for m_name, m_type in self._members:
257 child=getattr(self, m_name)
258 if child is None :
259 continue
260 if isinstance(m_type, (str, unicode)):
261 res[m_name]=unicode(child)
262 elif isinstance(m_type, int):
263 res[m_name]=int(child)
264 elif isinstance(m_type, float):
265 res[m_name]=float(child)
266 elif isinstance(m_type, bool):
267 res[m_name]=bool(child)
268 elif isinstance(m_type, dict): # copy as is
269 res[m_name]=child
270 elif isinstance(m_type, list):
271 res[m_name]=[]
272 for subchild in child:
273 res[m_name].append(subchild.to_jsonable())
274 elif isinstance(m_type, tuple):
275 if child in m_type:
276 res[m_name]=child
277 elif inspect.isclass(m_type) and issubclass(m_type, jsonable):
278 res[m_name]=child.to_jsonable() # recursively
279 return res
280
281 def __str__(self):
282 return str(self.to_jsonable())
283
284# Gwibber message structure hierarchy:
285
286class errMessage(jsonable):
287 type=""
288 account={}
289 message=""
290
291class error(jsonable):
292 error=errMessage
293
294class user(jsonable):
295 name=""
296 id=""
297 is_me=False
298 image=""
299 url=""
300
301class lazyUserLoader(jsonable):
302 """Loads user profile only when to_json() called"""
303 def __init__(self, callback, **kwargs):
304 super(lazyUserLoader, self).__init__(**kwargs)
305 self._cb=callback
306
307 def to_jsonable(self):
308 res=self._cb.res()
309 return res.to_jsonable() if isinstance(res, jsonable) else {}
310
311class like(jsonable):
312 count=0
313
314class photoAttachment(jsonable):
315 url=""
316 picture=""
317
318class videoAttachment(jsonable):
319 url=""
320 name=""
321 source=""
322 picture=""
323
324class linkAttachment(jsonable):
325 name=""
326 url=""
327 description=""
328 picture=""
329
330class comment(jsonable):
331 text=""
332 time=""
333 sender=lazyUserLoader
334
335class message(jsonable):
336 mid=""
337 service=""
338 account=""
339 time=0
340 sender=user
341 url=""
342 text=""
343 html=""
344 content=""
345 likes=like
346 photo=photoAttachment
347 video=videoAttachment
348 link=linkAttachment
349 type=("photo", "video", "link")
350 comments=[comment, ]
351
352###########
353# END declarative description of Gwibber message JSON structure
354###########
355
356class Client:
357
358 def __init__(self, acct):
359 self.account=acct
360 self.access_token=acct.get("access_token")
361 self.api=vk_api(access_token=self.access_token)
362 self.attachment_processor=attachment_processor(self)
363
364 def __call__(self, opname, **args):
365 try:
366 ret=[item for item in getattr(self, opname)(**args) if isinstance(item, jsonable)] # read all yield's
367 #for i in ret:
368 # print str(i)
369 return [item.to_jsonable() for item in ret]
370 except vkException, e:
371 return [self._format_error(e).to_jsonable()]
372
373 def _format_error(self, e, type=None, message=None):
374 log.logger.error('Vkontakte error #%d: "%s" in url %s', e.code, e.msg, e.url)
375 if e.code in (3, 5, 7):
376 _type="auth"
377 #elif e.code in (2, 4, 100):
378 # _type="internal"
379 else:
380 _type="unknown"
381 return error(error=errMessage(type=type or _type,
382 account=self.account,
383 message=message or e.msg))
384
385 def _user(self, acc_data):
386 """Extract single user data from API response"""
387 if acc_data.has_key("gid"):
388 return user(name=acc_data["name"],
389 id=str(acc_data["gid"]*-1),
390 is_me=False,
391 image=acc_data["photo"],
392 url="%sclub%s"%(URL_PREFIX, acc_data["gid"]))
393 else:
394 return user(name="%s %s"%(acc_data["first_name"], acc_data["last_name"]),
395 id=str(acc_data["uid"]),
396 is_me=acc_data["uid"]==int(self.account["uid"]),
397 image=acc_data["photo"],
398 url="%sid%s"%(URL_PREFIX, acc_data["uid"]))
399
400 def _message(self, item, account, comment_author_factory):
401 """Extract single message data from API response"""
402 m=message()
403 m.mid=str(item["post_id"])
404 m.service="vkontakte"
405 m.account=self.account["id"]
406 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!!!
407 m.sender=self._user(account)
408 m.url="%swall%s_%s"%(URL_PREFIX, m.sender.id, item["post_id"])
409 m.text=item["text"]
410 m.html=util.linkify(item["text"], escape=False)
411 m.content=m.html
412
413 if item.get("likes", 0):
414 m.likes=like(count=item["likes"]["count"])
415
416 if item.has_key("attachment"):
417 if item["attachment"]["type"]=="audio":
418 return#FIXME: Gwibber don't support audio
419 m=self.attachment_processor.process(item["attachment"], m)
420
421 if item["comments"]["count"]>0:
422 try:
423 comments=self.api.wall.getComments(owner_id=m.sender.id,
424 post_id=item["post_id"],
425 sort="asc",
426 count=50)
427 except vkException, e:
428 return self._format_error(e) # XXX: may be return "m" ?
429 if comments.has_key("response"):
430 for comm in comments["response"]:
431 if not isinstance(comm, dict):#drop first(?) element, because it is integer count of comments
432 continue
433 mask=comment_author_factory.new_mask(comm["uid"])
434 m.comments.append(
435 comment(
436 text=comm["text"],
437 time=comm["date"],
438 sender=lazyUserLoader(mask)
439 )
440 )
441 return m
442
443 def _get_lazy_profiles_factory(self):
444 def lazy_comment_profiles_loader(slaves):
445 '''magick (see comments upper) lazy worker (transform map of
446 profile id's to map of profiles)'''
447 ids=[]#slaves.keys()
448 for slave in slaves.itervalues():
449 ids.append(str(slave.id))
450 uids=",".join(ids)
451 try:
452 profiles=self.api.getProfiles(uids=uids, fields="uid,photo,first_name,last_name")
453 except vkException, e:
454 self._format_error(e)#log message to console
455 return {}
456 res={}
457 for profile in profiles['response']:
458 res[profile["uid"]]=self._user(profile)
459 return res
460
461 return lazy_master(worker=lazy_comment_profiles_loader)
462
463 def receive(self, since=None):
464 '''Retrieve messages from friend's and own walls.'''
465 if not since:
466 since=int(time.time()-60*60*24)#24 hours
467 response=self.api.newsfeed.get(filters="post", start_time=since)["response"]
468
469 comment_author_factory=self._get_lazy_profiles_factory()
470
471 profiles_by_id={}#index user and group profiles by user id
472 for _acc in response["profiles"]:
473 profiles_by_id[_acc["uid"]]=_acc
474 if self.account["receive_groups"]:
475 for _acc in response["groups"]:
476 profiles_by_id[_acc["gid"]*-1]=_acc
477 for item in response["items"]:
478 if item["source_id"]<0 and not self.account["receive_groups"]:#source_id > 0 for peoples and < 0 for groups
479 continue#skip groups if "receive_groups" option disabled
480 account=profiles_by_id[item["source_id"]]
481 message=self._message(item, account, comment_author_factory)
482 yield message
483 for msg in self._own_wall_posts(5, comment_author_factory):
484 yield msg
485
486 def _own_wall_posts(self, count, comment_author_factory):
487 '''Download all posts user post in own wall'''
488 try:
489 response=self.api.wall.get(filter="owner", count=count)["response"]
490 except vkException, e:
491 yield self._format_error(e)
492 return
493 me=None
494 for post in response:
495 if not isinstance(post, dict):#skip first element - number of items
496 continue
497 post["post_id"]=post["id"]
498 post["source_id"]=post["from_id"]
499 if not me:
500 me=self.api.getProfiles(uids=post["source_id"], fields="uid,photo,first_name,last_name")["response"][0]
501 yield self._message(post, me, comment_author_factory)
502
503 def user_messages(self, id=None, count=util.COUNT, since=None):
504 """Messages posted by some user"""
505 raise NotImplementedError
506 if count>100:#vk limit
507 count=100
508 comment_author_factory=self._get_lazy_profiles_factory()
509 return self._own_wall_posts(count, comment_author_factory)
510
511 def send(self, message):
512 """Send new post to own wall"""
513 self.api.wall.post(message=message)
514 return self._own_wall_posts(1, self._get_lazy_profiles_factory())
515
516 def send_thread(self, message, target):
517 """Send comment with text @message to post @target
518 @param message: message text
519 @param target: target post"""
520 self.api.wall.addComment(owner_id=target["sender"]["id"],
521 post_id=target["mid"],
522 text=message)
523 # Update post data
524 # XXX: this doesn't update Gwibber UI and this is a Gwibber bug...
525 return self._post_by_id(target["sender"]["id"], target["mid"])
526
527 def _post_by_id(self, owner_id, post_id):
528 response=self.api.wall.getById(posts="%s_%s"%(owner_id, post_id))["response"]
529 comment_author_factory=self._get_lazy_profiles_factory()
530 me=None
531 for post in response:
532 if not isinstance(post, dict):#skip first element - number of items
533 continue
534 post["post_id"]=post["id"]
535 post["source_id"]=post["from_id"]
536 if not me:
537 me=self.api.getProfiles(uids=post["source_id"], fields="uid,photo,first_name,last_name")["response"][0]
538 yield self._message(post, me, comment_author_factory)
539
540 def like(self, message):
541 """Like message
542 @param message: target post you like"""
543 self.api.wall.addLike(owner_id=message["sender"]["id"],
544 post_id=message["mid"])
545 return []
546
547 def delete(self, message):
548 """Remove message from you wall
549 @param message: post you want to delete"""
550 self.api.wall.delete(owner_id=message["sender"]["id"],
551 post_id=message["mid"])
552 return []
0553
=== added directory 'gwibber/microblog/plugins/vkontakte/gtk'
=== added file 'gwibber/microblog/plugins/vkontakte/gtk/__init__.py'
=== added directory 'gwibber/microblog/plugins/vkontakte/gtk/vkontakte'
=== added file 'gwibber/microblog/plugins/vkontakte/gtk/vkontakte/__init__.py'
--- gwibber/microblog/plugins/vkontakte/gtk/vkontakte/__init__.py 1970-01-01 00:00:00 +0000
+++ gwibber/microblog/plugins/vkontakte/gtk/vkontakte/__init__.py 2011-05-15 21:54:31 +0000
@@ -0,0 +1,162 @@
1
2import gtk
3from gwibber.microblog.util import resources
4from gwibber.microblog.util.const import *
5try:
6 from gwibber.microblog.plugins.vkontakte.vk_api_wrapper import vk_api
7except ImportError:
8 try:
9 from vkontakte.vk_api_wrapper import vk_api
10 except ImportError:
11 vk_api=None
12
13import gnomekeyring
14import webkit
15import urllib
16import urlparse
17
18import gettext
19from gettext import gettext as _
20if hasattr(gettext, 'bind_textdomain_codeset'):
21 gettext.bind_textdomain_codeset('gwibber', 'UTF-8')
22gettext.textdomain('gwibber')
23
24VK_APP_ID="2036925"#XXX: remove me! see gwibber.microblog.util.const.VK_APP_ID
25
26class AccountWidget(gtk.VBox):
27 """AccountWidget: A widget that provides a user interface for configuring
28 Vkontakte accounts in Gwibber"""
29
30 def __init__(self, account=None, dialog=None):
31 """Creates the account pane for configuring Vkontakte accounts"""
32 gtk.VBox.__init__(self, False, 20)
33 self._state_init()
34 if account:
35 self.account=account
36 else:
37 self.account={}
38 self.dialog=dialog
39 has_access_token=True
40 if self.account.has_key("id"):#check is current account already authorized
41 try:
42 _value=gnomekeyring.find_items_sync(gnomekeyring.ITEM_GENERIC_SECRET, {"id": str("%s/%s"%(self.account["id"], "access_token"))})[0].secret
43 except gnomekeyring.NoMatchError:
44 has_access_token=False
45 try:#if account authorized, don't show "authorize" button
46 if self.account["access_token"] and self.account["username"] and has_access_token and not self.dialog.condition:
47 self._state_authorized()
48 else:#else don't show "authorized" label
49 self._state_not_authorized()
50 except:
51 self._state_not_authorized()
52
53
54 def on_vk_auth_clicked(self, widget, data=None):
55 """Start embed browser and show authorization dialog"""
56 web=webkit.WebView()
57 web.get_settings().set_property("enable-plugins", False)
58 web.load_html_string(_("<p>Please wait...</p>"), "file:///")
59
60 qstring=urllib.urlencode({#call to vkontakte API authorization
61 "client_id": VK_APP_ID,
62 "redirect_uri": "http://vkontakte.ru/api/login_success.html", #FIXME: http://api.vkontakte.ru/blank.html don't fire "title-changed"
63 "response_type": "token",
64 "display": "popup",
65 "scope": ",".join(("video", "offline", "wall"))
66 })
67 web.set_size_request(550, 440)
68 web.load_uri("http://api.vkontakte.ru/oauth/authorize?"+qstring)
69 #http://api.vkontakte.ru/oauth/authorize?client_id=2036925&response_type=token&scope=video,offline,wal&redirect_uri=http://api.vkontakte.ru/blank.html
70 web.connect("title-changed", self.on_vk_auth_title_change)
71 self._state_show_browser(web)
72
73 def on_vk_auth_title_change(self, web=None, title=None, data=None):
74 """When user confirm or revoke authorization in embed browser"""
75 saved=False
76 url=web.get_main_frame().get_uri()
77 if "access_token=" in url:
78 """When user successfully authorize our application, extract access_token secret...
79 http://api.vkontakte.ru/blank.html#access_token=55a69...c55&expires_in=0&user_id=535...7"""
80 query_string=url.split("#", 1)[1]
81 data=urlparse.parse_qs(query_string)
82 self.account["access_token"]=str(data["access_token"][0])
83 self.account["uid"]=data["user_id"][0]
84 if vk_api:#try retrieve user name and last name from API
85 profile=vk_api(access_token=self.account["access_token"]).getProfiles(uids=self.account["uid"],
86 fields="first_name,last_name")
87 self.account["username"]=u"%(first_name)s %(last_name)s"%(profile['response'][0])
88 else:
89 self.account["username"]="id%s"%self.account["uid"]
90 saved=self.dialog.on_edit_account_save()#store user account data
91 self._state_authorized()
92 self._state_login_success(saved)
93 self._state_hide_browser(web)
94 if "error=" in url:
95 query_string=url.split("?", 1)[1]
96 desc=urlparse.parse_qs(query_string)["error_description"][0]
97 self._state_not_authorized()
98 self._state_login_failed(_("Authentication error: %s")%desc)
99 self._state_hide_browser(web)
100
101 ### UI-manipulation methods ###
102
103 def _state_init(self):
104 """create UI from UI file"""
105 self._browser_scroll=None
106 self.ui=gtk.Builder()
107 self.ui.set_translation_domain("gwibber")
108 self.ui.add_from_file(resources.get_ui_asset("gwibber-accounts-vkontakte.ui"))
109 self.ui.connect_signals(self)#attach events (on_vk_auth_clicked)
110 self.vbox_settings=self.ui.get_object("vbox_settings")
111 self.pack_start(self.vbox_settings, False, False)
112 self.show_all()
113
114 def _state_show_browser(self, web):
115 """Show webkit widget, hide settings etc.."""
116 (self.win_w, self.win_h)=self.window.get_size()
117 if not self._browser_scroll:
118 self._browser_scroll=gtk.ScrolledWindow()
119 self.pack_start(self._browser_scroll, True, True, 0)
120 self._browser_scroll.child=None
121 self._browser_scroll.add(web)
122 self.show_all()
123 self.ui.get_object("vbox1").hide()
124 self.ui.get_object("vbox_advanced").hide()
125
126 def _state_hide_browser(self, web):
127 """Hide webkit, show advanced settings, etc.."""
128 web.hide()
129 self.window.resize(self.win_w, self.win_h)
130 self.ui.get_object("vbox1").show()
131 self.ui.get_object("vbox_advanced").show()
132
133 def _state_login_success(self, saved):
134 """Show 'update' or 'create' buttons if not saved automatically"""
135 if self.dialog.ui and self.account.has_key("id") and not saved:
136 self.dialog.ui.get_object("vbox_save").show()
137 elif self.dialog.ui and not saved:
138 self.dialog.ui.get_object("vbox_create").show()
139
140 def _state_login_failed(self, reason=None):
141 """When user fail or revoke authorization, show popup message"""
142 gtk.gdk.threads_enter()
143 if not reason:
144 reason=_("Vkontakte authorization failed. Please try again.")
145 else:
146 reason=_(reason)
147 d=gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, reason)
148 if d.run(): d.destroy()
149 gtk.gdk.threads_leave()
150
151 def _state_not_authorized(self):
152 """Show 'Authorize with vkontakte' button"""
153 self.ui.get_object("hbox_vk_auth_done").hide()
154 self.ui.get_object("hbox_vk_auth").show()
155 if self.dialog.ui:
156 self.dialog.ui.get_object('vbox_create').hide()
157
158 def _state_authorized(self):
159 """Hide 'Authorize with vkontakte' button, show 'Authorized'"""
160 self.ui.get_object("hbox_vk_auth").hide()
161 self.ui.get_object("vk_auth_done_label").set_label(_("%s has been authorized by Vkontakte")%str(self.account["username"]))
162 self.ui.get_object("hbox_vk_auth_done").show()
0163
=== added directory 'gwibber/microblog/plugins/vkontakte/ui'
=== added file 'gwibber/microblog/plugins/vkontakte/ui/gwibber-accounts-vkontakte.ui'
--- gwibber/microblog/plugins/vkontakte/ui/gwibber-accounts-vkontakte.ui 1970-01-01 00:00:00 +0000
+++ gwibber/microblog/plugins/vkontakte/ui/gwibber-accounts-vkontakte.ui 2011-05-15 21:54:31 +0000
@@ -0,0 +1,182 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<interface>
3 <requires lib="gtk+" version="2.16"/>
4 <!-- interface-naming-policy toplevel-contextual -->
5 <object class="GtkVBox" id="vbox_settings">
6 <property name="visible">True</property>
7 <property name="spacing">6</property>
8 <child>
9 <object class="GtkVBox" id="vbox1">
10 <property name="visible">True</property>
11 <child>
12 <object class="GtkHBox" id="hbox_vk_auth">
13 <property name="visible">True</property>
14 <child>
15 <object class="GtkButton" id="vk_auth_button">
16 <property name="label" translatable="yes">_Authorize</property>
17 <property name="visible">True</property>
18 <property name="can_focus">True</property>
19 <property name="receives_default">True</property>
20 <property name="use_underline">True</property>
21 <signal name="clicked" handler="on_vk_auth_clicked"/>
22 </object>
23 <packing>
24 <property name="fill">False</property>
25 <property name="position">0</property>
26 </packing>
27 </child>
28 <child>
29 <object class="GtkLabel" id="vk_auth_label">
30 <property name="visible">True</property>
31 <property name="label" translatable="yes">Authorize with vkontakte</property>
32 </object>
33 <packing>
34 <property name="position">1</property>
35 </packing>
36 </child>
37 </object>
38 <packing>
39 <property name="position">0</property>
40 </packing>
41 </child>
42 <child>
43 <object class="GtkHBox" id="hbox_vk_auth_done">
44 <property name="visible">True</property>
45 <child>
46 <object class="GtkLabel" id="vk_auth_done_label">
47 <property name="visible">True</property>
48 <property name="label" translatable="yes">Vkontakte authorized</property>
49 </object>
50 <packing>
51 <property name="position">0</property>
52 </packing>
53 </child>
54 </object>
55 <packing>
56 <property name="position">1</property>
57 </packing>
58 </child>
59 </object>
60 <packing>
61 <property name="position">0</property>
62 </packing>
63 </child>
64 <child>
65 <object class="GtkHSeparator" id="hseparator1">
66 <property name="visible">True</property>
67 </object>
68 <packing>
69 <property name="expand">False</property>
70 <property name="position">1</property>
71 </packing>
72 </child>
73 <child>
74 <object class="GtkVBox" id="vbox_advanced">
75 <property name="visible">True</property>
76 <property name="spacing">6</property>
77 <child>
78 <object class="GtkLabel" id="label3">
79 <property name="visible">True</property>
80 <property name="xalign">0</property>
81 <property name="label" translatable="yes">Account Settings:</property>
82 <attributes>
83 <attribute name="weight" value="bold"/>
84 </attributes>
85 </object>
86 <packing>
87 <property name="position">0</property>
88 </packing>
89 </child>
90 <child>
91 <object class="GtkVBox" id="vbox2">
92 <property name="visible">True</property>
93 <child>
94 <object class="GtkCheckButton" id="receive_enabled">
95 <property name="label" translatable="yes">_Receive Messages</property>
96 <property name="visible">True</property>
97 <property name="can_focus">True</property>
98 <property name="receives_default">False</property>
99 <property name="tooltip_text" translatable="yes">Include this account when downloading messages</property>
100 <property name="use_underline">True</property>
101 <property name="active">True</property>
102 <property name="draw_indicator">True</property>
103 </object>
104 <packing>
105 <property name="position">0</property>
106 </packing>
107 </child>
108 <child>
109 <object class="GtkCheckButton" id="send_enabled">
110 <property name="label" translatable="yes">_Send Messages</property>
111 <property name="visible">True</property>
112 <property name="can_focus">True</property>
113 <property name="receives_default">False</property>
114 <property name="tooltip_text" translatable="yes">Allow sending posts to this account</property>
115 <property name="use_underline">True</property>
116 <property name="active">True</property>
117 <property name="draw_indicator">True</property>
118 </object>
119 <packing>
120 <property name="position">1</property>
121 </packing>
122 </child>
123 <child>
124 <object class="GtkCheckButton" id="receive_groups">
125 <property name="label" translatable="yes" comments="Receive messages from the groups, in addition to messages from users?">_Receive groups</property>
126 <property name="visible">True</property>
127 <property name="can_focus">True</property>
128 <property name="receives_default">False</property>
129 <property name="tooltip_text" translatable="yes">Receive news from groups</property>
130 <property name="use_underline">True</property>
131 <property name="active">True</property>
132 <property name="draw_indicator">True</property>
133 </object>
134 <packing>
135 <property name="position">2</property>
136 </packing>
137 </child>
138 </object>
139 <packing>
140 <property name="position">1</property>
141 </packing>
142 </child>
143 <child>
144 <object class="GtkHBox" id="hbox1">
145 <property name="visible">True</property>
146 <property name="homogeneous">True</property>
147 <child>
148 <object class="GtkLabel" id="label4">
149 <property name="visible">True</property>
150 <property name="tooltip_text" translatable="yes">Color used to help distinguish accounts</property>
151 <property name="xalign">0</property>
152 <property name="label" translatable="yes">Account Color:</property>
153 </object>
154 <packing>
155 <property name="position">0</property>
156 </packing>
157 </child>
158 <child>
159 <object class="GtkColorButton" id="color">
160 <property name="visible">True</property>
161 <property name="can_focus">True</property>
162 <property name="receives_default">True</property>
163 <property name="tooltip_text" translatable="yes">Color used to help distinguish accounts</property>
164 <property name="color">#000000000000</property>
165 </object>
166 <packing>
167 <property name="expand">False</property>
168 <property name="position">1</property>
169 </packing>
170 </child>
171 </object>
172 <packing>
173 <property name="position">2</property>
174 </packing>
175 </child>
176 </object>
177 <packing>
178 <property name="position">2</property>
179 </packing>
180 </child>
181 </object>
182</interface>
0183
=== added file 'gwibber/microblog/plugins/vkontakte/vk_api_wrapper.py'
--- gwibber/microblog/plugins/vkontakte/vk_api_wrapper.py 1970-01-01 00:00:00 +0000
+++ gwibber/microblog/plugins/vkontakte/vk_api_wrapper.py 2011-05-15 21:54:31 +0000
@@ -0,0 +1,81 @@
1# -*- coding: utf-8 -*-
2'''
3Created on 07.12.2010
4
5@author: Sergey Prokhorov <root@seriyps.ru>
6'''
7from gwibber.microblog import network
8from gwibber.microblog.util import log
9import urllib
10import time
11
12class vkException(Exception):
13
14 def __init__(self, code, msg, url, response):
15 self.code=code
16 self.msg=msg
17 self.response=response
18 self.url=url
19 Exception.__init__(self, code, msg, url, response)
20
21
22class vk_api:
23 """Vkontakte.ru (vk.com) API wrapper for Gwibber
24 == Usage ==
25 Initialization:
26 api=vk_api(access_token)
27 "access_token" can be retrieved from call to http://api.vkontakte.ru/oauth/authorize
28 (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)
29 Make calls to simple methods (has no dots in name, like "getProfiles", "isAppUser"):
30 res=api.method(arg_name1=arg1, arg_name2=arg2)
31 eg res=api.getProfiles(uids="535397,1", fields="uid,first_name,last_name")
32 Make calls to namespaced methods (has dots in name, like "wall.post", "newsfeed.get"):
33 res=api.namespace.method(arg_name1=arg1, arg_name2=arg2)
34 eg res=api.wall.post(message="New wall post from API")
35 Alternative call syntax (better performance):
36 res=api._load('namespace.method', arg_name1=arg1, arg_name2=arg2)
37 eg res=api._get('wall.post', message="New wall post from API")
38 """
39
40 api_url='https://api.vkontakte.ru/method/%s'
41
42 def __init__(self, access_token):
43 self._access_token=access_token
44 self._prefix=[]
45
46 def _load(self, method, **params):
47 """Make call by method name and arguments"""
48 params["access_token"]=self._access_token
49 url=self.api_url%method
50 for _i in xrange(3):#this cycle used for API error #6 "Too many requests"
51 log.logger.debug("Perform API request to method %s. URL: %s?%s", method, url, urllib.urlencode(params))
52 res=network.Download(url, params, False).get_json()
53 if not res.has_key("error"):
54 #log.logger.debug("%s"%res)
55 return res
56
57 if res["error"]["error_code"]!=6:
58 break
59 log.logging.warning('Error #6 "%s". Wait 0.5s and retry...', res["error"]["error_msg"])
60 '''if we get error #6 "Too many requests per second" (vk allow
61 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 )
62 try to sleep with small delay and retry request not more then 3x times'''
63 time.sleep(0.5)#TODO: play with sleep value
64 raise vkException(res["error"]["error_code"],
65 res["error"]["error_msg"],
66 "%s?%s"%(url, urllib.urlencode(params)),
67 res)
68
69
70 """Support for api.namespace.method(**kwargs) calls below"""
71 def __getattr__(self, name):
72 self._prefix.append(name)
73 return self
74
75 def __call__(self, **kwargs):
76 if self._prefix:
77 method=".".join(self._prefix)
78 self._prefix=[]
79 else:
80 method=kwargs.pop("method")
81 return self._load(method, **kwargs)
082
=== modified file 'gwibber/microblog/util/const.py'
--- gwibber/microblog/util/const.py 2011-04-22 15:53:40 +0000
+++ gwibber/microblog/util/const.py 2011-05-15 21:54:31 +0000
@@ -15,6 +15,8 @@
15TWITTER_OAUTH_KEY = "VDOuA5qCJ1XhjaSa4pl76g"15TWITTER_OAUTH_KEY = "VDOuA5qCJ1XhjaSa4pl76g"
16TWITTER_OAUTH_SECRET = "BqHlB8sMz5FhZmmFimwgiIdB0RiBr72Y0bio49IVJM"16TWITTER_OAUTH_SECRET = "BqHlB8sMz5FhZmmFimwgiIdB0RiBr72Y0bio49IVJM"
1717
18VK_APP_ID="2036925"
19
18# Gwibber20# Gwibber
19MAX_MESSAGE_LENGTH = 14021MAX_MESSAGE_LENGTH = 140
20MAX_MESSAGE_COUNT = 2000022MAX_MESSAGE_COUNT = 20000
2123
=== modified file 'setup.py'
--- setup.py 2011-04-22 16:12:39 +0000
+++ setup.py 2011-05-15 21:54:31 +0000
@@ -34,6 +34,10 @@
34 ('share/gwibber/plugins/facebook/gtk', glob("gwibber/microblog/plugins/facebook/gtk/*.*")),34 ('share/gwibber/plugins/facebook/gtk', glob("gwibber/microblog/plugins/facebook/gtk/*.*")),
35 ('share/gwibber/plugins/facebook/gtk/facebook', glob("gwibber/microblog/plugins/facebook/gtk/facebook/*.*")),35 ('share/gwibber/plugins/facebook/gtk/facebook', glob("gwibber/microblog/plugins/facebook/gtk/facebook/*.*")),
36 ('share/gwibber/plugins/facebook/ui', glob("gwibber/microblog/plugins/facebook/ui/*.*")),36 ('share/gwibber/plugins/facebook/ui', glob("gwibber/microblog/plugins/facebook/ui/*.*")),
37 ('share/gwibber/plugins/vkontakte', glob("gwibber/microblog/plugins/vkontakte/*.*")),
38 ('share/gwibber/plugins/vkontakte/gtk', glob("gwibber/microblog/plugins/vkontakte/gtk/*.*")),
39 ('share/gwibber/plugins/vkontakte/gtk/vkontakte', glob("gwibber/microblog/plugins/vkontakte/gtk/vkontakte/*.*")),
40 ('share/gwibber/plugins/vkontakte/ui', glob("gwibber/microblog/plugins/vkontakte/ui/*.*")),
37 ('share/gwibber/plugins/flickr', glob("gwibber/microblog/plugins/flickr/*.*")),41 ('share/gwibber/plugins/flickr', glob("gwibber/microblog/plugins/flickr/*.*")),
38 ('share/gwibber/plugins/flickr/gtk', glob("gwibber/microblog/plugins/flickr/gtk/*.*")),42 ('share/gwibber/plugins/flickr/gtk', glob("gwibber/microblog/plugins/flickr/gtk/*.*")),
39 ('share/gwibber/plugins/flickr/gtk/flickr', glob("gwibber/microblog/plugins/flickr/gtk/flickr/*.*")),43 ('share/gwibber/plugins/flickr/gtk/flickr', glob("gwibber/microblog/plugins/flickr/gtk/flickr/*.*")),
4044
=== added file 'ui/icons/breakdance/16x16/vkontakte.png'
41Binary 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 differ45Binary 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
=== added file 'ui/icons/breakdance/22x22/vkontakte.png'
42Binary 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 differ46Binary 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
=== added file 'ui/icons/breakdance/32x32/vkontakte.png'
43Binary 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 differ47Binary 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
=== added file 'ui/icons/breakdance/scalable/vkontakte.png'
44Binary 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 differ48Binary 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
=== added file 'ui/icons/breakdance/scalable/vkontakte.svg'
--- ui/icons/breakdance/scalable/vkontakte.svg 1970-01-01 00:00:00 +0000
+++ ui/icons/breakdance/scalable/vkontakte.svg 2011-05-15 21:54:31 +0000
@@ -0,0 +1,55 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3<!-- SVG Created by Sergey A Prochorov <root@seriyps.ru> (http://seriyps.ru/) -->
4<svg
5 xmlns:dc="http://purl.org/dc/elements/1.1/"
6 xmlns:cc="http://creativecommons.org/ns#"
7 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8 xmlns:svg="http://www.w3.org/2000/svg"
9 xmlns="http://www.w3.org/2000/svg"
10 version="1.1"
11 width="285"
12 height="285"
13 id="svg3036">
14 <defs
15 id="defs8">
16 <filter
17 color-interpolation-filters="sRGB"
18 id="filter3773">
19 <feGaussianBlur
20 id="feGaussianBlur3775"
21 stdDeviation="3.931875" />
22 </filter>
23 </defs>
24 <metadata
25 id="metadata3042">
26 <rdf:RDF>
27 <cc:Work
28 rdf:about="">
29 <dc:format>image/svg+xml</dc:format>
30 <dc:type
31 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
32 <dc:title></dc:title>
33 </cc:Work>
34 </rdf:RDF>
35 </metadata>
36 <g
37 transform="translate(-45.0635,-38.936486)"
38 id="g3796">
39 <rect
40 width="168"
41 height="197"
42 x="108"
43 y="87"
44 id="rect4024"
45 style="fill:#4c77a0;fill-opacity:1;stroke:none" />
46 <path
47 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"
48 id="path3978"
49 style="opacity:0.62999998;fill:#000000;fill-opacity:1;stroke:none;filter:url(#filter3773)" />
50 <path
51 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"
52 id="path3046"
53 style="fill:#ffffff;fill-opacity:1;stroke:none" />
54 </g>
55</svg>