Mercurial > libervia-backend
annotate src/plugins/plugin_misc_cs.py @ 510:886754295efe
quick frontend, primitivus, wix: MUC private messages management
/!\ not fully finished, backend part is not done yet /!\
- as resources are discarded to manage chat windows lists, a pretty dirty hack is done to work around this:
full jid is escaped using a prefix (it becomes invalid and resource is preserved).
- new quick_utils module, with helper methods. escapePrivate and unescapePrivate implementations
- MUC private messages are not managed in Wix yet
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 11 Oct 2012 00:48:35 +0200 |
parents | 2a072735e459 |
children |
rev | line source |
---|---|
101 | 1 #!/usr/bin/python |
2 # -*- coding: utf-8 -*- | |
3 | |
4 """ | |
5 SAT plugin for managing xep-0045 | |
459 | 6 Copyright (C) 2009, 2010, 2011, 2012 Jérôme Poisson (goffi@goffi.org) |
101 | 7 |
8 This program is free software: you can redistribute it and/or modify | |
480
2a072735e459
Licence modification: the full project is now under AGPL v3+ instead of GPL v3+
Goffi <goffi@goffi.org>
parents:
459
diff
changeset
|
9 it under the terms of the GNU Affero General Public License as published by |
101 | 10 the Free Software Foundation, either version 3 of the License, or |
11 (at your option) any later version. | |
12 | |
13 This program is distributed in the hope that it will be useful, | |
14 but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
480
2a072735e459
Licence modification: the full project is now under AGPL v3+ instead of GPL v3+
Goffi <goffi@goffi.org>
parents:
459
diff
changeset
|
16 GNU Affero General Public License for more details. |
101 | 17 |
480
2a072735e459
Licence modification: the full project is now under AGPL v3+ instead of GPL v3+
Goffi <goffi@goffi.org>
parents:
459
diff
changeset
|
18 You should have received a copy of the GNU Affero General Public License |
101 | 19 along with this program. If not, see <http://www.gnu.org/licenses/>. |
20 """ | |
21 | |
22 from logging import debug, info, warning, error | |
23 from twisted.words.xish import domish | |
24 from twisted.internet import protocol, defer, threads, reactor | |
25 from twisted.words.protocols.jabber import client, jid, xmlstream | |
26 from twisted.words.protocols.jabber import error as jab_error | |
27 from twisted.words.protocols.jabber.xmlstream import IQ | |
28 from twisted.web.client import getPage | |
440
48277946348b
plugin CS: use of PersistentBinaryData to store profile's information, and clean up on profile disconnection.
Goffi <goffi@goffi.org>
parents:
437
diff
changeset
|
29 from sat.memory.persistent import PersistentBinaryDict |
101 | 30 import os.path |
31 import pdb | |
32 | |
33 from zope.interface import implements | |
34 | |
35 from wokkel import disco, iwokkel, data_form | |
223 | 36 from sat.tools.xml_tools import XMLUI |
101 | 37 import urllib |
108
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
38 import webbrowser |
101 | 39 |
102 | 40 from BeautifulSoup import BeautifulSoup |
106 | 41 import re |
101 | 42 |
43 | |
44 PLUGIN_INFO = { | |
45 "name": "CouchSurfing plugin", | |
46 "import_name": "CS", | |
47 "type": "Misc", | |
48 "protocols": [], | |
49 "dependencies": [], | |
50 "main": "CS_Plugin", | |
51 "handler": "no", | |
52 "description": _(u"""This plugin allow to manage your CouchSurfing account throught your SàT frontend""") | |
53 } | |
54 | |
218
5c68a65548c3
Plugin CS: fixed forgotten debug stuff, CS plugin is now working again
Goffi <goffi@goffi.org>
parents:
109
diff
changeset
|
55 AGENT = 'Salut à Toi XMPP/CS Plugin' |
101 | 56 |
57 class CS_Plugin(): | |
58 | |
59 params = """ | |
60 <params> | |
61 <individual> | |
62 <category name="CouchSurfing"> | |
63 <param name="Login" type="string" /> | |
64 <param name="Password" type="password" /> | |
65 </category> | |
66 </individual> | |
67 </params> | |
68 """ | |
69 | |
70 def __init__(self, host): | |
71 info(_("Plugin CS initialization")) | |
72 self.host = host | |
73 #parameters | |
74 host.memory.importParams(CS_Plugin.params) | |
75 #menu | |
218
5c68a65548c3
Plugin CS: fixed forgotten debug stuff, CS plugin is now working again
Goffi <goffi@goffi.org>
parents:
109
diff
changeset
|
76 host.importMenu(_("Plugin"), "CouchSurfing", self.menuSelected, help_string = _("Launch CoushSurfing management interface")) |
440
48277946348b
plugin CS: use of PersistentBinaryData to store profile's information, and clean up on profile disconnection.
Goffi <goffi@goffi.org>
parents:
437
diff
changeset
|
77 self.data={} #TODO: delete cookies/data after a while |
107 | 78 self.host.registerGeneralCB("plugin_CS_sendMessage", self.sendMessage) |
108
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
79 self.host.registerGeneralCB("plugin_CS_showUnreadMessages", self.showUnreadMessages) |
102 | 80 |
440
48277946348b
plugin CS: use of PersistentBinaryData to store profile's information, and clean up on profile disconnection.
Goffi <goffi@goffi.org>
parents:
437
diff
changeset
|
81 def profileConnected(self, profile): |
48277946348b
plugin CS: use of PersistentBinaryData to store profile's information, and clean up on profile disconnection.
Goffi <goffi@goffi.org>
parents:
437
diff
changeset
|
82 self.data[profile] = PersistentBinaryDict("plugin_CS", profile) |
48277946348b
plugin CS: use of PersistentBinaryData to store profile's information, and clean up on profile disconnection.
Goffi <goffi@goffi.org>
parents:
437
diff
changeset
|
83 def dataLoaded(ignore): |
48277946348b
plugin CS: use of PersistentBinaryData to store profile's information, and clean up on profile disconnection.
Goffi <goffi@goffi.org>
parents:
437
diff
changeset
|
84 if not self.data[profile]: |
48277946348b
plugin CS: use of PersistentBinaryData to store profile's information, and clean up on profile disconnection.
Goffi <goffi@goffi.org>
parents:
437
diff
changeset
|
85 self.data[profile] = {'cookies':{}} |
48277946348b
plugin CS: use of PersistentBinaryData to store profile's information, and clean up on profile disconnection.
Goffi <goffi@goffi.org>
parents:
437
diff
changeset
|
86 |
48277946348b
plugin CS: use of PersistentBinaryData to store profile's information, and clean up on profile disconnection.
Goffi <goffi@goffi.org>
parents:
437
diff
changeset
|
87 self.data[profile].load().addCallback(dataLoaded) |
48277946348b
plugin CS: use of PersistentBinaryData to store profile's information, and clean up on profile disconnection.
Goffi <goffi@goffi.org>
parents:
437
diff
changeset
|
88 |
48277946348b
plugin CS: use of PersistentBinaryData to store profile's information, and clean up on profile disconnection.
Goffi <goffi@goffi.org>
parents:
437
diff
changeset
|
89 def profileDisconnected(self, profile): |
48277946348b
plugin CS: use of PersistentBinaryData to store profile's information, and clean up on profile disconnection.
Goffi <goffi@goffi.org>
parents:
437
diff
changeset
|
90 del self.data[profile] |
48277946348b
plugin CS: use of PersistentBinaryData to store profile's information, and clean up on profile disconnection.
Goffi <goffi@goffi.org>
parents:
437
diff
changeset
|
91 |
102 | 92 def erroCB(self, e, id): |
93 """Called when something is going wrong when contacting CS website""" | |
107 | 94 #pdb.set_trace() |
102 | 95 message_data={"reason": "connection error", "message":_(u"Impossible to contact CS website, please check your login/password, connection or try again later")} |
96 self.host.bridge.actionResult("ERROR", id, message_data) | |
97 | |
101 | 98 def menuSelected(self, id, profile): |
99 """Called when the couchsurfing menu item is selected""" | |
100 login = self.host.memory.getParamA("Login", "CouchSurfing", profile_key=profile) | |
101 password = self.host.memory.getParamA("Password", "CouchSurfing", profile_key=profile) | |
102 if not login or not password: | |
103 message_data={"reason": "uncomplete", "message":_(u"You have to fill your CouchSurfing login & password in parameters before using this interface")} | |
104 self.host.bridge.actionResult("ERROR", id, message_data) | |
105 return | |
106 | |
107 post_data = urllib.urlencode({'auth_login[un]':login,'auth_login[pw]':password,'auth_login[action]':'Login...'}) | |
108 | |
440
48277946348b
plugin CS: use of PersistentBinaryData to store profile's information, and clean up on profile disconnection.
Goffi <goffi@goffi.org>
parents:
437
diff
changeset
|
109 self.data[profile]['cookies'] = {} |
218
5c68a65548c3
Plugin CS: fixed forgotten debug stuff, CS plugin is now working again
Goffi <goffi@goffi.org>
parents:
109
diff
changeset
|
110 |
107 | 111 d = getPage('http://www.couchsurfing.org/login.html', method='POST', postdata=post_data, headers={'Content-Type':'application/x-www-form-urlencoded'} , agent=AGENT, cookies=self.data[profile]['cookies']) |
106 | 112 d.addCallback(self.__connectionCB, id, profile) |
107 | 113 d.addErrback(self.erroCB, id) |
102 | 114 |
218
5c68a65548c3
Plugin CS: fixed forgotten debug stuff, CS plugin is now working again
Goffi <goffi@goffi.org>
parents:
109
diff
changeset
|
115 #self.host.bridge.actionResult("SUPPRESS", id, {}) |
106 | 116 |
117 | |
118 #pages parsing callbacks | |
119 def savePage(self, name, html): | |
218
5c68a65548c3
Plugin CS: fixed forgotten debug stuff, CS plugin is now working again
Goffi <goffi@goffi.org>
parents:
109
diff
changeset
|
120 f = open ('/tmp/CS_'+name+'.html','w') |
106 | 121 f.write(html) |
122 f.close() | |
123 print "page [%s] sauvee" % name | |
124 #pdb.set_trace() | |
125 | |
126 def __connectionCB(self, html, id, profile): | |
127 print 'Response received' | |
218
5c68a65548c3
Plugin CS: fixed forgotten debug stuff, CS plugin is now working again
Goffi <goffi@goffi.org>
parents:
109
diff
changeset
|
128 #self.savePage('principale',html) |
106 | 129 soup = BeautifulSoup(html) |
130 self.data[profile]['user_nick'] = soup.find('a','item_link',href='/home.html').contents[0] | |
131 self.data[profile]['user_name'] = soup.html.head.title.string.split(' - ')[1] | |
132 #unread messages | |
133 try: | |
134 self.data[profile]['unread_messages'] = int(soup.find(lambda tag: tag.name=='div' and ('class','item_bubble') in tag.attrs and tag.find('a', href="/messages.html?message_status=inbox")).find(text=True)) | |
135 except: | |
136 self.data[profile]['unread_messages'] = 0 | |
137 #unread couchrequest messages | |
138 try: | |
139 self.data[profile]['unread_CR_messages'] = int(soup.find(lambda tag: tag.name=='div' and ('class','item_bubble') in tag.attrs and tag.find('a', href="/couchmanager")).find(text=True)) | |
140 except: | |
141 self.data[profile]['unread_CR_messages'] = 0 | |
142 | |
143 #if we have already the list of friend, no need to make new requests | |
144 if not self.data[profile].has_key('friends'): | |
145 self.data[profile]['friends'] = {} | |
146 d = getPage('http://www.couchsurfing.org/connections.html?type=myfriends&show=10000', agent=AGENT, cookies=self.data[profile]['cookies']) | |
147 d.addCallback(self.__friendsPageCB, id=id, profile=profile) | |
148 d.addErrback(self.erroCB, id) | |
149 else: | |
150 self.host.bridge.actionResult("XMLUI", id, {"type":"window", "xml":self.__buildUI(self.data[profile])}) | |
151 | |
152 def __buildUI(self, data): | |
153 """Build the XML UI of the plugin | |
154 @param data: data store for the profile""" | |
155 user_nick = data['user_nick'] | |
156 user_name = data['user_name'] | |
157 unread_mess = data['unread_messages'] | |
158 unread_CR_mess = data['unread_CR_messages'] | |
159 friends_list = data['friends'].keys() | |
160 friends_list.sort() | |
107 | 161 interface = XMLUI('window','tabs', title='CouchSurfing management') |
106 | 162 interface.addCategory(_("Messages"), "vertical") |
107 | 163 interface.addText(_("G'day %(name)s, you have %(nb_message)i unread message%(plural_mess)s and %(unread_CR_mess)s unread couch request message%(plural_CR)s\nIf you want to send a message, select the recipient(s) in the list below") % {'name':user_name, 'nb_message':unread_mess, 'plural_mess':'s' if unread_mess>1 else '', 'unread_CR_mess': unread_CR_mess, 'plural_CR':'s' if unread_CR_mess>1 else ''}) |
108
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
164 if unread_mess: |
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
165 interface.addButton('plugin_CS_showUnreadMessages', 'showUnreadMessages', _('Show unread message%(plural)s in external web browser') % {'plural':'s' if unread_mess>1 else ''}) |
107 | 166 interface.addList(friends_list, 'friends', style=['multi']) |
167 interface.changeLayout('pairs') | |
168 interface.addLabel(_("Subject")) | |
169 interface.addString('subject') | |
170 interface.changeLayout('vertical') | |
171 interface.addLabel(_("Message")) | |
172 interface.addText("(use %name% for contact name and %firstname% for guessed first name)") | |
173 interface.addTextBox('message') | |
174 interface.addButton('plugin_CS_sendMessage', 'sendMessage', _('send'), fields_back=['friends','subject','message']) | |
109
18b0cf49a6f1
Plugin CS minor changes (Events & Couch Search tabs temporarly removed, open_new_tab user to open link in browser)
Goffi <goffi@goffi.org>
parents:
108
diff
changeset
|
175 #interface.addCategory(_("Events"), "vertical") #TODO: coming soon, hopefuly :) |
18b0cf49a6f1
Plugin CS minor changes (Events & Couch Search tabs temporarly removed, open_new_tab user to open link in browser)
Goffi <goffi@goffi.org>
parents:
108
diff
changeset
|
176 #interface.addCategory(_("Couch search"), "vertical") |
106 | 177 return interface.toXml() |
178 | |
179 def __meetingPageCB(self, html): | |
180 """Called when the meeting page has been received""" | |
181 | |
182 def __friendsPageCB(self, html, id, profile): | |
183 """Called when the friends list page has been received""" | |
184 self.savePage('friends',html) | |
107 | 185 soup = BeautifulSoup(html.replace('"formtable width="400','"formtable" width="400"')) #CS html fix #TODO: report the bug to CS dev team |
106 | 186 friends = self.data[profile]['friends'] |
187 for _tr in soup.findAll('tr', {'class':re.compile("^msgRow*")}): #we parse the row with friends infos | |
188 _nobr = _tr.find('nobr') #contain the friend name | |
189 friend_name = unicode(_nobr.string) | |
190 friend_link = u'http://www.couchsurfing.org'+_nobr.parent['href'] | |
191 regex_href = re.compile(r'/connections\.html\?id=([^&]+)') | |
192 a_tag = _tr.find('a',href=regex_href) | |
193 friend_id = regex_href.search(unicode(a_tag)).groups()[0] | |
194 | |
195 debug(_("CS friend found: %(friend_name)s (id: %(friend_id)s, link: %(friend_link)s)") % {'friend_name':friend_name, 'friend_id':friend_id, 'friend_link':friend_link}) | |
196 friends[friend_name] = {'link':friend_link,'id':friend_id} | |
197 a = soup.find('td','barmiddle next').a #is there several pages ? | |
198 if a: | |
199 #yes, we parse the next page | |
200 d = getPage('http://www.couchsurfing.org/'+str(a['href']), agent=AGENT, cookies=self.data[profile]['cookies']) | |
201 d.addCallback(self.__friendsPageCB, id=id, profile=profile) | |
202 d.addErrback(self.erroCB, id) | |
203 else: | |
204 #no, we show the result | |
205 self.host.bridge.actionResult("XMLUI", id, {"type":"window", "xml":self.__buildUI(self.data[profile])}) | |
107 | 206 |
207 def __sendMessage(self, answer, subject, message, data, recipient_list, id, profile): | |
208 """Send actually the message | |
209 @param subject: subject of the message | |
210 @param message: body of the message | |
211 @param data: data of the profile | |
212 @param recipient_list: list of friends names, names are removed once message is sent | |
213 @param id: id of the action | |
214 @param profile: profile who launched the action | |
215 """ | |
216 if answer: | |
217 if not 'Here is a copy of the email that was sent' in answer: | |
218 error(_("INTERNAL ERROR: no confirmation of message sent by CS, maybe the site has been modified ?")) | |
219 #TODO: throw a warning to the frontend, saying that maybe the message has not been sent and to contact dev of this plugin | |
220 #debug(_('HTML answer: %s') % answer) | |
221 if recipient_list: | |
222 recipient = recipient_list.pop() | |
223 try: | |
224 friend_id = data['friends'][recipient]['id'] | |
225 except KeyError: | |
226 error('INTERNAL ERROR: unknown friend') | |
227 return #send an error to the frontend | |
228 mess = message.replace('%name%',recipient).replace('%firstname%',recipient.split(' ')[0]) | |
229 info(_('Sending message to %s') % recipient) | |
230 debug(_("\nsubject: %(subject)s\nmessage: \n---\n%(message)s\n---\n\n") % {'subject':subject,'message':mess}) | |
231 post_data = urllib.urlencode({'email[subject]':subject.encode('utf-8'),'email[body]':mess.encode('utf-8'),'email[id]':friend_id,'email[action]':'Send Message','email[replied_id]':'','email[couchsurf]':'','email[directions_to_add]':''}) | |
232 d = getPage("http://www.couchsurfing.org/send_message.html", method='POST', postdata=post_data, headers={'Content-Type':'application/x-www-form-urlencoded'} , agent=AGENT, cookies=data['cookies']) | |
233 d.addCallback(self.__sendMessage, subject, message, data, recipient_list, id, profile) | |
234 d.addErrback(self.erroCB, id) | |
235 else: | |
236 interface = XMLUI('window', title=_('Message sent')) #TODO: create particular actionResult for alerts ? | |
237 interface.addText(_('The message has been sent to every recipients')) | |
238 self.host.bridge.actionResult("XMLUI", id, {"type":"window", "xml":interface.toXml()}) | |
102 | 239 |
107 | 240 def sendMessage(self, id, data, profile): |
241 """Called to send a message to a friend | |
242 @param data: dict with the following keys: | |
243 friend: name of the recipient | |
244 subject: subject of the message | |
245 message: body of the message, with the following special keywords: | |
246 - %name%: name of the friend | |
247 - %firstname%: guessed first name of the friend (currently the first part of the name) | |
248 """ | |
249 if not data['friends']: | |
250 message_data={"reason": "bad data", "message":_(u"There is not recipient selected for this message !")} | |
251 self.host.bridge.actionResult("ERROR", id, message_data) | |
252 return | |
253 friends = data['friends'].split('\t') | |
254 subject = data['subject'] | |
255 message = data['message'] | |
256 info(_("sending message to %(friends)s with subject [%(subject)s]" % {'friends':friends, 'subject':subject})) | |
257 self.__sendMessage(None, subject, message, self.data[profile], friends, id, profile) | |
108
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
258 |
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
259 def __showUnreadMessages2(self, html, id, profile): |
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
260 """Called when the inbox page has been received""" |
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
261 #FIXME: that's really too fragile, only works if the unread messages are in the first page, and it would be too resources consuming for the website to DL each time all pages. In addition, the show attribute doesn't work as expected. |
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
262 soup = BeautifulSoup(html) |
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
263 for tag in soup.findAll(lambda tag: tag.name=='strong' and tag.a and tag.a['href'].startswith('messages.html?message_status=inbox')): |
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
264 link = "http://www.couchsurfing.org/"+str(tag.a['href']) |
109
18b0cf49a6f1
Plugin CS minor changes (Events & Couch Search tabs temporarly removed, open_new_tab user to open link in browser)
Goffi <goffi@goffi.org>
parents:
108
diff
changeset
|
265 webbrowser.open_new_tab(link) #TODO: the web browser need to already have CS cookies (i.e. already be opened & logged on CS, or be permanently loggued), a warning to the user should be sent/or a balloon-tip |
108
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
266 |
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
267 def showUnreadMessages(self, id, data, profile): |
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
268 """Called when user want to see all unread messages in the external browser""" |
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
269 d = getPage("http://www.couchsurfing.org/messages.html?message_status=inbox&show=10000", agent=AGENT, cookies=self.data[profile]['cookies']) |
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
270 d.addCallback(self.__showUnreadMessages2, id, profile) |
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
271 d.addErrback(self.erroCB, id) |
e24e080e6b16
CS plugin: unread messages can now be openned in external web browser
Goffi <goffi@goffi.org>
parents:
107
diff
changeset
|
272 |