-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathim_chat.py
364 lines (330 loc) · 18.2 KB
/
im_chat.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# -*- coding: utf-8 -*-
import base64
import datetime
import logging
import time
import uuid
import random
import simplejson
import openerp
from openerp.http import request
from openerp.osv import osv, fields
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
from openerp.addons.bus.bus import TIMEOUT
_logger = logging.getLogger(__name__)
DISCONNECTION_TIMER = TIMEOUT + 5
AWAY_TIMER = 600 # 10 minutes
#----------------------------------------------------------
# Models
#----------------------------------------------------------
class im_chat_conversation_state(osv.Model):
""" Adds a state on the m2m between user and session. """
_name = 'im_chat.conversation_state'
_table = "im_chat_session_res_users_rel"
_columns = {
"state" : fields.selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')]),
"session_id" : fields.many2one('im_chat.session', 'Session', required=True, ondelete="cascade"),
"user_id" : fields.many2one('res.users', 'Users', required=True, ondelete="cascade"),
}
_defaults = {
"state" : 'open'
}
class im_chat_session(osv.Model):
""" Conversations."""
_order = 'id desc'
_name = 'im_chat.session'
_rec_name = 'uuid'
_columns = {
'uuid': fields.char('UUID', size=50, select=True),
'message_ids': fields.one2many('im_chat.message', 'to_id', 'Messages'),
'user_ids': fields.many2many('res.users', 'im_chat_session_res_users_rel', 'session_id', 'user_id', "Session Users"),
'session_res_users_rel': fields.one2many('im_chat.conversation_state', 'session_id', 'Relation Session Users'),
}
_defaults = {
'uuid': lambda *args: '%s' % uuid.uuid4(),
}
def users_infos(self, cr, uid, ids, context=None):
""" get the user infos for all the user in the session """
for session in self.pool["im_chat.session"].browse(cr, uid, ids, context=context):
users_infos = self.pool["res.users"].read(cr, uid, [u.id for u in session.user_ids], ['id','name', 'im_status'], context=context)
return users_infos
def is_private(self, cr, uid, ids, context=None):
for session_id in ids:
""" return true if the session is private between users no external messages """
mess_ids = self.pool["im_chat.message"].search(cr, uid, [('to_id','=',session_id),('from_id','=',None)], context=context)
return len(mess_ids) == 0
def session_info(self, cr, uid, ids, context=None):
""" get the session info/header of a given session """
for session in self.browse(cr, uid, ids, context=context):
info = {
'uuid': session.uuid,
'users': session.users_infos(),
'state': 'open',
}
# add uid_state if available
if uid:
domain = [('user_id','=',uid), ('session_id','=',session.id)]
uid_state = self.pool['im_chat.conversation_state'].search_read(cr, uid, domain, ['state'], context=context)
if uid_state:
info['state'] = uid_state[0]['state']
return info
def session_get(self, cr, uid, user_to, context=None):
""" returns the canonical session between 2 users, create it if needed """
session_id = False
if user_to:
sids = self.search(cr, uid, [('user_ids','in', user_to),('user_ids', 'in', [uid])], context=context, limit=1)
for sess in self.browse(cr, uid, sids, context=context):
if len(sess.user_ids) == 2 and sess.is_private():
session_id = sess.id
break
else:
session_id = self.create(cr, uid, { 'user_ids': [(6,0, (user_to, uid))] }, context=context)
return self.session_info(cr, uid, [session_id], context=context)
def update_state(self, cr, uid, uuid, state=None, context=None):
""" modify the fold_state of the given session, and broadcast to himself (e.i. : to sync multiple tabs) """
domain = [('user_id','=',uid), ('session_id.uuid','=',uuid)]
ids = self.pool['im_chat.conversation_state'].search(cr, uid, domain, context=context)
for sr in self.pool['im_chat.conversation_state'].browse(cr, uid, ids, context=context):
if not state:
state = sr.state
if sr.state == 'open':
state = 'folded'
else:
state = 'open'
self.pool['im_chat.conversation_state'].write(cr, uid, ids, {'state': state}, context=context)
self.pool['bus.bus'].sendone(cr, uid, (cr.dbname, 'im_chat.session', uid), sr.session_id.session_info())
def add_user(self, cr, uid, uuid, user_id, context=None):
""" add the given user to the given session """
sids = self.search(cr, uid, [('uuid', '=', uuid)], context=context, limit=1)
for session in self.browse(cr, uid, sids, context=context):
if user_id not in [u.id for u in session.user_ids]:
self.write(cr, uid, [session.id], {'user_ids': [(4, user_id)]}, context=context)
# notify the all the channel users and anonymous channel
notifications = []
for channel_user_id in session.user_ids:
info = self.session_info(cr, channel_user_id.id, [session.id], context=context)
notifications.append([(cr.dbname, 'im_chat.session', channel_user_id.id), info])
# Anonymous are not notified when a new user is added : cannot exec session_info as uid = None
info = self.session_info(cr, openerp.SUPERUSER_ID, [session.id], context=context)
notifications.append([session.uuid, info])
self.pool['bus.bus'].sendmany(cr, uid, notifications)
# send a message to the conversation
user = self.pool['res.users'].read(cr, uid, user_id, ['name'], context=context)
self.pool["im_chat.message"].post(cr, uid, uid, session.uuid, "meta", user['name'] + " joined the conversation.", context=context)
def get_image(self, cr, uid, uuid, user_id, context=None):
""" get the avatar of a user in the given session """
#default image
image_b64 = 'R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
# get the session
if user_id:
session_id = self.pool["im_chat.session"].search(cr, uid, [('uuid','=',uuid), ('user_ids','in', user_id)])
if session_id:
# get the image of the user
res = self.pool["res.users"].read(cr, uid, [user_id], ["image_small"])[0]
image_b64 = res["image_small"]
return image_b64
class im_chat_message(osv.Model):
""" Sessions messsages type can be 'message' or 'meta'.
For anonymous message, the from_id is False.
Messages are sent to a session not to users.
"""
_name = 'im_chat.message'
_order = "id desc"
_columns = {
'create_date': fields.datetime('Create Date', required=True, select=True),
'from_id': fields.many2one('res.users', 'Author'),
'to_id': fields.many2one('im_chat.session', 'Session To', required=True, select=True, ondelete='cascade'),
'type': fields.selection([('message','Message'), ('meta','Meta')], 'Type'),
'message': fields.char('Message'),
}
_defaults = {
'type' : 'message',
}
def init_messages(self, cr, uid, context=None):
""" get unread messages and old messages received less than AWAY_TIMER
ago and the session_info for open or folded window
"""
# get the message since the AWAY_TIMER
threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER)
threshold = threshold.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
domain = [('to_id.user_ids', 'in', [uid]), ('create_date','>',threshold)]
# get the message since the last poll of the user
presence_ids = self.pool['im_chat.presence'].search(cr, uid, [('user_id', '=', uid)], context=context)
if presence_ids:
presence = self.pool['im_chat.presence'].browse(cr, uid, presence_ids, context=context)[0]
threshold = presence.last_poll
domain.append(('create_date','>',threshold))
messages = self.search_read(cr, uid, domain, ['from_id','to_id','create_date','type','message'], order='id asc', context=context)
# get the session of the messages and the not-closed ones
session_ids = map(lambda m: m['to_id'][0], messages)
domain = [('user_id','=',uid), '|', ('state','!=','closed'), ('session_id', 'in', session_ids)]
session_rels_ids = self.pool['im_chat.conversation_state'].search(cr, uid, domain, context=context)
# re-open the session where a message have been recieve recently
session_rels = self.pool['im_chat.conversation_state'].browse(cr, uid, session_rels_ids, context=context)
reopening_session = []
notifications = []
for sr in session_rels:
si = sr.session_id.session_info()
si['state'] = sr.state
if sr.state == 'closed':
si['state'] = 'folded'
reopening_session.append(sr.id)
notifications.append([(cr.dbname,'im_chat.session', uid), si])
for m in messages:
notifications.append([(cr.dbname,'im_chat.session', uid), m])
self.pool['im_chat.conversation_state'].write(cr, uid, reopening_session, {'state': 'folded'}, context=context)
return notifications
def post(self, cr, uid, from_uid, uuid, message_type, message_content, context=None):
""" post and broadcast a message, return the message id """
message_id = False
Session = self.pool['im_chat.session']
session_ids = Session.search(cr, uid, [('uuid','=',uuid)], context=context)
notifications = []
for session in Session.browse(cr, uid, session_ids, context=context):
# build the new message
vals = {
"from_id": from_uid,
"to_id": session.id,
"type": message_type,
"message": message_content,
}
# save it
message_id = self.create(cr, uid, vals, context=context)
# broadcast it to channel (anonymous users) and users_ids
data = self.read(cr, uid, [message_id], ['from_id','to_id','create_date','type','message'], context=context)[0]
notifications.append([uuid, data])
for user in session.user_ids:
notifications.append([(cr.dbname, 'im_chat.session', user.id), data])
self.pool['bus.bus'].sendmany(cr, uid, notifications)
return message_id
class im_chat_presence(osv.Model):
""" im_chat_presence status can be: online, away or offline.
This model is a one2one, but is not attached to res_users to avoid database concurrence errors
"""
_name = 'im_chat.presence'
_columns = {
'user_id' : fields.many2one('res.users', 'Users', required=True, select=True),
'last_poll': fields.datetime('Last Poll'),
'last_presence': fields.datetime('Last Presence'),
'status' : fields.selection([('online','Online'), ('away','Away'), ('offline','Offline')], 'IM Status'),
}
_defaults = {
'last_poll' : fields.datetime.now,
'last_presence' : fields.datetime.now,
'status' : 'offline'
}
_sql_constraints = [('im_chat_user_status_unique','unique(user_id)', 'A user can only have one IM status.')]
def update(self, cr, uid, presence=True, context=None):
""" register the poll, and change its im status if necessary. It also notify the Bus if the status has changed. """
presence_ids = self.search(cr, uid, [('user_id', '=', uid)], context=context)
presences = self.browse(cr, uid, presence_ids, context=context)
# set the default values
send_notification = True
vals = {
'last_poll': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'status' : presences and presences[0].status or 'offline'
}
# update the user or a create a new one
if not presences:
vals['status'] = 'online'
vals['user_id'] = uid
self.create(cr, uid, vals, context=context)
else:
if presence:
vals['last_presence'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
vals['status'] = 'online'
else:
threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER)
if datetime.datetime.strptime(presences[0].last_presence, DEFAULT_SERVER_DATETIME_FORMAT) < threshold:
vals['status'] = 'away'
send_notification = presences[0].status != vals['status']
# write only if the last_poll is passed TIMEOUT, or if the status has changed
delta = datetime.datetime.now() - datetime.datetime.strptime(presences[0].last_poll, DEFAULT_SERVER_DATETIME_FORMAT)
if (delta > datetime.timedelta(seconds=TIMEOUT) or send_notification):
self.write(cr, uid, presence_ids, vals, context=context)
# avoid TransactionRollbackError
cr.commit()
# notify if the status has changed
if send_notification:
self.pool['bus.bus'].sendone(cr, uid, (cr.dbname,'im_chat.presence'), {'id': uid, 'im_status': vals['status']})
# gc : disconnect the users having a too old last_poll. 1 on 100 chance to do it.
if random.random() < 0.01:
self.check_users_disconnection(cr, uid, context=context)
return True
def check_users_disconnection(self, cr, uid, context=None):
""" disconnect the users having a too old last_poll """
dt = (datetime.datetime.now() - datetime.timedelta(0, DISCONNECTION_TIMER)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
presence_ids = self.search(cr, uid, [('last_poll', '<', dt), ('status' , '!=', 'offline')], context=context)
self.write(cr, uid, presence_ids, {'status': 'offline'}, context=context)
presences = self.browse(cr, uid, presence_ids, context=context)
notifications = []
for presence in presences:
notifications.append([(cr.dbname,'im_chat.presence'), {'id': presence.user_id.id, 'im_status': presence.status}])
self.pool['bus.bus'].sendmany(cr, uid, notifications)
return True
class res_users(osv.Model):
_inherit = "res.users"
def _get_im_status(self, cr, uid, ids, fields, arg, context=None):
""" function computing the im_status field of the users """
r = dict((i, 'offline') for i in ids)
status_ids = self.pool['im_chat.presence'].search(cr, uid, [('user_id', 'in', ids)], context=context)
status = self.pool['im_chat.presence'].browse(cr, uid, status_ids, context=context)
for s in status:
r[s.user_id.id] = s.status
return r
_columns = {
'im_status' : fields.function(_get_im_status, type="char", string="IM Status"),
}
def im_search(self, cr, uid, name, limit, context=None):
""" search users with a name and return its id, name and im_status """
group_user_id = self.pool.get("ir.model.data").get_object_reference(cr, uid, 'base', 'group_user')[1]
user_ids = self.name_search(cr, uid, name, [('id','!=', uid), ('groups_id', 'in', [group_user_id])], limit=limit, context=context)
domain = [('user_id', 'in', [i[0] for i in user_ids])]
ids = self.pool['im_chat.presence'].search(cr, uid, domain, order="last_poll desc", context=context)
presences = self.pool['im_chat.presence'].read(cr, uid, ids, ['user_id','status'], context=context)
res = []
for user_id in user_ids:
user = {
'id' : user_id[0],
'name' : user_id[1]
}
tmp = filter(lambda p: p['user_id'][0] == user_id[0], presences)
user['im_status'] = len(tmp) > 0 and tmp[0]['status'] or 'offline'
res.append(user)
return res
#----------------------------------------------------------
# Controllers
#----------------------------------------------------------
class Controller(openerp.addons.bus.bus.Controller):
def _poll(self, dbname, channels, last, options):
if request.session.uid:
registry, cr, uid, context = request.registry, request.cr, request.session.uid, request.context
registry.get('im_chat.presence').update(cr, uid, ('im_presence' in options), context=context)
# listen to connection and disconnections
channels.append((request.db,'im_chat.presence'))
# channel to receive message
channels.append((request.db,'im_chat.session', request.uid))
return super(Controller, self)._poll(dbname, channels, last, options)
@openerp.http.route('/im_chat/init', type="json", auth="none")
def init(self):
registry, cr, uid, context = request.registry, request.cr, request.session.uid, request.context
notifications = registry['im_chat.message'].init_messages(cr, uid, context=context)
return notifications
@openerp.http.route('/im_chat/post', type="json", auth="none")
def post(self, uuid, message_type, message_content):
registry, cr, uid, context = request.registry, request.cr, request.session.uid, request.context
# execute the post method as SUPERUSER_ID
message_id = registry["im_chat.message"].post(cr, openerp.SUPERUSER_ID, uid, uuid, message_type, message_content, context=context)
return message_id
@openerp.http.route(['/im_chat/image/<string:uuid>/<string:user_id>'], type='http', auth="none")
def image(self, uuid, user_id):
registry, cr, context, uid = request.registry, request.cr, request.context, request.session.uid
# get the image
Session = registry.get("im_chat.session")
image_b64 = Session.get_image(cr, openerp.SUPERUSER_ID, uuid, simplejson.loads(user_id), context)
# built the response
image_data = base64.b64decode(image_b64)
headers = [('Content-Type', 'image/png')]
headers.append(('Content-Length', len(image_data)))
return request.make_response(image_data, headers)
# vim:et: