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

Proposed by Sergey Prokhorov
Status: Superseded
Proposed branch: lp:~seriy-pr/gwibber/vkontakte-ru-plugin
Merge into: lp:gwibber
Diff against target: 1108 lines (+1041/-0)
8 files modified
.bzrignore (+3/-0)
gwibber/microblog/plugins/vkontakte/__init__.py (+552/-0)
gwibber/microblog/plugins/vkontakte/gtk/vkontakte/__init__.py (+162/-0)
gwibber/microblog/plugins/vkontakte/ui/gwibber-accounts-vkontakte.ui (+182/-0)
gwibber/microblog/plugins/vkontakte/vk_api_wrapper.py (+81/-0)
gwibber/microblog/util/const.py (+2/-0)
setup.py (+4/-0)
ui/icons/breakdance/scalable/vkontakte.svg (+55/-0)
To merge this branch: bzr merge lp:~seriy-pr/gwibber/vkontakte-ru-plugin
Reviewer Review Type Date Requested Status
gwibber-committers Pending
Review via email: mp+47651@code.launchpad.net

This proposal has been superseded by a proposal from 2011-05-15.

Description of the change

Added plugin for vkontakte.ru - most popular social network in Russia, Ukraine and many ex-USSR countries.

This branch contains a fix of whishlist bug #572753
Also there is a related blueprint https://blueprints.edge.launchpad.net/gwibber/+spec/vkontakte.ru

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

To post a comment you must log in.
961. By Sergey Prokhorov

Fix duplicate browser windows when try re-authorize

962. By Sergey Prokhorov

Merged with latest trunk

963. By Sergey Prokhorov

Merged with trunk

964. By Sergey Prokhorov

Code moved to new authorization method (OAuth2.0)

965. By Sergey Prokhorov

Fixed error with authorization (appears after migration to new API)

966. By Sergey Prokhorov

Merged with latest trunk

967. By Sergey Prokhorov

Add video/photo/links streams support + extra debug messages

968. By Sergey Prokhorov

Fix bug: "post comments doesn't loaded" + add declarative JSON structure description.

969. By Sergey Prokhorov

Simplify lazy loading mechanizm

970. By Sergey Prokhorov

Linkify comments and links description

971. By Sergey Prokhorov

Add mentions support

972. By Sergey Prokhorov

Fix notifications

973. By Sergey Prokhorov

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

974. By Sergey Prokhorov

Add support for vkontakte hashtags.

975. By Sergey Prokhorov

Merged with trunk

976. By Sergey Prokhorov

Merged with head

977. By Sergey Prokhorov

merged with trunk

978. By Sergey Prokhorov

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

979. By Sergey Prokhorov

merged with trunk

980. By Sergey Prokhorov

move icons to plugin directory

981. By Sergey Prokhorov

setup.py don't used in gwibber now

982. By Sergey Prokhorov

merged with trunk

983. By Sergey Prokhorov

Fix logging issue

984. By Sergey Prokhorov

Fix indentation

985. By Sergey Prokhorov

Fix logger issue

986. By Sergey Prokhorov

More logging fixes

987. By Sergey Prokhorov

fix code conventions

988. By Sergey Prokhorov

Fix issue of renaming the vk api wrapper

989. By Sergey Prokhorov

Fix indentation on gui module

990. By Sergey Prokhorov

Compatible with 3.3.92 on ubuntu 12.04

Unmerged revisions

990. By Sergey Prokhorov

Compatible with 3.3.92 on ubuntu 12.04

989. By Sergey Prokhorov

Fix indentation on gui module

988. By Sergey Prokhorov

Fix issue of renaming the vk api wrapper

987. By Sergey Prokhorov

fix code conventions

986. By Sergey Prokhorov

More logging fixes

985. By Sergey Prokhorov

Fix logger issue

984. By Sergey Prokhorov

Fix indentation

983. By Sergey Prokhorov

Fix logging issue

982. By Sergey Prokhorov

merged with trunk

981. By Sergey Prokhorov

setup.py don't used in gwibber now

Preview Diff

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