root/branches/gajim_0.11/src/common/logger.py

Revision 7829, 19.3 kB (checked in by asterix, 2 years ago)

merge diff from trunc to 0.11 branch

  • Property svn:eol-style set to LF
  • Property svn:keywords set to LastChangedDate LastChangedRevision LastChangedBy HeadURL Id
Line 
1## logger.py
2##
3## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
4## Copyright (C) 2005-2006 Yann Le Boulanger <asterix@lagaule.org>
5##
6## This program is free software; you can redistribute it and/or modify
7## it under the terms of the GNU General Public License as published
8## by the Free Software Foundation; version 2 only.
9##
10## This program is distributed in the hope that it will be useful,
11## but WITHOUT ANY WARRANTY; without even the implied warranty of
12## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13## GNU General Public License for more details.
14##
15
16import os
17import sys
18import time
19import datetime
20
21import exceptions
22import gajim
23
24try:
25        import sqlite3 as sqlite # python 2.5
26except ImportError:
27        try:
28                from pysqlite2 import dbapi2 as sqlite
29        except ImportError:
30                raise exceptions.PysqliteNotAvailable
31
32import configpaths
33LOG_DB_PATH = configpaths.gajimpaths['LOG_DB']
34LOG_DB_FOLDER, LOG_DB_FILE = os.path.split(LOG_DB_PATH)
35
36class Constants:
37        def __init__(self):
38                (
39                        self.JID_NORMAL_TYPE,
40                        self.JID_ROOM_TYPE
41                ) = range(2)
42               
43                (
44                        self.KIND_STATUS,
45                        self.KIND_GCSTATUS,
46                        self.KIND_GC_MSG,
47                        self.KIND_SINGLE_MSG_RECV,
48                        self.KIND_CHAT_MSG_RECV,
49                        self.KIND_SINGLE_MSG_SENT,
50                        self.KIND_CHAT_MSG_SENT,
51                        self.KIND_ERROR
52                ) = range(8)
53               
54                (
55                        self.SHOW_ONLINE,
56                        self.SHOW_CHAT,
57                        self.SHOW_AWAY,
58                        self.SHOW_XA,
59                        self.SHOW_DND,
60                        self.SHOW_OFFLINE
61                ) = range(6)
62
63                (
64                        self.TYPE_AIM,
65                        self.TYPE_GG,
66                        self.TYPE_HTTP_WS,
67                        self.TYPE_ICQ,
68                        self.TYPE_MSN,
69                        self.TYPE_QQ,
70                        self.TYPE_SMS,
71                        self.TYPE_SMTP,
72                        self.TYPE_TLEN,
73                        self.TYPE_YAHOO,
74                        self.TYPE_NEWMAIL,
75                        self.TYPE_RSS,
76                        self.TYPE_WEATHER,
77                ) = range(13)
78
79constants = Constants()
80
81class Logger:
82        def __init__(self):
83                self.jids_already_in = [] # holds jids that we already have in DB
84                self.con = None
85
86                if not os.path.exists(LOG_DB_PATH):
87                        # this can happen only the first time (the time we create the db)
88                        # db is not created here but in src/common/checks_paths.py
89                        return
90                self.init_vars()
91
92        def close_db(self):
93                if self.con:
94                        self.con.close()
95                self.con = None
96                self.cur = None
97
98        def open_db(self):
99                self.close_db()
100
101                # FIXME: sqlite3_open wants UTF8 strings. So a path with
102                # non-ascii chars doesn't work. See #2812 and
103                # http://lists.initd.org/pipermail/pysqlite/2005-August/000134.html
104                back = os.getcwd()
105                os.chdir(LOG_DB_FOLDER)
106
107                # if locked, wait up to 20 sec to unlock
108                # before raise (hopefully should be enough)
109
110                self.con = sqlite.connect(LOG_DB_FILE, timeout = 20.0,
111                        isolation_level = 'IMMEDIATE')
112                os.chdir(back)
113                self.cur = self.con.cursor()
114                self.set_synchronous(False)
115
116        def set_synchronous(self, sync):
117                try:
118                        if sync:
119                                self.cur.execute("PRAGMA synchronous = NORMAL")
120                        else:
121                                self.cur.execute("PRAGMA synchronous = OFF")
122                except sqlite.Error, e:
123                        gajim.log.debug("Failed to set_synchronous(%s): %s" % (sync, str(e)))
124
125        def init_vars(self):
126                self.open_db()
127                self.get_jids_already_in_db()
128
129        def get_jids_already_in_db(self):
130                self.cur.execute('SELECT jid FROM jids')
131                rows = self.cur.fetchall() # list of tupples: [(u'aaa@bbb',), (u'cc@dd',)]
132                self.jids_already_in = []
133                for row in rows:
134                        # row[0] is first item of row (the only result here, the jid)
135                        self.jids_already_in.append(row[0])
136
137        def jid_is_from_pm(self, jid):
138                '''if jid is gajim@conf/nkour it's likely a pm one, how we know
139                gajim@conf is not a normal guy and nkour is not his resource?
140                we ask if gajim@conf is already in jids (with type room jid)
141                this fails if user disables logging for room and only enables for
142                pm (so higly unlikely) and if we fail we do not go chaos
143                (user will see the first pm as if it was message in room's public chat)
144                and after that all okay'''
145               
146                possible_room_jid, possible_nick = jid.split('/', 1)
147                return self.jid_is_room_jid(possible_room_jid)
148
149        def jid_is_room_jid(self, jid):
150                self.cur.execute('SELECT jid_id FROM jids WHERE jid=?  AND type=?', 
151                        (jid, constants.JID_ROOM_TYPE))
152                row = self.cur.fetchone()
153                if row is None:
154                        return False
155                else:
156                        return True
157       
158        def get_jid_id(self, jid, typestr = None):
159                '''jids table has jid and jid_id
160                logs table has log_id, jid_id, contact_name, time, kind, show, message
161                so to ask logs we need jid_id that matches our jid in jids table
162                this method wants jid and returns the jid_id for later sql-ing on logs
163                typestr can be 'ROOM' or anything else depending on the type of JID
164                and is only needed to be specified when the JID is new in DB
165                '''
166                if jid.find('/') != -1: # if it has a /
167                        jid_is_from_pm = self.jid_is_from_pm(jid)
168                        if not jid_is_from_pm: # it's normal jid with resource
169                                jid = jid.split('/', 1)[0] # remove the resource
170                if jid in self.jids_already_in: # we already have jids in DB
171                        self.cur.execute('SELECT jid_id FROM jids WHERE jid=?', [jid])
172                        jid_id = self.cur.fetchone()[0]
173                else: # oh! a new jid :), we add it now
174                        if typestr == 'ROOM':
175                                typ = constants.JID_ROOM_TYPE
176                        else:
177                                typ = constants.JID_NORMAL_TYPE
178                        self.cur.execute('INSERT INTO jids (jid, type) VALUES (?, ?)', (jid, typ))
179                        try:
180                                self.con.commit()
181                        except sqlite.OperationalError, e:
182                                print >> sys.stderr, str(e)
183                        jid_id = self.cur.lastrowid
184                        self.jids_already_in.append(jid)
185                return jid_id
186       
187        def convert_human_values_to_db_api_values(self, kind, show):
188                '''coverts from string style to constant ints for db'''
189                if kind == 'status':
190                        kind_col = constants.KIND_STATUS
191                elif kind == 'gcstatus':
192                        kind_col = constants.KIND_GCSTATUS
193                elif kind == 'gc_msg':
194                        kind_col = constants.KIND_GC_MSG
195                elif kind == 'single_msg_recv':
196                        kind_col = constants.KIND_SINGLE_MSG_RECV
197                elif kind == 'single_msg_sent':
198                        kind_col = constants.KIND_SINGLE_MSG_SENT
199                elif kind == 'chat_msg_recv':
200                        kind_col = constants.KIND_CHAT_MSG_RECV
201                elif kind == 'chat_msg_sent':
202                        kind_col = constants.KIND_CHAT_MSG_SENT
203                elif kind == 'error':
204                        kind_col = constants.KIND_ERROR
205
206                if show == 'online':
207                        show_col = constants.SHOW_ONLINE
208                elif show == 'chat':
209                        show_col = constants.SHOW_CHAT
210                elif show == 'away':
211                        show_col = constants.SHOW_AWAY
212                elif show == 'xa':
213                        show_col = constants.SHOW_XA
214                elif show == 'dnd':
215                        show_col = constants.SHOW_DND
216                elif show == 'offline':
217                        show_col = constants.SHOW_OFFLINE
218                elif show is None:
219                        show_col = None
220                else: # invisible in GC when someone goes invisible
221                        # it's a RFC violation .... but we should not crash
222                        show_col = 'UNKNOWN'
223               
224                return kind_col, show_col
225
226        def convert_human_transport_type_to_db_api_values(self, type_):
227                '''converts from string style to constant ints for db'''
228                if type_ == 'aim':
229                        return constants.TYPE_AIM
230                if type_ == 'gadu-gadu':
231                        return constants.TYPE_GG
232                if type_ == 'http-ws':
233                        return constants.TYPE_HTTP_WS
234                if type_ == 'icq':
235                        return constants.TYPE_ICQ
236                if type_ == 'msn':
237                        return constants.TYPE_MSN
238                if type_ == 'qq':
239                        return constants.TYPE_QQ
240                if type_ == 'sms':
241                        return constants.TYPE_SMS
242                if type_ == 'smtp':
243                        return constants.TYPE_SMTP
244                if type_ in ('tlen', 'x-tlen'):
245                        return constants.TYPE_TLEN
246                if type_ == 'yahoo':
247                        return constants.TYPE_YAHOO
248                if type_ == 'newmail':
249                        return constants.TYPE_NEWMAIL
250                if type_ == 'rss':
251                        return constants.TYPE_RSS
252                if type_ == 'weather':
253                        return constants.TYPE_WEATHER
254                return None
255
256        def convert_api_values_to_human_transport_type(self, type_id):
257                '''converts from constant ints for db to string style'''
258                if type_id == constants.TYPE_AIM:
259                        return 'aim'
260                if type_id == constants.TYPE_GG:
261                        return 'gadu-gadu'
262                if type_id == constants.TYPE_HTTP_WS:
263                        return 'http-ws'
264                if type_id == constants.TYPE_ICQ:
265                        return 'icq'
266                if type_id == constants.TYPE_MSN:
267                        return 'msn'
268                if type_id == constants.TYPE_QQ:
269                        return 'qq'
270                if type_id == constants.TYPE_SMS:
271                        return 'sms'
272                if type_id == constants.TYPE_SMTP:
273                        return 'smtp'
274                if type_id == constants.TYPE_TLEN:
275                        return 'tlen'
276                if type_id == constants.TYPE_YAHOO:
277                        return 'yahoo'
278                if type_id == constants.TYPE_NEWMAIL:
279                        return 'newmail'
280                if type_id == constants.TYPE_RSS:
281                        return 'rss'
282                if type_id == constants.TYPE_WEATHER:
283                        return 'weather'
284
285        def commit_to_db(self, values, write_unread = False):
286                #print 'saving', values
287                sql = 'INSERT INTO logs (jid_id, contact_name, time, kind, show, message, subject) VALUES (?, ?, ?, ?, ?, ?, ?)'
288                self.cur.execute(sql, values)
289                message_id = None
290                try:
291                        self.con.commit()
292                        if write_unread:
293                                message_id = self.cur.lastrowid
294                except sqlite.OperationalError, e:
295                        print >> sys.stderr, str(e)
296                if message_id:
297                        self.insert_unread_events(message_id, values[0])
298                return message_id
299       
300        def insert_unread_events(self, message_id, jid_id):
301                ''' add unread message with id: message_id'''
302                sql = 'INSERT INTO unread_messages VALUES (%d, %d)' % (message_id, jid_id)
303                self.cur.execute(sql)
304                try:
305                        self.con.commit()
306                except sqlite.OperationalError, e:
307                        print >> sys.stderr, str(e)
308       
309        def set_read_messages(self, message_ids):
310                ''' mark all messages with ids in message_ids as read'''
311                ids = ','.join([str(i) for i in message_ids])
312                sql = 'DELETE FROM unread_messages WHERE message_id IN (%s)' % ids
313                self.cur.execute(sql)
314                try:
315                        self.con.commit()
316                except sqlite.OperationalError, e:
317                        print >> sys.stderr, str(e)
318       
319        def get_unread_msgs_for_jid(self, jid):
320                ''' get unread messages for jid '''
321                if not jid:
322                        return
323                jid = jid.lower()
324                jid_id = self.get_jid_id(jid)
325                all_messages = []
326                try:
327                        self.cur.execute(
328                                'SELECT message_id from unread_messages WHERE jid_id = %d' % jid_id)
329                        results = self.cur.fetchall()
330                except:
331                        pass
332
333                for message in results:
334                        msg_id = message[0]
335                        self.cur.execute('''
336                                SELECT log_line_id, message, time, subject FROM logs
337                                WHERE jid_id = %d AND log_line_id = %d
338                                ''' % (jid_id, msg_id)
339                                )
340                        results = self.cur.fetchall()
341                        all_messages.append(results[0])
342                return all_messages
343               
344        def write(self, kind, jid, message = None, show = None, tim = None,
345        subject = None):
346                '''write a row (status, gcstatus, message etc) to logs database
347                kind can be status, gcstatus, gc_msg, (we only recv for those 3),
348                single_msg_recv, chat_msg_recv, chat_msg_sent, single_msg_sent
349                we cannot know if it is pm or normal chat message, we try to guess
350                see jid_is_from_pm() which is called by get_jid_id()
351               
352                we analyze jid and store it as follows:
353                jids.jid text column will hold JID if TC-related, room_jid if GC-related,
354                ROOM_JID/nick if pm-related.'''
355
356                if self.jids_already_in == []: # only happens if we just created the db
357                        self.open_db()
358
359                jid = jid.lower()
360                contact_name_col = None # holds nickname for kinds gcstatus, gc_msg
361                # message holds the message unless kind is status or gcstatus,
362                # then it holds status message
363                message_col = message
364                subject_col = subject
365                if tim:
366                        time_col = int(float(time.mktime(tim)))
367                else:
368                        time_col = int(float(time.time()))
369               
370                kind_col, show_col = self.convert_human_values_to_db_api_values(kind,
371                        show)
372               
373                write_unread = False
374               
375                # now we may have need to do extra care for some values in columns
376                if kind == 'status': # we store (not None) time, jid, show, msg
377                        # status for roster items
378                        jid_id = self.get_jid_id(jid)
379                        if show is None: # show is None (xmpp), but we say that 'online'
380                                show_col = constants.SHOW_ONLINE
381
382                elif kind == 'gcstatus':
383                        # status in ROOM (for pm status see status)
384                        if show is None: # show is None (xmpp), but we say that 'online'
385                                show_col = constants.SHOW_ONLINE
386                        jid, nick = jid.split('/', 1)
387                        jid_id = self.get_jid_id(jid, 'ROOM') # re-get jid_id for the new jid
388                        contact_name_col = nick
389
390                elif kind == 'gc_msg':
391                        if jid.find('/') != -1: # if it has a /
392                                jid, nick = jid.split('/', 1)
393                        else:
394                                # it's server message f.e. error message
395                                # when user tries to ban someone but he's not allowed to
396                                nick = None
397                        jid_id = self.get_jid_id(jid, 'ROOM') # re-get jid_id for the new jid
398                        contact_name_col = nick
399                else:
400                        jid_id = self.get_jid_id(jid)
401                        if kind == 'chat_msg_recv':
402                                write_unread = True
403               
404                if show_col == 'UNKNOWN': # unknown show, do not log
405                        return
406                       
407                values = (jid_id, contact_name_col, time_col, kind_col, show_col,
408                        message_col, subject_col)
409                return self.commit_to_db(values, write_unread)
410               
411        def get_last_conversation_lines(self, jid, restore_how_many_rows,
412                pending_how_many, timeout, account):
413                '''accepts how many rows to restore and when to time them out (in minutes)
414                (mark them as too old) and number of messages that are in queue
415                and are already logged but pending to be viewed,
416                returns a list of tupples containg time, kind, message,
417                list with empty tupple if nothing found to meet our demands'''
418                jid = jid.lower()
419                jid_id = self.get_jid_id(jid)
420                where_sql = self._build_contact_where(account, jid)
421               
422                now = int(float(time.time()))
423                timed_out = now - (timeout * 60) # before that they are too old
424                # so if we ask last 5 lines and we have 2 pending we get
425                # 3 - 8 (we avoid the last 2 lines but we still return 5 asked)
426                self.cur.execute('''
427                        SELECT time, kind, message FROM logs
428                        WHERE (%s) AND kind IN (%d, %d, %d, %d, %d) AND time > %d
429                        ORDER BY time DESC LIMIT %d OFFSET %d
430                        ''' % (where_sql, constants.KIND_SINGLE_MSG_RECV,
431                                constants.KIND_CHAT_MSG_RECV, constants.KIND_SINGLE_MSG_SENT,
432                                constants.KIND_CHAT_MSG_SENT, constants.KIND_ERROR,
433                                timed_out, restore_how_many_rows, pending_how_many)
434                        )
435
436                results = self.cur.fetchall()
437                results.reverse()
438                return results
439       
440        def get_unix_time_from_date(self, year, month, day):
441                # year (fe 2005), month (fe 11), day (fe 25)
442                # returns time in seconds for the second that starts that date since epoch
443                # gimme unixtime from year month day:
444                d = datetime.date(year, month, day)
445                local_time = d.timetuple() # time tupple (compat with time.localtime())
446                start_of_day = int(time.mktime(local_time)) # we have time since epoch baby :)
447                return start_of_day
448       
449        def get_conversation_for_date(self, jid, year, month, day, account):
450                '''returns contact_name, time, kind, show, message
451                for each row in a list of tupples,
452                returns list with empty tupple if we found nothing to meet our demands'''
453                jid = jid.lower()
454                jid_id = self.get_jid_id(jid)
455                where_sql = self._build_contact_where(account, jid)
456
457                start_of_day = self.get_unix_time_from_date(year, month, day)
458                seconds_in_a_day = 86400 # 60 * 60 * 24
459                last_second_of_day = start_of_day + seconds_in_a_day - 1
460               
461                self.cur.execute('''
462                        SELECT contact_name, time, kind, show, message FROM logs
463                        WHERE (%s)
464                        AND time BETWEEN %d AND %d
465                        ORDER BY time
466                        ''' % (where_sql, start_of_day, last_second_of_day))
467               
468                results = self.cur.fetchall()
469                return results
470
471        def get_search_results_for_query(self, jid, query, account):
472                '''returns contact_name, time, kind, show, message
473                for each row in a list of tupples,
474                returns list with empty tupple if we found nothing to meet our demands'''
475                jid = jid.lower()
476                jid_id = self.get_jid_id(jid)
477
478                if False: #query.startswith('SELECT '): # it's SQL query (FIXME)
479                        try:
480                                self.cur.execute(query)
481                        except sqlite.OperationalError, e:
482                                results = [('', '', '', '', str(e))]
483                                return results
484                       
485                else: # user just typed something, we search in message column
486                        where_sql = self._build_contact_where(account, jid)
487                        like_sql = '%' + query + '%'
488                        self.cur.execute('''
489                                SELECT contact_name, time, kind, show, message, subject FROM logs
490                                WHERE (%s) AND message LIKE '%s'
491                                ORDER BY time
492                                ''' % (where_sql, like_sql))
493
494                results = self.cur.fetchall()
495                return results
496
497        def get_days_with_logs(self, jid, year, month, max_day, account):
498                '''returns the list of days that have logs (not status messages)'''
499                jid = jid.lower()
500                jid_id = self.get_jid_id(jid)
501                days_with_logs = []
502                where_sql = self._build_contact_where(account, jid)
503               
504                # First select all date of month whith logs we want
505                start_of_month = self.get_unix_time_from_date(year, month, 1)
506                seconds_in_a_day = 86400 # 60 * 60 * 24
507                last_second_of_month = start_of_month + (seconds_in_a_day * max_day) - 1
508
509                self.cur.execute('''
510                        SELECT time FROM logs
511                        WHERE (%s)
512                        AND time BETWEEN %d AND %d
513                        AND kind NOT IN (%d, %d)
514                        ORDER BY time
515                        ''' % (where_sql, start_of_month, last_second_of_month,
516                        constants.KIND_STATUS, constants.KIND_GCSTATUS))
517                result = self.cur.fetchall()
518
519                # Copy all interesting times in a temporary table
520                self.cur.execute('CREATE TEMPORARY TABLE blabla(time,INTEGER)') 
521                for line in result: 
522                        self.cur.execute('''
523                                INSERT INTO blabla (time) VALUES (%d)
524                                ''' % (line[0])) 
525
526                # then search in this small temp table for each day
527                for day in xrange(1, max_day + 1):  # count from 1 to 28 or to 30 or to 31
528                        start_of_day = self.get_unix_time_from_date(year, month, day) 
529                        last_second_of_day = start_of_day + seconds_in_a_day - 1 
530
531                        # just ask one row to see if we have sth for this date
532                        self.cur.execute('''
533                                SELECT time FROM blabla
534                                WHERE time BETWEEN %d AND %d 
535                                LIMIT 1
536                                ''' % (start_of_day, last_second_of_day)) 
537                        result = self.cur.fetchone() 
538                        if result: 
539                                days_with_logs[0:0]=[day]
540
541                # Delete temporary table
542                self.cur.execute('DROP TABLE blabla') 
543                result = self.cur.fetchone()
544                return days_with_logs
545
546        def get_last_date_that_has_logs(self, jid, account = None, is_room = False):
547                '''returns last time (in seconds since EPOCH) for which
548                we had logs (excluding statuses)'''
549                jid = jid.lower()
550       
551                where_sql = ''
552                if not is_room:
553                        where_sql = self._build_contact_where(account, jid)
554                else:
555                        jid_id = self.get_jid_id(jid, 'ROOM')
556                        where_sql = 'jid_id = %s' % jid_id     
557                self.cur.execute('''
558                        SELECT MAX(time) FROM logs
559                        WHERE (%s)
560                        AND kind NOT IN (%d, %d)
561                        ''' % (where_sql, constants.KIND_STATUS, constants.KIND_GCSTATUS))
562
563                results = self.cur.fetchone()
564                if results is not None:
565                        result = results[0]
566                else:
567                        result = None
568
569                return result
570
571        def _build_contact_where(self, account, jid):
572                '''build the where clause for a jid, including metacontacts
573                jid(s) if any'''
574                where_sql = ''   
575                # will return empty list if jid is not associated with
576                # any metacontacts
577                family = gajim.contacts.get_metacontacts_family(account, jid)
578                if family:
579                        for user in family:
580                                jid_id = self.get_jid_id(user['jid'])
581                                where_sql += 'jid_id = %s' % jid_id
582                                if user != family[-1]:
583                                        where_sql += ' OR '
584                else: # if jid was not associated with metacontacts
585                        jid_id = self.get_jid_id(jid)
586                        where_sql = 'jid_id = %s' % jid_id
587                return where_sql
588
589        def save_transport_type(self, jid, type_):
590                '''save the type of the transport in DB'''
591                type_id = self.convert_human_transport_type_to_db_api_values(type_)
592                if not type_id:
593                        # unknown type
594                        return
595                self.cur.execute(
596                        'SELECT type from transports_cache WHERE transport = "%s"' % jid)
597                results = self.cur.fetchall()
598                if results:
599                        result = results[0][0]
600                        if result == type_id:
601                                return
602                        self.cur.execute(
603                                'UPDATE transports_cache SET type = %d WHERE transport = "%s"' % (type_id,
604                                        jid))
605                        try:
606                                self.con.commit()
607                        except sqlite.OperationalError, e:
608                                print >> sys.stderr, str(e)
609                        return
610                self.cur.execute(
611                        'INSERT INTO transports_cache VALUES ("%s", %d)' % (jid, type_id))
612                try:
613                        self.con.commit()
614                except sqlite.OperationalError, e:
615                        print >> sys.stderr, str(e)
616
617        def get_transports_type(self):
618                '''return all the type of the transports in DB'''
619                self.cur.execute(
620                        'SELECT * from transports_cache')
621                results = self.cur.fetchall()
622                if not results:
623                        return {}
624                answer = {}
625                for result in results:
626                        answer[result[0]] = self.convert_api_values_to_human_transport_type(result[1])
627                return answer
Note: See TracBrowser for help on using the browser.