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

Proposed by Sergey Prokhorov on 2011-01-27
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 2011-01-27 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 on 2011-02-06

Fix duplicate browser windows when try re-authorize

962. By Sergey Prokhorov on 2011-04-02

Merged with latest trunk

963. By Sergey Prokhorov on 2011-04-12

Merged with trunk

964. By Sergey Prokhorov on 2011-04-13

Code moved to new authorization method (OAuth2.0)

965. By Sergey Prokhorov on 2011-04-26

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

966. By Sergey Prokhorov on 2011-04-26

Merged with latest trunk

967. By Sergey Prokhorov on 2011-05-11

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

968. By Sergey Prokhorov on 2011-05-15

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

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

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

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