1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
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
29 from openerp.osv import osv, fields
35 _logger = logging.getLogger(__name__)
37 def listen_channel(cr, channel_name, handle_message, check_stop=(lambda: False), check_stop_timer=60.):
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).
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
56 cr.execute("listen " + channel_name + ";")
63 if select.select([conn], [], [], check_stop_timer) == ([],[],[]):
68 message = json.loads(conn.notifies.pop().payload)
69 handle_message(message)
72 cr.execute("unlisten " + channel_name + ";")
75 pass # can't do anything if that fails
77 def notify_channel(cr, channel_name, message):
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).
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.
88 cr.execute("notify " + channel_name + ", %s", [json.dumps(message)])
92 DISCONNECTION_TIMER = POLL_TIMER + 5
93 WATCHER_ERROR_DELAY = 10
95 class LongPollingController(http.Controller):
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):
100 if not openerp.evented:
101 raise Exception("Not usable in a server not running gevent")
102 from openerp.addons.im.watcher import ImWatcher
104 request.session.authenticate(db=db, uid=uid, password=password)
106 request.session.authenticate(db=request.session._db, uid=request.session._uid, password=request.session._password)
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"]
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:
119 ImWatcher.get_watcher(res["dbname"]).stop(my_id, users_watch or [], POLL_TIMER)
121 @http.route('/longpolling/im/activated', type="json", auth="none")
123 return not not openerp.evented
125 @http.route('/longpolling/im/gen_uuid', type="json", auth="none")
128 return "%s" % uuid.uuid1()
130 def assert_uuid(uuid):
131 if not isinstance(uuid, (str, unicode, type(None))):
132 raise Exception("%s is not a uuid" % uuid)
135 class im_message(osv.osv):
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),
148 'date': lambda *args: datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
151 def get_messages(self, cr, uid, last=None, users_watch=None, uuid=None, context=None):
153 users_watch = users_watch or []
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)
160 if c_user.im_last_received < last:
161 users.write(cr, openerp.SUPERUSER_ID, my_id, {'im_last_received': last}, context=context)
163 last = c_user.im_last_received or -1
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)
169 for i in xrange(len(mess)):
170 index[mess[i]["id"]] = mess[i]
173 mess.append(index[i])
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}
180 def post(self, cr, uid, message, to_user_id, uuid=None, context=None):
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})
187 class im_user(osv.osv):
190 def _im_status(self, cr, uid, ids, something, something_else, context=None):
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)
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
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)
206 def im_connect(self, cr, uid, uuid=None, context=None):
208 return self._im_change_status(cr, uid, True, uuid, context)
210 def im_disconnect(self, cr, uid, uuid=None, context=None):
212 return self._im_change_status(cr, uid, False, uuid, context)
214 def _im_change_status(self, cr, uid, new_one, uuid=None, context=None):
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})
224 def get_by_user_id(self, cr, uid, id, context=None):
225 ids = self.get_by_user_ids(cr, uid, [id], context=context)
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)
236 inside[i["user"][0]] = True
238 inside[i["uuid"]] = True
241 if not (i in inside):
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, ""]})
248 created = self.create(cr, openerp.SUPERUSER_ID, {"uuid": to_create}, context=context)
249 records.append({"id": created, "uuid": to_create})
252 def assign_name(self, cr, uid, uuid, name, context=None):
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)
258 def _get_name(self, cr, uid, ids, name, arg, context=None):
260 for record in self.browse(cr, uid, ids, context=context):
261 res[record.id] = record.assigned_name
263 res[record.id] = record.user.name
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'),
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),