Merge lp:~dpm/ubuntu/lucid/gwibber/update-pot into lp:ubuntu/lucid/gwibber

Proposed by David Planella
Status: Rejected
Rejected by: James Westby
Proposed branch: lp:~dpm/ubuntu/lucid/gwibber/update-pot
Merge into: lp:ubuntu/lucid/gwibber
Diff against target: 47079 lines (+45683/-0) (has conflicts)
211 files modified
.bzrignore (+3/-0)
AUTHORS (+26/-0)
COPYING (+339/-0)
INSTALL (+41/-0)
MANIFEST.in (+16/-0)
README (+6/-0)
bin/gwibber (+63/-0)
bin/gwibber-accounts (+50/-0)
bin/gwibber-poster (+100/-0)
bin/gwibber-preferences (+49/-0)
bin/gwibber-service (+46/-0)
com.Gwibber.Service.service (+3/-0)
com.GwibberClient.service (+3/-0)
gwibber-accounts.desktop.in (+9/-0)
gwibber-poster.1 (+31/-0)
gwibber-preferences.desktop.in (+9/-0)
gwibber.1 (+27/-0)
gwibber.desktop (+56/-0)
gwibber.desktop.in (+9/-0)
gwibber/accounts.py (+331/-0)
gwibber/actions.py (+164/-0)
gwibber/client.py (+467/-0)
gwibber/gwui.py (+871/-0)
gwibber/lib/__init__.py (+34/-0)
gwibber/lib/gtk/__init__.py (+1/-0)
gwibber/lib/gtk/facebook.py (+110/-0)
gwibber/lib/gtk/flickr.py (+38/-0)
gwibber/lib/gtk/friendfeed.py (+38/-0)
gwibber/lib/gtk/identica.py (+38/-0)
gwibber/lib/gtk/twitter.py (+76/-0)
gwibber/lib/gtk/widgets.py (+65/-0)
gwibber/microblog/brightkite.py (+177/-0)
gwibber/microblog/can.py (+23/-0)
gwibber/microblog/digg.py (+92/-0)
gwibber/microblog/dispatcher.py (+486/-0)
gwibber/microblog/facebook.py (+174/-0)
gwibber/microblog/flickr.py (+84/-0)
gwibber/microblog/friendfeed.py (+144/-0)
gwibber/microblog/greader.py (+151/-0)
gwibber/microblog/identica.py (+165/-0)
gwibber/microblog/jaiku.py (+142/-0)
gwibber/microblog/laconica.py (+219/-0)
gwibber/microblog/network.py (+38/-0)
gwibber/microblog/opencollaboration.py (+143/-0)
gwibber/microblog/pingfm.py (+40/-0)
gwibber/microblog/pownce.py (+99/-0)
gwibber/microblog/qaiku.py (+155/-0)
gwibber/microblog/rss.py (+69/-0)
gwibber/microblog/storage.py (+29/-0)
gwibber/microblog/support/facelib.py (+1059/-0)
gwibber/microblog/twitter.py (+167/-0)
gwibber/microblog/urlshorter/__init__.py (+13/-0)
gwibber/microblog/urlshorter/cligs.py (+24/-0)
gwibber/microblog/urlshorter/isgd.py (+24/-0)
gwibber/microblog/urlshorter/snipurlcom.py (+24/-0)
gwibber/microblog/urlshorter/tinyurlcom.py (+24/-0)
gwibber/microblog/urlshorter/trim.py (+24/-0)
gwibber/microblog/urlshorter/ur1ca.py (+38/-0)
gwibber/microblog/urlshorter/zima.py (+24/-0)
gwibber/microblog/util/__init__.py (+62/-0)
gwibber/microblog/util/const.py (+316/-0)
gwibber/microblog/util/couch.py (+109/-0)
gwibber/microblog/util/facelib.py (+1059/-0)
gwibber/microblog/util/imagehandler.py (+59/-0)
gwibber/microblog/util/resources.py (+86/-0)
gwibber/pidgin.py (+34/-0)
gwibber/preferences.py (+109/-0)
gwibber/resources.py (+109/-0)
gwibber/upgrade.py (+70/-0)
gwibber/util.py (+127/-0)
indicator/gwibber (+1/-0)
po/POTFILES.in (+16/-0)
po/ar.po (+580/-0)
po/bg.po (+532/-0)
po/ca.po (+579/-0)
po/cy.po (+547/-0)
po/da.po (+558/-0)
po/de.po (+590/-0)
po/en_CA.po (+560/-0)
po/en_GB.po (+586/-0)
po/es.po (+596/-0)
po/fi.po (+570/-0)
po/fr.po (+580/-0)
po/gwibber.pot (+115/-0)
po/id.po (+554/-0)
po/it.po (+594/-0)
po/lo.po (+550/-0)
po/nb.po (+533/-0)
po/nl.po (+572/-0)
po/pl.po (+584/-0)
po/pt.po (+560/-0)
po/pt_BR.po (+580/-0)
po/ru.po (+552/-0)
po/sq.po (+530/-0)
po/sv.po (+579/-0)
po/tr.po (+536/-0)
po/uk.po (+531/-0)
po/zh_CN.po (+546/-0)
run-tests (+26/-0)
scripts/add_account.py (+21/-0)
scripts/gwibber-client (+11/-0)
scripts/gwibber-daemon-shutdown (+3/-0)
scripts/gwibber-errors.py (+16/-0)
setup.cfg (+10/-0)
setup.py (+63/-0)
tests/__init__.py (+47/-0)
ui/gwibber-about-dialog.ui (+51/-0)
ui/gwibber-accounts-dialog.ui (+468/-0)
ui/gwibber-accounts-facebook.ui (+193/-0)
ui/gwibber-accounts-flickr.ui (+187/-0)
ui/gwibber-accounts-friendfeed.ui (+237/-0)
ui/gwibber-accounts-identica.ui (+237/-0)
ui/gwibber-accounts-twitter.ui (+237/-0)
ui/gwibber-preferences-dialog.ui (+478/-0)
ui/gwibber.svg (+1184/-0)
ui/icons/breakdance/16x16/brightkite.svg (+878/-0)
ui/icons/breakdance/16x16/digg.svg (+506/-0)
ui/icons/breakdance/16x16/facebook.svg (+216/-0)
ui/icons/breakdance/16x16/flickr.svg (+314/-0)
ui/icons/breakdance/16x16/friendfeed.svg (+425/-0)
ui/icons/breakdance/16x16/identica.svg (+342/-0)
ui/icons/breakdance/16x16/jaiku.svg (+299/-0)
ui/icons/breakdance/16x16/laconica.svg (+332/-0)
ui/icons/breakdance/16x16/openid.svg (+197/-0)
ui/icons/breakdance/16x16/pingfm.svg (+271/-0)
ui/icons/breakdance/16x16/qaiku.svg (+717/-0)
ui/icons/breakdance/16x16/stumbleupon.svg (+168/-0)
ui/icons/breakdance/16x16/twitter.svg (+181/-0)
ui/icons/breakdance/22x22/brightkite.svg (+669/-0)
ui/icons/breakdance/22x22/digg.svg (+360/-0)
ui/icons/breakdance/22x22/facebook.svg (+225/-0)
ui/icons/breakdance/22x22/flickr.svg (+286/-0)
ui/icons/breakdance/22x22/friendfeed.svg (+372/-0)
ui/icons/breakdance/22x22/identica.svg (+336/-0)
ui/icons/breakdance/22x22/jaiku.svg (+264/-0)
ui/icons/breakdance/22x22/laconica.svg (+326/-0)
ui/icons/breakdance/22x22/openid.svg (+220/-0)
ui/icons/breakdance/22x22/pingfm.svg (+201/-0)
ui/icons/breakdance/22x22/qaiku.svg (+733/-0)
ui/icons/breakdance/22x22/stumbleupon.svg (+192/-0)
ui/icons/breakdance/22x22/twitter.svg (+203/-0)
ui/icons/breakdance/32x32/brightkite.svg (+524/-0)
ui/icons/breakdance/32x32/digg.svg (+358/-0)
ui/icons/breakdance/32x32/facebook.svg (+272/-0)
ui/icons/breakdance/32x32/flickr.svg (+246/-0)
ui/icons/breakdance/32x32/friendfeed.svg (+348/-0)
ui/icons/breakdance/32x32/identica.svg (+253/-0)
ui/icons/breakdance/32x32/jaiku.svg (+242/-0)
ui/icons/breakdance/32x32/laconica.svg (+246/-0)
ui/icons/breakdance/32x32/openid.svg (+212/-0)
ui/icons/breakdance/32x32/pingfm.svg (+201/-0)
ui/icons/breakdance/32x32/qaiku.svg (+527/-0)
ui/icons/breakdance/32x32/stumbleupon.svg (+193/-0)
ui/icons/breakdance/32x32/twitter.svg (+203/-0)
ui/icons/breakdance/AUTHORS (+2/-0)
ui/icons/breakdance/COPYING (+21/-0)
ui/icons/breakdance/ChangeLog (+11/-0)
ui/icons/breakdance/README (+4/-0)
ui/icons/breakdance/scalable/brightkite.svg (+379/-0)
ui/icons/breakdance/scalable/digg.svg (+362/-0)
ui/icons/breakdance/scalable/facebook.svg (+227/-0)
ui/icons/breakdance/scalable/flickr.svg (+206/-0)
ui/icons/breakdance/scalable/friendfeed.svg (+252/-0)
ui/icons/breakdance/scalable/identica.svg (+207/-0)
ui/icons/breakdance/scalable/jaiku.svg (+203/-0)
ui/icons/breakdance/scalable/laconica.svg (+190/-0)
ui/icons/breakdance/scalable/openid.svg (+223/-0)
ui/icons/breakdance/scalable/pingfm.svg (+203/-0)
ui/icons/breakdance/scalable/qaiku.svg (+333/-0)
ui/icons/breakdance/scalable/stumbleupon.svg (+201/-0)
ui/icons/breakdance/scalable/twitter.svg (+185/-0)
ui/templates/base.mako (+283/-0)
ui/templates/css.mako (+29/-0)
ui/templates/defaultcss.mako (+202/-0)
ui/templates/jquery.js (+152/-0)
ui/templates/navigation.mako (+250/-0)
ui/templates/targetbar.mako (+111/-0)
ui/themes/compact/errors.mako (+27/-0)
ui/themes/compact/home.mako (+25/-0)
ui/themes/compact/jquery.js (+152/-0)
ui/themes/compact/template.mako (+42/-0)
ui/themes/compact/theme.version (+1/-0)
ui/themes/default/errors.mako (+27/-0)
ui/themes/default/home.mako (+25/-0)
ui/themes/default/jquery.js (+152/-0)
ui/themes/default/template.mako (+34/-0)
ui/themes/default/theme.version (+1/-0)
ui/themes/flat/errors.mako (+26/-0)
ui/themes/flat/home.mako (+24/-0)
ui/themes/flat/jquery.js (+152/-0)
ui/themes/flat/template.mako (+28/-0)
ui/themes/flat/theme.css (+20/-0)
ui/themes/flat/theme.version (+1/-0)
ui/themes/gwilouche/errors.mako (+26/-0)
ui/themes/gwilouche/home.mako (+24/-0)
ui/themes/gwilouche/jquery.js (+152/-0)
ui/themes/gwilouche/template.mako (+31/-0)
ui/themes/gwilouche/theme.css (+12/-0)
ui/themes/gwilouche/theme.version (+1/-0)
ui/themes/simple/errors.mako (+26/-0)
ui/themes/simple/home.mako (+24/-0)
ui/themes/simple/jquery.js (+152/-0)
ui/themes/simple/template.mako (+31/-0)
ui/themes/simple/theme.css (+20/-0)
ui/themes/simple/theme.version (+1/-0)
ui/themes/ubuntu/errors.mako (+27/-0)
ui/themes/ubuntu/home.mako (+25/-0)
ui/themes/ubuntu/jquery.js (+32/-0)
ui/themes/ubuntu/main.css (+186/-0)
ui/themes/ubuntu/template.mako (+91/-0)
ui/themes/ubuntu/theme.version (+1/-0)
Conflict adding file AUTHORS.  Moved existing file to AUTHORS.moved.
Conflict adding file COPYING.  Moved existing file to COPYING.moved.
Conflict adding file INSTALL.  Moved existing file to INSTALL.moved.
Conflict adding file MANIFEST.in.  Moved existing file to MANIFEST.in.moved.
Conflict adding file README.  Moved existing file to README.moved.
Conflict adding file bin.  Moved existing file to bin.moved.
Conflict adding file com.Gwibber.Service.service.  Moved existing file to com.Gwibber.Service.service.moved.
Conflict adding file com.GwibberClient.service.  Moved existing file to com.GwibberClient.service.moved.
Conflict adding file gwibber-accounts.desktop.in.  Moved existing file to gwibber-accounts.desktop.in.moved.
Conflict adding file gwibber-poster.1.  Moved existing file to gwibber-poster.1.moved.
Conflict adding file gwibber-preferences.desktop.in.  Moved existing file to gwibber-preferences.desktop.in.moved.
Conflict adding file gwibber.1.  Moved existing file to gwibber.1.moved.
Conflict adding file gwibber.desktop.in.  Moved existing file to gwibber.desktop.in.moved.
Conflict adding file gwibber.desktop.  Moved existing file to gwibber.desktop.moved.
Conflict adding file gwibber.  Moved existing file to gwibber.moved.
Conflict adding file indicator.  Moved existing file to indicator.moved.
Conflict adding file po.  Moved existing file to po.moved.
Conflict adding file setup.cfg.  Moved existing file to setup.cfg.moved.
Conflict adding file setup.py.  Moved existing file to setup.py.moved.
Conflict adding file ui.  Moved existing file to ui.moved.
To merge this branch: bzr merge lp:~dpm/ubuntu/lucid/gwibber/update-pot
Reviewer Review Type Date Requested Status
Ubuntu branches Pending
Review via email: mp+19084@code.launchpad.net
To post a comment you must log in.
Revision history for this message
David Planella (dpm) wrote :

The new version of Gwibber, apart from being awesome, has added a few new translatable strings.

However, these cannot be translated because the POT template is not up to date. Here's a branch that simply updates the POT template after having run 'cd po; intltool-update -p -g gwibber'. Once the file is committed, these strings will be automatically exposed in Launchpad, so that translators can do their work.

I'd recommend always updating the POT template (*) a few days before a new release and sending an announcement to launchpad-translators(at)lists(dot)launchpad(dot)net so that they are aware that there are new strings to translate.

One thing I've also noticed is that gwibber is not using automatic bzr exports of translations. Enabling this will allow maintainers not having to worry anymore about manually exporting translations from Launchpad, as they can be automatically committed daily to a branch of your choice and effectively adding more automation to the translations process. This is also beneficial for translators, which then know that their translations are committed straight away to the project.

* To enable automatic exports, simply go to https://translations.launchpad.net/gwibber/trunk/+translations-settings and choose a branch where translations will be exported to. Direct link: https://translations.launchpad.net/gwibber/trunk/+link-translations-branch.

(Beware of the fact that at the moment, team-owned branches don't work as expected. To work around this, make yourself the owner of the branch, set it as the translations branch, and then make the team the owner of the branch again. More details at https://help.launchpad.net/Translations/YourProject/Exports)

All this shouldn't take more than a few minutes, and will greatly improve the translations process and usability for international users. If you need any help, I'll be glad to give a hand, just ping me on IRC.

(*) In time, Launchpad Translations will grow the ability of automatically regenerating the template, but for now it has to be manually updated.

Revision history for this message
James Westby (james-w) wrote :

Hey David,

Did you intend this change to be made to the Ubuntu package
or upstream?

Thanks,

James

Revision history for this message
David Planella (dpm) wrote :

Hey James,

It was intended to be against gwibber upstream. I cannot recall why I branched off the lucid package, but it might have well been a mistake. Sorry for any confusion this might have caused.

The merge proposal simply updated the POT file, so that new strings could be translatable. It seems that in the meantime it has been updated already upstream, and they've followed the suggestion of using automatic translation exports as well, so I think the merge proposal can be at this point considered obsolete.

Cheers,
David.

Revision history for this message
James Westby (james-w) wrote :

Hi David,

Thanks for letting me know, I've rejected this as it is taken care of.

Thanks,

James

Unmerged revisions

586. By David Planella

Refreshed POT template with new strings

585. By Ryan Paul

Remember position of the sidebar

584. By Ryan Paul

When replying to a thread, don't put original user's name in the message

583. By Ryan Paul

Adjust theme CSS to improve support for dark themes

582. By Ken VanDine

tagged 2.29.1

581. By Ken VanDine

added simple and flat themes to setup.py

580. By Ken VanDine

merged

579. By Ken VanDine

* Fixed up the GwibberPosterVBox to include the Send button
* Fixed some gettext issues so localizing will work when we get
  the translations updated.

578. By Ken VanDine

Install the images for the ubuntu theme in the images dir

577. By Ken VanDine

images shouldn't be executable

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.bzrignore'
2--- .bzrignore 1970-01-01 00:00:00 +0000
3+++ .bzrignore 2010-02-11 10:03:14 +0000
4@@ -0,0 +1,3 @@
5+gwibber/semantic.cache
6+semantic.cache
7+*.mo
8
9=== added file 'AUTHORS'
10--- AUTHORS 1970-01-01 00:00:00 +0000
11+++ AUTHORS 2010-02-11 10:03:14 +0000
12@@ -0,0 +1,26 @@
13+Gwibber Authors
14+---------------
15+
16+ * Ryan Paul (SegPhault) <segphault@arstechnica.com>
17+ * Dominic Evans <oldmanuk@gmail.com>
18+ * Greg Grossmeier <greg.grossmeier@gmail.com>
19+ * Ken VanDine <ken.vandine@canonical.com>
20+
21+Localization and Translations
22+-----------------------------
23+
24+ * Milo Casagrande <milo@ubuntu.com>
25+ * Julián Alarcón <alarconj@ubuntu.com>
26+
27+Other Contributors
28+------------------
29+
30+ * James Westby <jw+debian@jameswestby.net>
31+ * Alexander Sack <asac@jwsdot.com>
32+ * David Futcher <bobbo@ubuntu.com>
33+ * Jussi Kekkonen <jussi.kekkonen@gmail.com>
34+ * Jordan Mantha <laserjock@ubuntu.com>
35+ * Steve Smith <tarkasteve@gmail.com>
36+ * Fabian Neumann
37+ * Bruno Bord
38+ * Michael Elkins
39
40=== renamed file 'AUTHORS' => 'AUTHORS.moved'
41=== added file 'COPYING'
42--- COPYING 1970-01-01 00:00:00 +0000
43+++ COPYING 2010-02-11 10:03:14 +0000
44@@ -0,0 +1,339 @@
45+ GNU GENERAL PUBLIC LICENSE
46+ Version 2, June 1991
47+
48+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
49+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
50+ Everyone is permitted to copy and distribute verbatim copies
51+ of this license document, but changing it is not allowed.
52+
53+ Preamble
54+
55+ The licenses for most software are designed to take away your
56+freedom to share and change it. By contrast, the GNU General Public
57+License is intended to guarantee your freedom to share and change free
58+software--to make sure the software is free for all its users. This
59+General Public License applies to most of the Free Software
60+Foundation's software and to any other program whose authors commit to
61+using it. (Some other Free Software Foundation software is covered by
62+the GNU Lesser General Public License instead.) You can apply it to
63+your programs, too.
64+
65+ When we speak of free software, we are referring to freedom, not
66+price. Our General Public Licenses are designed to make sure that you
67+have the freedom to distribute copies of free software (and charge for
68+this service if you wish), that you receive source code or can get it
69+if you want it, that you can change the software or use pieces of it
70+in new free programs; and that you know you can do these things.
71+
72+ To protect your rights, we need to make restrictions that forbid
73+anyone to deny you these rights or to ask you to surrender the rights.
74+These restrictions translate to certain responsibilities for you if you
75+distribute copies of the software, or if you modify it.
76+
77+ For example, if you distribute copies of such a program, whether
78+gratis or for a fee, you must give the recipients all the rights that
79+you have. You must make sure that they, too, receive or can get the
80+source code. And you must show them these terms so they know their
81+rights.
82+
83+ We protect your rights with two steps: (1) copyright the software, and
84+(2) offer you this license which gives you legal permission to copy,
85+distribute and/or modify the software.
86+
87+ Also, for each author's protection and ours, we want to make certain
88+that everyone understands that there is no warranty for this free
89+software. If the software is modified by someone else and passed on, we
90+want its recipients to know that what they have is not the original, so
91+that any problems introduced by others will not reflect on the original
92+authors' reputations.
93+
94+ Finally, any free program is threatened constantly by software
95+patents. We wish to avoid the danger that redistributors of a free
96+program will individually obtain patent licenses, in effect making the
97+program proprietary. To prevent this, we have made it clear that any
98+patent must be licensed for everyone's free use or not licensed at all.
99+
100+ The precise terms and conditions for copying, distribution and
101+modification follow.
102+
103+ GNU GENERAL PUBLIC LICENSE
104+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
105+
106+ 0. This License applies to any program or other work which contains
107+a notice placed by the copyright holder saying it may be distributed
108+under the terms of this General Public License. The "Program", below,
109+refers to any such program or work, and a "work based on the Program"
110+means either the Program or any derivative work under copyright law:
111+that is to say, a work containing the Program or a portion of it,
112+either verbatim or with modifications and/or translated into another
113+language. (Hereinafter, translation is included without limitation in
114+the term "modification".) Each licensee is addressed as "you".
115+
116+Activities other than copying, distribution and modification are not
117+covered by this License; they are outside its scope. The act of
118+running the Program is not restricted, and the output from the Program
119+is covered only if its contents constitute a work based on the
120+Program (independent of having been made by running the Program).
121+Whether that is true depends on what the Program does.
122+
123+ 1. You may copy and distribute verbatim copies of the Program's
124+source code as you receive it, in any medium, provided that you
125+conspicuously and appropriately publish on each copy an appropriate
126+copyright notice and disclaimer of warranty; keep intact all the
127+notices that refer to this License and to the absence of any warranty;
128+and give any other recipients of the Program a copy of this License
129+along with the Program.
130+
131+You may charge a fee for the physical act of transferring a copy, and
132+you may at your option offer warranty protection in exchange for a fee.
133+
134+ 2. You may modify your copy or copies of the Program or any portion
135+of it, thus forming a work based on the Program, and copy and
136+distribute such modifications or work under the terms of Section 1
137+above, provided that you also meet all of these conditions:
138+
139+ a) You must cause the modified files to carry prominent notices
140+ stating that you changed the files and the date of any change.
141+
142+ b) You must cause any work that you distribute or publish, that in
143+ whole or in part contains or is derived from the Program or any
144+ part thereof, to be licensed as a whole at no charge to all third
145+ parties under the terms of this License.
146+
147+ c) If the modified program normally reads commands interactively
148+ when run, you must cause it, when started running for such
149+ interactive use in the most ordinary way, to print or display an
150+ announcement including an appropriate copyright notice and a
151+ notice that there is no warranty (or else, saying that you provide
152+ a warranty) and that users may redistribute the program under
153+ these conditions, and telling the user how to view a copy of this
154+ License. (Exception: if the Program itself is interactive but
155+ does not normally print such an announcement, your work based on
156+ the Program is not required to print an announcement.)
157+
158+These requirements apply to the modified work as a whole. If
159+identifiable sections of that work are not derived from the Program,
160+and can be reasonably considered independent and separate works in
161+themselves, then this License, and its terms, do not apply to those
162+sections when you distribute them as separate works. But when you
163+distribute the same sections as part of a whole which is a work based
164+on the Program, the distribution of the whole must be on the terms of
165+this License, whose permissions for other licensees extend to the
166+entire whole, and thus to each and every part regardless of who wrote it.
167+
168+Thus, it is not the intent of this section to claim rights or contest
169+your rights to work written entirely by you; rather, the intent is to
170+exercise the right to control the distribution of derivative or
171+collective works based on the Program.
172+
173+In addition, mere aggregation of another work not based on the Program
174+with the Program (or with a work based on the Program) on a volume of
175+a storage or distribution medium does not bring the other work under
176+the scope of this License.
177+
178+ 3. You may copy and distribute the Program (or a work based on it,
179+under Section 2) in object code or executable form under the terms of
180+Sections 1 and 2 above provided that you also do one of the following:
181+
182+ a) Accompany it with the complete corresponding machine-readable
183+ source code, which must be distributed under the terms of Sections
184+ 1 and 2 above on a medium customarily used for software interchange; or,
185+
186+ b) Accompany it with a written offer, valid for at least three
187+ years, to give any third party, for a charge no more than your
188+ cost of physically performing source distribution, a complete
189+ machine-readable copy of the corresponding source code, to be
190+ distributed under the terms of Sections 1 and 2 above on a medium
191+ customarily used for software interchange; or,
192+
193+ c) Accompany it with the information you received as to the offer
194+ to distribute corresponding source code. (This alternative is
195+ allowed only for noncommercial distribution and only if you
196+ received the program in object code or executable form with such
197+ an offer, in accord with Subsection b above.)
198+
199+The source code for a work means the preferred form of the work for
200+making modifications to it. For an executable work, complete source
201+code means all the source code for all modules it contains, plus any
202+associated interface definition files, plus the scripts used to
203+control compilation and installation of the executable. However, as a
204+special exception, the source code distributed need not include
205+anything that is normally distributed (in either source or binary
206+form) with the major components (compiler, kernel, and so on) of the
207+operating system on which the executable runs, unless that component
208+itself accompanies the executable.
209+
210+If distribution of executable or object code is made by offering
211+access to copy from a designated place, then offering equivalent
212+access to copy the source code from the same place counts as
213+distribution of the source code, even though third parties are not
214+compelled to copy the source along with the object code.
215+
216+ 4. You may not copy, modify, sublicense, or distribute the Program
217+except as expressly provided under this License. Any attempt
218+otherwise to copy, modify, sublicense or distribute the Program is
219+void, and will automatically terminate your rights under this License.
220+However, parties who have received copies, or rights, from you under
221+this License will not have their licenses terminated so long as such
222+parties remain in full compliance.
223+
224+ 5. You are not required to accept this License, since you have not
225+signed it. However, nothing else grants you permission to modify or
226+distribute the Program or its derivative works. These actions are
227+prohibited by law if you do not accept this License. Therefore, by
228+modifying or distributing the Program (or any work based on the
229+Program), you indicate your acceptance of this License to do so, and
230+all its terms and conditions for copying, distributing or modifying
231+the Program or works based on it.
232+
233+ 6. Each time you redistribute the Program (or any work based on the
234+Program), the recipient automatically receives a license from the
235+original licensor to copy, distribute or modify the Program subject to
236+these terms and conditions. You may not impose any further
237+restrictions on the recipients' exercise of the rights granted herein.
238+You are not responsible for enforcing compliance by third parties to
239+this License.
240+
241+ 7. If, as a consequence of a court judgment or allegation of patent
242+infringement or for any other reason (not limited to patent issues),
243+conditions are imposed on you (whether by court order, agreement or
244+otherwise) that contradict the conditions of this License, they do not
245+excuse you from the conditions of this License. If you cannot
246+distribute so as to satisfy simultaneously your obligations under this
247+License and any other pertinent obligations, then as a consequence you
248+may not distribute the Program at all. For example, if a patent
249+license would not permit royalty-free redistribution of the Program by
250+all those who receive copies directly or indirectly through you, then
251+the only way you could satisfy both it and this License would be to
252+refrain entirely from distribution of the Program.
253+
254+If any portion of this section is held invalid or unenforceable under
255+any particular circumstance, the balance of the section is intended to
256+apply and the section as a whole is intended to apply in other
257+circumstances.
258+
259+It is not the purpose of this section to induce you to infringe any
260+patents or other property right claims or to contest validity of any
261+such claims; this section has the sole purpose of protecting the
262+integrity of the free software distribution system, which is
263+implemented by public license practices. Many people have made
264+generous contributions to the wide range of software distributed
265+through that system in reliance on consistent application of that
266+system; it is up to the author/donor to decide if he or she is willing
267+to distribute software through any other system and a licensee cannot
268+impose that choice.
269+
270+This section is intended to make thoroughly clear what is believed to
271+be a consequence of the rest of this License.
272+
273+ 8. If the distribution and/or use of the Program is restricted in
274+certain countries either by patents or by copyrighted interfaces, the
275+original copyright holder who places the Program under this License
276+may add an explicit geographical distribution limitation excluding
277+those countries, so that distribution is permitted only in or among
278+countries not thus excluded. In such case, this License incorporates
279+the limitation as if written in the body of this License.
280+
281+ 9. The Free Software Foundation may publish revised and/or new versions
282+of the General Public License from time to time. Such new versions will
283+be similar in spirit to the present version, but may differ in detail to
284+address new problems or concerns.
285+
286+Each version is given a distinguishing version number. If the Program
287+specifies a version number of this License which applies to it and "any
288+later version", you have the option of following the terms and conditions
289+either of that version or of any later version published by the Free
290+Software Foundation. If the Program does not specify a version number of
291+this License, you may choose any version ever published by the Free Software
292+Foundation.
293+
294+ 10. If you wish to incorporate parts of the Program into other free
295+programs whose distribution conditions are different, write to the author
296+to ask for permission. For software which is copyrighted by the Free
297+Software Foundation, write to the Free Software Foundation; we sometimes
298+make exceptions for this. Our decision will be guided by the two goals
299+of preserving the free status of all derivatives of our free software and
300+of promoting the sharing and reuse of software generally.
301+
302+ NO WARRANTY
303+
304+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
305+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
306+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
307+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
308+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
309+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
310+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
311+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
312+REPAIR OR CORRECTION.
313+
314+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
315+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
316+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
317+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
318+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
319+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
320+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
321+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
322+POSSIBILITY OF SUCH DAMAGES.
323+
324+ END OF TERMS AND CONDITIONS
325+
326+ How to Apply These Terms to Your New Programs
327+
328+ If you develop a new program, and you want it to be of the greatest
329+possible use to the public, the best way to achieve this is to make it
330+free software which everyone can redistribute and change under these terms.
331+
332+ To do so, attach the following notices to the program. It is safest
333+to attach them to the start of each source file to most effectively
334+convey the exclusion of warranty; and each file should have at least
335+the "copyright" line and a pointer to where the full notice is found.
336+
337+ <one line to give the program's name and a brief idea of what it does.>
338+ Copyright (C) <year> <name of author>
339+
340+ This program is free software; you can redistribute it and/or modify
341+ it under the terms of the GNU General Public License as published by
342+ the Free Software Foundation; either version 2 of the License, or
343+ (at your option) any later version.
344+
345+ This program is distributed in the hope that it will be useful,
346+ but WITHOUT ANY WARRANTY; without even the implied warranty of
347+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
348+ GNU General Public License for more details.
349+
350+ You should have received a copy of the GNU General Public License along
351+ with this program; if not, write to the Free Software Foundation, Inc.,
352+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
353+
354+Also add information on how to contact you by electronic and paper mail.
355+
356+If the program is interactive, make it output a short notice like this
357+when it starts in an interactive mode:
358+
359+ Gnomovision version 69, Copyright (C) year name of author
360+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
361+ This is free software, and you are welcome to redistribute it
362+ under certain conditions; type `show c' for details.
363+
364+The hypothetical commands `show w' and `show c' should show the appropriate
365+parts of the General Public License. Of course, the commands you use may
366+be called something other than `show w' and `show c'; they could even be
367+mouse-clicks or menu items--whatever suits your program.
368+
369+You should also get your employer (if you work as a programmer) or your
370+school, if any, to sign a "copyright disclaimer" for the program, if
371+necessary. Here is a sample; alter the names:
372+
373+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
374+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
375+
376+ <signature of Ty Coon>, 1 April 1989
377+ Ty Coon, President of Vice
378+
379+This General Public License does not permit incorporating your program into
380+proprietary programs. If your program is a subroutine library, you may
381+consider it more useful to permit linking proprietary applications with the
382+library. If this is what you want to do, use the GNU Lesser General
383+Public License instead of this License.
384
385=== renamed file 'COPYING' => 'COPYING.moved'
386=== added file 'INSTALL'
387--- INSTALL 1970-01-01 00:00:00 +0000
388+++ INSTALL 2010-02-11 10:03:14 +0000
389@@ -0,0 +1,41 @@
390+Installing Gwibber
391+==================
392+
393+ Requirements
394+ ------------
395+
396+ Please note that the version numbers listed below only reflect my test
397+ environment. Gwibber is known to work with those specific versions, but
398+ will probably work fine on most current desktop distributions that include
399+ Python's WebKit GTK+ bindings.
400+
401+ * python (2.5)
402+ * python-dbus (0.80.2)
403+ * python-gtk2 (2.10.4)
404+ * python-gconf (2.18.0)
405+ * python-imaging (1.1.6)
406+ * python-notify (0.1.1)
407+ * python-webkitgtk (1.0.1)
408+ * python-simplejson (1.9.1)
409+ * python-egenix-mxdatetime (3.0.0)
410+ * python-distutils-extra
411+ * python-feedparser (4.1)
412+ * python-xdg (0.15)
413+ * python-mako (0.2.2)
414+
415+ Installation
416+ ------------
417+
418+ Gwibber uses Python's distutils framework for installation. In order to
419+ install Gwibber, you will need root access. To install Gwibber, perform
420+ the following command as root:
421+
422+ $ python setup.py install
423+
424+ Run Gwibber
425+ -----------
426+
427+ If you installed Gwibber using the setup.py script, you can launch the
428+ program by typing "gwibber" at the command line. If you want to run
429+ Gwibber without installing it, start "bin/gwibber" from within the
430+ Gwibber directory.
431
432=== renamed file 'INSTALL' => 'INSTALL.moved'
433=== added file 'MANIFEST.in'
434--- MANIFEST.in 1970-01-01 00:00:00 +0000
435+++ MANIFEST.in 2010-02-11 10:03:14 +0000
436@@ -0,0 +1,16 @@
437+include AUTHORS COPYING INSTALL README
438+include MANIFEST.in MANIFEST
439+include com.Gwibber.Service.service
440+include com.GwibberClient.service
441+include po/*
442+include ui/*
443+include ui/templates/*
444+include ui/themes/*/*
445+include ui/icons/*
446+include ui/icons/*/*
447+include gwibber.1
448+include gwibber-poster.1
449+include gwibber.desktop.in gwibber.desktop
450+include gwibber-preferences.desktop.in gwibber-preferences.desktop
451+include gwibber-accounts.desktop.in gwibber-accounts.desktop
452+include indicator/gwibber
453
454=== renamed file 'MANIFEST.in' => 'MANIFEST.in.moved'
455=== added file 'README'
456--- README 1970-01-01 00:00:00 +0000
457+++ README 2010-02-11 10:03:14 +0000
458@@ -0,0 +1,6 @@
459+Gwibber
460+-------
461+
462+Gwibber is a status management client application for the GNOME desktop
463+environment. It can transmit status message updates to Twitter, Jaiku,
464+Facebook, and Pidgin.
465
466=== renamed file 'README' => 'README.moved'
467=== added directory 'bin'
468=== renamed directory 'bin' => 'bin.moved'
469=== added file 'bin/gwibber'
470--- bin/gwibber 1970-01-01 00:00:00 +0000
471+++ bin/gwibber 2010-02-11 10:03:14 +0000
472@@ -0,0 +1,63 @@
473+#!/usr/bin/env python
474+
475+"""
476+
477+Gwibber Client
478+SegPhault (Ryan Paul) - 05/29/2007
479+
480+"""
481+
482+import sys, logging, gtk, optparse, dbus
483+from os.path import join, dirname, exists, realpath, abspath
484+from dbus.mainloop.glib import DBusGMainLoop
485+
486+######################################################################
487+# Options and configuration
488+def parse_opts():
489+ usage = "usage: %prog [options] <config-file>"
490+
491+ oparse = optparse.OptionParser(usage=usage)
492+
493+ oparse.add_option("-v", "--verbose", action="store_true",
494+ help="Verbose messages", default=False)
495+ oparse.add_option("-d", "--debug", action="store_true",
496+ help="Debug messages", default=False)
497+
498+ opts, rest = oparse.parse_args()
499+ return opts
500+
501+opts = parse_opts()
502+
503+if opts.debug:
504+ logging.root.setLevel(logging.DEBUG)
505+elif opts.verbose:
506+ logging.root.setLevel(logging.INFO)
507+
508+
509+######################################################################
510+# Setup path
511+
512+LAUNCH_DIR = abspath(sys.path[0])
513+SOURCE_DIR = join(LAUNCH_DIR, "..", "gwibber")
514+logging.debug("Launched from %s", LAUNCH_DIR)
515+
516+DBusGMainLoop(set_as_default=True)
517+obj = dbus.SessionBus().get_object("org.desktopcouch.CouchDB", "/")
518+cdb = dbus.Interface(obj, "org.desktopcouch.CouchDB")
519+cdb.getPort()
520+
521+# If we were invoked from a Gwibber source directory add that as the
522+# preferred module path ...
523+if exists(join(SOURCE_DIR, "client.py")):
524+ logging.info("Running Gwibber from the source tree")
525+ sys.path.insert(0, realpath(dirname(SOURCE_DIR)))
526+ try:
527+ from gwibber import client
528+ finally:
529+ del sys.path[0]
530+else:
531+ logging.debug("Running Gwibber from the system path")
532+ from gwibber import client
533+
534+client.Client()
535+gtk.main()
536
537=== added file 'bin/gwibber-accounts'
538--- bin/gwibber-accounts 1970-01-01 00:00:00 +0000
539+++ bin/gwibber-accounts 2010-02-11 10:03:14 +0000
540@@ -0,0 +1,50 @@
541+#!/usr/bin/env python
542+#
543+# Copyright (C) 2010 Canonical Ltd
544+#
545+# This program is free software: you can redistribute it and/or modify
546+# it under the terms of the GNU General Public License version 2 as
547+# published by the Free Software Foundation.
548+#
549+# This program is distributed in the hope that it will be useful,
550+# but WITHOUT ANY WARRANTY; without even the implied warranty of
551+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
552+# GNU General Public License for more details.
553+#
554+# You should have received a copy of the GNU General Public License
555+# along with this program. If not, see <http://www.gnu.org/licenses/>.
556+#
557+# Copyright (C) 2010 Ken VanDine <ken.vandine@canonical.com>
558+#
559+# Launch script for Gwibber Accounts
560+#
561+
562+import logging
563+import sys
564+from os.path import join, dirname, exists, realpath, abspath
565+import gtk, gobject
566+from dbus.mainloop.glib import DBusGMainLoop
567+
568+######################################################################
569+# Setup path
570+
571+LAUNCH_DIR = abspath(sys.path[0])
572+logging.debug("Launched from %s", LAUNCH_DIR)
573+source_tree_gwibber = join(LAUNCH_DIR, "..", "gwibber")
574+
575+# If we were invoked from a Gwibber source directory add that as the
576+# preferred module path ...
577+if exists(join(source_tree_gwibber, "client.py")):
578+ logging.info("Running from source tree; adjusting path")
579+ sys.path.insert(0, realpath(dirname(source_tree_gwibber)))
580+ try:
581+ from gwibber import accounts
582+ finally:
583+ del sys.path[0]
584+else:
585+ logging.debug("Assuming path is correct")
586+ from gwibber import accounts
587+
588+DBusGMainLoop(set_as_default=True)
589+accounts.GwibberAccountManager()
590+gtk.main()
591
592=== added file 'bin/gwibber-poster'
593--- bin/gwibber-poster 1970-01-01 00:00:00 +0000
594+++ bin/gwibber-poster 2010-02-11 10:03:14 +0000
595@@ -0,0 +1,100 @@
596+#!/usr/bin/env python
597+#
598+# Copyright (C) 2010 Canonical Ltd
599+#
600+# This program is free software: you can redistribute it and/or modify
601+# it under the terms of the GNU General Public License version 2 as
602+# published by the Free Software Foundation.
603+#
604+# This program is distributed in the hope that it will be useful,
605+# but WITHOUT ANY WARRANTY; without even the implied warranty of
606+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
607+# GNU General Public License for more details.
608+#
609+# You should have received a copy of the GNU General Public License
610+# along with this program. If not, see <http://www.gnu.org/licenses/>.
611+#
612+# Copyright (C) 2010 Ken VanDine <ken.vandine@canonical.com>
613+#
614+# A poster for Gwibber
615+#
616+
617+
618+######################################################################
619+# Setup path
620+from os.path import join, dirname, exists, realpath, abspath
621+import sys
622+
623+LAUNCH_DIR = abspath(sys.path[0])
624+SOURCE_DIR = join(LAUNCH_DIR, "..", "gwibber")
625+
626+# If we were invoked from a Gwibber source directory add that as the
627+# preferred module path ...
628+if exists(join(SOURCE_DIR, "client.py")):
629+ sys.path.insert(0, realpath(dirname(SOURCE_DIR)))
630+ try:
631+ from gwibber import client
632+ finally:
633+ del sys.path[0]
634+else:
635+ from gwibber import client
636+######################################################################
637+
638+from gwibber.lib.gtk import widgets
639+import gtk
640+import gwibber.gwui, gwibber.util, gwibber.resources
641+
642+from optparse import OptionParser
643+parser = OptionParser()
644+parser.add_option("-p", "--persist", action="store_true",
645+ dest="persist", default=False,
646+ help="Don't exit after posting")
647+parser.add_option("-w", "--window", action="store_true",
648+ dest="window", default=False,
649+ help="Don't exit when the window loses focus, also implies -d")
650+parser.add_option("-d", "--decorate", action="store_true",
651+ dest="decorate", default=False,
652+ help="Display window decorations")
653+parser.add_option("-m", "--message", dest="message",
654+ help="Message to post")
655+(options, args) = parser.parse_args()
656+
657+class GwibberPosterWindow(gtk.Window):
658+ def __init__(self):
659+ gtk.Window.__init__(self)
660+ self.set_default_size(420,140)
661+ self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
662+ self.set_title("Gwibber Poster")
663+ gtk.icon_theme_add_builtin_icon("gwibber", 22,
664+ gtk.gdk.pixbuf_new_from_file_at_size(
665+ gwibber.resources.get_ui_asset("gwibber.svg"), 24, 24))
666+ self.set_icon_name("gwibber")
667+
668+ self.connect("delete-event", self.on_window_close)
669+
670+ if not options.decorate and not options.window:
671+ self.set_decorated(False)
672+
673+ if not options.window:
674+ self.connect("focus-out-event", self.on_window_close)
675+
676+ poster = widgets.GwibberPosterVBox()
677+
678+ poster.input.connect("submit", self.on_input_activate)
679+
680+ self.add(poster)
681+ self.show_all()
682+
683+ if options.message:
684+ poster.input.set_text(options.message)
685+
686+ def on_input_activate(self, *args):
687+ if not options.persist:
688+ self.on_window_close()
689+
690+ def on_window_close(self, *args):
691+ gtk.main_quit()
692+
693+
694+w = GwibberPosterWindow()
695+gtk.main()
696
697=== added file 'bin/gwibber-preferences'
698--- bin/gwibber-preferences 1970-01-01 00:00:00 +0000
699+++ bin/gwibber-preferences 2010-02-11 10:03:14 +0000
700@@ -0,0 +1,49 @@
701+#!/usr/bin/env python
702+#
703+# Copyright (C) 2010 Canonical Ltd
704+#
705+# This program is free software: you can redistribute it and/or modify
706+# it under the terms of the GNU General Public License version 2 as
707+# published by the Free Software Foundation.
708+#
709+# This program is distributed in the hope that it will be useful,
710+# but WITHOUT ANY WARRANTY; without even the implied warranty of
711+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
712+# GNU General Public License for more details.
713+#
714+# You should have received a copy of the GNU General Public License
715+# along with this program. If not, see <http://www.gnu.org/licenses/>.
716+#
717+# Copyright (C) 2010 Ken VanDine <ken.vandine@canonical.com>
718+#
719+# Launch script for Gwibber Preferences
720+#
721+
722+
723+import logging
724+import sys
725+from os.path import join, dirname, exists, realpath, abspath
726+import gtk, gobject
727+
728+######################################################################
729+# Setup path
730+
731+LAUNCH_DIR = abspath(sys.path[0])
732+logging.debug("Launched from %s", LAUNCH_DIR)
733+source_tree_gwibber = join(LAUNCH_DIR, "..", "gwibber")
734+
735+# If we were invoked from a Gwibber source directory add that as the
736+# preferred module path ...
737+if exists(join(source_tree_gwibber, "client.py")):
738+ logging.info("Running from source tree; adjusting path")
739+ sys.path.insert(0, realpath(dirname(source_tree_gwibber)))
740+ try:
741+ from gwibber import preferences
742+ finally:
743+ del sys.path[0]
744+else:
745+ logging.debug("Assuming path is correct")
746+ from gwibber import preferences
747+
748+preferences.GwibberPreferences()
749+gtk.main()
750
751=== added file 'bin/gwibber-service'
752--- bin/gwibber-service 1970-01-01 00:00:00 +0000
753+++ bin/gwibber-service 2010-02-11 10:03:14 +0000
754@@ -0,0 +1,46 @@
755+#!/usr/bin/env python
756+
757+import sys, logging, optparse, gobject, dbus
758+from os.path import join, dirname, exists, realpath, abspath
759+
760+from dbus.mainloop.glib import DBusGMainLoop
761+
762+logging.basicConfig(level=logging.DEBUG)
763+
764+DBusGMainLoop(set_as_default=True)
765+loop = gobject.MainLoop()
766+
767+obj = dbus.SessionBus().get_object("org.desktopcouch.CouchDB", "/")
768+cdb = dbus.Interface(obj, "org.desktopcouch.CouchDB")
769+cdb.getPort()
770+
771+LAUNCH_DIR = abspath(sys.path[0])
772+SOURCE_DIR = join(LAUNCH_DIR, "..", "gwibber")
773+DISPATCHER = join(SOURCE_DIR, "microblog", "dispatcher.py")
774+
775+# If we were invoked from a Gwibber source directory add that as the
776+# preferred module path ...
777+
778+if exists(DISPATCHER):
779+ logging.debug("Running Gwibber from the source tree")
780+ sys.path.insert(0, realpath(dirname(SOURCE_DIR)))
781+
782+ try:
783+ from gwibber.microblog import dispatcher
784+ finally:
785+ del sys.path[0]
786+
787+else:
788+ logging.debug("Running Gwibber from the system path")
789+ from gwibber.microblog import dispatcher
790+
791+connection_monitor = dispatcher.ConnectionMonitor()
792+account_monitor = dispatcher.AccountMonitor()
793+stream_monitor = dispatcher.StreamMonitor()
794+message_monitor = dispatcher.MessagesMonitor()
795+urlshortener = dispatcher.URLShorten()
796+
797+dispatcher = dispatcher.Dispatcher(loop)
798+dispatcher.daemon = True
799+dispatcher.start()
800+loop.run()
801
802=== added file 'com.Gwibber.Service.service'
803--- com.Gwibber.Service.service 1970-01-01 00:00:00 +0000
804+++ com.Gwibber.Service.service 2010-02-11 10:03:14 +0000
805@@ -0,0 +1,3 @@
806+[D-BUS Service]
807+Name=com.Gwibber.Service
808+Exec=/usr/bin/gwibber-service
809
810=== renamed file 'com.Gwibber.Service.service' => 'com.Gwibber.Service.service.moved'
811=== added file 'com.GwibberClient.service'
812--- com.GwibberClient.service 1970-01-01 00:00:00 +0000
813+++ com.GwibberClient.service 2010-02-11 10:03:14 +0000
814@@ -0,0 +1,3 @@
815+[D-BUS Service]
816+Name=com.GwibberClient
817+Exec=/usr/bin/gwibber
818
819=== renamed file 'com.GwibberClient.service' => 'com.GwibberClient.service.moved'
820=== added directory 'gwibber'
821=== added file 'gwibber-accounts.desktop.in'
822--- gwibber-accounts.desktop.in 1970-01-01 00:00:00 +0000
823+++ gwibber-accounts.desktop.in 2010-02-11 10:03:14 +0000
824@@ -0,0 +1,9 @@
825+[Desktop Entry]
826+_Name=Gwibber
827+_GenericName=Social Accounts
828+_X-GNOME-FullName=Gwibber Social Accounts
829+Type=Application
830+_Comment=Microblogging client for GNOME
831+Exec=gwibber-accounts
832+Icon=gwibber
833+Categories=GTK;GNOME;Settings;
834
835=== renamed file 'gwibber-accounts.desktop.in' => 'gwibber-accounts.desktop.in.moved'
836=== added file 'gwibber-poster.1'
837--- gwibber-poster.1 1970-01-01 00:00:00 +0000
838+++ gwibber-poster.1 2010-02-11 10:03:14 +0000
839@@ -0,0 +1,31 @@
840+.\" Process this file with
841+.\" groff -man -Tascii gwibber.1
842+.\"
843+.TH GWIBBER-POSTER 1 "JANUARY 2010" Linux "User Manuals"
844+.SH NAME
845+Gwibber \- Microblogging client for GNOME
846+.SH SYNOPSIS
847+.B gwibber-poster [
848+.I OPTIONS
849+.B ]
850+.SH DESCRIPTION
851+.B Gwibber
852+is an open source microblogging client for GNOME developed with Python and GTK. It supports Twitter, Jaiku, Identi.ca, Facebook, Flickr, Digg, and RSS.
853+.SH OPTIONS
854+.IP "-h, --help"
855+Show help message and exit
856+.IP "-p, --persist"
857+Don't exit after posting
858+.IP "-w, --window"
859+Don't exit when the window loses focus, also implies -d
860+.IP "-d, --decorate"
861+Display window decorations
862+.IP "-m, --message"
863+Message to post
864+.SH BUGS
865+Please report all bugs to the Gwibber bug tracker:
866+http://bugs.launchpad.net/gwibber
867+.SH AUTHOR
868+Ken VanDine <ken.vandine@canonical.com>
869+
870+Copyright 2010. Licensed under a Creative Commons Attribution-ShareAlike License <http://creativecommons.org/licenses/by-sa/3.0/>
871
872=== renamed file 'gwibber-poster.1' => 'gwibber-poster.1.moved'
873=== added file 'gwibber-preferences.desktop.in'
874--- gwibber-preferences.desktop.in 1970-01-01 00:00:00 +0000
875+++ gwibber-preferences.desktop.in 2010-02-11 10:03:14 +0000
876@@ -0,0 +1,9 @@
877+[Desktop Entry]
878+_Name=Gwibber
879+_GenericName=Social Settings
880+_X-GNOME-FullName=Gwibber Social Settings
881+Type=Application
882+_Comment=Microblogging client for GNOME
883+Exec=gwibber-preferences
884+Icon=gwibber
885+Categories=GTK;GNOME;Settings;
886
887=== renamed file 'gwibber-preferences.desktop.in' => 'gwibber-preferences.desktop.in.moved'
888=== added file 'gwibber.1'
889--- gwibber.1 1970-01-01 00:00:00 +0000
890+++ gwibber.1 2010-02-11 10:03:14 +0000
891@@ -0,0 +1,27 @@
892+.\" Process this file with
893+.\" groff -man -Tascii gwibber.1
894+.\"
895+.TH GWIBBER 1 "FEBRUARY 2009" Linux "User Manuals"
896+.SH NAME
897+Gwibber \- Microblogging client for GNOME
898+.SH SYNOPSIS
899+.B gwibber [
900+.I OPTIONS
901+.B ]
902+.SH DESCRIPTION
903+.B Gwibber
904+is an open source microblogging client for GNOME developed with Python and GTK. It supports Twitter, Jaiku, Identi.ca, Facebook, Flickr, Digg, and RSS.
905+.SH OPTIONS
906+.IP "-h, --help"
907+Show help message and exit
908+.IP "-v, --verbose"
909+Verbose messages
910+.IP "-d, --debug"
911+Debug messages
912+.SH BUGS
913+Please report all bugs to the Gwibber bug tracker:
914+http://launchpad.net/gwibber
915+.SH AUTHOR
916+Greg Grossmeier <greg@grossmeier.net>
917+
918+Copyright 2009. Licensed under a Creative Commons Attribution-ShareAlike License <http://creativecommons.org/licenses/by-sa/3.0/>
919
920=== renamed file 'gwibber.1' => 'gwibber.1.moved'
921=== added file 'gwibber.desktop'
922--- gwibber.desktop 1970-01-01 00:00:00 +0000
923+++ gwibber.desktop 2010-02-11 10:03:14 +0000
924@@ -0,0 +1,56 @@
925+[Desktop Entry]
926+Name=Gwibber
927+X-GNOME-FullName=Gwibber Social Client
928+Type=Application
929+Comment=Microblogging client for GNOME
930+Comment[ar]=خادم المدونات القصيرة لجنوم
931+Comment[bg]=Микроблог клиент за GNOME
932+Comment[ca]=Client de Microblogging per a GNOME
933+Comment[cy]=Cleient microblogio ar gyfer GNOME
934+Comment[da]=Mikroblogging klient til GNOME
935+Comment[de]=Mikro-Blogging-Klient für GNOME
936+Comment[en_CA]=Microblogging client for GNOME
937+Comment[en_GB]=Microblogging client for GNOME
938+Comment[es]=Cliente de microblogging para GNOME
939+Comment[fi]=Mikrobloggausohjelma Gnomelle
940+Comment[fr]=Client de microblogging pour GNOME
941+Comment[it]=Client GNOME per micro-blog
942+Comment[lo]=Microblogging client ສຳລັບ GNOME
943+Comment[nb]=Mikroblogging klient for GNOME
944+Comment[nl]=Microblogging-cliënt voor GNOME
945+Comment[pl]=Klient mikroblogowania dla GNOME
946+Comment[pt]=Cliente de Microblogging para o GNOME
947+Comment[pt_BR]=Cliente microblog para GNOME
948+Comment[ru]=Клиент GNOME для микроблоггинга
949+Comment[sv]=Mikroblogg klient för GNOME
950+Comment[tr]=GNOME için mikroblog istemcisi
951+Comment[uk]=Клієнт Мікроблоґування длч GNOME
952+Comment[zh_CN]=基于GNOME的微博客客户端
953+Exec=gwibber
954+Icon=gwibber
955+Categories=GTK;GNOME;Network;
956+GenericName=Microblogging Client
957+GenericName[ar]=خادم المدونات القصيرة
958+GenericName[bg]=Микроблог клиент
959+GenericName[ca]=Client de Microblogging
960+GenericName[cy]=Cleient Microblogio
961+GenericName[da]=Mikroblogging klient
962+GenericName[de]=Mikro-Blogging-Klient
963+GenericName[en_CA]=Microblogging Client
964+GenericName[en_GB]=Microblogging Client
965+GenericName[es]=Cliente de microblogging
966+GenericName[fi]=Mikrobloggausohjelma
967+GenericName[fr]=Client de Microblogging
968+GenericName[id]=Microblogging Client
969+GenericName[it]=Client per micro-blog
970+GenericName[lo]=Microblogging Client
971+GenericName[nb]=Mikroblogging Klient
972+GenericName[nl]=Microblogging-cliënt
973+GenericName[pl]=Klient mikroblogowania
974+GenericName[pt]=Cliente de Microbogging
975+GenericName[pt_BR]=Cliente Microblog
976+GenericName[ru]=Клиент для микроблоггинга
977+GenericName[sv]=Mikroblogg Klient
978+GenericName[tr]=Mikroblog İstemcisi
979+GenericName[uk]=Клієнт Мікроблоґування
980+GenericName[zh_CN]=微博客客户端
981
982=== added file 'gwibber.desktop.in'
983--- gwibber.desktop.in 1970-01-01 00:00:00 +0000
984+++ gwibber.desktop.in 2010-02-11 10:03:14 +0000
985@@ -0,0 +1,9 @@
986+[Desktop Entry]
987+_Name=Gwibber
988+_GenericName=Social Client
989+_X-GNOME-FullName=Gwibber Social Client
990+Type=Application
991+_Comment=Microblogging client for GNOME
992+Exec=gwibber
993+Icon=gwibber
994+Categories=GTK;GNOME;Network;
995
996=== renamed file 'gwibber.desktop.in' => 'gwibber.desktop.in.moved'
997=== renamed file 'gwibber.desktop' => 'gwibber.desktop.moved'
998=== renamed directory 'gwibber' => 'gwibber.moved'
999=== added file 'gwibber/__init__.py'
1000=== added file 'gwibber/accounts.py'
1001--- gwibber/accounts.py 1970-01-01 00:00:00 +0000
1002+++ gwibber/accounts.py 2010-02-11 10:03:14 +0000
1003@@ -0,0 +1,331 @@
1004+#
1005+# Copyright (C) 2010 Canonical Ltd
1006+#
1007+# This program is free software: you can redistribute it and/or modify
1008+# it under the terms of the GNU General Public License version 2 as
1009+# published by the Free Software Foundation.
1010+#
1011+# This program is distributed in the hope that it will be useful,
1012+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1013+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1014+# GNU General Public License for more details.
1015+#
1016+# You should have received a copy of the GNU General Public License
1017+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1018+#
1019+# Copyright (C) 2010 Ken VanDine <ken.vandine@canonical.com>
1020+#
1021+# Accounts interface for Gwibber
1022+#
1023+
1024+import pygtk
1025+try:
1026+ pygtk.require("2.0")
1027+except:
1028+ print "Requires pygtk 2.0 or later"
1029+ exit
1030+
1031+import gettext
1032+from gettext import lgettext as _
1033+if hasattr(gettext, 'bind_textdomain_codeset'):
1034+ gettext.bind_textdomain_codeset('gwibber','UTF-8')
1035+gettext.textdomain('gwibber')
1036+
1037+from . import resources
1038+import gtk
1039+import json
1040+import gwibber.lib
1041+import gwibber.gwui
1042+import gwibber.util
1043+from gwibber.lib.gtk import *
1044+
1045+from microblog.util.const import *
1046+
1047+from desktopcouch.records.server import CouchDatabase
1048+from desktopcouch.records.record import Record as CouchRecord
1049+
1050+from dbus.mainloop.glib import DBusGMainLoop
1051+DBusGMainLoop(set_as_default=True)
1052+
1053+
1054+
1055+class GwibberAccountManager(object):
1056+
1057+ def __init__(self):
1058+ self.pub = gwibber.lib.GwibberPublic()
1059+ self.ui = gtk.Builder()
1060+ self.ui.add_from_file (resources.get_ui_asset("gwibber-accounts-dialog.ui"))
1061+ self.ui.connect_signals(self)
1062+ dialog = self.ui.get_object("accounts_dialog")
1063+ gtk.icon_theme_add_builtin_icon("gwibber", 22,
1064+ gtk.gdk.pixbuf_new_from_file_at_size(
1065+ resources.get_ui_asset("gwibber.svg"), 24, 24))
1066+ dialog.set_icon_name("gwibber")
1067+ self.accounts_tree = self.ui.get_object("accounts_tree")
1068+
1069+ #self.pub.MonitorAccountChanged(self.load_accounts)
1070+ #self.pub.MonitorAccountDeleted(self.load_accounts)
1071+
1072+ dialog.show_all()
1073+
1074+ # hide the help button until we have help :)
1075+ button_help = self.ui.get_object("button_help")
1076+ button_help.hide()
1077+
1078+ self.gwibber_service = gwibber.util.getbus("Service")
1079+
1080+ # FIXME: this should check for configured accounts, and if there are any hide this
1081+ self.ui.get_object('frame_new_account').hide()
1082+ self.ui.get_object('vbox_save').hide()
1083+ self.ui.get_object('vbox_create').hide()
1084+
1085+ self.protocols = json.loads(self.pub.GetServices())
1086+ self.load_accounts()
1087+ self.icon_column = gtk.TreeViewColumn('Icon')
1088+ self.name_column = gtk.TreeViewColumn('Account')
1089+ self.accounts_tree.append_column(self.icon_column)
1090+ self.accounts_tree.append_column(self.name_column)
1091+ self.celltxt = gtk.CellRendererText()
1092+ self.cellimg = gtk.CellRendererPixbuf()
1093+ self.icon_column.pack_start(self.cellimg, False)
1094+ self.name_column.pack_start(self.celltxt, True)
1095+ self.icon_column.add_attribute(self.cellimg, 'pixbuf', 1)
1096+ self.name_column.add_attribute(self.celltxt, 'text', 0)
1097+ if len(self.accounts_tree.get_model()) > 0:
1098+ self.accounts_tree.set_cursor(0)
1099+
1100+
1101+ def account_verify(self, account):
1102+ for required in self.protocols[account['protocol']]['config']:
1103+ try:
1104+ required = required.split(":")[1]
1105+ except:
1106+ pass
1107+ if not self.account.has_key(required):
1108+ return False
1109+ return True
1110+
1111+ def load_accounts(self, *args):
1112+ accounts_store = gtk.TreeStore(
1113+ str, # Account name
1114+ gtk.gdk.Pixbuf, # Icon
1115+ str) # Protocol properties
1116+ self.accounts_tree.set_model(accounts_store)
1117+ self.accounts = json.loads(self.pub.GetAccounts())
1118+ if len(self.accounts) == 0:
1119+ self.add_account()
1120+ return
1121+ for self.account in self.accounts:
1122+ if self.account_verify(self.account):
1123+ try:
1124+ icf = resources.icon(self.account["protocol"], use_theme=False) or resources.get_ui_asset("icons/breakdance/16x16/%s.png" % self.account["protocol"])
1125+ icon = gtk.gdk.pixbuf_new_from_file(icf)
1126+ except:
1127+ icf = resources.get_ui_asset("gwibber.svg")
1128+ icon = gtk.icon_theme_add_builtin_icon("gwibber", 22,
1129+ gtk.gdk.pixbuf_new_from_file_at_size(
1130+ resources.get_ui_asset("gwibber.svg"), 16, 16))
1131+ accounts_store.append(None, [self.account["protocol"] + " (" + self.account["username"] + ")", icon, self.account])
1132+ if len(self.accounts_tree.get_model()) > 0:
1133+ self.accounts_tree.set_cursor(0)
1134+ else:
1135+ self.add_account()
1136+
1137+ def on_edit_account(self, widget, data=None):
1138+ self.ui.get_object('vbox_save').show()
1139+ self.ui.get_object('vbox_create').hide()
1140+ #if self.button_edit_save_handler_id:
1141+ # self.ui.get_object("button_edit_save").disconnect(self.button_edit_save_handler_id)
1142+ self.button_edit_save_handler_id = self.ui.get_object("button_edit_save").connect_after("clicked", self.on_edit_account_save)
1143+ self.button_edit_cancel_handler_id = self.ui.get_object("button_edit_cancel").connect_after("clicked", self.on_edit_account_cancel)
1144+ self.button_create_save_handler_id = self.ui.get_object("button_create_save").connect_after("clicked", self.on_edit_account_save)
1145+ self.button_create_cancel_handler_id = self.ui.get_object("button_create_cancel").connect_after("clicked", self.on_edit_account_cancel)
1146+
1147+ def on_edit_account_cancel(self, widget):
1148+ self.ui.get_object('vbox_save').hide()
1149+ self.ui.get_object('vbox_create').hide()
1150+ self.ui.get_object("button_edit_cancel").disconnect(self.button_edit_cancel_handler_id)
1151+ self.ui.get_object("button_create_cancel").disconnect(self.button_create_cancel_handler_id)
1152+ if len(self.accounts_tree.get_model()) > 0:
1153+ self.accounts_tree.set_cursor(0)
1154+ else:
1155+ self.add_account()
1156+
1157+ def on_account_delete(self, widget):
1158+ accounts = CouchDatabase(COUCH_DB_ACCOUNTS, create=True)
1159+ if self.account.has_key("_id"):
1160+ if accounts.record_exists(self.account["_id"]):
1161+ accounts.delete_record(self.account["_id"])
1162+ self.load_accounts()
1163+ if len(self.accounts_tree.get_model()) > 0:
1164+ self.accounts_tree.set_cursor(0)
1165+ else:
1166+ self.add_account()
1167+
1168+ def on_edit_account_save(self, widget):
1169+ self.get_account_data()
1170+ accounts = CouchDatabase(COUCH_DB_ACCOUNTS, create=True)
1171+ if not self.account_verify(self.account):
1172+ return False
1173+ if self.account.has_key("_id"):
1174+ if accounts.record_exists(self.account["_id"]):
1175+ accounts.update_fields(self.account["_id"], self.account)
1176+ else:
1177+ id = accounts.put_record(CouchRecord(self.account, record_type=COUCH_TYPE_ACCOUNT))
1178+ self.gwibber_service.PerformOp('{"account": "%s"}' % id)
1179+ self.ui.get_object('vbox_save').hide()
1180+ self.ui.get_object('vbox_create').hide()
1181+ self.ui.get_object("button_edit_save").disconnect(self.button_edit_save_handler_id)
1182+ self.ui.get_object("button_create_save").disconnect(self.button_create_save_handler_id)
1183+ # refresh the account tree
1184+ self.load_accounts()
1185+ return True
1186+
1187+ def on_button_add_activate(self, widget=None, data=None):
1188+ self.add_account()
1189+
1190+ def add_account(self):
1191+ # Populate protocols combobox
1192+ self.ui.get_object('frame_new_account').show()
1193+ self.ui.get_object('vbox_details').hide()
1194+ self.ui.get_object('vbox_account').hide()
1195+ self.ui.get_object('vbox_save').hide()
1196+ self.ui.get_object('vbox_create').hide()
1197+ protocol_combobox = self.ui.get_object("protocol_combobox")
1198+ protocol_store = gtk.ListStore(
1199+ str, # Protocol title
1200+ gtk.gdk.Pixbuf, # Icon
1201+ str) # Protocol properties
1202+
1203+ for name, properties in self.protocols.items():
1204+ icf = resources.icon(name, use_theme=False) or resources.get_ui_asset("icons/breakdance/16x16/%s.png" % name)
1205+ icon = gtk.gdk.pixbuf_new_from_file(icf)
1206+ protocol_store.append((properties['name'], icon, dict([('name', name), ('properties', properties)])))
1207+ protocol_combobox.clear()
1208+ protocol_combobox.set_model(protocol_store)
1209+ self.protocol_cell_txt = gtk.CellRendererText()
1210+ self.protocol_cell_img = gtk.CellRendererPixbuf()
1211+ protocol_combobox.pack_start(self.protocol_cell_img, False)
1212+ protocol_combobox.pack_start(self.protocol_cell_txt, False)
1213+ protocol_combobox.add_attribute(self.protocol_cell_img, 'pixbuf', 1)
1214+ protocol_combobox.add_attribute(self.protocol_cell_txt, 'text', 0)
1215+ protocol_combobox.set_active(0)
1216+ # End populate protocols combobox
1217+
1218+ def on_button_create_clicked(self, widget, data=None):
1219+ model = widget.get_model()
1220+ iter = widget.get_active_iter()
1221+ protocol_title = model.get_value(iter, 0)
1222+ icon = model.get_value(iter, 1)
1223+ protocol = eval(model.get_value(iter, 2))
1224+ self.account_show(protocol=protocol, icon=icon)
1225+
1226+ def account_show(self, protocol=None, icon=None):
1227+ if protocol is not None:
1228+ new_account = True
1229+ else:
1230+ new_account = False
1231+ protocol = dict([('name', self.account['protocol']), ('properties', self.protocols[self.account['protocol']])])
1232+
1233+ vbox_account = self.ui.get_object('vbox_account')
1234+ for child in vbox_account.get_children():
1235+ child.destroy()
1236+ vbox_details = self.ui.get_object('vbox_details')
1237+ self.ui.get_object('vbox_details').show()
1238+ self.ui.get_object('vbox_account').show()
1239+ self.ui.get_object('frame_new_account').hide()
1240+ label = self.ui.get_object('label_name')
1241+ image_type = self.ui.get_object('image_type')
1242+ label.set_label(protocol["properties"]["name"])
1243+ if icon:
1244+ image_type.set_from_pixbuf(icon)
1245+ try:
1246+ if not new_account:
1247+ self.aw = eval(protocol["name"]).AccountWidget(self.account)
1248+ else:
1249+ self.aw = eval(protocol["name"]).AccountWidget()
1250+ self.aw.show()
1251+ except NameError:
1252+ print ("%s not available", protocol["name"])
1253+ vbox_account.pack_start(self.aw, False, False)
1254+ vbox_account.show()
1255+ if new_account:
1256+ self.account = {}
1257+ self.account["protocol"] = protocol["name"]
1258+ self.ui.get_object('vbox_create').show()
1259+ self.button_edit_save_handler_id = self.ui.get_object("button_edit_save").connect_after("clicked", self.on_edit_account_save)
1260+ self.button_edit_cancel_handler_id = self.ui.get_object("button_edit_cancel").connect_after("clicked", self.on_edit_account_cancel)
1261+ self.button_create_save_handler_id = self.ui.get_object("button_create_save").connect_after("clicked", self.on_edit_account_save)
1262+ self.button_create_cancel_handler_id = self.ui.get_object("button_create_cancel").connect_after("clicked", self.on_edit_account_cancel)
1263+ self.aw.account = self.account
1264+ self.populate_account_data()
1265+
1266+ def populate_account_data(self):
1267+ # set the default color based on default defined by the service module
1268+ if self.protocols[self.account["protocol"]].has_key("color"):
1269+ self.aw.ui.get_object("color").set_color(gtk.color_selection_palette_from_string(self.protocols[self.account["protocol"]]["color"])[0])
1270+ try:
1271+ for config in self.protocols[self.account["protocol"]]["config"]:
1272+ if config == "private:password": config = "password"
1273+ if isinstance (self.aw.ui.get_object(config), gtk.Entry):
1274+ self.aw.ui.get_object(config).set_text(self.account[config])
1275+ self.aw.ui.get_object(config).connect("changed", self.on_edit_account)
1276+ if isinstance (self.aw.ui.get_object(config), gtk.CheckButton):
1277+ self.aw.ui.get_object(config).set_active(self.account[config])
1278+ self.aw.ui.get_object(config).connect("toggled", self.on_edit_account)
1279+ if isinstance (self.aw.ui.get_object(config), gtk.ColorButton):
1280+ self.aw.ui.get_object(config).set_color(gtk.color_selection_palette_from_string(self.account[config])[0])
1281+ self.aw.ui.get_object(config).connect("color-set", self.on_edit_account)
1282+ except:
1283+ pass
1284+
1285+ def get_account_data(self):
1286+ try:
1287+ del self.account["_rev"]
1288+ except:
1289+ pass
1290+ try:
1291+ for value in self.aw.account:
1292+ self.account[value] = self.aw.account[value]
1293+ except:
1294+ pass
1295+
1296+ for config in self.protocols[self.account["protocol"]]["config"]:
1297+ if config == "private:password": config = "password"
1298+ try:
1299+ try:
1300+ value = self.aw.ui.get_object(config).get_text()
1301+ self.account[config] = value
1302+ except:
1303+ pass
1304+ try:
1305+ value = self.aw.ui.get_object(config).get_property("active")
1306+ self.account[config] = value
1307+ except:
1308+ pass
1309+ try:
1310+ value = gtk.color_selection_palette_to_string(gtk.color_selection_palette_from_string(
1311+ self.aw.ui.get_object(config).get_color().to_string()))
1312+ self.account[config] = value
1313+ except:
1314+ pass
1315+ except:
1316+ break
1317+
1318+ def on_accounts_dialog_destroy(self, widget, data=None):
1319+ gtk.main_quit()
1320+
1321+ def on_accounts_tree_row_activated(self, widget, data=None):
1322+ icon = None
1323+ model, rows = widget.get_selection().get_selected_rows()
1324+ if rows:
1325+ for row in rows:
1326+ iter = model.get_iter(row)
1327+ self.account = eval(model.get_value(iter, 2))
1328+ icon = model.get_value(iter, 1)
1329+ self.ui.get_object('vbox_save').hide()
1330+ self.ui.get_object('vbox_create').hide()
1331+ self.account_show(icon=icon)
1332+
1333+ def on_button_close_clicked(self, widget, data=None):
1334+ gtk.main_quit()
1335
1336=== added file 'gwibber/actions.py'
1337--- gwibber/actions.py 1970-01-01 00:00:00 +0000
1338+++ gwibber/actions.py 2010-02-11 10:03:14 +0000
1339@@ -0,0 +1,164 @@
1340+import gtk, gwui, microblog, resources, util, json
1341+from microblog.util.const import *
1342+
1343+class MessageAction:
1344+ icon = None
1345+ label = None
1346+
1347+ @classmethod
1348+ def get_icon_path(self, size=16, use_theme=True):
1349+ return resources.icon(self.icon, size, use_theme)
1350+
1351+ @classmethod
1352+ def include(self, client, msg):
1353+ return self.__name__ in client.model.services[msg["protocol"]]["features"]
1354+
1355+ @classmethod
1356+ def action(self, w, client, msg):
1357+ pass
1358+
1359+class reply(MessageAction):
1360+ icon = "mail-reply-sender"
1361+ label = "_Reply"
1362+
1363+ @classmethod
1364+ def action(self, w, client, msg):
1365+ client.reply(msg)
1366+
1367+class thread(MessageAction):
1368+ icon = "mail-reply-all"
1369+ label = "View reply t_hread"
1370+
1371+ @classmethod
1372+ def action(self, w, client, msg):
1373+ tab_label = msg.original_title if hasattr(msg, "original_title") else msg.text
1374+ client.add_transient_stream(msg.account, "thread",
1375+ "message:/" + msg.gwibber_path, "Thread")
1376+
1377+class retweet(MessageAction):
1378+ icon = "mail-forward"
1379+ label = "R_etweet"
1380+
1381+ @classmethod
1382+ def action(self, w, client, msg):
1383+ if "retweet" in client.model.services[msg["protocol"]]["features"]:
1384+ text = RETWEET_FORMATS[client.model.settings["retweet_style"]]
1385+ symbol = "RD" if msg["protocol"] == "identica" else "RT"
1386+ text = text.format(text=msg["text"], nick=msg["sender"]["nick"], R=symbol)
1387+
1388+ if not client.model.settings["global_retweet"]:
1389+ client.message_target.set_target(msg, "repost")
1390+
1391+ client.input.set_text(text)
1392+ client.input.textview.grab_focus()
1393+ buf = client.input.textview.get_buffer()
1394+ buf.place_cursor(buf.get_end_iter())
1395+
1396+class like(MessageAction):
1397+ icon = "bookmark_add"
1398+ label = "_Like this message"
1399+
1400+ @classmethod
1401+ def action(self, w, client, msg):
1402+ client.model.service.PerformOp(json.dumps({
1403+ "account": msg["account"],
1404+ "operation": "like",
1405+ "args": {"message": msg},
1406+ "transient": False,
1407+ }))
1408+
1409+ d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK)
1410+ d.set_markup("You have marked this message as liked.")
1411+ if d.run(): d.destroy()
1412+
1413+class delete(MessageAction):
1414+ icon = "gtk-delete"
1415+ label = "_Delete this message"
1416+
1417+ @classmethod
1418+ def action(self, w, client, msg):
1419+ client.model.service.PerformOp(json.dumps({
1420+ "account": msg["account"],
1421+ "operation": "delete",
1422+ "args": {"message": msg},
1423+ "transient": False,
1424+ }))
1425+
1426+ d = gtk.MessageDialog(buttons=gtk.BUTTONS_OK)
1427+ d.set_markup("The message has been deleted.")
1428+ if d.run(): d.destroy()
1429+
1430+ @classmethod
1431+ def include(self, client, msg):
1432+ if "delete" in client.model.services[msg["protocol"]]["features"]:
1433+ if msg.get("sender", {}).get("is_me", 0):
1434+ return True
1435+
1436+class search(MessageAction):
1437+ icon = "gtk-find"
1438+ label = "_Search for a query"
1439+
1440+ @classmethod
1441+ def action(self, w, client, query=None):
1442+ pass
1443+
1444+class read(MessageAction):
1445+ icon = "mail-read"
1446+ label = "View _Message"
1447+
1448+ @classmethod
1449+ def action(self, w, client, msg):
1450+ util.load_url(msg["url"])
1451+
1452+ @classmethod
1453+ def include(self, client, msg):
1454+ return "url" in msg
1455+
1456+class user(MessageAction):
1457+ icon = "face-monkey"
1458+ label = "View user _Profile"
1459+
1460+ @classmethod
1461+ def action(self, w, client, acct=None, name=None):
1462+ client.add_stream({
1463+ "name": name,
1464+ "account": acct,
1465+ "operation": "user_messages",
1466+ "parameters": {"id": name, "count": 50},
1467+ })
1468+
1469+class menu(MessageAction):
1470+ @classmethod
1471+ def action(self, w, client, msg):
1472+ client.on_message_action_menu(msg)
1473+
1474+class tag(MessageAction):
1475+ @classmethod
1476+ def action(self, w, client, query):
1477+ print "Searching for tag", query
1478+
1479+class group(MessageAction):
1480+ icon = "face-monkey"
1481+
1482+ @classmethod
1483+ def action(self, w, client, acct, query):
1484+ client.add_transient_stream(acct, "group", query)
1485+ print "Searching for group", query
1486+
1487+class tomboy(MessageAction):
1488+ icon = "tomboy"
1489+ label = "Save to _Tomboy"
1490+
1491+ @classmethod
1492+ def action(self, w, client, msg):
1493+ util.create_tomboy_note(
1494+ "%s message from %s at %s\n\n%s\n\nSource: %s" % (
1495+ client.model.services[msg["protocol"]]["name"],
1496+ msg["sender"]["name"], msg["time"],
1497+ msg["text"], msg["url"]))
1498+
1499+ @classmethod
1500+ def include(self, client, msg):
1501+ return util.service_is_running("org.gnome.Tomboy")
1502+
1503+MENU_ITEMS = [reply, thread, retweet, read, user, like, delete, tomboy]
1504
1505=== added file 'gwibber/client.py'
1506--- gwibber/client.py 1970-01-01 00:00:00 +0000
1507+++ gwibber/client.py 2010-02-11 10:03:14 +0000
1508@@ -0,0 +1,467 @@
1509+#!/usr/bin/env python
1510+
1511+import gtk, gobject, gwui, util, resources, actions, json, gconf
1512+import subprocess, os
1513+import time, datetime
1514+
1515+import gettext
1516+from gettext import lgettext as _
1517+if hasattr(gettext, 'bind_textdomain_codeset'):
1518+ gettext.bind_textdomain_codeset('gwibber','UTF-8')
1519+gettext.textdomain('gwibber')
1520+
1521+from microblog.util.couch import RecordMonitor
1522+from desktopcouch.records.server import CouchDatabase
1523+from desktopcouch.records.record import Record as CouchRecord
1524+
1525+from microblog.util.const import *
1526+from dbus.mainloop.glib import DBusGMainLoop
1527+import dbus, dbus.service
1528+
1529+DBusGMainLoop(set_as_default=True)
1530+
1531+class GwibberClient(gtk.Window):
1532+ def __init__(self):
1533+ gtk.Window.__init__(self)
1534+ self.ui = gtk.Builder()
1535+
1536+ self.model = gwui.Model()
1537+
1538+ self.service = self.model.daemon
1539+ self.service.connect_to_signal("LoadingStarted", self.on_loading_started)
1540+ self.service.connect_to_signal("LoadingComplete", self.on_loading_complete)
1541+
1542+ self.connection = util.getbus("Connection")
1543+ self.connection.connect_to_signal("ConnectionOnline", self.on_connection_online)
1544+ self.connection.connect_to_signal("ConnectionOffline", self.on_connection_offline)
1545+
1546+ util.getbus("Streams").connect_to_signal("SettingChanged", self.on_setting_changed)
1547+
1548+ if len(json.loads(self.service.GetAccounts())) == 0:
1549+ import upgrade
1550+ account_migrate = upgrade.GwibberAccountMigrate()
1551+ if not account_migrate.run():
1552+ if os.path.exists(os.path.join("bin", "gwibber-accounts")):
1553+ cmd = os.path.join("bin", "gwibber-accounts")
1554+ else:
1555+ cmd = "gwibber-accounts"
1556+ ret = 1
1557+ while ret != 0:
1558+ ret = subprocess.call([cmd])
1559+ self.service.Refresh()
1560+
1561+ self.setup_ui()
1562+
1563+ # set state online/offline
1564+ if not self.connection.isConnected():
1565+ self.actions.get_action("refresh").set_sensitive(False)
1566+
1567+ gc = gconf.client_get_default()
1568+ gc.add_dir("/desktop/gnome/interface/font_name", gconf.CLIENT_PRELOAD_NONE)
1569+ gc.notify_add("/desktop/gnome/interface/font_name", self.on_font_changed)
1570+
1571+ def on_setting_changed(self):
1572+ self.update_view()
1573+
1574+ def on_font_changed(self, *args):
1575+ self.update_view()
1576+
1577+ def setup_ui(self):
1578+ self.set_title("Gwibber")
1579+ gtk.icon_theme_add_builtin_icon("gwibber", 22,
1580+ gtk.gdk.pixbuf_new_from_file_at_size(
1581+ resources.get_ui_asset("gwibber.svg"), 24, 24))
1582+ self.set_icon_name("gwibber")
1583+ self.connect("delete-event", self.on_window_close)
1584+
1585+ # Load the application menu
1586+ menu_ui = self.setup_menu()
1587+ self.add_accel_group(menu_ui.get_accel_group())
1588+
1589+ # Create animated loading spinner
1590+ self.loading_spinner = gtk.Image()
1591+ menu_spin = gtk.ImageMenuItem("")
1592+ menu_spin.set_right_justified(True)
1593+ menu_spin.set_sensitive(False)
1594+ menu_spin.set_image(self.loading_spinner)
1595+
1596+ # Force the spinner to show in newer versions of Gtk
1597+ if hasattr(menu_spin, "set_always_show_image"):
1598+ menu_spin.set_always_show_image(True)
1599+
1600+ menubar = menu_ui.get_widget("/menubar_main")
1601+ menubar.append(menu_spin)
1602+
1603+ view_class = getattr(gwui,
1604+ "MultiStreamUi" if len(self.model.settings["streams"]) > 1
1605+ else "SingleStreamUi")
1606+
1607+ self.stream_view = view_class(self.model)
1608+ self.stream_view.connect("action", self.on_action)
1609+ self.stream_view.connect("search", self.on_perform_search)
1610+ self.stream_view.connect("stream-changed", self.on_stream_changed)
1611+ self.stream_view.connect("stream-closed", self.on_stream_closed)
1612+ if isinstance(self.stream_view, gwui.MultiStreamUi):
1613+ self.stream_view.connect("pane-closed", self.on_pane_closed)
1614+
1615+ self.input = gwui.Input()
1616+ self.input.connect("submit", self.on_input_activate)
1617+ self.input.connect("changed", self.on_input_changed)
1618+
1619+ self.input_splitter = gtk.VPaned()
1620+ self.input_splitter.pack1(self.stream_view, resize=True)
1621+ self.input_splitter.pack2(self.input, resize=False)
1622+
1623+ self.button_send = gtk.Button(_("Send"))
1624+ self.button_send.connect("clicked", self.on_button_send_clicked)
1625+
1626+ self.message_target = gwui.AccountTargetBar(self.model)
1627+ self.message_target.pack_end(self.button_send, False)
1628+ #self.message_target.pack_end(self.character_count, False)
1629+ self.message_target.connect("canceled", self.on_reply_cancel)
1630+
1631+ content = gtk.VBox(spacing=5)
1632+ content.pack_start(self.input_splitter, True)
1633+ content.pack_start(self.message_target, False)
1634+ content.set_border_width(5)
1635+
1636+ layout = gtk.VBox()
1637+ layout.pack_start(menubar, False)
1638+ layout.pack_start(content, True)
1639+
1640+ self.add(layout)
1641+
1642+ # Apply the user's settings from CouchDB
1643+ self.resize(*self.model.settings["window_size"])
1644+ self.move(*self.model.settings["window_position"])
1645+ self.input_splitter.set_position(self.model.settings["window_splitter"])
1646+ self.show_all()
1647+
1648+ self.stream_view.set_state(self.model.settings["streams"] or DEFAULT_SETTINGS["streams"])
1649+ self.update_menu_availability()
1650+
1651+ def set_view(self, view=None):
1652+ state = None
1653+ if view: self.view_class = getattr(gwui, view)
1654+ if self.stream_view:
1655+ state = self.stream_view.get_state()
1656+ self.stream_view.destroy()
1657+
1658+ self.stream_view = self.view_class(self.model)
1659+ self.stream_view.connect("action", self.on_action)
1660+ self.stream_view.connect("search", self.on_perform_search)
1661+ self.stream_view.connect("stream-changed", self.on_stream_changed)
1662+ self.stream_view.connect("stream-closed", self.on_stream_closed)
1663+
1664+ if isinstance(self.stream_view, gwui.MultiStreamUi):
1665+ self.stream_view.connect("pane-closed", self.on_pane_closed)
1666+
1667+ self.input_splitter.add1(self.stream_view)
1668+ self.stream_view.show_all()
1669+ if state: self.stream_view.set_state(state)
1670+
1671+ def setup_menu(self):
1672+ ui_string = """
1673+ <ui>
1674+ <menubar name="menubar_main">
1675+ <menu action="menu_gwibber">
1676+ <menuitem action="refresh" />
1677+ <menuitem action="search" />
1678+ <separator/>
1679+ <menuitem action="new_stream" />
1680+ <menuitem action="close_window" />
1681+ <menuitem action="close_stream" />
1682+ <separator/>
1683+ <menuitem action="quit" />
1684+ </menu>
1685+
1686+ <menu action="menu_edit">
1687+ <menuitem action="accounts" />
1688+ <menuitem action="preferences" />
1689+ </menu>
1690+
1691+ <menu action="menu_help">
1692+ <menuitem action="help_online" />
1693+ <menuitem action="help_translate" />
1694+ <menuitem action="help_report" />
1695+ <separator/>
1696+ <menuitem action="about" />
1697+ </menu>
1698+ </menubar>
1699+
1700+ <popup name="menu_tray">
1701+ <menuitem action="refresh" />
1702+ <separator />
1703+ <menuitem action="accounts" />
1704+ <menuitem action="preferences" />
1705+ <separator />
1706+ <menuitem action="quit" />
1707+ </popup>
1708+ </ui>
1709+ """
1710+
1711+ self.actions = gtk.ActionGroup("Actions")
1712+ self.actions.add_actions([
1713+ ("menu_gwibber", None, _("_Gwibber")),
1714+ ("menu_edit", None, _("_Edit")),
1715+ ("menu_help", None, _("_Help")),
1716+
1717+ ("refresh", gtk.STOCK_REFRESH, _("_Refresh"), "<ctrl>R", None, self.on_refresh),
1718+ ("search", gtk.STOCK_FIND, _("_Search"), "<ctrl>F", None, self.on_search),
1719+ ("accounts", None, _("_Accounts"), "<ctrl>A", None, self.on_accounts),
1720+ ("preferences", gtk.STOCK_PREFERENCES, _("_Preferences"), "<ctrl>P", None, self.on_preferences),
1721+ ("about", gtk.STOCK_ABOUT, _("_About"), None, None, self.on_about),
1722+ ("quit", gtk.STOCK_QUIT, _("_Quit"), "<ctrl>Q", None, self.on_quit),
1723+
1724+ ("new_stream", gtk.STOCK_NEW, _("_New Stream"), "<ctrl>n", None, self.on_new_stream),
1725+ ("close_window", gtk.STOCK_CLOSE, _("_Close Window"), "<ctrl><shift>W", None, self.on_window_close),
1726+ ("close_stream", gtk.STOCK_CLOSE, _("_Close Stream"), "<ctrl>W", None, self.on_close_stream),
1727+
1728+ ("help_online", None, _("Get Help Online..."), None, None, lambda *a: util.load_url("https://answers.launchpad.net/gwibber")),
1729+ ("help_translate", None, _("Translate This Application..."), None, None, lambda *a: util.load_url("https://translations.launchpad.net/gwibber")),
1730+ ("help_report", None, _("Report A Problem..."), None, None, lambda *a: util.load_url(BUG_URL)),
1731+ ])
1732+
1733+
1734+ ui = gtk.UIManager()
1735+ ui.insert_action_group(self.actions, pos=0)
1736+ ui.add_ui_from_string(ui_string)
1737+ return ui
1738+
1739+ def update_menu_availability(self):
1740+ state = self.stream_view.get_state()
1741+ if state:
1742+ a = self.actions.get_action("close_stream")
1743+ a.set_visible(bool(state[0].get("transient", False)))
1744+
1745+ def update_view(self):
1746+ self.stream_view.update()
1747+
1748+ def reply(self, message):
1749+ features = self.model.services[message["protocol"]]["features"]
1750+ if "reply" in features:
1751+ if message["sender"].get("nick", 0) and not "thread" in features:
1752+ self.input.set_text("@%s: " % message["sender"]["nick"])
1753+
1754+ self.message_target.set_target(message)
1755+ self.input.textview.grab_focus()
1756+ buf = self.input.textview.get_buffer()
1757+ buf.place_cursor(buf.get_end_iter())
1758+
1759+ def on_reply_cancel(self, widget):
1760+ self.input.clear()
1761+
1762+ def get_message(self, id):
1763+ return dict(self.model.messages.get_record(id).items())
1764+
1765+ def on_refresh(self, *args):
1766+ self.service.Refresh()
1767+
1768+ def add_stream(self, data, kind=COUCH_TYPE_STREAM):
1769+ id = self.model.streams.put_record(CouchRecord(data, kind))
1770+ self.model.refresh()
1771+ self.service.PerformOp('{"id": "%s"}' % id)
1772+
1773+ if "operation" in data:
1774+ stream = str(self.model.features[data["operation"]]["stream"])
1775+ else: stream = "search"
1776+
1777+ self.stream_view.new_stream({
1778+ "stream": stream,
1779+ "account": data.get("account", None),
1780+ "transient": id,
1781+ })
1782+ self.update_menu_availability()
1783+
1784+ def save_window_settings(self):
1785+ self.model.settings.update({
1786+ "window_size": self.get_size(),
1787+ "window_position": self.get_position(),
1788+ "window_splitter": self.input_splitter.get_position(),
1789+ })
1790+
1791+ if hasattr(self.stream_view, "splitter"):
1792+ self.model.settings["sidebar_splitter"] = self.stream_view.splitter.get_position()
1793+
1794+ self.model.settings["streams"] = self.stream_view.get_state()
1795+ self.model.settings.save()
1796+
1797+ def on_pane_closed(self, widget, count):
1798+ if count < 2 and isinstance(self.stream_view, gwui.MultiStreamUi):
1799+ self.set_view("SingleStreamUi")
1800+
1801+ def on_window_close(self, *args):
1802+ self.save_window_settings()
1803+ gtk.main_quit()
1804+
1805+ def on_quit(self, *args):
1806+ self.service.Quit()
1807+ self.on_window_close()
1808+
1809+ def on_search(self, *args):
1810+ self.stream_view.set_state([{
1811+ "stream": "search",
1812+ "account": None,
1813+ "transient": False,
1814+ }])
1815+
1816+ self.stream_view.search_box.focus()
1817+
1818+ def on_perform_search(self, widget, query):
1819+ self.add_stream({
1820+ "name": query,
1821+ "query": query,
1822+ }, COUCH_TYPE_SEARCH)
1823+
1824+ def on_accounts(self, *args):
1825+ if os.path.exists(os.path.join("bin", "gwibber-accounts")):
1826+ cmd = os.path.join("bin", "gwibber-accounts")
1827+ else:
1828+ cmd = "gwibber-accounts"
1829+ return subprocess.Popen(cmd, shell=False)
1830+
1831+ def on_preferences(self, *args):
1832+ if os.path.exists(os.path.join("bin", "gwibber-preferences")):
1833+ cmd = os.path.join("bin", "gwibber-preferences")
1834+ else:
1835+ cmd = "gwibber-preferences"
1836+ return subprocess.Popen(cmd, shell=False)
1837+
1838+ def on_about(self, *args):
1839+ self.ui.add_from_file (resources.get_ui_asset("gwibber-about-dialog.ui"))
1840+ about_dialog = self.ui.get_object("about_dialog")
1841+ about_dialog.set_version(str(VERSION_NUMBER))
1842+ about_dialog.connect("response", lambda *a: about_dialog.hide())
1843+ about_dialog.show_all()
1844+
1845+ def on_close_stream(self, *args):
1846+ state = self.stream_view.get_state()
1847+ if state and state[0].get("transient", 0):
1848+ id = state[0]["transient"]
1849+ if self.model.streams.record_exists(id):
1850+ self.model.streams.delete_record(id)
1851+ self.stream_view.set_state([{"stream": "messages", "account": None}])
1852+
1853+ def on_message_action_menu(self, msg):
1854+ theme = gtk.icon_theme_get_default()
1855+ menu = gtk.Menu()
1856+
1857+ for a in actions.MENU_ITEMS:
1858+ if a.include(self, msg):
1859+ image = gtk.image_new_from_icon_name(a.icon, gtk.ICON_SIZE_MENU)
1860+ mi = gtk.ImageMenuItem()
1861+ mi.set_label(a.label)
1862+ mi.set_image(image)
1863+ mi.set_property("use_underline", True)
1864+ mi.connect("activate", a.action, self, msg)
1865+ menu.append(mi)
1866+
1867+ menu.show_all()
1868+ menu.popup(None, None, None, 3, 0)
1869+
1870+ def on_action(self, widget, uri, cmd, query):
1871+ if hasattr(actions, cmd):
1872+ if "msg" in query:
1873+ query["msg"] = self.get_message(query["msg"])
1874+ getattr(actions, cmd).action(None, self, **query)
1875+
1876+ def on_stream_closed(self, widget, id, kind):
1877+ if self.model.streams.record_exists(id):
1878+ self.model.streams.delete_record(id)
1879+
1880+ def on_stream_changed(self, widget, stream):
1881+ self.update_menu_availability()
1882+
1883+ def on_input_changed(self, w, text, cnt):
1884+ self.input.textview.set_overlay_text(str(140 - cnt))
1885+
1886+ def on_input_activate(self, w, text, cnt):
1887+ self.send_message(text)
1888+ self.input.clear()
1889+
1890+ def on_button_send_clicked(self, w):
1891+ self.send_message(self.input.get_text())
1892+ self.input.clear()
1893+
1894+ def send_message(self, text):
1895+ target = self.message_target.target
1896+ action = self.message_target.action
1897+
1898+ if target:
1899+ if action == "reply":
1900+ data = {"message": text, "target": target}
1901+ self.service.Send(json.dumps(data))
1902+ elif action == "repost":
1903+ data = {"message": text, "accounts": [target["account"]]}
1904+ self.service.Send(json.dumps(data))
1905+ self.message_target.end()
1906+ else: self.service.SendMessage(text)
1907+
1908+ def on_new_stream(self, *args):
1909+ if isinstance(self.stream_view, gwui.MultiStreamUi):
1910+ self.stream_view.new_stream()
1911+ else:
1912+ self.set_view("MultiStreamUi")
1913+ self.stream_view.new_stream()
1914+
1915+ def on_loading_started(self, *args):
1916+ self.loading_spinner.set_from_animation(
1917+ gtk.gdk.PixbufAnimation(resources.get_ui_asset("progress.gif")))
1918+
1919+ def on_loading_complete(self, *args):
1920+ self.loading_spinner.clear()
1921+ self.update_view()
1922+
1923+ def on_connection_online(self, *args):
1924+ self.actions.get_action("refresh").set_sensitive(True)
1925+
1926+ def on_connection_offline(self, *args):
1927+ self.actions.get_action("refresh").set_sensitive(False)
1928+
1929+class Client(dbus.service.Object):
1930+ __dbus_object_path__ = "/com/GwibberClient"
1931+
1932+ def __init__(self):
1933+ # Setup a Client dbus interface
1934+ self.bus = dbus.SessionBus()
1935+ self.bus_name = dbus.service.BusName("com.GwibberClient", self.bus)
1936+ dbus.service.Object.__init__(self, self.bus_name, self.__dbus_object_path__)
1937+
1938+ # Methods the client exposes via dbus, return from the list method
1939+ self.exposed_methods = [
1940+ 'focus_client',
1941+ 'show_replies',
1942+ ]
1943+
1944+ self.w = GwibberClient()
1945+
1946+ @dbus.service.method("com.GwibberClient", in_signature="", out_signature="")
1947+ def focus_client(self):
1948+ """
1949+ This method focuses the client UI displaying the default view.
1950+ Currently used when the client is activated via dbus activation.
1951+ """
1952+ self.w.present_with_time(int(time.mktime(datetime.datetime.now().timetuple())))
1953+ try:
1954+ self.w.move(*self.w.settings["window_position"])
1955+ except:
1956+ pass
1957+
1958+ @dbus.service.method("com.GwibberClient", in_signature="", out_signature="")
1959+ def show_replies(self):
1960+ """
1961+ This method focuses the client UI and displays the replies view.
1962+ Currently used when activated via the messaging indicator.
1963+ """
1964+ self.w.present_with_time(int(time.mktime(datetime.datetime.now().timetuple())))
1965+ self.w.move(*self.w.settings["window_position"])
1966+ # FIXME: we need to be able to select the stream to view
1967+ #self.w.account_tree.get_selection().unselect_all()
1968+ #self.w.account_tree.get_selection().select_path((2,))
1969+
1970+ @dbus.service.method("com.GwibberClient")
1971+ def list(self):
1972+ """
1973+ This method returns a list of exposed dbus methods
1974+ """
1975+ return self.exposed_methods
1976
1977=== added file 'gwibber/gwui.py'
1978--- gwibber/gwui.py 1970-01-01 00:00:00 +0000
1979+++ gwibber/gwui.py 2010-02-11 10:03:14 +0000
1980@@ -0,0 +1,871 @@
1981+
1982+import os, json, urlparse, resources, util
1983+import gtk, gobject, pango, webkit, time, gconf
1984+
1985+import gettext
1986+from gettext import lgettext as _
1987+if hasattr(gettext, 'bind_textdomain_codeset'):
1988+ gettext.bind_textdomain_codeset('gwibber','UTF-8')
1989+gettext.textdomain('gwibber')
1990+
1991+from mako.template import Template
1992+from mako.lookup import TemplateLookup
1993+
1994+from microblog.util.const import *
1995+from microblog.util.couch import Monitor as CouchMonitor
1996+
1997+from desktopcouch.records.server import CouchDatabase
1998+from desktopcouch.records.record import Record as CouchRecord
1999+
2000+gtk.gdk.threads_init()
2001+
2002+if "gwibber" not in urlparse.uses_query:
2003+ urlparse.uses_query.append("gwibber")
2004+
2005+class Model(gobject.GObject):
2006+ __gsignals__ = {
2007+ "changed": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, ()),
2008+ }
2009+ def __init__(self):
2010+ gobject.GObject.__init__(self)
2011+
2012+ self.daemon = util.getbus("Service")
2013+
2014+ self.account_monitor = util.getbus("Accounts")
2015+ self.account_monitor.connect_to_signal("AccountChanged", self.on_stream_changed)
2016+ self.account_monitor.connect_to_signal("AccountDeleted", self.on_stream_changed)
2017+
2018+ self.streams_monitor = util.getbus("Streams")
2019+ self.streams_monitor.connect_to_signal("StreamChanged", self.on_stream_changed)
2020+ self.streams_monitor.connect_to_signal("StreamClosed", self.on_stream_changed)
2021+
2022+ self.settings = util.SettingsMonitor()
2023+ self.services = json.loads(self.daemon.GetServices())
2024+ self.features = json.loads(self.daemon.GetFeatures())
2025+ self.messages = CouchDatabase(COUCH_DB_MESSAGES, create=True)
2026+ self.accounts = CouchDatabase(COUCH_DB_ACCOUNTS, create=True)
2027+ self.streams = CouchDatabase(COUCH_DB_SETTINGS, create=True)
2028+
2029+ self.model = None
2030+ self.model_valid = False
2031+
2032+ def on_stream_changed(self, id):
2033+ self.model_valid = False
2034+ self.emit("changed")
2035+
2036+ @classmethod
2037+ def to_state(self, item):
2038+ if not item: return {}
2039+ return dict((x, item[x]) for x in ["transient", "account", "stream"])
2040+
2041+ @classmethod
2042+ def match(self, d1, d2):
2043+ return all(k in d1 and d1[k] == v for k, v in d2.items())
2044+
2045+ def find(self, **params):
2046+ for i in self.find_all(**params):
2047+ return i
2048+
2049+ def find_all(self, **params):
2050+ for stream in self.get_streams():
2051+ if self.match(stream, params): yield stream
2052+
2053+ if "items" in stream:
2054+ for stream in stream["items"]:
2055+ if self.match(stream, params): yield stream
2056+
2057+ def get_streams(self):
2058+ if not self.model_valid: self.refresh()
2059+ return self.model
2060+
2061+ def refresh(self):
2062+ self.model = self.generate_streams()
2063+ self.model_valid = True
2064+
2065+ def generate_streams(self):
2066+ items = []
2067+ transients = self.streams.get_records(COUCH_TYPE_STREAM, True)
2068+
2069+ items.append({
2070+ "name": "Home",
2071+ "stream": "home",
2072+ "view": self.messages.execute_view("home", "messages")[[{}]:[0]],
2073+ "account": None,
2074+ "transient": False,
2075+ "color": None,
2076+ })
2077+
2078+ for stream in ["messages", "replies", "private"]:
2079+ items.append({
2080+ "name": stream.capitalize(),
2081+ "stream": stream,
2082+ "view": self.messages.execute_view("stream_time", "messages")[[stream, {}]:[stream, 0]],
2083+ "account": None,
2084+ "transient": False,
2085+ "color": None,
2086+ })
2087+
2088+ items.append({
2089+ "name": "Sent",
2090+ "stream": "sent",
2091+ "view": self.messages.execute_view("mine", "messages")[[{}]:[0]],
2092+ "account": None,
2093+ "transient": False,
2094+ "color": None,
2095+ })
2096+
2097+ for account in self.accounts.get_records(COUCH_TYPE_ACCOUNT, True):
2098+ account = account.value
2099+ aId = account["_id"]
2100+
2101+ item = {
2102+ "name": account.get("username", "None"),
2103+ "account": aId,
2104+ "stream": None,
2105+ "view": self.messages.execute_view("account_time", "messages")[[aId, {}]:[aId, 0]],
2106+ "transient": False,
2107+ "color": util.Color(account["color"]),
2108+ "protocol": account["protocol"],
2109+ "items": [],
2110+ }
2111+
2112+ default_streams = self.services[account["protocol"]]["default_streams"]
2113+
2114+ if len(default_streams) > 1:
2115+ for feature in default_streams:
2116+ aname = self.features[feature]["stream"]
2117+ item["items"].append({
2118+ "name": aname.capitalize(),
2119+ "account": aId,
2120+ "stream": aname,
2121+ "view": self.messages.execute_view("account_stream_time", "messages")[[aId, aname, {}]:[aId, aname, 0]],
2122+ "transient": False,
2123+ "color": util.Color(account["color"]),
2124+ "protocol": account["protocol"],
2125+ })
2126+
2127+ item["items"].append({
2128+ "name": "Sent",
2129+ "account": aId,
2130+ "stream": "sent",
2131+ "view": self.messages.execute_view("mine_account", "messages")[[aId, {}]:[aId, 0]],
2132+ "transient": False,
2133+ "color": util.Color(account["color"]),
2134+ "protocol": account["protocol"],
2135+ })
2136+
2137+ for transient in transients:
2138+ transient = transient.value
2139+ tId = transient["_id"]
2140+
2141+ if transient["account"] == aId:
2142+ if transient["operation"] == "user_messages" and account["protocol"] in ["twitter", "identica"]:
2143+ uId = transient["parameters"]["id"]
2144+ view = self.messages.execute_view("user_protocol_time", "messages")[[uId, account["protocol"], {}]:[uId, account["protocol"], 0]]
2145+ else:
2146+ view = self.messages.execute_view("transient_time", "messages")[[tId, {}]:[tId, 0]],
2147+
2148+ item["items"].append({
2149+ "name": transient["name"],
2150+ "account": aId,
2151+ "stream": self.features[transient["operation"]]["stream"],
2152+ "view": view,
2153+ "transient": tId,
2154+ "color": util.Color(account["color"]),
2155+ "protocol": account["protocol"],
2156+ })
2157+
2158+ items.append(item)
2159+
2160+ searches = {
2161+ "name": "Search",
2162+ "account": None,
2163+ "stream": "search",
2164+ "view": self.messages.execute_view("search_time", "messages")[{}:0],
2165+ "transient": False,
2166+ "color": None,
2167+ "items": [],
2168+ }
2169+
2170+ for search in self.streams.get_records(COUCH_TYPE_SEARCH, True):
2171+ search = search.value
2172+ sId = search["_id"]
2173+ searches["items"].append({
2174+ "name": search["name"],
2175+ "account": None,
2176+ "view": self.messages.execute_view("transient_time", "messages")[[sId, {}]:[sId, 0]],
2177+ "stream": "search",
2178+ "transient": sId,
2179+ "color": None,
2180+ })
2181+
2182+ items.append(searches)
2183+ return items
2184+
2185+class WebUi(webkit.WebView):
2186+ __gsignals__ = {
2187+ "action": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str, str, object)),
2188+ }
2189+
2190+ def __init__(self):
2191+ webkit.WebView.__init__(self)
2192+ self.web_settings = webkit.WebSettings()
2193+ self.set_settings(self.web_settings)
2194+ self.gc = gconf.client_get_default()
2195+
2196+ self.connect("navigation-requested", self.on_click_link)
2197+ self.template = None
2198+
2199+ def on_click_link(self, view, frame, req):
2200+ uri = req.get_uri()
2201+
2202+ if uri.startswith("file:///"): return False
2203+ elif uri.startswith("gwibber:"):
2204+ url = urlparse.urlparse(uri)
2205+ cmd = url.path.split("/")[1]
2206+ query = urlparse.parse_qs(url.query)
2207+ query = dict((x,y[0]) for x,y in query.items())
2208+ self.emit("action", uri, cmd, query)
2209+ else: util.load_url(uri)
2210+ return True
2211+
2212+ def render(self, theme, template, **kwargs):
2213+ default_font = self.gc.get_string("/desktop/gnome/interface/font_name")
2214+ font_name, font_size = default_font.rsplit(None, 1)
2215+ self.web_settings.set_property("sans-serif-font-family", font_name)
2216+ self.web_settings.set_property("default-font-size", int(font_size))
2217+
2218+ if not resources.theme_exists(theme):
2219+ theme = "default"
2220+
2221+ theme_path = resources.get_theme_path(theme)
2222+ template_path = resources.get_template_path(template, theme)
2223+ lookup_paths = list(resources.get_template_dirs()) + [theme_path]
2224+
2225+ template = open(template_path).read()
2226+ template = Template(template, lookup=TemplateLookup(directories=lookup_paths))
2227+ content = template.render(theme=util.get_theme_colors(), resources=resources, _=_, **kwargs)
2228+
2229+ self.load_html_string(content, "file://%s/" % os.path.dirname(template_path))
2230+ return content
2231+
2232+class Navigation(WebUi):
2233+ __gsignals__ = {
2234+ "stream-selected": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (object,)),
2235+ "stream-closed": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str, str)),
2236+ }
2237+
2238+ def __init__(self, model):
2239+ WebUi.__init__(self)
2240+
2241+ self.model = model
2242+ self.model.connect("changed", self.on_model_update)
2243+ self.connect("action", self.on_perform_action)
2244+
2245+ self.selected_stream = None
2246+ self.tree_enabled = False
2247+ self.small_icons = False
2248+
2249+ def render(self):
2250+ return WebUi.render(self, self.model.settings["theme"], "navigation.mako",
2251+ streams=self.model.get_streams(),
2252+ tree=self.tree_enabled,
2253+ selected=self.selected_stream,
2254+ small_icons=self.small_icons)
2255+
2256+ def on_model_update(self, model):
2257+ self.render()
2258+
2259+ def on_perform_action(self, w, uri, cmd, query):
2260+ if cmd == "close" and "transient" in query:
2261+ self.emit("stream-closed", query["transient"], "transient")
2262+
2263+ if cmd == "stream":
2264+ query = dict((k, None if v == "None" else v) for k, v in query.items())
2265+ target = self.model.find(**query)
2266+ if target: self.emit("stream-selected", target)
2267+
2268+class SingleStreamUi(gtk.VBox):
2269+ __gsignals__ = {
2270+ "action": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str, str, object)),
2271+ "stream-closed": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str, str)),
2272+ "stream-changed": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (object,)),
2273+ "search": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)),
2274+ }
2275+ def __init__(self, model):
2276+ gtk.VBox.__init__(self)
2277+ self.model = model
2278+
2279+ # Build the side navigation bar
2280+ self.navigation = Navigation(self.model)
2281+ self.navigation.connect("stream-selected", self.on_stream_change)
2282+ self.navigation.connect("stream-closed", self.on_stream_closed)
2283+ self.navigation.render()
2284+
2285+ self.navigation_scroll = gtk.ScrolledWindow()
2286+ self.navigation_scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_NEVER)
2287+ self.navigation_scroll.add(self.navigation)
2288+
2289+ # Build the main message view
2290+ self.message_view = MessageStreamView(self.model)
2291+ self.message_view.connect("action", self.on_action)
2292+
2293+ self.search_box = GwibberSearch()
2294+ self.search_box.connect("search", self.on_search)
2295+
2296+ self.message_scroll = gtk.ScrolledWindow()
2297+ self.message_scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
2298+ self.message_scroll.set_shadow_type(gtk.SHADOW_IN)
2299+ self.message_scroll.add(self.message_view)
2300+
2301+ layout = gtk.VBox(spacing=5)
2302+ layout.pack_start(self.search_box, False)
2303+ layout.pack_start(self.message_scroll, True)
2304+
2305+ # Build the pane layout
2306+ self.splitter = gtk.HPaned()
2307+ self.splitter.add1(self.navigation_scroll)
2308+ self.splitter.add2(layout)
2309+
2310+ self.splitter.set_position(self.model.settings["sidebar_splitter"])
2311+ self.handle_splitter_position_change(self.model.settings["sidebar_splitter"])
2312+ self.splitter.connect("notify", self.on_splitter_drag)
2313+
2314+ self.pack_start(self.splitter, True)
2315+
2316+ def handle_splitter_position_change(self, pos):
2317+ if pos < 70 and self.navigation.tree_enabled:
2318+ #self.navigation_scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_NEVER)
2319+ self.navigation_scroll.set_shadow_type(gtk.SHADOW_NONE)
2320+ self.navigation.tree_enabled = False
2321+ self.navigation.render()
2322+
2323+ if pos > 70 and not self.navigation.tree_enabled:
2324+ #self.navigation_scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_NEVER)
2325+ self.navigation_scroll.set_shadow_type(gtk.SHADOW_IN)
2326+ self.navigation.tree_enabled = True
2327+ self.navigation.render()
2328+
2329+ if pos < 30 and not self.navigation.small_icons:
2330+ self.navigation.small_icons = True
2331+ self.navigation.render()
2332+
2333+ if pos > 30 and self.navigation.small_icons:
2334+ self.navigation.small_icons = False
2335+ self.navigation.render()
2336+
2337+ if pos < 25:
2338+ self.splitter.set_position(25)
2339+
2340+ def on_splitter_drag(self, pane, ev):
2341+ if ev.name == 'position':
2342+ pos = pane.get_position()
2343+ self.handle_splitter_position_change(pos)
2344+
2345+ def on_stream_closed(self, widget, id, kind):
2346+ self.emit("stream-closed", id, kind)
2347+
2348+ def on_stream_change(self, widget, stream):
2349+ self.navigation.selected_stream = stream
2350+ self.emit("stream-changed", stream)
2351+ self.update_search_visibility()
2352+ self.update()
2353+
2354+ def update_search_visibility(self):
2355+ stream = self.navigation.selected_stream
2356+ if stream is not None:
2357+ is_search = stream["stream"] == "search" and stream["name"] == "Search"
2358+ self.search_box.set_visible(not is_search)
2359+
2360+ def on_action(self, widget, uri, cmd, query):
2361+ self.emit("action", uri, cmd, query)
2362+
2363+ def on_search(self, widget, query):
2364+ self.emit("search", query)
2365+
2366+ def update(self, *args):
2367+ if self.navigation.selected_stream:
2368+ self.message_view.render([self.navigation.selected_stream["view"]])
2369+
2370+ def get_state(self):
2371+ return [self.model.to_state(self.navigation.selected_stream)]
2372+
2373+ def new_stream(self, state=None):
2374+ if state: self.set_state([state])
2375+
2376+ def set_state(self, streams):
2377+ self.navigation.selected_stream = self.model.find(**streams[0])
2378+ self.navigation.render()
2379+ self.update_search_visibility()
2380+ self.update()
2381+
2382+
2383+class MultiStreamUi(gtk.HBox):
2384+ __gsignals__ = {
2385+ "action": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str, str, object)),
2386+ "stream-closed": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str, str)),
2387+ "stream-changed": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (object,)),
2388+ "pane-closed": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (int,)),
2389+ "search": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)),
2390+ }
2391+ def __init__(self, model):
2392+ gtk.HBox.__init__(self)
2393+ self.model = model
2394+
2395+ self.container = gtk.HBox(spacing=5)
2396+ self.container.set_border_width(5)
2397+
2398+ self.scroll = gtk.ScrolledWindow()
2399+ self.scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_NEVER)
2400+ self.scroll.add_with_viewport(self.container)
2401+
2402+ self.pack_start(self.scroll, True)
2403+
2404+ def on_stream_closed(self, widget, id, kind):
2405+ self.emit("stream-closed", id, kind)
2406+
2407+ def on_pane_closed(self, widget):
2408+ widget.destroy()
2409+ self.emit("pane-closed", len(self.container))
2410+
2411+ def on_search(self, widget, query):
2412+ self.emit("search", query)
2413+
2414+ def on_action(self, widget, uri, cmd, query):
2415+ self.emit("action", uri, cmd, query)
2416+
2417+ def new_stream(self, state=None):
2418+ item = MultiStreamPane(self.model)
2419+ item.set_property("width-request", 350)
2420+ item.connect("search", self.on_search)
2421+ item.connect("action", self.on_action)
2422+ item.connect("stream-closed", self.on_stream_closed)
2423+ item.connect("pane-closed", self.on_pane_closed)
2424+ item.show_all()
2425+
2426+ item.search_box.hide()
2427+ if state: item.set_state(state)
2428+ self.container.pack_start(item)
2429+ return item
2430+
2431+ def set_state(self, state):
2432+ for item in self.container: item.destroy()
2433+ for item in state: self.new_stream(item)
2434+
2435+ def get_state(self):
2436+ return [pane.get_state() for pane in self.container]
2437+
2438+ def update(self):
2439+ for stream in self.container:
2440+ stream.update()
2441+
2442+class MultiStreamPane(gtk.VBox):
2443+ __gsignals__ = {
2444+ "action": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str, str, object)),
2445+ "stream-closed": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str, str)),
2446+ "pane-closed": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, ()),
2447+ "search": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)),
2448+ }
2449+
2450+ def __init__(self, model):
2451+ gtk.VBox.__init__(self, spacing = 2)
2452+ self.model = Model()
2453+ self.selected_stream = None
2454+
2455+ # Build the top navigation bar
2456+ close_icon = gtk.image_new_from_stock("gtk-close", gtk.ICON_SIZE_MENU)
2457+ down_arrow = gtk.Arrow(gtk.ARROW_DOWN, gtk.SHADOW_NONE)
2458+
2459+ btn_arrow = gtk.Button()
2460+ btn_arrow.set_relief(gtk.RELIEF_NONE)
2461+ btn_arrow.add(down_arrow)
2462+ btn_arrow.connect("clicked", self.on_dropdown)
2463+
2464+ self.arrow = gtk.EventBox()
2465+ self.arrow.add(btn_arrow)
2466+
2467+ btn_close = gtk.Button()
2468+ btn_close.set_relief(gtk.RELIEF_NONE)
2469+ btn_close.set_image(close_icon)
2470+ btn_close.connect("clicked", self.on_close)
2471+
2472+ self.icon_protocol = gtk.Image()
2473+ self.icon_stream = gtk.Image()
2474+ self.nav_label = gtk.Label()
2475+
2476+ self.search_box = GwibberSearch()
2477+ self.search_box.connect("search", self.on_search)
2478+
2479+ self.navigation_bar = gtk.HBox(spacing=5)
2480+ self.navigation_bar.pack_start(self.arrow, False)
2481+ self.navigation_bar.pack_start(self.icon_protocol, False)
2482+ self.navigation_bar.pack_start(self.icon_stream, False)
2483+ self.navigation_bar.pack_start(self.nav_label, False)
2484+ self.navigation_bar.pack_end(btn_close, False)
2485+
2486+ # Build the main message view
2487+ self.message_view = MessageStreamView(self.model)
2488+ self.message_view.connect("action", self.on_action)
2489+
2490+ self.message_scroll = gtk.ScrolledWindow()
2491+ self.message_scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
2492+ self.message_scroll.set_shadow_type(gtk.SHADOW_IN)
2493+ self.message_scroll.add(self.message_view)
2494+
2495+ self.pack_start(self.navigation_bar, False)
2496+ self.pack_start(self.search_box, False)
2497+ self.pack_start(self.message_scroll, True)
2498+
2499+ def on_close(self, *args):
2500+ self.emit("pane-closed")
2501+
2502+ def on_dropdown(self, button):
2503+ w, h = self.arrow.window.get_geometry()[2:4]
2504+ x, y = self.arrow.window.get_origin()
2505+
2506+ window = gtk.Window()
2507+ window.move(x, y + h)
2508+ window.set_decorated(False)
2509+ window.set_property("skip-taskbar-hint", True)
2510+ window.set_property("skip-pager-hint", True)
2511+ window.set_events(gtk.gdk.FOCUS_CHANGE_MASK)
2512+ window.connect("focus-out-event", lambda w,x: w.destroy())
2513+
2514+ def on_change(widget, stream):
2515+ self.set_stream(stream)
2516+ self.update()
2517+ window.destroy()
2518+
2519+ def on_stream_close(widget, id, kind):
2520+ self.emit("stream-closed", id, kind)
2521+
2522+ navigation = Navigation(self.model)
2523+ navigation.connect("stream-selected", on_change)
2524+ navigation.connect("stream-closed", on_stream_close)
2525+ navigation.selected_stream = self.selected_stream
2526+ navigation.tree_enabled = True
2527+ navigation.small_icons = True
2528+ navigation.render()
2529+
2530+ window.add(navigation)
2531+ window.show_all()
2532+ window.grab_focus()
2533+
2534+ def set_stream(self, stream):
2535+ self.selected_stream = stream
2536+ self.nav_label.set_text(stream["name"])
2537+
2538+ is_search = stream["stream"] == "search" and stream["name"] == "Search"
2539+ self.search_box.set_visible(not is_search)
2540+
2541+ if stream["account"]:
2542+ fname = resources.get_ui_asset("icons/breakdance/16x16/%s.png" % stream["protocol"])
2543+ self.icon_protocol.set_from_file(fname)
2544+ else: self.icon_protocol.clear()
2545+
2546+ if stream["stream"]:
2547+ fname = resources.get_ui_asset("icons/streams/16x16/%s.png" % stream["stream"])
2548+ self.icon_stream.set_from_file(fname)
2549+ else: self.icon_stream.clear()
2550+
2551+ def on_search(self, widget, query):
2552+ self.emit("search", query)
2553+
2554+ def on_action(self, widget, uri, cmd, query):
2555+ self.emit("action", uri, cmd, query)
2556+
2557+ def update(self, *args):
2558+ if self.selected_stream:
2559+ self.message_view.render([self.selected_stream["view"]])
2560+
2561+ def get_state(self):
2562+ return self.model.to_state(self.selected_stream)
2563+
2564+ def set_state(self, stream):
2565+ self.set_stream(self.model.find(**stream))
2566+ self.update()
2567+
2568+class AccountTargetBar(gtk.HBox):
2569+ __gsignals__ = {
2570+ "canceled": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, ())
2571+ }
2572+
2573+ def __init__(self, model):
2574+ gtk.HBox.__init__(self, spacing=5)
2575+ self.model = model
2576+ self.model.connect("changed", self.on_account_changed)
2577+ self.accounts = []
2578+
2579+ self.target = None
2580+ self.action = None
2581+
2582+ self.targetbar = WebUi()
2583+ self.targetbar.connect("action", self.on_action)
2584+ self.targetbar.set_size_request(0, 24)
2585+ self.pack_start(self.targetbar, True)
2586+
2587+ self.populate()
2588+ self.render()
2589+
2590+ def set_target(self, message, action="reply"):
2591+ self.target = message
2592+ self.target["account_data"] = dict(self.model.accounts.get_record(message["account"]).items())
2593+ self.action = action
2594+ self.render()
2595+
2596+ def end(self):
2597+ self.emit("canceled")
2598+ self.target = None
2599+ self.action = None
2600+ self.render()
2601+
2602+ def on_action(self, w, uri, cmd, query):
2603+ if cmd == "cancel": return self.end()
2604+ if cmd == "account" and "id" in query and \
2605+ self.model.accounts.record_exists(query["id"]):
2606+
2607+ if "send_enabled" in query:
2608+ self.model.accounts.update_fields(query["id"],
2609+ {"send_enabled": bool(query["send_enabled"] == "true")})
2610+
2611+ def on_account_changed(self, id):
2612+ self.populate()
2613+ self.render()
2614+
2615+ def populate(self):
2616+ self.accounts = []
2617+ for account in self.model.accounts.get_records(COUCH_TYPE_ACCOUNT, True):
2618+ if "send" in self.model.services[account.value["protocol"]]["features"]:
2619+ self.accounts.append(account.value)
2620+
2621+ def render(self):
2622+ return self.targetbar.render(self.model.settings["theme"], "targetbar.mako",
2623+ services=self.model.services,
2624+ target=self.target,
2625+ action=self.action,
2626+ accounts=self.accounts)
2627+
2628+class MessageStreamView(WebUi):
2629+ def __init__(self, model):
2630+ WebUi.__init__(self)
2631+ self.model = model
2632+
2633+ def render(self, views):
2634+ accounts = CouchDatabase(COUCH_DB_ACCOUNTS).get_records(COUCH_TYPE_ACCOUNT, True)
2635+
2636+ accounts = dict((a.id, a.value) for a in accounts)
2637+ messages = []
2638+ seen = {}
2639+
2640+ for view in views:
2641+ view.options["descending"] = True
2642+ view.options["limit"] = 100
2643+ view._fetch()
2644+
2645+ for item in view:
2646+ message = item.value
2647+ message["dupes"] = []
2648+ message["txtid"] = util.remove_urls(message["text"]).strip()[:140] or None
2649+ message["color"] = util.Color(accounts.get(message["account"], {"color": "#5A5A5A"})["color"])
2650+ message["time_string"] = util.generate_time_string(message["time"])
2651+ messages.append(message)
2652+
2653+ def dupematch(item, message):
2654+ if item["protocol"] == message["protocol"] and item["id"] == message["id"]:
2655+ return True
2656+
2657+ for item in item["dupes"]:
2658+ if item["protocol"] == message["protocol"] and item["id"] == message["id"]:
2659+ return True
2660+
2661+ # Detect duplicates
2662+ for n, message in enumerate(messages):
2663+ message["is_dupe"] = message["txtid"] in seen
2664+ if message["is_dupe"]:
2665+ item = messages[seen[message["txtid"]]]
2666+ if not dupematch(item, message):
2667+ item["dupes"].append(message)
2668+ else:
2669+ if message["txtid"]:
2670+ seen[message["txtid"]] = n
2671+
2672+ WebUi.render(self, self.model.settings["theme"], "template.mako",
2673+ message_store=messages,
2674+ preferences=self.model.settings,
2675+ services=self.model.services,
2676+ accounts=accounts)
2677+
2678+class GwibberSearch(gtk.HBox):
2679+ __gsignals__ = {
2680+ "search": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)),
2681+ }
2682+
2683+ def __init__(self):
2684+ gtk.HBox.__init__(self, spacing=2)
2685+
2686+ self.entry = gtk.Entry()
2687+ self.entry.connect("activate", self.on_search)
2688+ self.entry.connect("changed", self.on_changed)
2689+
2690+ self.button = gtk.Button("Search")
2691+ self.button.connect("clicked", self.on_search)
2692+
2693+ self.pack_start(self.entry, True)
2694+ self.pack_start(self.button, False)
2695+
2696+ try:
2697+ self.entry.set_property("primary-icon-stock", gtk.STOCK_FIND)
2698+ self.entry.connect("icon-press", self.on_icon_press)
2699+ except: pass
2700+
2701+ def on_search(self, *args):
2702+ self.emit("search", self.entry.get_text())
2703+ self.clear()
2704+
2705+ def clear(self):
2706+ self.entry.set_text("")
2707+
2708+ def on_icon_press(self, w, pos, e):
2709+ if pos == 1: return self.clear()
2710+
2711+ def on_changed(self, widget):
2712+ self.entry.set_property("secondary-icon-stock",
2713+ gtk.STOCK_CLEAR if self.entry.get_text().strip() else None)
2714+
2715+ def set_visible(self, value):
2716+ if value: self.hide()
2717+ else: self.show_all()
2718+
2719+ def focus(self):
2720+ self.entry.grab_focus()
2721+
2722+class Input(gtk.Frame):
2723+ __gsignals__ = {
2724+ "submit": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str, int)),
2725+ "changed": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str, int))
2726+ }
2727+
2728+ def __init__(self):
2729+ gtk.Frame.__init__(self)
2730+
2731+ self.textview = InputTextView()
2732+ self.textview.connect("submit", self.do_submit_event)
2733+ self.textview.get_buffer().connect("changed", self.do_changed_event)
2734+
2735+ scroll = gtk.ScrolledWindow()
2736+ scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
2737+ scroll.add(self.textview)
2738+ self.add(scroll)
2739+
2740+ def get_text(self):
2741+ return self.textview.get_text()
2742+
2743+ def set_text(self, t):
2744+ self.textview.get_buffer().set_text(t)
2745+
2746+ def clear(self):
2747+ self.set_text("")
2748+
2749+ def do_changed_event(self, tb):
2750+ text = self.textview.get_text()
2751+ chars = self.textview.get_char_count()
2752+ self.emit("changed", text, chars)
2753+
2754+ def do_submit_event(self, tv):
2755+ text = tv.get_text()
2756+ chars = tv.get_char_count()
2757+ self.emit("submit", text, chars)
2758+
2759+class InputTextView(gtk.TextView):
2760+ __gsignals__ = {
2761+ "submit": (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, ())
2762+ }
2763+
2764+ def __init__(self):
2765+ gtk.TextView.__init__(self)
2766+ self.drawable = None
2767+
2768+ self.model = Model()
2769+
2770+ self.overlay_color = util.get_theme_colors()["text"].darker(3).hex
2771+ self.overlay_text = '<span weight="bold" size="xx-large" foreground="%s">%s</span>'
2772+
2773+ self.shortener = util.getbus("URLShorten")
2774+
2775+ self.connection = util.getbus("Connection")
2776+ self.connection.connect_to_signal("ConnectionOnline", self.on_connection_online)
2777+ self.connection.connect_to_signal("ConnectionOffline", self.on_connection_offline)
2778+
2779+ self.get_buffer().connect("insert-text", self.on_add_text)
2780+ self.get_buffer().connect("changed", self.on_text_changed)
2781+ self.connect("expose-event", self.expose_view)
2782+
2783+ # set properties
2784+ self.set_border_width(0)
2785+ self.set_accepts_tab(True)
2786+ self.set_editable(True)
2787+ self.set_cursor_visible(True)
2788+ self.set_wrap_mode(gtk.WRAP_WORD_CHAR)
2789+ self.set_left_margin(2)
2790+ self.set_right_margin(2)
2791+ self.set_pixels_above_lines(2)
2792+ self.set_pixels_below_lines(2)
2793+
2794+ self.base_color = util.get_style().base[gtk.STATE_NORMAL]
2795+ self.error_color = gtk.gdk.color_parse("indianred")
2796+
2797+ # set state online/offline
2798+ if not self.connection.isConnected():
2799+ self.set_sensitive(False)
2800+
2801+ if util.gtkspell:
2802+ try:
2803+ self.spell = util.gtkspell.Spell(self, None)
2804+ except:
2805+ pass
2806+
2807+ def get_text(self):
2808+ buf = self.get_buffer()
2809+ return buf.get_text(*buf.get_bounds()).strip()
2810+
2811+ def get_char_count(self):
2812+ return len(unicode(self.get_text(), "utf-8"))
2813+
2814+ def on_add_text(self, buf, iter, text, tlen):
2815+ if self.model.settings["shorten_urls"]:
2816+ if text and text.startswith("http") and not " " in text \
2817+ and len(text) > 30:
2818+
2819+ buf = self.get_buffer()
2820+ buf.stop_emission("insert-text")
2821+ service = self.model.settings["urlshorter"] or "is.gd"
2822+ short = self.shortener.Shorten(text, service)
2823+ buf.insert(iter, short)
2824+
2825+ def set_overlay_text(self, text):
2826+ self.pango_overlay.set_markup(self.overlay_text % (self.overlay_color, text))
2827+
2828+ def expose_view(self, window, event):
2829+ if not self.drawable:
2830+ self.drawable = self.get_window(gtk.TEXT_WINDOW_TEXT)
2831+ self.pango_overlay = self.create_pango_layout("")
2832+ self.set_overlay_text(140)
2833+
2834+ gc = self.drawable.new_gc()
2835+ ww, wh = self.drawable.get_size()
2836+ tw, th = self.pango_overlay.get_pixel_size()
2837+ self.drawable.draw_layout(gc, ww - tw, wh - th, self.pango_overlay)
2838+
2839+ def on_text_changed(self, w):
2840+ chars = self.get_char_count()
2841+ color = self.error_color if chars > 140 else self.base_color
2842+ self.modify_base(gtk.STATE_NORMAL, color)
2843+
2844+ def on_connection_online(self, w):
2845+ self.set_sensitive(True)
2846+
2847+ def on_connection_offline(self, w):
2848+ self.set_sensitive(False)
2849+
2850+gtk.binding_entry_add_signal(InputTextView, gtk.keysyms.Return, 0, "submit")
2851+gtk.binding_entry_add_signal(InputTextView, gtk.keysyms.KP_Enter, 0, "submit")
2852
2853=== added directory 'gwibber/lib'
2854=== added file 'gwibber/lib/__init__.py'
2855--- gwibber/lib/__init__.py 1970-01-01 00:00:00 +0000
2856+++ gwibber/lib/__init__.py 2010-02-11 10:03:14 +0000
2857@@ -0,0 +1,34 @@
2858+import dbus
2859+from gwibber import util
2860+
2861+class GwibberPublic:
2862+
2863+ def __init__(self):
2864+ bus = dbus.SessionBus()
2865+ self.service = util.getbus("Service")
2866+ self.accounts_monitor = util.getbus("Accounts")
2867+
2868+ def post(self, message):
2869+ args = [message]
2870+ self.microblog.operation({
2871+ "args": args,
2872+ "opname": "send",
2873+ })
2874+
2875+ def GetServices(self):
2876+ return self.service.GetServices()
2877+
2878+ def GetAccounts(self):
2879+ return self.service.GetAccounts()
2880+
2881+ def SendMessage(self, message):
2882+ return self.service.SendMessage(message)
2883+
2884+ def Refresh(self):
2885+ return self.service.Refresh()
2886+
2887+ def MonitorAccountChanged(self, cb):
2888+ self.accounts_monitor.connect_to_signal("AccountChanged", cb)
2889+
2890+ def MonitorAccountDeleted(self, cb):
2891+ self.accounts_monitor.connect_to_signal("AccountDeleted", cb)
2892
2893=== added directory 'gwibber/lib/gtk'
2894=== added file 'gwibber/lib/gtk/__init__.py'
2895--- gwibber/lib/gtk/__init__.py 1970-01-01 00:00:00 +0000
2896+++ gwibber/lib/gtk/__init__.py 2010-02-11 10:03:14 +0000
2897@@ -0,0 +1,1 @@
2898+__all__ = ["twitter", "identica", "flickr", "facebook", "friendfeed"]
2899
2900=== added file 'gwibber/lib/gtk/facebook.py'
2901--- gwibber/lib/gtk/facebook.py 1970-01-01 00:00:00 +0000
2902+++ gwibber/lib/gtk/facebook.py 2010-02-11 10:03:14 +0000
2903@@ -0,0 +1,110 @@
2904+#
2905+# Copyright (C) 2010 Canonical Ltd
2906+#
2907+# This program is free software: you can redistribute it and/or modify
2908+# it under the terms of the GNU General Public License version 2 as
2909+# published by the Free Software Foundation.
2910+#
2911+# This program is distributed in the hope that it will be useful,
2912+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2913+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2914+# GNU General Public License for more details.
2915+#
2916+# You should have received a copy of the GNU General Public License
2917+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2918+#
2919+# Copyright (C) 2010 Ken VanDine <ken.vandine@canonical.com>
2920+#
2921+# facebook widgets for Gwibber
2922+#
2923+
2924+import gtk
2925+import urllib
2926+import webkit
2927+from gtk import Builder
2928+import gwibber.microblog
2929+from gwibber.microblog import facebook
2930+from gwibber.microblog.support import facelib
2931+import json, urlparse
2932+
2933+APP_KEY = "71b85c6d8cb5bbb9f1a3f8bbdcdd4b05"
2934+
2935+class AccountWidget(gtk.VBox):
2936+ """AccountWidget: A widget that provides a user interface for configuring facebook accounts in Gwibber
2937+ """
2938+
2939+ def __init__(self, account=None):
2940+ """Creates the account pane for configuring facebook accounts"""
2941+ gtk.VBox.__init__( self, False, 20 )
2942+ self.ui = gtk.Builder()
2943+ self.ui.add_from_file (gwibber.resources.get_ui_asset("gwibber-accounts-facebook.ui"))
2944+ self.ui.connect_signals(self)
2945+ self.vbox_settings = self.ui.get_object("vbox_settings")
2946+ self.pack_start(self.vbox_settings, False, False)
2947+ self.vbox_settings.show_all()
2948+ if account:
2949+ self.account = account
2950+ else:
2951+ self.account = {}
2952+ try:
2953+ if self.account["session_key"] and self.account["secret_key"] and self.account["username"]:
2954+ self.ui.get_object("hbox_facebook_auth").hide()
2955+ self.ui.get_object("fb_auth_done_label").set_label(str(self.account["username"]) + " has been authorized by Facebook")
2956+ self.ui.get_object("hbox_facebook_auth_done").show()
2957+ except:
2958+ self.ui.get_object("hbox_facebook_auth_done").hide()
2959+
2960+ def on_facebook_auth_clicked(self, widget, data=None):
2961+ (self.win_w, self.win_h) = self.window.get_size()
2962+
2963+ web = webkit.WebView()
2964+ web.load_html_string("<p>Please wait...</p>", "file:///")
2965+
2966+ url = urllib.urlencode({
2967+ "api_key": "71b85c6d8cb5bbb9f1a3f8bbdcdd4b05",
2968+ "connect_display": "popup",
2969+ "v": "1.0",
2970+ "next": "http://www.facebook.com/connect/login_success.html",
2971+ "cancel_url": "http://www.facebook.com/connect/login_failure.html",
2972+ "fbconnect": "true",
2973+ "return_session": "true",
2974+ "req_perms": "publish_stream,read_stream,status_update,offline_access"
2975+ })
2976+ web.set_size_request(450, 340)
2977+ web.open("http://www.facebook.com/login.php?" + url)
2978+ web.connect("title-changed", self.on_facebook_auth_title_change)
2979+ self.pack_start(web, True, True, 0)
2980+ self.show_all()
2981+ self.ui.get_object("vbox1").hide()
2982+ self.ui.get_object("expander1").hide()
2983+
2984+ def on_facebook_auth_title_change(self, web=None, title=None, data=None):
2985+ if title.get_title() == "Success":
2986+ try:
2987+ url = web.get_main_frame().get_uri()
2988+ data = json.loads(urlparse.parse_qs(url.split("?", 1)[1])["session"][0])
2989+ self.account["session_key"] = str(data["session_key"])
2990+ self.account["secret_key"] = str(data["secret"])
2991+ fbuid = self.account["session_key"].split("-")[1]
2992+ fbc = facelib.Facebook(APP_KEY, "")
2993+ fbc.session_key = self.account["session_key"]
2994+ fbc.secret_key = self.account["secret_key"]
2995+ self.account["username"] = str(fbc.users.getInfo(fbuid)[0]["name"])
2996+ self.ui.get_object("hbox_facebook_auth").hide()
2997+ self.ui.get_object("fb_auth_done_label").set_label(str(self.account["username"]) + " has been authorized by Facebook")
2998+ self.ui.get_object("hbox_facebook_auth_done").show()
2999+ except:
3000+ #FIXME: We should do this in the same window
3001+ pass
3002+ web.hide()
3003+ self.window.resize(self.win_w, self.win_h)
3004+ self.ui.get_object("vbox1").show()
3005+ self.ui.get_object("expander1").show()
3006+
3007+ if title.get_title() == "Failure":
3008+ d = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR,
3009+ gtk.BUTTONS_OK, "Facebook authorization failed. Please try again.")
3010+ if d.run(): d.destroy()
3011+
3012+ web.hide()
3013+ self.window.resize(self.win_w, self.win_h)
3014
3015=== added file 'gwibber/lib/gtk/flickr.py'
3016--- gwibber/lib/gtk/flickr.py 1970-01-01 00:00:00 +0000
3017+++ gwibber/lib/gtk/flickr.py 2010-02-11 10:03:14 +0000
3018@@ -0,0 +1,38 @@
3019+#
3020+# Copyright (C) 2010 Canonical Ltd
3021+#
3022+# This program is free software: you can redistribute it and/or modify
3023+# it under the terms of the GNU General Public License version 2 as
3024+# published by the Free Software Foundation.
3025+#
3026+# This program is distributed in the hope that it will be useful,
3027+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3028+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3029+# GNU General Public License for more details.
3030+#
3031+# You should have received a copy of the GNU General Public License
3032+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3033+#
3034+# Copyright (C) 2010 Ken VanDine <ken.vandine@canonical.com>
3035+#
3036+# Flickr widgets for Gwibber
3037+#
3038+
3039+import gtk
3040+from gtk import Builder
3041+import gwibber.resources
3042+
3043+class AccountWidget(gtk.VBox):
3044+ """AccountWidget: A widget that provides a user interface for configuring flickr accounts in Gwibber
3045+ """
3046+
3047+ def __init__(self, account=None):
3048+ """Creates the account pane for configuring flickr accounts"""
3049+ gtk.VBox.__init__( self, False, 20 )
3050+ self.ui = gtk.Builder()
3051+ self.ui.add_from_file (gwibber.resources.get_ui_asset("gwibber-accounts-flickr.ui"))
3052+ self.ui.connect_signals(self)
3053+ self.vbox_settings = self.ui.get_object("vbox_settings")
3054+ self.pack_start(self.vbox_settings, False, False)
3055+ self.vbox_settings.show_all()
3056+
3057
3058=== added file 'gwibber/lib/gtk/friendfeed.py'
3059--- gwibber/lib/gtk/friendfeed.py 1970-01-01 00:00:00 +0000
3060+++ gwibber/lib/gtk/friendfeed.py 2010-02-11 10:03:14 +0000
3061@@ -0,0 +1,38 @@
3062+#
3063+# Copyright (C) 2010 Canonical Ltd
3064+#
3065+# This program is free software: you can redistribute it and/or modify
3066+# it under the terms of the GNU General Public License version 2 as
3067+# published by the Free Software Foundation.
3068+#
3069+# This program is distributed in the hope that it will be useful,
3070+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3071+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3072+# GNU General Public License for more details.
3073+#
3074+# You should have received a copy of the GNU General Public License
3075+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3076+#
3077+# Copyright (C) 2010 Ken VanDine <ken.vandine@canonical.com>
3078+#
3079+# FriendFeed widgets for Gwibber
3080+#
3081+
3082+import gtk
3083+from gtk import Builder
3084+import gwibber.microblog
3085+
3086+class AccountWidget(gtk.VBox):
3087+ """AccountWidget: A widget that provides a user interface for configuring friendfeed accounts in Gwibber
3088+ """
3089+
3090+ def __init__(self, account=None):
3091+ """Creates the account pane for configuring friendfeed accounts"""
3092+ gtk.VBox.__init__( self, False, 20 )
3093+ self.ui = gtk.Builder()
3094+ self.ui.add_from_file (gwibber.resources.get_ui_asset("gwibber-accounts-friendfeed.ui"))
3095+ self.ui.connect_signals(self)
3096+ self.vbox_settings = self.ui.get_object("vbox_settings")
3097+ self.pack_start(self.vbox_settings, False, False)
3098+ self.vbox_settings.show_all()
3099+
3100
3101=== added file 'gwibber/lib/gtk/identica.py'
3102--- gwibber/lib/gtk/identica.py 1970-01-01 00:00:00 +0000
3103+++ gwibber/lib/gtk/identica.py 2010-02-11 10:03:14 +0000
3104@@ -0,0 +1,38 @@
3105+#
3106+# Copyright (C) 2010 Canonical Ltd
3107+#
3108+# This program is free software: you can redistribute it and/or modify
3109+# it under the terms of the GNU General Public License version 2 as
3110+# published by the Free Software Foundation.
3111+#
3112+# This program is distributed in the hope that it will be useful,
3113+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3114+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3115+# GNU General Public License for more details.
3116+#
3117+# You should have received a copy of the GNU General Public License
3118+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3119+#
3120+# Copyright (C) 2010 Ken VanDine <ken.vandine@canonical.com>
3121+#
3122+# Identi.ca widgets for Gwibber
3123+#
3124+
3125+import gtk
3126+from gtk import Builder
3127+import gwibber.microblog
3128+
3129+class AccountWidget(gtk.VBox):
3130+ """AccountWidget: A widget that provides a user interface for configuring identica accounts in Gwibber
3131+ """
3132+
3133+ def __init__(self, account=None):
3134+ """Creates the account pane for configuring identica accounts"""
3135+ gtk.VBox.__init__( self, False, 20 )
3136+ self.ui = gtk.Builder()
3137+ self.ui.add_from_file (gwibber.resources.get_ui_asset("gwibber-accounts-identica.ui"))
3138+ self.ui.connect_signals(self)
3139+ self.vbox_settings = self.ui.get_object("vbox_settings")
3140+ self.pack_start(self.vbox_settings, False, False)
3141+ self.vbox_settings.show_all()
3142+
3143
3144=== added file 'gwibber/lib/gtk/twitter.py'
3145--- gwibber/lib/gtk/twitter.py 1970-01-01 00:00:00 +0000
3146+++ gwibber/lib/gtk/twitter.py 2010-02-11 10:03:14 +0000
3147@@ -0,0 +1,76 @@
3148+#
3149+# Copyright (C) 2010 Canonical Ltd
3150+#
3151+# This program is free software: you can redistribute it and/or modify
3152+# it under the terms of the GNU General Public License version 2 as
3153+# published by the Free Software Foundation.
3154+#
3155+# This program is distributed in the hope that it will be useful,
3156+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3157+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3158+# GNU General Public License for more details.
3159+#
3160+# You should have received a copy of the GNU General Public License
3161+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3162+#
3163+# Copyright (C) 2010 Ken VanDine <ken.vandine@canonical.com>
3164+#
3165+# Twitter widgets for Gwibber
3166+#
3167+
3168+import gtk
3169+from gtk import Builder
3170+import gwibber.microblog
3171+
3172+class AccountWidget(gtk.VBox):
3173+ """AccountWidget: A widget that provides a user interface for configuring twitter accounts in Gwibber
3174+ """
3175+
3176+ def __init__(self, account=None):
3177+ """Creates the account pane for configuring twitter accounts"""
3178+ gtk.VBox.__init__( self, False, 20 )
3179+ self.ui = gtk.Builder()
3180+ self.ui.add_from_file (gwibber.resources.get_ui_asset("gwibber-accounts-twitter.ui"))
3181+ self.ui.connect_signals(self)
3182+ self.vbox_settings = self.ui.get_object("vbox_settings")
3183+ self.pack_start(self.vbox_settings, False, False)
3184+ self.vbox_settings.show_all()
3185+
3186+"""
3187+ gtk.VBox.__init__( self, False, 20 )
3188+ username_box = gtk.HBox(False,2)
3189+ self.pack_start(username_box, False, False)
3190+ username_label = gtk.Label("Username:")
3191+ username_box.pack_start(username_label, False, False)
3192+ username_entry = gtk.Entry()
3193+ username_box.pack_start(username_entry, False, False)
3194+ username_box.show_all()
3195+
3196+ password_box = gtk.HBox(False,2)
3197+ self.pack_start(password_box, False, False)
3198+ password_label = gtk.Label("Password:")
3199+ password_box.pack_start(password_label, False, False)
3200+ password_entry = gtk.Entry()
3201+ password_entry.set_visibility(False)
3202+ password_box.pack_start(password_entry, False, False)
3203+ password_box.show_all()
3204+
3205+ button_box = gtk.HButtonBox()
3206+ button_box.set_border_width(5)
3207+ button_box.set_layout(gtk.BUTTONBOX_END)
3208+ button_box.set_spacing(0)
3209+ cancel_button = gtk.Button(stock='gtk-cancel')
3210+ button_box.add(cancel_button)
3211+ cancel_button.connect("clicked", self.on_cancel_clicked)
3212+ save_button = gtk.Button(stock='gtk-save')
3213+ button_box.add(save_button)
3214+ save_button.connect("clicked", self.on_save_clicked)
3215+ self.pack_end(button_box, False, False)
3216+ button_box.show_all()
3217+
3218+ def on_cancel_clicked(elf, widget, data=None):
3219+ print "on_cancel_clicked"
3220+
3221+ def on_save_clicked(elf, widget, data=None):
3222+ print "on_save_clicked"
3223+"""
3224
3225=== added file 'gwibber/lib/gtk/widgets.py'
3226--- gwibber/lib/gtk/widgets.py 1970-01-01 00:00:00 +0000
3227+++ gwibber/lib/gtk/widgets.py 2010-02-11 10:03:14 +0000
3228@@ -0,0 +1,65 @@
3229+#
3230+# Copyright (C) 2010 Canonical Ltd
3231+#
3232+# This program is free software: you can redistribute it and/or modify
3233+# it under the terms of the GNU General Public License version 2 as
3234+# published by the Free Software Foundation.
3235+#
3236+# This program is distributed in the hope that it will be useful,
3237+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3238+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3239+# GNU General Public License for more details.
3240+#
3241+# You should have received a copy of the GNU General Public License
3242+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3243+#
3244+# Copyright (C) 2010 Ken VanDine <ken.vandine@canonical.com>
3245+#
3246+# widgets for Gwibber
3247+#
3248+
3249+from dbus.mainloop.glib import DBusGMainLoop
3250+import gobject, gtk
3251+import gwibber.gwui, gwibber.util, gwibber.resources
3252+import gettext
3253+from gettext import lgettext as _
3254+if hasattr(gettext, 'bind_textdomain_codeset'):
3255+ gettext.bind_textdomain_codeset('gwibber','UTF-8')
3256+gettext.textdomain('gwibber')
3257+
3258+class GwibberPosterVBox(gtk.VBox):
3259+ def __init__(self):
3260+ gtk.VBox.__init__(self)
3261+ DBusGMainLoop(set_as_default=True)
3262+ loop = gobject.MainLoop()
3263+ self.service = gwibber.util.getbus("Service")
3264+ self.stream_model = gwibber.gwui.Model()
3265+
3266+ self.input = gwibber.gwui.Input()
3267+ self.input.connect("submit", self.on_input_activate)
3268+ self.input.connect("changed", self.on_input_changed)
3269+ self.input_splitter = gtk.VPaned()
3270+ self.input_splitter.add1(self.input)
3271+
3272+ self.button_send = gtk.Button(_("Send"))
3273+ self.button_send.connect("clicked", self.on_input_activate)
3274+ self.message_target = gwibber.gwui.AccountTargetBar(self.stream_model)
3275+ self.message_target.pack_end(self.button_send, False)
3276+
3277+ content = gtk.VBox(spacing=5)
3278+ content.pack_start(self.input_splitter, True)
3279+ content.pack_start(self.message_target, False)
3280+ content.set_border_width(5)
3281+
3282+ layout = gtk.VBox()
3283+ layout.pack_start(content, True)
3284+ self.add(layout)
3285+
3286+ def on_input_changed(self, w, text, cnt):
3287+ self.input.textview.set_overlay_text(str(140 - cnt))
3288+
3289+ def on_input_activate(self, w, text, cnt):
3290+ self.service.SendMessage(text)
3291+ w.clear()
3292+
3293+
3294
3295=== added directory 'gwibber/microblog'
3296=== added file 'gwibber/microblog/__init__.py'
3297=== added file 'gwibber/microblog/brightkite.py'
3298--- gwibber/microblog/brightkite.py 1970-01-01 00:00:00 +0000
3299+++ gwibber/microblog/brightkite.py 2010-02-11 10:03:14 +0000
3300@@ -0,0 +1,177 @@
3301+"""
3302+
3303+BrightKite interface for Gwibber
3304+SegPhault (Ryan Paul) - 10/19/2008
3305+
3306+"""
3307+
3308+from . import support
3309+import urllib2, urllib, base64, re, simplejson
3310+from xml.dom import minidom
3311+
3312+PROTOCOL_INFO = {
3313+ "name": "BrightKite",
3314+ "version": 0.2,
3315+
3316+ "config": [
3317+ "password",
3318+ "username",
3319+ "message_color",
3320+ "receive_enabled",
3321+ "send_enabled"
3322+ ],
3323+
3324+ "features": [
3325+ "receive",
3326+ "responses",
3327+ "thread",
3328+ ],
3329+}
3330+
3331+NICK_PARSE = re.compile("@([A-Za-z0-9]+)")
3332+
3333+class Message:
3334+ def __init__(self, client, data):
3335+ self.client = client
3336+ self.account = client.account
3337+ self.protocol = client.account["protocol"]
3338+ self.username = client.account["username"]
3339+
3340+ self.sender = data["creator"]["fullname"]
3341+ self.sender_nick = data["creator"]["login"]
3342+ self.sender_id = data["creator"]["login"]
3343+ self.image = data["creator"]["small_avatar_url"]
3344+
3345+ self.time = support.parse_time(data["created_at"])
3346+ self.text = data["body"] or ""
3347+ self.bgcolor = "message_color"
3348+ self.id = data["id"]
3349+
3350+ self.url = "http://brightkite.com/objects/%s" % data["id"]
3351+ self.profile_url = "http://brightkite.com/people/%s" % self.sender_nick
3352+
3353+ self.html_string = '<span class="text">%s</span>' % \
3354+ NICK_PARSE.sub('@<a class="inlinenick" href="http://brightkite.com/people/\\1">\\1</a>',
3355+ support.linkify(self.text))
3356+
3357+ self.is_reply = ("@%s" % self.username) in self.text
3358+ self.can_thread = data["comments_count"] > 0
3359+
3360+ # Geolocation
3361+ self.location_lon = data["place"]["longitude"]
3362+ self.location_lat = data["place"]["latitude"]
3363+ self.location_id = data["place"]["id"]
3364+ self.location_name = data["place"]["name"]
3365+ self.location_fullname = data["place"]["display_location"]
3366+ self.geo_position = (self.location_lat, self.location_lon)
3367+
3368+ if "photo" in data:
3369+ self.thumbnails = [{"src": data["photo"], "href": data["photo"]}]
3370+
3371+class Comment:
3372+ def __init__(self, client, data):
3373+ self.client = client
3374+ self.account = client.account
3375+ self.protocol = client.account["protocol"]
3376+ self.username = client.account["username"]
3377+
3378+ self.sender = data["user"]["fullname"]
3379+ self.sender_nick = data["user"]["login"]
3380+ self.sender_id = data["user"]["login"]
3381+ self.image = data["user"]["small_avatar_url"]
3382+
3383+ self.time = support.parse_time(data["created_at"])
3384+ self.text = data["comment"]
3385+ self.bgcolor = "message_color"
3386+
3387+ self.url = "http://brightkite.com/objects/%s" % data["place_object"]["id"]
3388+ self.profile_url = "http://brightkite.com/people/%s" % self.sender_nick
3389+
3390+ self.html_string = '<span class="text">%s</span>' % \
3391+ NICK_PARSE.sub('@<a class="inlinenick" href="http://brightkite.com/people/\\1">\\1</a>',
3392+ support.linkify(self.text))
3393+
3394+ self.is_reply = ("@%s" % self.username) in self.text
3395+
3396+class FriendPosition:
3397+ def __init__(self, client, data):
3398+ self.client = client
3399+ self.account = client.account
3400+ self.protocol = client.account["protocol"]
3401+ self.username = client.account["username"]
3402+ self.sender = data["fullname"]
3403+ self.sender_nick = data["login"]
3404+ self.sender_id = self.sender_nick
3405+ self.time = support.parse_time(data["last_checked_in"])
3406+ self.text = data["place"]["display_location"]
3407+ self.image = data["small_avatar_url"]
3408+ self.image_small = data["smaller_avatar_url"]
3409+ self.bgcolor = "message_color"
3410+ self.url = "http://brightkite.com" # TODO
3411+ self.profile_url = "http://brightkite.com" # TODO
3412+ self.is_reply = False
3413+
3414+ # Geolocation
3415+ self.location_lon = data["place"]["longitude"]
3416+ self.location_lat = data["place"]["latitude"]
3417+ self.location_id = data["place"]["id"]
3418+ self.location_name = data["place"]["name"]
3419+ self.location_fullname = data["place"]["display_location"]
3420+
3421+class Client:
3422+ def __init__(self, acct):
3423+ self.account = acct
3424+
3425+ def get_auth(self):
3426+ return "Basic %s" % base64.encodestring(
3427+ ("%s:%s" % (self.account["username"], self.account["password"]))).strip()
3428+
3429+ def connect(self, url, data = None):
3430+ return urllib2.urlopen(urllib2.Request(
3431+ url, data, {"Authorization": self.get_auth()}))
3432+
3433+ def get_friend_positions(self):
3434+ return simplejson.load(self.connect(
3435+ "http://brightkite.com/me/friends.json"))
3436+
3437+ def get_messages(self):
3438+ return simplejson.load(self.connect(
3439+ "http://brightkite.com/me/friendstream.json"))
3440+
3441+ def get_responses(self):
3442+ return simplejson.load(self.connect(
3443+ "http://brightkite.com/me/mentionsstream.json"))
3444+
3445+ def get_thread_data(self, msg):
3446+ return simplejson.load(self.connect(
3447+ "http://brightkite.com/objects/%s/comments.json" % msg.id))
3448+
3449+ def get_search(self, query):
3450+ return minidom.parseString(urllib2.urlopen(
3451+ urllib2.Request("http://identi.ca/search/notice/rss",
3452+ urllib.urlencode({"q": query}))).read()).getElementsByTagName("item")
3453+
3454+ def thread(self, msg):
3455+ yield msg
3456+ for data in self.get_thread_data(msg):
3457+ yield Comment(self, data)
3458+
3459+ def friend_positions(self):
3460+ for data in self.get_friend_positions():
3461+ yield FriendPosition(self, data)
3462+
3463+ def search(self, query):
3464+ for data in self.get_search(query):
3465+ yield SearchResult(self, data, query)
3466+
3467+ def responses(self):
3468+ for data in self.get_responses():
3469+ yield Message(self, data)
3470+
3471+ def receive(self):
3472+ for data in self.get_messages():
3473+ yield Message(self, data)
3474+
3475+ def send(self, message):
3476+ return self.connect("http://identi.ca/api/statuses/update.json",
3477+ urllib.urlencode({"status":message}))
3478
3479=== added file 'gwibber/microblog/can.py'
3480--- gwibber/microblog/can.py 1970-01-01 00:00:00 +0000
3481+++ gwibber/microblog/can.py 2010-02-11 10:03:14 +0000
3482@@ -0,0 +1,23 @@
3483+
3484+"""
3485+
3486+Microblog feature index
3487+
3488+"""
3489+
3490+SEND = 0
3491+RECEIVE = 1
3492+SEARCH = 2
3493+REPLY = 3
3494+RESPONSES = 4
3495+DELETE = 5
3496+THREAD = 6
3497+THREAD_REPLY = 7
3498+TAG = 8
3499+GROUP = 9
3500+SEARCH_URL = 10
3501+USER_MESSAGES = 11
3502+READ = 12
3503+RETWEET = 13
3504+LIKE = 14
3505+GEO_FRIEND_POSITIONS = 50
3506
3507=== added file 'gwibber/microblog/digg.py'
3508--- gwibber/microblog/digg.py 1970-01-01 00:00:00 +0000
3509+++ gwibber/microblog/digg.py 2010-02-11 10:03:14 +0000
3510@@ -0,0 +1,92 @@
3511+
3512+"""
3513+
3514+Digg interface for Gwibber
3515+SegPhault (Ryan Paul) - 01/06/2008
3516+
3517+"""
3518+
3519+import urllib2, support, re
3520+import time, simplejson
3521+
3522+PROTOCOL_INFO = {
3523+ "name": "Digg",
3524+ "version": 0.1,
3525+
3526+ "config": [
3527+ "username",
3528+ "digg_color",
3529+ "comment_color",
3530+ "receive_enabled",
3531+ ],
3532+
3533+ "features": [
3534+ "receive",
3535+ ],
3536+}
3537+
3538+LINK_PARSE = re.compile("<a[^>]+href=\"(https?://[^\"]+)\">[^<]+</a>")
3539+
3540+def sanitize_text(t):
3541+ return LINK_PARSE.sub("\\1", t.strip())
3542+
3543+class Message:
3544+ def __init__(self, client, data):
3545+ self.client = client
3546+ self.account = client.account
3547+ self.protocol = client.account["protocol"]
3548+ self.username = client.account["username"]
3549+ self.title = data["title"]
3550+ sender = data["friends"]["users"][0]
3551+ self.sender = "fullname" in sender and sender["fullname"] or sender["name"]
3552+ self.sender_nick = sender["name"]
3553+ self.sender_id = sender["name"]
3554+ self.id = data["id"]
3555+
3556+ try:
3557+ self.time = support.parse_time(data["submit_date"])
3558+ except:
3559+ self.time = support.parse_time(time.asctime(time.gmtime(data["submit_date"])))
3560+
3561+ self.text = sanitize_text(data["description"])
3562+ self.image = sender["icon"]
3563+ self.bgcolor = "comment_color"
3564+ self.url = data["link"]
3565+ self.profile_url = "http://digg.com/users/%s" % self.sender_nick
3566+ self.liked_by = data["diggs"]
3567+
3568+class Digg(Message):
3569+ def __init__(self, client, data):
3570+ Message.__init__(self, client, data)
3571+ self.title = "%s <small>dugg %s</small>" % (self.sender_nick, self.title)
3572+ self.bgcolor = "digg_color"
3573+
3574+class Client:
3575+ def __init__(self, acct):
3576+ self.account = acct
3577+
3578+ def receive_enabled(self):
3579+ return self.account["receive_enabled"] and \
3580+ self.account["username"] != None
3581+
3582+ def connect(self, url, data = None):
3583+ return urllib2.urlopen(urllib2.Request(url, data))
3584+
3585+ def get_comments(self):
3586+ return simplejson.load(self.connect(
3587+ "http://services.digg.com/user/%s/friends/commented?appkey=http://launchpad.net/gwibber&type=json" %
3588+ self.account["username"]))["stories"]
3589+
3590+ def get_diggs(self):
3591+ return simplejson.load(self.connect(
3592+ "http://services.digg.com/user/%s/friends/dugg?appkey=http://launchpad.net/gwibber&type=json" %
3593+ (self.account["username"])))["stories"]
3594+
3595+ def receive(self):
3596+ #for data in self.get_comments()[0:10]:
3597+ # yield Message(self, data)
3598+
3599+ for data in self.get_diggs():
3600+ yield Digg(self, data)
3601+
3602+
3603
3604=== added file 'gwibber/microblog/dispatcher.py'
3605--- gwibber/microblog/dispatcher.py 1970-01-01 00:00:00 +0000
3606+++ gwibber/microblog/dispatcher.py 2010-02-11 10:03:14 +0000
3607@@ -0,0 +1,486 @@
3608+#!/usr/bin/env python
3609+
3610+import multiprocessing, threading, traceback, json, time, logging
3611+import gobject, dbus, dbus.service, mx.DateTime
3612+import twitter, identica, flickr, facebook, friendfeed
3613+
3614+import urlshorter
3615+import util, util.couch
3616+from util import resources
3617+from util.couch import Monitor as CouchMonitor
3618+from util.couch import RecordMonitor
3619+from desktopcouch.records.server import CouchDatabase
3620+from desktopcouch.records.record import Record as CouchRecord
3621+
3622+from util.const import *
3623+
3624+try:
3625+ import indicate
3626+except:
3627+ indicate = None
3628+
3629+gobject.threads_init()
3630+
3631+PROTOCOLS = {
3632+ "twitter": twitter,
3633+ "identica": identica,
3634+ "flickr": flickr,
3635+ "facebook": facebook,
3636+ "friendfeed": friendfeed,
3637+}
3638+
3639+FEATURES = json.loads(GWIBBER_OPERATIONS)
3640+SERVICES = dict([(k, v.PROTOCOL_INFO) for k, v in PROTOCOLS.items()])
3641+SETTINGS = RecordMonitor(COUCH_DB_SETTINGS, COUCH_RECORD_SETTINGS, COUCH_TYPE_CONFIG, DEFAULT_SETTINGS)
3642+
3643+def perform_operation((acctid, opname, args, transient)):
3644+ try:
3645+ stream = FEATURES[opname]["stream"] or opname
3646+ accounts = CouchDatabase(COUCH_DB_ACCOUNTS, create=True)
3647+ messages = CouchDatabase(COUCH_DB_MESSAGES, create=True)
3648+ account = dict(accounts.get_record(acctid).items())
3649+
3650+ logtext = "<%s:%s>" % (account["protocol"], opname)
3651+ logging.debug("%s Performing operation", logtext)
3652+
3653+ args = dict((str(k), v) for k, v in args.items())
3654+ message_data = PROTOCOLS[account["protocol"]].Client(account)(opname, **args)
3655+ new_messages = []
3656+
3657+ for m in message_data:
3658+ key = (m["id"], m["account"], opname, transient)
3659+ key = "-".join(x for x in key if x)
3660+ if not messages.record_exists(key):
3661+ m["operation"] = opname
3662+ m["stream"] = stream
3663+ m["transient"] = transient
3664+
3665+ logging.debug("%s Adding record", logtext)
3666+ new_messages.append(m)
3667+ messages.put_record(CouchRecord(m, COUCH_TYPE_MESSAGE, key))
3668+
3669+ logging.debug("%s Finished operation", logtext)
3670+ return ("Success", new_messages)
3671+ except Exception as e:
3672+ if not "logtext" in locals(): logtext = "<UNKNOWN>"
3673+ logging.debug("%s Operation failed", logtext)
3674+ logging.debug("Traceback:\n%s", traceback.format_exc())
3675+ return ("Failure", traceback.format_exc())
3676+
3677+class OperationCollector:
3678+ def __init__(self):
3679+ self.accounts = CouchDatabase(COUCH_DB_ACCOUNTS, create=True)
3680+ self.settings = CouchDatabase(COUCH_DB_SETTINGS, create=True)
3681+ self.messages = CouchDatabase(COUCH_DB_MESSAGES, create=True)
3682+ util.couch.init_design_doc(self.messages, "messages", COUCH_VIEW_MESSAGES)
3683+
3684+ def handle_max_id(self, acct, opname, id=None):
3685+ if not id: id = acct["_id"]
3686+ if "sinceid" in SERVICES[acct["protocol"]]["features"]:
3687+ view = self.messages.execute_view("maxid", "messages")
3688+ result = view[[id, opname]][[id, opname]].rows
3689+ if len(result) > 0: return {"since": result[0].value}
3690+ return {}
3691+
3692+ def validate_operation(self, acct, opname, enabled="receive_enabled"):
3693+ protocol = SERVICES[acct["protocol"]]
3694+ return acct["protocol"] in PROTOCOLS and \
3695+ opname in protocol["features"] and \
3696+ opname in FEATURES and acct[enabled]
3697+
3698+ def stream_to_operation(self, stream):
3699+ account = self.accounts.get_record(stream["account"])
3700+ args = stream["parameters"]
3701+ opname = stream["operation"]
3702+ if self.validate_operation(account, opname):
3703+ args.update(self.handle_max_id(account, opname, stream["_id"]))
3704+ return (stream["account"], stream["operation"], args, stream["_id"])
3705+
3706+ def search_to_operations(self, search):
3707+ for account in self.accounts.get_records(COUCH_TYPE_ACCOUNT, True):
3708+ account = account.value
3709+ args = {"query": search["query"]}
3710+ if self.validate_operation(account, "search"):
3711+ args.update(self.handle_max_id(account, "search", search["_id"]))
3712+ yield (account["_id"], "search", args, search["_id"])
3713+
3714+ def account_to_operations(self, acct):
3715+ if isinstance(acct, basestring):
3716+ acct = dict(self.accounts.get_record(acct).items())
3717+ for opname in SERVICES[acct["protocol"]]["default_streams"]:
3718+ if self.validate_operation(acct, opname):
3719+ args = self.handle_max_id(acct, opname)
3720+ yield (acct["_id"], opname, args, False)
3721+
3722+ def get_send_operations(self, message):
3723+ for account in self.accounts.get_records(COUCH_TYPE_ACCOUNT, True):
3724+ account = account.value
3725+ if self.validate_operation(account, "send", "send_enabled"):
3726+ yield (account["_id"], "send", {"message": message}, False)
3727+
3728+ def get_operation_by_id(self, id):
3729+ if self.settings.record_exists(id):
3730+ item = dict(self.settings.get_record(id).items())
3731+ if item["record_type"] == COUCH_TYPE_STREAM:
3732+ return [self.stream_to_operation(item)]
3733+ if item["record_type"] == COUCH_TYPE_SEARCH:
3734+ return list(self.search_to_operations(item))
3735+
3736+ def get_operations(self):
3737+ for acct in self.accounts.get_records(COUCH_TYPE_ACCOUNT, True):
3738+ acct = acct.value
3739+ for o in self.account_to_operations(acct):
3740+ yield o
3741+
3742+ for stream in self.settings.get_records(COUCH_TYPE_STREAM, True):
3743+ stream = stream.value
3744+ if self.accounts.record_exists(stream["account"]):
3745+ o = self.stream_to_operation(stream)
3746+ if o: yield o
3747+
3748+ for search in self.settings.get_records(COUCH_TYPE_SEARCH, True):
3749+ search = search.value
3750+ for o in self.search_to_operations(search):
3751+ yield o
3752+
3753+class StreamMonitor(dbus.service.Object):
3754+ __dbus_object_path__ = "/com/gwibber/Streams"
3755+
3756+ def __init__(self):
3757+ self.bus = dbus.SessionBus()
3758+ bus_name = dbus.service.BusName("com.Gwibber.Streams", bus=self.bus)
3759+ dbus.service.Object.__init__(self, bus_name, self.__dbus_object_path__)
3760+
3761+ setting_monitor = CouchMonitor(COUCH_DB_SETTINGS)
3762+ setting_monitor.connect("record-updated", self.on_setting_changed)
3763+ setting_monitor.connect("record-deleted", self.on_setting_deleted)
3764+
3765+ def on_setting_changed(self, monitor, id):
3766+ if id == "settings": self.SettingChanged()
3767+ else:
3768+ logging.debug("Stream changed: %s", id)
3769+ self.StreamChanged(id)
3770+
3771+ def on_setting_deleted(self, monitor, id):
3772+ logging.debug("Stream closed: %s", id)
3773+ self.StreamClosed(id)
3774+
3775+ @dbus.service.signal("com.Gwibber.Streams", signature="s")
3776+ def StreamChanged(self, id): pass
3777+
3778+ @dbus.service.signal("com.Gwibber.Streams", signature="s")
3779+ def StreamClosed(self, id): pass
3780+
3781+ @dbus.service.signal("com.Gwibber.Streams")
3782+ def SettingChanged(self): pass
3783+
3784+class AccountMonitor(dbus.service.Object):
3785+ __dbus_object_path__ = "/com/gwibber/Accounts"
3786+
3787+ def __init__(self):
3788+ self.bus = dbus.SessionBus()
3789+ bus_name = dbus.service.BusName("com.Gwibber.Accounts", bus=self.bus)
3790+ dbus.service.Object.__init__(self, bus_name, self.__dbus_object_path__)
3791+
3792+ account_monitor = CouchMonitor(COUCH_DB_ACCOUNTS)
3793+ account_monitor.connect("record-updated", self.on_account_changed)
3794+ account_monitor.connect("record-deleted", self.on_account_deleted)
3795+
3796+ def on_account_changed(self, monitor, id):
3797+ logging.debug("Account changed: %s", id)
3798+ self.AccountChanged(id)
3799+
3800+ def on_account_deleted(self, monitor, id):
3801+ logging.debug("Account deleted: %s", id)
3802+ self.AccountDeleted(id)
3803+
3804+ @dbus.service.signal("com.Gwibber.Accounts", signature="s")
3805+ def AccountChanged(self, id): pass
3806+
3807+ @dbus.service.signal("com.Gwibber.Accounts", signature="s")
3808+ def AccountDeleted(self, id): pass
3809+
3810+class MessagesMonitor(dbus.service.Object):
3811+ __dbus_object_path__ = "/com/gwibber/Messages"
3812+
3813+ def __init__(self):
3814+ self.bus = dbus.SessionBus()
3815+ bus_name = dbus.service.BusName("com.Gwibber.Messages", bus=self.bus)
3816+ dbus.service.Object.__init__(self, bus_name, self.__dbus_object_path__)
3817+
3818+ self.monitor = CouchMonitor(COUCH_DB_MESSAGES)
3819+ self.monitor.connect("record-updated", self.on_message_updated)
3820+
3821+ self.messages = CouchDatabase(COUCH_DB_MESSAGES, create=True)
3822+
3823+ self.indicator_items = {}
3824+ self.notified_items = []
3825+
3826+ if indicate and util.resources.get_desktop_file():
3827+ self.indicate = indicate.indicate_server_ref_default()
3828+ self.indicate.set_type("message.gwibber")
3829+ self.indicate.set_desktop_file(util.resources.get_desktop_file())
3830+ self.indicate.connect("server-display", self.on_indicator_activate)
3831+ self.indicate.show()
3832+
3833+ def on_message_updated(self, monitor, id):
3834+ try:
3835+ logging.debug("Message updated: %s", id)
3836+ message = self.messages.get_record(id)
3837+ self.new_message(message)
3838+ except:
3839+ logging.debug("Message updated: %s, failed", id)
3840+
3841+
3842+ @dbus.service.signal("com.Gwibber.Messages", signature="s")
3843+ def MessageUpdated(self, id): pass
3844+
3845+ def on_indicator_activate(self, indicator, timestamp):
3846+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
3847+ client_bus = dbus.SessionBus()
3848+ logging.debug("Raising gwibber client")
3849+ try:
3850+ client_obj = client_bus.get_object("com.GwibberClient",
3851+ "/com/GwibberClient", follow_name_owner_changes = True,
3852+ introspect = False)
3853+ gw = dbus.Interface(client_obj, "com.GwibberClient")
3854+ gw.focus_client(reply_handler=self.handle_focus_reply,
3855+ error_handler=self.handle_focus_error)
3856+ except dbus.DBusException:
3857+ print_exc()
3858+
3859+
3860+ def on_indicator_reply_activate(self, indicator, timestamp):
3861+ logging.debug("Raising gwibber client, focusing replies stream")
3862+ client_bus = dbus.SessionBus()
3863+ try:
3864+ client_obj = client_bus.get_object("com.GwibberClient", "/com/GwibberClient")
3865+ gw = dbus.Interface(client_obj, "com.GwibberClient")
3866+ gw.show_replies(reply_handler=self.handle_focus_reply,
3867+ error_handler=self.handle_focus_error)
3868+ indicator.hide()
3869+ except dbus.DBusException:
3870+ print_exc()
3871+
3872+ def handle_focus_reply(self, *args):
3873+ logging.error("Failed to raise client %s", args)
3874+
3875+ def handle_focus_error(self, *args):
3876+ logging.error("Failed to raise client %s", args)
3877+
3878+ def new_message(self, message):
3879+ min_time = mx.DateTime.DateTimeFromTicks() - mx.DateTime.TimeDelta(minutes=10.0)
3880+ logging.debug("Checking message %s timestamp (%s) to see if it is newer than %s", message["id"], mx.DateTime.DateTimeFromTicks(message["time"]).localtime(), min_time)
3881+ if mx.DateTime.DateTimeFromTicks(message["time"]).localtime() > mx.DateTime.DateTimeFromTicks(min_time):
3882+ logging.debug("Message %s newer than %s, notifying", message["id"], min_time)
3883+ if indicate and message["stream"] == "replies":
3884+ if message["id"] not in self.indicator_items:
3885+ logging.debug("Message %s is a reply, adding messaging indicator", message["id"])
3886+ self.handle_indicator_items(message)
3887+ if message["id"] not in self.notified_items:
3888+ self.notified_items.append(message["id"])
3889+ self.show_notification_bubble(message)
3890+
3891+ def handle_indicator_item(self, message):
3892+ indicator = indicate.Indicator() if hasattr(indicate, "Indicator") else indicate.IndicatorMessage()
3893+ indicator.connect("user-display", self.on_indicator_reply_activate)
3894+ indicator.set_property("subtype", "im")
3895+ indicator.set_property("sender", message["sender"].get("name", ""))
3896+ indicator.set_property("body", message["text"])
3897+ indicator.set_property_time("time",
3898+ mx.DateTime.DateTimeFromTicks(message["time"]).localtime().ticks())
3899+ self.indicator_items[message["id"]] = indicator
3900+ indicator.show()
3901+
3902+ def show_notification_bubble(self, message):
3903+ if util.can_notify and SETTINGS["show_notifications"]:
3904+ if SETTINGS["notify_mentions_only"] and not message["stream"] == "replies": return
3905+ #until image caching is working again, we will post the gwibber icon
3906+ #image = hasattr(message, "image_path") and message["image_path"] or ''
3907+ image = util.resources.get_ui_asset("gwibber.svg")
3908+ expire_timeout = 5000
3909+ n = util.notify(message["sender"].get("name", ""), message["text"], image, expire_timeout)
3910+
3911+class MapAsync(threading.Thread):
3912+ def __init__(self, func, iterable, cbsuccess, cbfailure, timeout=60):
3913+ threading.Thread.__init__(self)
3914+ self.iterable = iterable
3915+ self.callback = cbsuccess
3916+ self.failure = cbfailure
3917+ self.timeout = timeout
3918+ self.daemon = True
3919+ self.func = func
3920+ self.start()
3921+
3922+ def run(self):
3923+ try:
3924+ pool = multiprocessing.Pool()
3925+ pool.map_async(self.func, self.iterable, callback=self.callback).get(self.timeout)
3926+ except Exception as e:
3927+ self.failure(e, traceback.format_exc())
3928+
3929+class Dispatcher(dbus.service.Object, threading.Thread):
3930+ __dbus_object_path__ = "/com/gwibber/Service"
3931+
3932+ def __init__(self, loop, autorefresh=True):
3933+ self.bus = dbus.SessionBus()
3934+ bus_name = dbus.service.BusName("com.Gwibber.Service", bus=self.bus)
3935+ dbus.service.Object.__init__(self, bus_name, self.__dbus_object_path__)
3936+ threading.Thread.__init__(self)
3937+
3938+ self.collector = OperationCollector()
3939+
3940+ self.refresh_count = 0
3941+ self.mainloop = loop
3942+
3943+ if autorefresh:
3944+ gobject.timeout_add(60 * 1000 * 5, self.refresh)
3945+ self.refresh()
3946+
3947+ self.accounts = CouchDatabase(COUCH_DB_ACCOUNTS, create=True)
3948+
3949+ @dbus.service.signal("com.Gwibber.Service")
3950+ def LoadingComplete(self): pass
3951+
3952+ @dbus.service.signal("com.Gwibber.Service")
3953+ def LoadingStarted(self): pass
3954+
3955+ @dbus.service.method("com.Gwibber.Service")
3956+ def Refresh(self): self.refresh()
3957+
3958+ @dbus.service.method("com.Gwibber.Service", in_signature="s")
3959+ def PerformOp(self, opdata):
3960+ try: o = json.loads(opdata)
3961+ except: return
3962+
3963+ logging.debug("** Starting Single Operation **")
3964+ self.LoadingStarted()
3965+
3966+ params = ["account", "operation", "args", "transient"]
3967+ operation = None
3968+
3969+ if "id" in o:
3970+ operation = self.collector.get_operation_by_id(o["id"])
3971+ elif all(i in o for i in params):
3972+ operation = [tuple(o[i] for i in params)]
3973+ elif "account" in o and self.accounts.record_exists(o["account"]):
3974+ operation = self.collector.account_to_operations(o["account"])
3975+
3976+ if operation:
3977+ MapAsync(perform_operation, operation, self.loading_complete, self.loading_failed)
3978+
3979+ @dbus.service.method("com.Gwibber.Service", in_signature="s")
3980+ def SendMessage(self, message):
3981+ self.send(self.collector.get_send_operations(message))
3982+
3983+ @dbus.service.method("com.Gwibber.Service", in_signature="s")
3984+ def Send(self, opdata):
3985+ try:
3986+ o = json.loads(opdata)
3987+ if "target" in o:
3988+ args = {"message": o["message"], "target": o["target"]}
3989+ operations = [(o["target"]["account"], "send_thread", args, None)]
3990+ elif "accounts" in o:
3991+ operations = [(a, "send", {"message": o["message"]}, None) for a in o["accounts"]]
3992+ self.send(operations)
3993+ except: pass
3994+
3995+ @dbus.service.method("com.Gwibber.Service", out_signature="s")
3996+ def GetServices(self): return json.dumps(SERVICES)
3997+
3998+ @dbus.service.method("com.Gwibber.Service", out_signature="s")
3999+ def GetFeatures(self): return json.dumps(FEATURES)
4000+
4001+ @dbus.service.method("com.Gwibber.Service", out_signature="s")
4002+ def GetAccounts(self):
4003+ all_accounts = []
4004+ for account in self.accounts.get_records(COUCH_TYPE_ACCOUNT, True):
4005+ all_accounts.append(account.value)
4006+ return json.dumps(all_accounts)
4007+
4008+ @dbus.service.method("com.Gwibber.Service")
4009+ def Quit(self): self.mainloop.quit()
4010+
4011+ def loading_complete(self, output):
4012+ self.refresh_count += 1
4013+ self.LoadingComplete()
4014+ logging.debug("Loading complete: %s - %s", self.refresh_count, [o[0] for o in output])
4015+
4016+ def loading_failed(self, exception, tb):
4017+ logging.debug("Loading failed: %s - %s", exception, tb)
4018+
4019+ def send(self, operations):
4020+ operations = util.compact(operations)
4021+ if operations:
4022+ self.LoadingStarted()
4023+ logging.debug("*** Sending Message ***")
4024+ MapAsync(perform_operation, operations, self.loading_complete, self.loading_failed)
4025+
4026+ def refresh(self):
4027+ operations = list(self.collector.get_operations())
4028+ if operations:
4029+ logging.debug("** Starting Refresh **")
4030+ self.LoadingStarted()
4031+ MapAsync(perform_operation, operations, self.loading_complete, self.loading_failed)
4032+ return True
4033+
4034+class ConnectionMonitor(dbus.service.Object):
4035+ __dbus_object_path__ = "/com/gwibber/Connection"
4036+
4037+ def __init__(self):
4038+ self.bus = dbus.SessionBus()
4039+ bus_name = dbus.service.BusName("com.Gwibber.Connection", bus=self.bus)
4040+ dbus.service.Object.__init__(self, bus_name, self.__dbus_object_path__)
4041+
4042+ self.sysbus = dbus.SystemBus()
4043+ try:
4044+ self.nm = self.sysbus.get_object(NM_DBUS_SERVICE, NM_DBUS_OBJECT_PATH)
4045+ self.nm.connect_to_signal("StateChanged", self.on_connection_changed)
4046+ except:
4047+ pass
4048+
4049+ def on_connection_changed(self, state):
4050+ if state == NM_STATE_CONNECTED:
4051+ logging.info("Network state changed to Online")
4052+ self.ConnectionOnline(state)
4053+ if state == NM_STATE_DISCONNECTED:
4054+ logging.info("Network state changed to Offline")
4055+ self.ConnectionOffline(state)
4056+
4057+ @dbus.service.signal("com.Gwibber.Connetion", signature="u")
4058+ def ConnectionOnline(self, state): pass
4059+
4060+ @dbus.service.signal("com.Gwibber.Connection", signature="u")
4061+ def ConnectionOffline(self, state): pass
4062+
4063+ @dbus.service.method("com.Gwibber.Connection")
4064+ def isConnected(self):
4065+ try:
4066+ if self.nm.state() == NM_STATE_CONNECTED:
4067+ return True
4068+ return False
4069+ except:
4070+ return True
4071+
4072+class URLShorten(dbus.service.Object):
4073+ __dbus_object_path__ = "/com/gwibber/URLShorten"
4074+
4075+ def __init__(self):
4076+ self.bus = dbus.SessionBus()
4077+ bus_name = dbus.service.BusName("com.Gwibber.URLShorten", bus=self.bus)
4078+ dbus.service.Object.__init__(self, bus_name, self.__dbus_object_path__)
4079+
4080+ @dbus.service.method("com.Gwibber.URLShorten")
4081+ def Shorten(self, url, service):
4082+ if self.IsShort(url): return url
4083+ try:
4084+ s = urlshorter.PROTOCOLS[service].URLShorter()
4085+ return s.short(url)
4086+ except: return url
4087+
4088+ @dbus.service.method("com.Gwibber.URLShorten")
4089+ def IsShort(self, url):
4090+ for us in urlshorter.PROTOCOLS.values():
4091+ if url.startswith(us.PROTOCOL_INFO["fqdn"]):
4092+ return True
4093+ return False
4094
4095=== added file 'gwibber/microblog/facebook.py'
4096--- gwibber/microblog/facebook.py 1970-01-01 00:00:00 +0000
4097+++ gwibber/microblog/facebook.py 2010-02-11 10:03:14 +0000
4098@@ -0,0 +1,174 @@
4099+import network, util
4100+import hashlib, mx.DateTime, time
4101+from gettext import lgettext as _
4102+
4103+PROTOCOL_INFO = {
4104+ "name": "Facebook",
4105+ "version": "1.0",
4106+
4107+ "config": [
4108+ "color",
4109+ "receive_enabled",
4110+ "send_enabled",
4111+ "username",
4112+ "session_key",
4113+ "secret_key"
4114+ ],
4115+
4116+ "authtype": "facebook",
4117+ "color": "#64006C",
4118+
4119+ "features": [
4120+ "send",
4121+ "reply",
4122+ "receive",
4123+ "thread",
4124+ "delete",
4125+ "send_thread",
4126+ "like",
4127+ ],
4128+
4129+ "default_streams": [
4130+ "receive",
4131+ ]
4132+}
4133+
4134+APP_KEY = "71b85c6d8cb5bbb9f1a3f8bbdcdd4b05"
4135+URL_PREFIX = "https://api.facebook.com/restserver.php"
4136+
4137+class Client:
4138+ def __init__(self, acct):
4139+ self.account = acct
4140+ self.user_id = acct["session_key"].split("-")[1]
4141+
4142+ def _get(self, operation, post=False, single=False, **args):
4143+ args.update({
4144+ "v": "1.0",
4145+ "format": "json",
4146+ "method": "facebook." + operation,
4147+ "api_key": APP_KEY,
4148+ "session_key": self.account["session_key"],
4149+ "call_id": str(int(time.time()) * 1000),
4150+ })
4151+
4152+ sig = "".join("%s=%s" % (k, v) for k, v in sorted(args.items()))
4153+ args["sig"] = hashlib.md5(sig + self.account["secret_key"]).hexdigest()
4154+ data = network.Download(URL_PREFIX, args, post).get_json()
4155+ return data
4156+
4157+ def _sender(self, user):
4158+ sender = {
4159+ "name": user["name"],
4160+ "id": str(user["id"]),
4161+ "is_me": str(user["id"]) == self.user_id,
4162+ "url": user["url"],
4163+ "image": user["pic_square"],
4164+ }
4165+
4166+ if not "?" in user["url"]:
4167+ sender["nick"] = user["url"].rsplit("/", 1)[-1]
4168+ return sender
4169+
4170+ def _message(self, data, profiles):
4171+ m = {}
4172+ m["id"] = str(data["post_id"])
4173+ m["protocol"] = "facebook"
4174+ m["account"] = self.account["_id"]
4175+ m["time"] = int(mx.DateTime.DateTimeFrom(data['created_time']).gmtime())
4176+
4177+ if data.get("attribution", 0):
4178+ m["source"] = util.strip_urls(data["attribution"]).replace("via ", "")
4179+
4180+ if data.get("message", "").strip():
4181+ m["text"] = data["message"]
4182+ m["html"] = util.linkify(data["message"])
4183+ m["content"] = m["html"]
4184+ else:
4185+ m["text"] = ""
4186+ m["html"] = ""
4187+ m["content"] = ""
4188+
4189+ if data.get("actor_id", 0) in profiles:
4190+ m["sender"] = self._sender(profiles[data["actor_id"]])
4191+
4192+ if data.get("likes", {}).get("count", None):
4193+ m["likes"] = {
4194+ "count": data["likes"]["count"],
4195+ "url": data["likes"]["href"],
4196+ }
4197+
4198+ if data.get("comments", 0):
4199+ m["comments"] = []
4200+ for item in data["comments"]["comment_list"]:
4201+ m["comments"].append({
4202+ "text": item["text"],
4203+ "time": int(mx.DateTime.DateTimeFrom(item["time"]).gmtime()),
4204+ "sender": self._sender(profiles[item["fromid"]]),
4205+ })
4206+
4207+ if data.get("attachment", 0):
4208+ if data["attachment"].get("name", 0):
4209+ m["content"] += "<p><b>%s</b></p>" % data["attachment"]["name"]
4210+
4211+ if data["attachment"].get("description", 0):
4212+ m["content"] += "<p>%s</p>" % data["attachment"]["description"]
4213+
4214+ m["images"] = []
4215+ for a in data["attachment"].get("media", []):
4216+ if a["type"] in ["photo", "video", "link"]:
4217+ if a.get("src", 0):
4218+ if a["src"].startswith("/"):
4219+ a["src"] = "http://facebook.com" + a["src"]
4220+ m["images"].append({"src": a["src"], "url": a["href"]})
4221+
4222+ return m
4223+
4224+ def _comment(self, data, profiles):
4225+ user = profiles[data["fromid"]]
4226+ return {
4227+ "id": str(data["id"]),
4228+ "protocol": "facebook",
4229+ "account": self.account["_id"],
4230+ "time": int(mx.DateTime.DateTimeFrom(data['time']).gmtime()),
4231+ "text": data["text"],
4232+ "sender": {
4233+ "nick": user["username"] or str(user["uid"]),
4234+ "name": user["name"],
4235+ "id": str(user["uid"]),
4236+ "url": user["profile_url"],
4237+ "image": user["pic_square"],
4238+ }
4239+ }
4240+
4241+ def __call__(self, opname, **args):
4242+ return getattr(self, opname)(**args)
4243+
4244+ def thread(self, id):
4245+ query = "SELECT name, profile_url, pic_square, username, uid FROM user WHERE uid in \
4246+ (SELECT fromid FROM comment WHERE post_id = '%s')" % id
4247+
4248+ profiles = dict((p["uid"], p) for p in self._get("fql.query", query=query))
4249+ comments = self._get("stream.getComments", post_id=id)
4250+ return [self._comment(comment, profiles) for comment in comments]
4251+
4252+ def receive(self):
4253+ data = self._get("stream.get", viewer_id=self.user_id, limit=80)
4254+ profiles = dict((p["id"], p) for p in data["profiles"])
4255+ return [self._message(post, profiles) for post in data["posts"]]
4256+
4257+ def delete(self, message):
4258+ self._get("stream.remove", post_id=message["id"])
4259+ return []
4260+
4261+ def like(self, message):
4262+ self._get("stream.addLike", post_id=message["id"])
4263+ return []
4264+
4265+ def send(self, message):
4266+ self._get("users.setStatus", status=message, status_includes_verb=False)
4267+ return []
4268+
4269+ def send_thread(self, message, target):
4270+ self._get("stream.addComment", post_id=target["id"], comment=message)
4271+ return []
4272+
4273
4274=== added file 'gwibber/microblog/flickr.py'
4275--- gwibber/microblog/flickr.py 1970-01-01 00:00:00 +0000
4276+++ gwibber/microblog/flickr.py 2010-02-11 10:03:14 +0000
4277@@ -0,0 +1,84 @@
4278+import re, network, util, mx.DateTime
4279+
4280+PROTOCOL_INFO = {
4281+ "name": "Flickr",
4282+ "version": 1.0,
4283+
4284+ "config": [
4285+ "username",
4286+ "color",
4287+ "receive_enabled",
4288+ ],
4289+
4290+ "authtype": "none",
4291+ "color": "#C31A00",
4292+
4293+ "features": [
4294+ "receive",
4295+ ],
4296+
4297+ "default_streams": [
4298+ "receive"
4299+ ]
4300+}
4301+
4302+API_KEY = "36f660117e6555a9cbda4309cfaf72d0"
4303+REST_SERVER = "http://api.flickr.com/services/rest"
4304+BUDDY_ICON_URL = "http://farm%s.static.flickr.com/%s/buddyicons/%s.jpg"
4305+IMAGE_URL = "http://farm%s.static.flickr.com/%s/%s_%s_%s.jpg"
4306+IMAGE_PAGE_URL = "http://www.flickr.com/photos/%s/%s"
4307+
4308+def parse_time(t):
4309+ return mx.DateTime.DateTimeFromTicks(int(t)).gmtime().ticks()
4310+
4311+class Client:
4312+ def __init__(self, acct):
4313+ self.account = acct
4314+
4315+ def _message(self, data):
4316+ m = {}
4317+ m["id"] = str(data["id"])
4318+ m["protocol"] = "flickr"
4319+ m["account"] = self.account["_id"]
4320+ m["time"] = parse_time(data["dateupload"])
4321+ m["source"] = False
4322+ m["text"] = data["title"]
4323+
4324+ m["sender"] = {}
4325+ m["sender"]["name"] = data["username"]
4326+ m["sender"]["nick"] = data["ownername"]
4327+ m["sender"]["is_me"] = data["ownername"] == self.account["username"]
4328+ m["sender"]["id"] = data["owner"]
4329+ m["sender"]["image"] = BUDDY_ICON_URL % (data["iconfarm"], data["iconserver"], data["owner"])
4330+ m["sender"]["url"] = "http://www.flickr.com/people/%s" % (data["owner"])
4331+
4332+ m["url"] = IMAGE_PAGE_URL % (data["owner"], data["id"])
4333+ m["images"] = [{
4334+ "src": IMAGE_URL % (data["farm"], data["server"], data["id"], data["secret"], "m"),
4335+ "url": IMAGE_PAGE_URL % (data["owner"], data["id"])
4336+ }]
4337+
4338+ m["content"] = data["title"]
4339+ m["html"] = data["title"]
4340+
4341+ return m
4342+
4343+ def _get(self, method, parse="message", post=False, **args):
4344+ args.update({"api_key": API_KEY, "method": method})
4345+ data = network.Download(REST_SERVER, args, post).get_json()
4346+ if parse == "message":
4347+ return [self._message(m) for m in data["photos"]["photo"]]
4348+ else: return data
4349+
4350+ def _getNSID(self):
4351+ return self._get("flickr.people.findByUsername",
4352+ parse=None, nojsoncallback="1", format="json",
4353+ username=self.account["username"])["user"]["nsid"]
4354+
4355+ def __call__(self, opname, **args):
4356+ return getattr(self, opname)(**args)
4357+
4358+ def receive(self):
4359+ return self._get("flickr.photos.getContactsPublicPhotos",
4360+ user_id=self._getNSID(), format="json", nojsoncallback="1",
4361+ extras="date_upload,owner_name,icon_server")
4362
4363=== added file 'gwibber/microblog/friendfeed.py'
4364--- gwibber/microblog/friendfeed.py 1970-01-01 00:00:00 +0000
4365+++ gwibber/microblog/friendfeed.py 2010-02-11 10:03:14 +0000
4366@@ -0,0 +1,144 @@
4367+
4368+import network, util
4369+
4370+PROTOCOL_INFO = {
4371+ "name": "FriendFeed",
4372+ "version": 0.1,
4373+
4374+ "config": [
4375+ "private:password",
4376+ "username",
4377+ "color",
4378+ "receive_enabled",
4379+ "send_enabled",
4380+ ],
4381+
4382+ "authtype": "login",
4383+ "color": "#198458",
4384+
4385+ "features": [
4386+ "send",
4387+ "receive",
4388+ "public",
4389+ "search",
4390+ "reply",
4391+ "thread",
4392+ "send_thread",
4393+ "like",
4394+ "delete",
4395+ "search_url",
4396+ "user_messages",
4397+ ],
4398+
4399+ "default_streams": [
4400+ "receive"
4401+ ]
4402+}
4403+
4404+URL_PREFIX = "https://friendfeed.com/api"
4405+
4406+class Client:
4407+ def __init__(self, acct):
4408+ self.account = acct
4409+
4410+ def _sender(self, user):
4411+ return {
4412+ "name": user["name"],
4413+ "nick": user["nickname"],
4414+ "is_me": self.account["username"] == user["nickname"],
4415+ "id": user["id"],
4416+ "url": user["profileUrl"],
4417+ "image": "http://friendfeed.com/%s/picture?size=medium" % user["nickname"],
4418+ }
4419+
4420+ def _message(self, data):
4421+ m = {
4422+ "id": data["id"],
4423+ "protocol": "friendfeed",
4424+ "account": self.account["_id"],
4425+ "time": util.parsetime(data["published"]),
4426+ "source": data.get("via", {}).get("name", None),
4427+ "text": data["title"],
4428+ "html": util.linkify(data["title"]),
4429+ "content": util.linkify(data["title"]),
4430+ "url": data["link"],
4431+ "sender": self._sender(data["user"]),
4432+ }
4433+
4434+ if data.get("service", 0):
4435+ m["service"] = {
4436+ "id": data["service"]["id"],
4437+ "name": data["service"]["name"],
4438+ "icon": data["service"]["iconUrl"],
4439+ "url": data["service"]["profileUrl"],
4440+ }
4441+
4442+ if data.get("likes", 0):
4443+ m["likes"] = {"count": len(data["likes"])}
4444+
4445+ if data.get("comments", 0):
4446+ m["comments"] = []
4447+ for item in data["comments"][-3:]:
4448+ m["comments"].append({
4449+ "text": item["body"],
4450+ "time": util.parsetime(item["date"]),
4451+ "sender": self._sender(item["user"]),
4452+ })
4453+
4454+ for i in data["media"]:
4455+ if i.get("thumbnails", 0):
4456+ m["images"] = []
4457+ for t in i["thumbnails"]:
4458+ m["images"].append({"src": t["url"], "url": i["link"]})
4459+
4460+ if data.get("geo", 0):
4461+ m["location"] = data["geo"]
4462+
4463+ return m
4464+
4465+ def _get(self, path, post=False, parse="message", kind="entries", single=False, **args):
4466+ url = "/".join((URL_PREFIX, path))
4467+ data = network.Download(url, util.compact(args), post,
4468+ self.account["username"], self.account["password"]).get_json()
4469+
4470+ if parse:
4471+ data = data[kind][0] if single else data[kind]
4472+ return [getattr(self, "_%s" % parse)(m) for m in data]
4473+
4474+ def __call__(self, opname, **args):
4475+ return getattr(self, opname)(**args)
4476+
4477+ def receive(self, count=util.COUNT, since=None):
4478+ return self._get("feed/home", num=count, start=since)
4479+
4480+ def public(self, count=util.COUNT, since=None):
4481+ return self._get("feed/public", num=count, start=since)
4482+
4483+ def thread(self, id, count=util.COUNT, since=None):
4484+ self._get("feed/entry/%s" % id, num=count, start=since)
4485+
4486+ def search(self, query, count=util.COUNT, since=None):
4487+ return self._get("feed/search", q=query, num=count, start=since)
4488+
4489+ def search_url(self, query, count=util.COUNT, since=None):
4490+ return self._get("feed/url", url=query, num=util.COUNT, start=None)
4491+
4492+ def user_messages(self, id, count=util.COUNT, since=None):
4493+ return self._get("feed/user/%s" % id, num=count, start=since)
4494+
4495+ def delete(self, message):
4496+ self._get("entry/delete", True, None, entry=message["id"])
4497+ return []
4498+
4499+ def like(self, message):
4500+ self._get("entry/like", True, None, entry=message["id"])
4501+ return []
4502+
4503+ def send(self, message):
4504+ self._get("share", True, single=True, title=message)
4505+ return []
4506+
4507+ def send_thread(self, message, target):
4508+ self._get("comment", True, None, body=message, entry=target["id"])
4509+ return []
4510+
4511
4512=== added file 'gwibber/microblog/greader.py'
4513--- gwibber/microblog/greader.py 1970-01-01 00:00:00 +0000
4514+++ gwibber/microblog/greader.py 2010-02-11 10:03:14 +0000
4515@@ -0,0 +1,151 @@
4516+
4517+"""
4518+
4519+Google Reader interface for Gwibber
4520+Grillo (Diego Herrera) - 7/02/2009
4521+
4522+bassed on RSS interface:
4523+SegPhault (Ryan Paul) - 11/08/2008
4524+
4525+"""
4526+
4527+from . import support
4528+import urlparse, logging, feedparser
4529+import urllib, urllib2, re
4530+import webbrowser
4531+
4532+PROTOCOL_INFO = {
4533+ "name": "Google Reader",
4534+ "version": 0.1,
4535+
4536+ "config": [
4537+ "private:password",
4538+ "username",
4539+ "message_color",
4540+ "receive_enabled",
4541+ ],
4542+
4543+ "features": [
4544+ can.RECEIVE,
4545+ can.READ,
4546+ ],
4547+}
4548+
4549+feedparser._HTMLSanitizer.acceptable_elements = []
4550+
4551+class Message:
4552+ def __init__(self, client, data):
4553+ self.client = client
4554+ self.account = client.account
4555+ self.protocol = client.account["protocol"]
4556+ self.username = client.account["username"]
4557+
4558+ self.source = ""
4559+ self.sender = data.get("author", "")
4560+
4561+ if hasattr(data, "source"):
4562+ self.source = data.source.get("title", "")
4563+ self.sender = data.source.get("title", "")
4564+ self.profile_url = data.source.link
4565+
4566+ self.gr_id = data.get("id", "")
4567+
4568+ self.image = "http://www.google.com/reader/ui/2296270177-logo-graphic.gif";
4569+ self.sender_nick = data.get("author", "")
4570+ self.sender_id = self.sender
4571+
4572+ self.time = support.parse_time(data.updated)
4573+ self.bgcolor = "message_color"
4574+ self.url = data.get("link", "")
4575+
4576+ if(self.source == ""):
4577+ self.title = self.sender
4578+ else:
4579+ self.title = "%s <small>By %s</small>" % (self.source, self.sender)
4580+
4581+ self.is_unread = True
4582+ self.categories = []
4583+
4584+ for category in data.tags:
4585+ if( (category.term.find("user/")>=0) and (category.term.find("/state/")>=0) and (category.label=='read') ):
4586+ self.is_unread = False
4587+ elif((category.term.find("user/")>=0) and (category.term.find("/label/")>=0)):
4588+ self.categories.append(category.label)
4589+
4590+ self.summary = data.get("summary", "")
4591+ self.html_string = data.title
4592+
4593+ if(len(self.summary) > 0):
4594+ self.text = self.summary
4595+ else:
4596+ self.text = data.source.get("title", "")
4597+
4598+class Client:
4599+ def __init__(self, acct):
4600+ self.account = acct
4601+ if(self.account["session"]!= None):
4602+ self.sid = self.account["session"]
4603+ else:
4604+ self.get_auth()
4605+
4606+
4607+ def get_auth(self):
4608+ header = {'User-agent' : 'Gwibber'}
4609+ post_data = urllib.urlencode({ 'Email': self.account["username"],
4610+ 'Passwd': self.account["private:password"],
4611+ 'service': 'reader',
4612+ 'source': 'Gwibber',
4613+ 'continue': 'http://www.google.com', })
4614+
4615+ request = urllib2.Request('https://www.google.com/accounts/ClientLogin',
4616+ post_data,
4617+ header)
4618+ try :
4619+ f = urllib2.urlopen( request )
4620+ res = f.read()
4621+ except:
4622+ raise
4623+ self.sid = re.search('SID=(\S*)', res).group(1)
4624+ self.account["session"] = self.sid
4625+
4626+ def get_results(self,url, data = None, count = 0):
4627+ header ={'User-agent' : 'Gwibber',
4628+ 'Cookie': 'Name=SID;SID=%s;Domain=.google.com;Path=/;Expires=160000000000' % self.sid}
4629+ request = urllib2.Request(url, data, header)
4630+ try :
4631+ f = urllib2.urlopen( request )
4632+ if(f.url != url):
4633+ self.get_auth()
4634+ if(count < 3):
4635+ res = self.get_results(url, data, count+1)
4636+ else:
4637+ res = None
4638+ else:
4639+ res = f.read()
4640+ except:
4641+ res = None
4642+ return res
4643+
4644+ def read_message(self, message):
4645+ webbrowser.open (message.url)
4646+ if message.is_unread:
4647+ token = self.get_results('http://www.google.com/reader/api/0/token')
4648+ post_data = urllib.urlencode ({ 'i' : message.gr_id,
4649+ 'T' : token,
4650+ 'ac' : 'edit-tags' ,
4651+ 'a' : 'user/-/state/com.google/read' })
4652+
4653+ url = 'http://www.google.com/reader/api/0/edit-tag'
4654+ self.get_results(url, post_data)
4655+ message.is_unread = False
4656+ return True
4657+ return False
4658+
4659+ def get_messages(self):
4660+ return feedparser.parse(
4661+ self.get_results(
4662+ 'http://www.google.com/reader/atom/user/-/state/com.google/reading-list?n=%s' % '20' if (self.account["receive_count"] == None) else self.account["receive_count"])).entries
4663+
4664+ def receive(self):
4665+ for data in self.get_messages():
4666+ yield Message(self, data)
4667
4668=== added file 'gwibber/microblog/identica.py'
4669--- gwibber/microblog/identica.py 1970-01-01 00:00:00 +0000
4670+++ gwibber/microblog/identica.py 2010-02-11 10:03:14 +0000
4671@@ -0,0 +1,165 @@
4672+import re, network, util
4673+from gettext import lgettext as _
4674+
4675+PROTOCOL_INFO = {
4676+ "name": "Identi.ca",
4677+ "version": 1.0,
4678+
4679+ "config": [
4680+ "private:password",
4681+ "username",
4682+ "color",
4683+ "receive_enabled",
4684+ "send_enabled",
4685+ ],
4686+
4687+ "authtype": "login",
4688+ "color": "#4E9A06",
4689+
4690+ "features": [
4691+ "send",
4692+ "receive",
4693+ "search",
4694+ "tag",
4695+ "reply",
4696+ "responses",
4697+ "private",
4698+ "public",
4699+ "delete",
4700+ "retweet",
4701+ "like",
4702+ "send_thread",
4703+ "user_messages",
4704+ "sinceid",
4705+ ],
4706+
4707+ "default_streams": [
4708+ "receive",
4709+ "responses",
4710+ "private",
4711+ ],
4712+}
4713+
4714+URL_PREFIX = "https://identi.ca"
4715+
4716+class Client:
4717+ def __init__(self, acct):
4718+ self.account = acct
4719+
4720+ def _common(self, data):
4721+ m = {}
4722+ m["id"] = str(data["id"])
4723+ m["protocol"] = "identica"
4724+ m["account"] = self.account["_id"]
4725+ m["time"] = util.parsetime(data["created_at"])
4726+ m["source"] = data.get("source", False)
4727+ m["text"] = data["text"]
4728+ m["to_me"] = ("@%s" % self.account["username"]) in data["text"]
4729+
4730+ m["html"] = util.linkify(m["text"],
4731+ ((util.PARSE_HASH, '#<a class="hash" href="%s#search?q=\\1">\\1</a>' % URL_PREFIX),
4732+ (util.PARSE_NICK, '@<a class="nick" href="%s/\\1">\\1</a>' % URL_PREFIX)))
4733+
4734+ m["content"] = util.linkify(m["text"],
4735+ ((util.PARSE_HASH, '#<a class="hash" href="gwibber:/tag?acct=%s&query=\\1">\\1</a>' % m["account"]),
4736+ (util.PARSE_NICK, '@<a class="nick" href="gwibber:/user?acct=%s&name=\\1">\\1</a>' % m["account"])))
4737+
4738+ return m
4739+
4740+ def _message(self, data):
4741+ m = self._common(data)
4742+
4743+ if data.get("in_reply_to_status_id", 0) and data.get("in_reply_to_screen_name", 0):
4744+ m["reply"] = {}
4745+ m["reply"]["id"] = data["in_reply_to_status_id"]
4746+ m["reply"]["nick"] = data["in_reply_to_screen_name"]
4747+ m["reply"]["url"] = "/".join((URL_PREFIX, "notice", str(m["reply"]["id"])))
4748+
4749+ user = data.get("user", data.get("sender", 0))
4750+
4751+ m["sender"] = {}
4752+ m["sender"]["name"] = user["name"]
4753+ m["sender"]["nick"] = user["screen_name"]
4754+ m["sender"]["id"] = user["id"]
4755+ m["sender"]["location"] = user["location"]
4756+ m["sender"]["followers"] = user["followers_count"]
4757+ m["sender"]["image"] = user["profile_image_url"]
4758+ m["sender"]["url"] = "/".join((URL_PREFIX, m["sender"]["nick"]))
4759+ m["sender"]["is_me"] = m["sender"]["nick"] == self.account["username"]
4760+ m["url"] = "/".join((URL_PREFIX, "notice", m["id"]))
4761+ return m
4762+
4763+ def _private(self, data):
4764+ m = self._message(data)
4765+ m["private"] = True
4766+ return m
4767+
4768+ def _result(self, data):
4769+ m = self._common(data)
4770+
4771+ if data["to_user_id"]:
4772+ m["reply"] = {}
4773+ m["reply"]["id"] = data["to_user_id"]
4774+ m["reply"]["nick"] = data["to_user"]
4775+
4776+ m["sender"] = {}
4777+ m["sender"]["nick"] = data["from_user"]
4778+ m["sender"]["id"] = data["from_user_id"]
4779+ m["sender"]["image"] = data["profile_image_url"]
4780+ m["sender"]["url"] = "/".join((URL_PREFIX, m["sender"]["nick"]))
4781+ m["url"] = "/".join((m["sender"]["url"], "statuses", m["id"]))
4782+ return m
4783+
4784+ def _get(self, path, parse="message", post=False, single=False, **args):
4785+ url = "/".join((URL_PREFIX, "api", path))
4786+ data = network.Download(url, util.compact(args), post,
4787+ self.account["username"], self.account["password"])
4788+ data = data.get_json()
4789+ if single: return [getattr(self, "_%s" % parse)(data)]
4790+ if parse: return [getattr(self, "_%s" % parse)(m) for m in data if "user" in m or "sender" in m]
4791+ else: return []
4792+
4793+ def _search(self, **args):
4794+ data = network.Download("%s/api/search.json" % URL_PREFIX, util.compact(args))
4795+ data = data.get_json()["results"]
4796+ return [self._result(m) for m in data]
4797+
4798+ def __call__(self, opname, **args):
4799+ return getattr(self, opname)(**args)
4800+
4801+ def receive(self, count=util.COUNT, since=None):
4802+ return self._get("statuses/friends_timeline.json", count=count, since_id=since)
4803+
4804+ def user_messages(self, id=None, count=util.COUNT, since=None):
4805+ return self._get("statuses/user_timeline.json", id=id, count=count, since_id=since)
4806+
4807+ def responses(self, count=util.COUNT, since=None):
4808+ return self._get("statuses/mentions.json", count=count, since_id=since)
4809+
4810+ def private(self, count=util.COUNT, since=None):
4811+ return self._get("direct_messages.json", "private", count=count, since_id=since)
4812+
4813+ def public(self):
4814+ return self._get("statuses/public_timeline.json")
4815+
4816+ def search(self, query, count=util.COUNT, since=None):
4817+ return self._search(q=query, rpp=count, since_id=since)
4818+
4819+ def tag(self, query, count=util.COUNT, since=None):
4820+ return self._search(q="#%s" % query, count=count, since_id=since)
4821+
4822+ def delete(self, message):
4823+ self._get("statuses/destroy/%s.json" % message["id"], None, post=True, do=1)
4824+ return []
4825+
4826+ def like(self, message):
4827+ self._get("favorites/create/%s.json" % message["id"], None, post=True, do=1)
4828+ return []
4829+
4830+ def send(self, message):
4831+ return self._get("statuses/update.json", post=True, single=True,
4832+ status=message, source="Gwibber")
4833+
4834+ def send_thread(self, message, target):
4835+ return self._get("statuses/update.json", post=True, single=True,
4836+ status=message, source="gwibber", in_reply_to_status_id=target["id"])
4837
4838=== added file 'gwibber/microblog/jaiku.py'
4839--- gwibber/microblog/jaiku.py 1970-01-01 00:00:00 +0000
4840+++ gwibber/microblog/jaiku.py 2010-02-11 10:03:14 +0000
4841@@ -0,0 +1,142 @@
4842+
4843+"""
4844+
4845+Jaiku interface for Gwibber
4846+SegPhault (Ryan Paul) - 01/05/2008
4847+
4848+"""
4849+
4850+from . import support
4851+import urllib2, urllib, re, simplejson, urlparse
4852+
4853+PROTOCOL_INFO = {
4854+ "name": "Jaiku",
4855+ "version": 0.3,
4856+
4857+ "config": [
4858+ "private:password",
4859+ "username",
4860+ "message_color",
4861+ "comment_color",
4862+ "receive_enabled",
4863+ "send_enabled"
4864+ ],
4865+
4866+ "features": [
4867+ "send",
4868+ "receive",
4869+ "reply",
4870+ "thread",
4871+ "send_thread",
4872+ ],
4873+}
4874+
4875+NONCE_PARSE = re.compile('.*_nonce" value="([^"]+)".*', re.M | re.S)
4876+LINK_MARKUP_PARSE = re.compile("\[([^\]]+)\]\(([^)]+)\)")
4877+
4878+class Message:
4879+ def __init__(self, client, data):
4880+ self.client = client
4881+ self.account = client.account
4882+ self.protocol = client.account["protocol"]
4883+ self.username = client.account["username"]
4884+ if "id" in data: self.id = data["id"]
4885+ self.sender = "%s %s" % (data["user"]["first_name"], data["user"]["last_name"])
4886+ self.sender_nick = data["user"]["nick"]
4887+ self.sender_id = data["user"]["nick"]
4888+
4889+ # Jaiku's timestamp format is lousy
4890+ d, t = data["created_at"].split("T")
4891+ self.time = support.parse_time(d + t.replace("-", ":"))
4892+
4893+ self.text = ""
4894+ if "title" in data:
4895+ self.text = data["title"]
4896+ #self.html_string = LINK_MARKUP_PARSE.sub('<a href="\\2">\\1</a>', self.text.replace(
4897+ # "&", "&amp;").replace("<", "&lt;").replace(">", "&gt;"))
4898+ self.image = data["user"]["avatar"]
4899+ self.bgcolor = "message_color"
4900+ self.url = data["url"]
4901+ self.profile_url = "http://%s.jaiku.com" % data["user"]["nick"]
4902+ if "icon" in data and data["icon"] != "": self.icon = data["icon"]
4903+ self.can_thread = True
4904+ self.is_reply = (re.compile("@%s[\W]+|@%s$" % (self.username, self.username)).search(self.text) != None) or \
4905+ (urlparse.urlparse(self.url)[1].split(".")[0].strip() == self.username and \
4906+ self.sender_nick != self.username)
4907+
4908+class Comment(Message):
4909+ def __init__(self, client, data):
4910+ Message.__init__(self, client, data)
4911+ self.text = data["content"]
4912+ self.bgcolor = "comment_color"
4913+ self.is_comment = True
4914+
4915+ self.text = data["content"]
4916+ #self.html_string = support.LINK_PARSE.sub('<a href="\\1">\\1</a>', LINK_MARKUP_PARSE.sub('<a href="\\2">\\1</a>', self.text.replace(
4917+ # "&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")))
4918+
4919+ if "entry_title" in data:
4920+ self.original_title = data["entry_title"]
4921+ self.title = "<small>Comment by</small> %s <small>on %s</small>" % (
4922+ self.sender, support.truncate(data["entry_title"],
4923+ client.account["comment_title_length"] or 20))
4924+
4925+ if "comment_id" in data:
4926+ self.id = data["comment_id"]
4927+ else: self.id = data["id"]
4928+
4929+class Client:
4930+ def __init__(self, acct):
4931+ self.account = acct
4932+
4933+ def send_enabled(self):
4934+ return self.account["send_enabled"] and \
4935+ self.account["username"] != None and \
4936+ self.account["private:password"] != None
4937+
4938+ def receive_enabled(self):
4939+ return self.account["receive_enabled"] and \
4940+ self.account["username"] != None and \
4941+ self.account["private:password"] != None
4942+
4943+ def get_messages(self):
4944+ return simplejson.loads(urllib2.urlopen(urllib2.Request(
4945+ "http://%s.jaiku.com/contacts/feed/json" % self.account["username"],
4946+ urllib.urlencode({"user": self.account["username"],
4947+ "personal_key":self.account["private:password"]}))).read())
4948+
4949+ def get_thread_data(self, msg):
4950+ return simplejson.loads(urllib2.urlopen(urllib2.Request(
4951+ "%s/json" % ("#" in msg.url and msg.url.split("#")[0] or msg.url),
4952+ urllib.urlencode({"user": self.account["username"],
4953+ "personal_key":self.account["private:password"]}))).read())
4954+
4955+ def thread(self, msg):
4956+ thread_content = self.get_thread_data(msg)
4957+ yield Message(self, thread_content)
4958+ for data in thread_content["comments"]:
4959+ yield Comment(self, data)
4960+
4961+ def receive(self):
4962+ for data in self.get_messages()["stream"]:
4963+ if "id" in data: yield Message(self, data)
4964+ else: yield Comment(self, data)
4965+
4966+ def send_thread(self, message, target):
4967+ print urllib2.urlopen(urllib2.Request("http://api.jaiku.com/json",
4968+ urllib.urlencode({
4969+ "method": "entry_add_comment",
4970+ "user": self.account["username"],
4971+ "personal_key":self.account["private:password"],
4972+ "stream": "stream/%s@jaiku.com/comments" % self.account["username"],
4973+ "entry": "stream/%s@jaiku.com/presence/%s" % (target.sender_nick, target.id),
4974+ "nick": "%s@jaiku.com" % self.account["username"],
4975+ "content": message
4976+ }))).read()
4977+
4978+ def send(self, message):
4979+ urllib2.urlopen(urllib2.Request(
4980+ "http://api.jaiku.com/json", urllib.urlencode({"user": self.account["username"],
4981+ "personal_key":self.account["private:password"],
4982+ "message": message, "method": "presence.send"}))).read()
4983+
4984
4985=== added file 'gwibber/microblog/laconica.py'
4986--- gwibber/microblog/laconica.py 1970-01-01 00:00:00 +0000
4987+++ gwibber/microblog/laconica.py 2010-02-11 10:03:14 +0000
4988@@ -0,0 +1,219 @@
4989+
4990+
4991+"""
4992+
4993+Laconi.ca interface for Gwibber
4994+SegPhault (Ryan Paul) - 11/15/2008
4995+
4996+"""
4997+
4998+from . import support
4999+import urllib2, urllib, base64, re, simplejson, feedparser
5000+
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: