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

Proposed by Sergey Prokhorov
Status: Rejected
Rejected by: Ken VanDine
Proposed branch: lp:~seriy-pr/gwibber/vkontakte-ru-plugin
Merge into: lp:gwibber
Diff against target: 1212 lines (+1149/-2)
7 files modified
.bzrignore (+0/-2)
gwibber/microblog/plugins/vkontakte/__init__.py (+593/-0)
gwibber/microblog/plugins/vkontakte/gtk/vkontakte/__init__.py (+185/-0)
gwibber/microblog/plugins/vkontakte/ui/gwibber-accounts-vkontakte.ui (+227/-0)
gwibber/microblog/plugins/vkontakte/ui/icons/scalable/vkontakte.svg (+55/-0)
gwibber/microblog/plugins/vkontakte/vk_api_wrapper.py (+87/-0)
gwibber/microblog/util/const.py.in (+2/-0)
To merge this branch: bzr merge lp:~seriy-pr/gwibber/vkontakte-ru-plugin
Reviewer Review Type Date Requested Status
Ken VanDine Needs Fixing
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

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

Revision history for this message
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
Revision history for this message
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

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
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