[MERGE] with trunk
[odoo/odoo.git] / addons / im / im.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import openerp
23 import openerp.tools.config
24 import openerp.modules.registry
25 import openerp.addons.web.http as http
26 from openerp.addons.web.http import request
27 from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
28 import datetime
29 from openerp.osv import osv, fields
30 import time
31 import logging
32 import json
33 import select
34
35 _logger = logging.getLogger(__name__)
36
37 def listen_channel(cr, channel_name, handle_message, check_stop=(lambda: False), check_stop_timer=60.):
38     """
39         Begin a loop, listening on a PostgreSQL channel. This method does never terminate by default, you need to provide a check_stop
40         callback to do so. This method also assume that all notifications will include a message formated using JSON (see the
41         corresponding notify_channel() method).
42
43         :param db_name: database name
44         :param channel_name: the name of the PostgreSQL channel to listen
45         :param handle_message: function that will be called when a message is received. It takes one argument, the message
46             attached to the notification.
47         :type handle_message: function (one argument)
48         :param check_stop: function that will be called periodically (see the check_stop_timer argument). If it returns True
49             this function will stop to watch the channel.
50         :type check_stop: function (no arguments)
51         :param check_stop_timer: The maximum amount of time between calls to check_stop_timer (can be shorter if messages
52             are received).
53     """
54     try:
55         conn = cr._cnx
56         cr.execute("listen " + channel_name + ";")
57         cr.commit();
58         stopping = False
59         while not stopping:
60             if check_stop():
61                 stopping = True
62                 break
63             if select.select([conn], [], [], check_stop_timer) == ([],[],[]):
64                 pass
65             else:
66                 conn.poll()
67                 while conn.notifies:
68                     message = json.loads(conn.notifies.pop().payload)
69                     handle_message(message)
70     finally:
71         try:
72             cr.execute("unlisten " + channel_name + ";")
73             cr.commit()
74         except:
75             pass # can't do anything if that fails
76
77 def notify_channel(cr, channel_name, message):
78     """
79         Send a message through a PostgreSQL channel. The message will be formatted using JSON. This method will
80         commit the given transaction because the notify command in Postgresql seems to work correctly when executed in
81         a separate transaction (despite what is written in the documentation).
82
83         :param cr: The cursor.
84         :param channel_name: The name of the PostgreSQL channel.
85         :param message: The message, must be JSON-compatible data.
86     """
87     cr.commit()
88     cr.execute("notify " + channel_name + ", %s", [json.dumps(message)])
89     cr.commit()
90
91 POLL_TIMER = 30
92 DISCONNECTION_TIMER = POLL_TIMER + 5
93 WATCHER_ERROR_DELAY = 10
94
95 class LongPollingController(http.Controller):
96
97     @http.route('/longpolling/im/poll', type="json", auth="none")
98     def poll(self, last=None, users_watch=None, db=None, uid=None, password=None, uuid=None):
99         assert_uuid(uuid)
100         if not openerp.evented:
101             raise Exception("Not usable in a server not running gevent")
102         from openerp.addons.im.watcher import ImWatcher
103         if db is not None:
104             request.session.authenticate(db=db, uid=uid, password=password)
105         else:
106             request.session.authenticate(db=request.session._db, uid=request.session._uid, password=request.session._password)
107
108         with request.registry.cursor() as cr:
109             request.registry.get('im.user').im_connect(cr, request.uid, uuid=uuid, context=request.context)
110             my_id = request.registry.get('im.user').get_by_user_id(cr, request.uid, uuid or request.session._uid, request.context)["id"]
111         num = 0
112         while True:
113             with request.registry.cursor() as cr:
114                 res = request.registry.get('im.message').get_messages(cr, request.uid, last, users_watch, uuid=uuid, context=request.context)
115             if num >= 1 or len(res["res"]) > 0:
116                 return res
117             last = res["last"]
118             num += 1
119             ImWatcher.get_watcher(res["dbname"]).stop(my_id, users_watch or [], POLL_TIMER)
120
121     @http.route('/longpolling/im/activated', type="json", auth="none")
122     def activated(self):
123         return not not openerp.evented
124
125     @http.route('/longpolling/im/gen_uuid', type="json", auth="none")
126     def gen_uuid(self):
127         import uuid
128         return "%s" % uuid.uuid1()
129
130 def assert_uuid(uuid):
131     if not isinstance(uuid, (str, unicode, type(None))):
132         raise Exception("%s is not a uuid" % uuid)
133
134
135 class im_message(osv.osv):
136     _name = 'im.message'
137
138     _order = "date desc"
139
140     _columns = {
141         'message': fields.char(string="Message", size=200, required=True),
142         'from_id': fields.many2one("im.user", "From", required= True, ondelete='cascade'),
143         'to_id': fields.many2one("im.user", "To", required=True, select=True, ondelete='cascade'),
144         'date': fields.datetime("Date", required=True, select=True),
145     }
146
147     _defaults = {
148         'date': lambda *args: datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
149     }
150     
151     def get_messages(self, cr, uid, last=None, users_watch=None, uuid=None, context=None):
152         assert_uuid(uuid)
153         users_watch = users_watch or []
154
155         # complex stuff to determine the last message to show
156         users = self.pool.get("im.user")
157         my_id = users.get_by_user_id(cr, uid, uuid or uid, context=context)["id"]
158         c_user = users.browse(cr, openerp.SUPERUSER_ID, my_id, context=context)
159         if last:
160             if c_user.im_last_received < last:
161                 users.write(cr, openerp.SUPERUSER_ID, my_id, {'im_last_received': last}, context=context)
162         else:
163             last = c_user.im_last_received or -1
164
165         # how fun it is to always need to reorder results from read
166         mess_ids = self.search(cr, openerp.SUPERUSER_ID, [['id', '>', last], ['to_id', '=', my_id]], order="id", context=context)
167         mess = self.read(cr, openerp.SUPERUSER_ID, mess_ids, ["id", "message", "from_id", "date"], context=context)
168         index = {}
169         for i in xrange(len(mess)):
170             index[mess[i]["id"]] = mess[i]
171         mess = []
172         for i in mess_ids:
173             mess.append(index[i])
174
175         if len(mess) > 0:
176             last = mess[-1]["id"]
177         users_status = users.read(cr, openerp.SUPERUSER_ID, users_watch, ["im_status"], context=context)
178         return {"res": mess, "last": last, "dbname": cr.dbname, "users_status": users_status}
179
180     def post(self, cr, uid, message, to_user_id, uuid=None, context=None):
181         assert_uuid(uuid)
182         my_id = self.pool.get('im.user').get_by_user_id(cr, uid, uuid or uid)["id"]
183         self.create(cr, openerp.SUPERUSER_ID, {"message": message, 'from_id': my_id, 'to_id': to_user_id}, context=context)
184         notify_channel(cr, "im_channel", {'type': 'message', 'receiver': to_user_id})
185         return False
186
187 class im_user(osv.osv):
188     _name = "im.user"
189
190     def _im_status(self, cr, uid, ids, something, something_else, context=None):
191         res = {}
192         current = datetime.datetime.now()
193         delta = datetime.timedelta(0, DISCONNECTION_TIMER)
194         data = self.read(cr, openerp.SUPERUSER_ID, ids, ["im_last_status_update", "im_last_status"], context=context)
195         for obj in data:
196             last_update = datetime.datetime.strptime(obj["im_last_status_update"], DEFAULT_SERVER_DATETIME_FORMAT)
197             res[obj["id"]] = obj["im_last_status"] and (last_update + delta) > current
198         return res
199
200     def search_users(self, cr, uid, domain, fields, limit, context=None):
201         # do not user openerp.SUPERUSER_ID, reserved to normal users
202         found = self.pool.get('res.users').search(cr, uid, domain, limit=limit, context=context)
203         found = self.get_by_user_ids(cr, uid, found, context=context)
204         return self.read(cr, uid, found, fields, context=context)
205
206     def im_connect(self, cr, uid, uuid=None, context=None):
207         assert_uuid(uuid)
208         return self._im_change_status(cr, uid, True, uuid, context)
209
210     def im_disconnect(self, cr, uid, uuid=None, context=None):
211         assert_uuid(uuid)
212         return self._im_change_status(cr, uid, False, uuid, context)
213
214     def _im_change_status(self, cr, uid, new_one, uuid=None, context=None):
215         assert_uuid(uuid)
216         id = self.get_by_user_id(cr, uid, uuid or uid, context=context)["id"]
217         current_status = self.read(cr, openerp.SUPERUSER_ID, id, ["im_status"], context=None)["im_status"]
218         self.write(cr, openerp.SUPERUSER_ID, id, {"im_last_status": new_one, 
219             "im_last_status_update": datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
220         if current_status != new_one:
221             notify_channel(cr, "im_channel", {'type': 'status', 'user': id})
222         return True
223
224     def get_by_user_id(self, cr, uid, id, context=None):
225         ids = self.get_by_user_ids(cr, uid, [id], context=context)
226         return ids[0]
227
228     def get_by_user_ids(self, cr, uid, ids, context=None):
229         user_ids = [x for x in ids if isinstance(x, int)]
230         uuids = [x for x in ids if isinstance(x, (str, unicode))]
231         users = self.search(cr, openerp.SUPERUSER_ID, ["|", ["user", "in", user_ids], ["uuid", "in", uuids]], context=None)
232         records = self.read(cr, openerp.SUPERUSER_ID, users, ["user", "uuid"], context=None)
233         inside = {}
234         for i in records:
235             if i["user"]:
236                 inside[i["user"][0]] = True
237             elif ["uuid"]:
238                 inside[i["uuid"]] = True
239         not_inside = {}
240         for i in ids:
241             if not (i in inside):
242                 not_inside[i] = True
243         for to_create in not_inside.keys():
244             if isinstance(to_create, int):
245                 created = self.create(cr, openerp.SUPERUSER_ID, {"user": to_create}, context=context)
246                 records.append({"id": created, "user": [to_create, ""]})
247             else:
248                 created = self.create(cr, openerp.SUPERUSER_ID, {"uuid": to_create}, context=context)
249                 records.append({"id": created, "uuid": to_create})
250         return records
251
252     def assign_name(self, cr, uid, uuid, name, context=None):
253         assert_uuid(uuid)
254         id = self.get_by_user_id(cr, uid, uuid or uid, context=context)["id"]
255         self.write(cr, openerp.SUPERUSER_ID, id, {"assigned_name": name}, context=context)
256         return True
257
258     def _get_name(self, cr, uid, ids, name, arg, context=None):
259         res = {}
260         for record in self.browse(cr, uid, ids, context=context):
261             res[record.id] = record.assigned_name
262             if record.user:
263                 res[record.id] = record.user.name
264                 continue
265         return res
266
267     _columns = {
268         'name': fields.function(_get_name, type='char', size=200, string="Name", store=True, readonly=True),
269         'assigned_name': fields.char(string="Assigned Name", size=200, required=False),
270         'image': fields.related('user', 'image_small', type='binary', string="Image", readonly=True),
271         'user': fields.many2one("res.users", string="User", select=True, ondelete='cascade'),
272         'uuid': fields.char(string="UUID", size=50, select=True),
273         'im_last_received': fields.integer(string="Instant Messaging Last Received Message"),
274         'im_last_status': fields.boolean(strint="Instant Messaging Last Status"),
275         'im_last_status_update': fields.datetime(string="Instant Messaging Last Status Update"),
276         'im_status': fields.function(_im_status, string="Instant Messaging Status", type='boolean'),
277     }
278
279     _defaults = {
280         'im_last_received': -1,
281         'im_last_status': False,
282         'im_last_status_update': lambda *args: datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
283     }