odoo/addons/im/im.py

363 lines
17 KiB
Python

# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import datetime
import json
import logging
import select
import time
import openerp
import openerp.tools.config
import openerp.modules.registry
from openerp import http
from openerp.http import request
from openerp.osv import osv, fields, expression
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
_logger = logging.getLogger(__name__)
def listen_channel(cr, channel_name, handle_message, check_stop=(lambda: False), check_stop_timer=60.):
"""
Begin a loop, listening on a PostgreSQL channel. This method does never terminate by default, you need to provide a check_stop
callback to do so. This method also assume that all notifications will include a message formated using JSON (see the
corresponding notify_channel() method).
:param db_name: database name
:param channel_name: the name of the PostgreSQL channel to listen
:param handle_message: function that will be called when a message is received. It takes one argument, the message
attached to the notification.
:type handle_message: function (one argument)
:param check_stop: function that will be called periodically (see the check_stop_timer argument). If it returns True
this function will stop to watch the channel.
:type check_stop: function (no arguments)
:param check_stop_timer: The maximum amount of time between calls to check_stop_timer (can be shorter if messages
are received).
"""
try:
conn = cr._cnx
cr.execute("listen " + channel_name + ";")
cr.commit();
stopping = False
while not stopping:
if check_stop():
stopping = True
break
if select.select([conn], [], [], check_stop_timer) == ([],[],[]):
pass
else:
conn.poll()
while conn.notifies:
message = json.loads(conn.notifies.pop().payload)
handle_message(message)
finally:
try:
cr.execute("unlisten " + channel_name + ";")
cr.commit()
except:
pass # can't do anything if that fails
def notify_channel(cr, channel_name, message):
"""
Send a message through a PostgreSQL channel. The message will be formatted using JSON. This method will
commit the given transaction because the notify command in Postgresql seems to work correctly when executed in
a separate transaction (despite what is written in the documentation).
:param cr: The cursor.
:param channel_name: The name of the PostgreSQL channel.
:param message: The message, must be JSON-compatible data.
"""
cr.commit()
cr.execute("notify " + channel_name + ", %s", [json.dumps(message)])
cr.commit()
POLL_TIMER = 30
DISCONNECTION_TIMER = POLL_TIMER + 5
WATCHER_ERROR_DELAY = 10
class LongPollingController(http.Controller):
@http.route('/longpolling/im/poll', type="json", auth="none")
def poll(self, last=None, users_watch=None, db=None, uid=None, password=None, uuid=None):
assert_uuid(uuid)
if not openerp.evented:
raise Exception("Not usable in a server not running gevent")
from openerp.addons.im.watcher import ImWatcher
if db is not None:
openerp.service.security.check(db, uid, password)
else:
uid = request.session.uid
db = request.session.db
registry = openerp.modules.registry.RegistryManager.get(db)
with registry.cursor() as cr:
registry.get('im.user').im_connect(cr, uid, uuid=uuid, context=request.context)
my_id = registry.get('im.user').get_my_id(cr, uid, uuid, request.context)
num = 0
while True:
with registry.cursor() as cr:
res = registry.get('im.message').get_messages(cr, uid, last, users_watch, uuid=uuid, context=request.context)
if num >= 1 or len(res["res"]) > 0:
return res
last = res["last"]
num += 1
ImWatcher.get_watcher(res["dbname"]).stop(my_id, users_watch or [], POLL_TIMER)
@http.route('/longpolling/im/activated', type="json", auth="none")
def activated(self):
return not not openerp.evented
@http.route('/longpolling/im/gen_uuid', type="json", auth="none")
def gen_uuid(self):
import uuid
return "%s" % uuid.uuid1()
def assert_uuid(uuid):
if not isinstance(uuid, (str, unicode, type(None))) and uuid != False:
raise Exception("%s is not a uuid" % uuid)
class im_message(osv.osv):
_name = 'im.message'
_order = "date desc"
_columns = {
'message': fields.text(string="Message", required=True),
'from_id': fields.many2one("im.user", "From", required= True, ondelete='cascade'),
'session_id': fields.many2one("im.session", "Session", required=True, select=True, ondelete='cascade'),
'to_id': fields.many2many("im.user", "im_message_users", 'message_id', 'user_id', 'To'),
'date': fields.datetime("Date", required=True, select=True),
'technical': fields.boolean("Technical Message"),
}
_defaults = {
'date': lambda *args: datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'technical': False,
}
def get_messages(self, cr, uid, last=None, users_watch=None, uuid=None, context=None):
assert_uuid(uuid)
users_watch = users_watch or []
# complex stuff to determine the last message to show
users = self.pool.get("im.user")
my_id = users.get_my_id(cr, uid, uuid, context=context)
c_user = users.browse(cr, openerp.SUPERUSER_ID, my_id, context=context)
if last:
if c_user.im_last_received < last:
users.write(cr, openerp.SUPERUSER_ID, my_id, {'im_last_received': last}, context=context)
else:
last = c_user.im_last_received or -1
# how fun it is to always need to reorder results from read
mess_ids = self.search(cr, openerp.SUPERUSER_ID, ["&", ['id', '>', last], "|", ['from_id', '=', my_id], ['to_id', 'in', [my_id]]], order="id", context=context)
mess = self.read(cr, openerp.SUPERUSER_ID, mess_ids, ["id", "message", "from_id", "session_id", "date", "technical"], context=context)
index = {}
for i in xrange(len(mess)):
index[mess[i]["id"]] = mess[i]
mess = []
for i in mess_ids:
mess.append(index[i])
if len(mess) > 0:
last = mess[-1]["id"]
users_status = users.read(cr, openerp.SUPERUSER_ID, users_watch, ["im_status"], context=context)
return {"res": mess, "last": last, "dbname": cr.dbname, "users_status": users_status}
def post(self, cr, uid, message, to_session_id, technical=False, uuid=None, context=None):
assert_uuid(uuid)
my_id = self.pool.get('im.user').get_my_id(cr, uid, uuid)
session_user_ids = self.pool.get('im.session').get_session_users(cr, uid, to_session_id, context=context).get("user_ids", [])
to_ids = [user_id for user_id in session_user_ids if user_id != my_id]
self.create(cr, openerp.SUPERUSER_ID, {"message": message, 'from_id': my_id,
'to_id': [(6, 0, to_ids)], 'session_id': to_session_id, 'technical': technical}, context=context)
notify_channel(cr, "im_channel", {'type': 'message', 'receivers': [my_id] + to_ids})
return False
class im_session(osv.osv):
_name = 'im.session'
def _calc_name(self, cr, uid, ids, something, something_else, context=None):
res = {}
for obj in self.browse(cr, uid, ids, context=context):
res[obj.id] = ", ".join([x.name for x in obj.user_ids])
return res
_columns = {
'user_ids': fields.many2many('im.user', 'im_session_im_user_rel', 'im_session_id', 'im_user_id', 'Users'),
"name": fields.function(_calc_name, string="Name", type='char'),
}
# Todo: reuse existing sessions if possible
def session_get(self, cr, uid, users_to, uuid=None, context=None):
my_id = self.pool.get("im.user").get_my_id(cr, uid, uuid, context=context)
users = [my_id] + users_to
domain = []
for user_to in users:
domain.append(('user_ids', 'in', [user_to]))
sids = self.search(cr, openerp.SUPERUSER_ID, domain, context=context, limit=1)
session_id = None
for session in self.browse(cr, uid, sids, context=context):
if len(session.user_ids) == len(users):
session_id = session.id
break
if not session_id:
session_id = self.create(cr, openerp.SUPERUSER_ID, {
'user_ids': [(6, 0, users)]
}, context=context)
return self.read(cr, uid, session_id, context=context)
def get_session_users(self, cr, uid, session_id, context=None):
return self.read(cr, openerp.SUPERUSER_ID, session_id, ['user_ids'], context=context)
def add_to_session(self, cr, uid, session_id, user_id, uuid=None, context=None):
my_id = self.pool.get("im.user").get_my_id(cr, uid, uuid, context=context)
session = self.read(cr, uid, session_id, context=context)
if my_id not in session.get("user_ids"):
raise Exception("Not allowed to modify a session when you are not in it.")
self.write(cr, uid, session_id, {"user_ids": [(4, user_id)]}, context=context)
def remove_me_from_session(self, cr, uid, session_id, uuid=None, context=None):
my_id = self.pool.get("im.user").get_my_id(cr, uid, uuid, context=context)
self.write(cr, openerp.SUPERUSER_ID, session_id, {"user_ids": [(3, my_id)]}, context=context)
class im_user(osv.osv):
_name = "im.user"
def _im_status(self, cr, uid, ids, something, something_else, context=None):
res = {}
current = datetime.datetime.now()
delta = datetime.timedelta(0, DISCONNECTION_TIMER)
data = self.read(cr, openerp.SUPERUSER_ID, ids, ["im_last_status_update", "im_last_status"], context=context)
for obj in data:
last_update = datetime.datetime.strptime(obj["im_last_status_update"], DEFAULT_SERVER_DATETIME_FORMAT)
res[obj["id"]] = obj["im_last_status"] and (last_update + delta) > current
return res
def _status_search(self, cr, uid, obj, name, domain, context=None):
current = datetime.datetime.now()
delta = datetime.timedelta(0, DISCONNECTION_TIMER)
field, operator, value = domain[0]
if operator in expression.NEGATIVE_TERM_OPERATORS:
value = not value
if value:
return ['&', ('im_last_status', '=', True), ('im_last_status_update', '>', (current - delta).strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
else:
return ['|', ('im_last_status', '=', False), ('im_last_status_update', '<=', (current - delta).strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
# TODO: Remove fields arg in trunk. Also in im.js.
def search_users(self, cr, uid, text_search, fields, limit, context=None):
my_id = self.get_my_id(cr, uid, None, context)
group_employee = self.pool['ir.model.data'].get_object_reference(cr, uid, 'base', 'group_user')[1]
found = self.search(cr, uid, [["name", "ilike", text_search], ["id", "<>", my_id], ["uuid", "=", False], ["im_status", "=", True], ["user_id.groups_id", "in", [group_employee]]],
order="name asc", limit=limit, context=context)
if len(found) < limit:
found += self.search(cr, uid, [["name", "ilike", text_search], ["id", "<>", my_id], ["uuid", "=", False], ["im_status", "=", True], ["id", "not in", found]],
order="name asc", limit=limit, context=context)
if len(found) < limit:
found += self.search(cr, uid, [["name", "ilike", text_search], ["id", "<>", my_id], ["uuid", "=", False], ["im_status", "=", False], ["id", "not in", found]],
order="name asc", limit=limit-len(found), context=context)
users = self.read(cr,openerp.SUPERUSER_ID, found, ["name", "user_id", "uuid", "im_status"], context=context)
users.sort(key=lambda obj: found.index(obj['id']))
return users
def im_connect(self, cr, uid, uuid=None, context=None):
assert_uuid(uuid)
return self._im_change_status(cr, uid, True, uuid, context)
def im_disconnect(self, cr, uid, uuid=None, context=None):
assert_uuid(uuid)
return self._im_change_status(cr, uid, False, uuid, context)
def _im_change_status(self, cr, uid, new_one, uuid=None, context=None):
assert_uuid(uuid)
id = self.get_my_id(cr, uid, uuid, context=context)
current_status = self.read(cr, openerp.SUPERUSER_ID, id, ["im_status"], context=None)["im_status"]
self.write(cr, openerp.SUPERUSER_ID, id, {"im_last_status": new_one,
"im_last_status_update": datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
if current_status != new_one:
notify_channel(cr, "im_channel", {'type': 'status', 'user': id})
return True
def get_my_id(self, cr, uid, uuid=None, context=None):
assert_uuid(uuid)
if uuid:
users = self.search(cr, openerp.SUPERUSER_ID, [["uuid", "=", uuid]], context=None)
else:
users = self.search(cr, openerp.SUPERUSER_ID, [["user_id", "=", uid]], context=None)
my_id = users[0] if len(users) >= 1 else False
if not my_id:
my_id = self.create(cr, openerp.SUPERUSER_ID, {"user_id": uid if not uuid else False, "uuid": uuid if uuid else False}, context=context)
return my_id
def assign_name(self, cr, uid, uuid, name, context=None):
assert_uuid(uuid)
id = self.get_my_id(cr, uid, uuid, context=context)
self.write(cr, openerp.SUPERUSER_ID, id, {"assigned_name": name}, context=context)
return True
def _get_name(self, cr, uid, ids, name, arg, context=None):
res = {}
for record in self.browse(cr, uid, ids, context=context):
res[record.id] = record.assigned_name
if record.user_id:
res[record.id] = record.user_id.name
continue
return res
def get_users(self, cr, uid, ids, context=None):
return self.read(cr,openerp.SUPERUSER_ID, ids, ["name", "im_status", "uuid"], context=context)
_columns = {
'name': fields.function(_get_name, type='char', size=200, string="Name", store=True, readonly=True),
'assigned_name': fields.char(string="Assigned Name", size=200, required=False),
'image': fields.related('user_id', 'image_small', type='binary', string="Image", readonly=True),
'user_id': fields.many2one("res.users", string="User", select=True, ondelete='cascade', oldname='user'),
'uuid': fields.char(string="UUID", size=50, select=True),
'im_last_received': fields.integer(string="Instant Messaging Last Received Message"),
'im_last_status': fields.boolean(strint="Instant Messaging Last Status"),
'im_last_status_update': fields.datetime(string="Instant Messaging Last Status Update"),
'im_status': fields.function(_im_status, string="Instant Messaging Status", type='boolean', fnct_search=_status_search),
}
_defaults = {
'im_last_received': -1,
'im_last_status': False,
'im_last_status_update': lambda *args: datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
}
_sql_constraints = [
('user_uniq', 'unique (user_id)', 'Only one chat user per OpenERP user.'),
('uuid_uniq', 'unique (uuid)', 'Chat identifier already used.'),
]
class res_users(osv.osv):
_inherit = "res.users"
def _get_im_user(self, cr, uid, ids, field_name, arg, context=None):
result = dict.fromkeys(ids, False)
for index, im_user in enumerate(self.pool['im.user'].search_read(cr, uid, domain=[('user_id', 'in', ids)], fields=['name', 'user_id'], context=context)):
result[ids[index]] = im_user.get('user_id') and (im_user['user_id'][0], im_user['name']) or False
return result
_columns = {
'im_user_id' : fields.function(_get_im_user, type='many2one', string="IM User", relation="im.user"),
}