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

Proposed by Sergey Prokhorov on 2011-05-15
Reviewer Review Type Date Requested Status
Ken VanDine 2011-05-15 Needs Fixing on 2011-10-13
Review via email: mp+61045@code.launchpad.net

This proposal supersedes a proposal from 2011-01-27.

Description of the Change

Added plugin for vkontakte.ru ( http://vk.com/ ) - most popular social network in Russia, Ukraine and many ex-USSR countries (>100 mln users, http://www.alexa.com/siteinfo/vkontakte.ru #44).

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/?z2ye2u9chtda9vu

To post a comment you must log in.
969. By Sergey Prokhorov on 2011-05-19

Simplify lazy loading mechanizm

970. By Sergey Prokhorov on 2011-05-19

Linkify comments and links description

971. By Sergey Prokhorov on 2011-05-19

Add mentions support

972. By Sergey Prokhorov on 2011-05-21

Fix notifications

973. By Sergey Prokhorov on 2011-05-28

Fix right-aligned text on "\ufeff" char appears on message

974. By Sergey Prokhorov on 2011-06-04

Add support for vkontakte hashtags.

975. By Sergey Prokhorov on 2011-06-07

Merged with trunk

976. By Sergey Prokhorov on 2011-10-02

Merged with head

Ken VanDine (ken-vandine) wrote :

I think this would be best as a standalone plugin, packaged separately from gwibber. We want to move most of the plugins out of the gwibber source and into separate projects. You can look at https://launchpad.net/gwibber-service-sina as an example. I think we will be keeping twitter, facebook and g+ (when it exists) in gwibber mainline and move the rest to new source packages. If you have questions, please join us in #gwibber on Freenode.

Thanks for the awesome work!

review: Needs Fixing
Sergey Prokhorov (seriy-pr) wrote :

> I think this would be best as a standalone plugin, packaged separately from
> gwibber. We want to move most of the plugins out of the gwibber source and
> into separate projects. You can look at https://launchpad.net/gwibber-
> service-sina as an example. I think we will be keeping twitter, facebook and
> g+ (when it exists) in gwibber mainline and move the rest to new source
> packages. If you have questions, please join us in #gwibber on Freenode.
>
> Thanks for the awesome work!

Ok, I think this is good idea. Also, we already has ppa for vkontakte plugin https://launchpad.net/~gwibber-vkontakte-plugin/+archive/ppa. I begin to create a separate project for plugin next week I think.

977. By Sergey Prokhorov on 2012-01-04

merged with trunk

978. By Sergey Prokhorov on 2012-01-05

Make code less ugly + replace <br> with <br/>

979. By Sergey Prokhorov on 2012-01-19

merged with trunk

980. By Sergey Prokhorov on 2012-01-19

move icons to plugin directory

981. By Sergey Prokhorov on 2012-01-19

setup.py don't used in gwibber now

982. By Sergey Prokhorov on 2012-03-16

merged with trunk

983. By Sergey Prokhorov on 2012-03-16

Fix logging issue

984. By Sergey Prokhorov on 2012-03-16

Fix indentation

985. By Sergey Prokhorov on 2012-03-16

Fix logger issue

986. By Sergey Prokhorov on 2012-03-17

More logging fixes

987. By Sergey Prokhorov on 2012-03-17

fix code conventions

988. By Sergey Prokhorov on 2012-03-17

Fix issue of renaming the vk api wrapper

989. By Sergey Prokhorov on 2012-03-17

Fix indentation on gui module

990. By Sergey Prokhorov on 2012-03-17

Compatible with 3.3.92 on ubuntu 12.04

Unmerged revisions

990. By Sergey Prokhorov on 2012-03-17

Compatible with 3.3.92 on ubuntu 12.04

989. By Sergey Prokhorov on 2012-03-17

Fix indentation on gui module

988. By Sergey Prokhorov on 2012-03-17

Fix issue of renaming the vk api wrapper

987. By Sergey Prokhorov on 2012-03-17

fix code conventions

986. By Sergey Prokhorov on 2012-03-17

More logging fixes

985. By Sergey Prokhorov on 2012-03-16

Fix logger issue

984. By Sergey Prokhorov on 2012-03-16

Fix indentation

983. By Sergey Prokhorov on 2012-03-16

Fix logging issue

982. By Sergey Prokhorov on 2012-03-16

merged with trunk

981. By Sergey Prokhorov on 2012-01-19

setup.py don't used in gwibber now

Preview Diff

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