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

Revision 4875, 13.4 kB (checked in by nk, 3 years ago)

fix it correctly

  • Property svn:eol-style set to LF
  • Property svn:keywords set to LastChangedDate LastChangedRevision LastChangedBy HeadURL Id
Line 
1## logger.py
2##
3## Contributors for this file:
4## - Yann Le Boulanger <asterix@lagaule.org>
5## - Nikos Kouremenos <kourem@gmail.com>
6##
7## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
8##                         Vincent Hanquez <tab@snarc.org>
9## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
10##                    Vincent Hanquez <tab@snarc.org>
11##                    Nikos Kouremenos <nkour@jabber.org>
12##                    Dimitur Kirov <dkirov@gmail.com>
13##                    Travis Shirk <travis@pobox.com>
14##                    Norman Rasmussen <norman@rasmussen.co.za>
15##
16## This program is free software; you can redistribute it and/or modify
17## it under the terms of the GNU General Public License as published
18## by the Free Software Foundation; version 2 only.
19##
20## This program is distributed in the hope that it will be useful,
21## but WITHOUT ANY WARRANTY; without even the implied warranty of
22## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23## GNU General Public License for more details.
24##
25
26import os
27import sys
28import time
29import datetime
30
31import exceptions
32import i18n
33_ = i18n._
34
35try:
36        from pysqlite2 import dbapi2 as sqlite
37except ImportError:
38        raise exceptions.PysqliteNotAvailable
39
40if os.name == 'nt':
41        try:
42                # Documents and Settings\[User Name]\Application Data\Gajim\logs.db
43                LOG_DB_PATH = os.path.join(os.environ['appdata'], 'Gajim', 'logs.db')
44        except KeyError:
45                # win9x, ./logs.db
46                LOG_DB_PATH = 'logs.db'
47else: # Unices
48        LOG_DB_PATH = os.path.expanduser('~/.gajim/logs.db')
49
50try:
51        LOG_DB_PATH = LOG_DB_PATH.decode(sys.getfilesystemencoding())
52except:
53        pass
54
55class Constants:
56        def __init__(self):
57                (
58                        self.JID_NORMAL_TYPE,
59                        self.JID_ROOM_TYPE
60                ) = range(2)
61               
62                (
63                        self.KIND_STATUS,
64                        self.KIND_GCSTATUS,
65                        self.KIND_GC_MSG,
66                        self.KIND_SINGLE_MSG_RECV,
67                        self.KIND_CHAT_MSG_RECV,
68                        self.KIND_SINGLE_MSG_SENT,
69                        self.KIND_CHAT_MSG_SENT
70                ) = range(7)
71               
72                (
73                        self.SHOW_ONLINE,
74                        self.SHOW_CHAT,
75                        self.SHOW_AWAY,
76                        self.SHOW_XA,
77                        self.SHOW_DND,
78                        self.SHOW_OFFLINE
79                ) = range(6)
80
81constants = Constants()
82
83class Logger:
84        def __init__(self):
85                self.jids_already_in = [] # holds jids that we already have in DB
86               
87                if not os.path.exists(LOG_DB_PATH):
88                        # this can happen only the first time (the time we create the db)
89                        # db is not created here but in src/common/checks_paths.py
90                        return
91                self.init_vars()
92               
93        def init_vars(self):
94                # if locked, wait up to 20 sec to unlock
95                # before raise (hopefully should be enough)
96                self.con = sqlite.connect(LOG_DB_PATH, timeout = 20.0,
97                        isolation_level = 'IMMEDIATE')
98                self.cur = self.con.cursor()
99               
100                self.get_jids_already_in_db()
101
102        def get_jids_already_in_db(self):
103                self.cur.execute('SELECT jid FROM jids')
104                rows = self.cur.fetchall() # list of tupples: (u'aaa@bbb',), (u'cc@dd',)]
105                for row in rows:
106                        # row[0] is first item of row (the only result here, the jid)
107                        self.jids_already_in.append(row[0])
108
109        def jid_is_from_pm(self, jid):
110                '''if jid is gajim@conf/nkour it's likely a pm one, how we know
111                gajim@conf is not a normal guy and nkour is not his resource?
112                we ask if gajim@conf is already in jids (with type room jid)
113                this fails if user disables logging for room and only enables for
114                pm (so higly unlikely) and if we fail we do not go chaos
115                (user will see the first pm as if it was message in room's public chat)
116                and after that all okay'''
117               
118                possible_room_jid, possible_nick = jid.split('/', 1)
119               
120                self.cur.execute('SELECT jid_id FROM jids WHERE jid="%s" AND type=%d' %\
121                        (possible_room_jid, constants.JID_ROOM_TYPE))
122                row = self.cur.fetchone()
123                if row is not None:
124                        return True
125                else:
126                        return False
127       
128        def get_jid_id(self, jid, typestr = None):
129                '''jids table has jid and jid_id
130                logs table has log_id, jid_id, contact_name, time, kind, show, message
131                so to ask logs we need jid_id that matches our jid in jids table
132                this method asks jid and returns the jid_id for later sql-ing on logs
133                '''
134                if jid.find('/') != -1: # if it has a /
135                        jid_is_from_pm = self.jid_is_from_pm(jid)
136                        if not jid_is_from_pm: # it's normal jid with resource
137                                jid = jid.split('/', 1)[0] # remove the resource
138                if jid in self.jids_already_in: # we already have jids in DB
139                        self.cur.execute('SELECT jid_id FROM jids WHERE jid="%s"' % jid)
140                        jid_id = self.cur.fetchone()[0]
141                else: # oh! a new jid :), we add it now
142                        if typestr == 'ROOM':
143                                typ = constants.JID_ROOM_TYPE
144                        else:
145                                typ = constants.JID_NORMAL_TYPE
146                        self.cur.execute('INSERT INTO jids (jid, type) VALUES (?, ?)', (jid, typ))
147                        try:
148                                self.con.commit()
149                        except sqlite.OperationalError, e:
150                                print >> sys.stderr, str(e)
151                        jid_id = self.cur.lastrowid
152                        self.jids_already_in.append(jid)
153                return jid_id
154       
155        def convert_human_values_to_db_api_values(self, kind, show):
156                '''coverts from string style to constant ints for db'''
157                if kind == 'status':
158                        kind_col = constants.KIND_STATUS
159                elif kind == 'gcstatus':
160                        kind_col = constants.KIND_GCSTATUS
161                elif kind == 'gc_msg':
162                        kind_col = constants.KIND_GC_MSG
163                elif kind == 'single_msg_recv':
164                        kind_col = constants.KIND_SINGLE_MSG_RECV
165                elif kind == 'single_msg_sent':
166                        kind_col = constants.KIND_SINGLE_MSG_SENT
167                elif kind == 'chat_msg_recv':
168                        kind_col = constants.KIND_CHAT_MSG_RECV
169                elif kind == 'chat_msg_sent':
170                        kind_col = constants.KIND_CHAT_MSG_SENT
171
172                if show == 'online':
173                        show_col = constants.SHOW_ONLINE
174                elif show == 'chat':
175                        show_col = constants.SHOW_CHAT
176                elif show == 'away':
177                        show_col = constants.SHOW_AWAY
178                elif show == 'xa':
179                        show_col = constants.SHOW_XA
180                elif show == 'dnd':
181                        show_col = constants.SHOW_DND
182                elif show == 'offline':
183                        show_col = constants.SHOW_OFFLINE
184                elif show is None:
185                        show_col = None
186                else: # invisible in GC when someone goes invisible
187                        # it's a RFC violation .... but we should not crash
188                        show_col = 'UNKNOWN'
189               
190                return kind_col, show_col
191       
192        def commit_to_db(self, values):
193                #print 'saving', values
194                sql = 'INSERT INTO logs (jid_id, contact_name, time, kind, show, message, subject) VALUES (?, ?, ?, ?, ?, ?, ?)'
195                self.cur.execute(sql, values)
196                try:
197                        self.con.commit()
198                except sqlite.OperationalError, e:
199                        print >> sys.stderr, str(e)
200       
201        def write(self, kind, jid, message = None, show = None, tim = None, subject = None):
202                '''write a row (status, gcstatus, message etc) to logs database
203                kind can be status, gcstatus, gc_msg, (we only recv for those 3),
204                single_msg_recv, chat_msg_recv, chat_msg_sent, single_msg_sent
205                we cannot know if it is pm or normal chat message, we try to guess
206                see jid_is_from_pm() which is called by get_jid_id()
207               
208                we analyze jid and store it as follows:
209                jids.jid text column will hold JID if TC-related, room_jid if GC-related,
210                ROOM_JID/nick if pm-related.'''
211
212                if self.jids_already_in == []: # only happens if we just created the db
213                        self.con = sqlite.connect(LOG_DB_PATH, timeout = 20.0,
214                                isolation_level = 'IMMEDIATE')
215                        self.cur = self.con.cursor()
216                       
217                jid = jid.lower()
218                contact_name_col = None # holds nickname for kinds gcstatus, gc_msg
219                # message holds the message unless kind is status or gcstatus,
220                # then it holds status message
221                message_col = message
222                subject_col = subject
223                if tim:
224                        time_col = int(float(time.mktime(tim)))
225                else:
226                        time_col = int(float(time.time()))
227               
228                kind_col, show_col = self.convert_human_values_to_db_api_values(kind,
229                        show)
230
231                # now we may have need to do extra care for some values in columns
232                if kind == 'status': # we store (not None) time, jid, show, msg
233                        # status for roster items
234                        jid_id = self.get_jid_id(jid)
235                        if show is None: # show is None (xmpp), but we say that 'online'
236                                show_col = constants.SHOW_ONLINE
237
238                elif kind == 'gcstatus':
239                        # status in ROOM (for pm status see status)
240                        if show is None: # show is None (xmpp), but we say that 'online'
241                                show_col = constants.SHOW_ONLINE
242                        jid, nick = jid.split('/', 1)
243                        jid_id = self.get_jid_id(jid, 'ROOM') # re-get jid_id for the new jid
244                        contact_name_col = nick
245
246                elif kind == 'gc_msg':
247                        if jid.find('/') != -1: # if it has a /
248                                jid, nick = jid.split('/', 1)
249                        else:
250                                # it's server message f.e. error message
251                                # when user tries to ban someone but he's not allowed to
252                                nick = None
253                        jid_id = self.get_jid_id(jid, 'ROOM') # re-get jid_id for the new jid
254                        contact_name_col = nick
255                else:
256                        jid_id = self.get_jid_id(jid)
257               
258                if show_col == 'UNKNOWN': # unknown show, do not log
259                        return
260                       
261                values = (jid_id, contact_name_col, time_col, kind_col, show_col,
262                        message_col, subject_col)
263                self.commit_to_db(values)
264
265        def get_last_conversation_lines(self, jid, restore_how_many_rows,
266                pending_how_many, timeout):
267                '''accepts how many rows to restore and when to time them out (in minutes)
268                (mark them as too old) and number of messages that are in queue
269                and are already logged but pending to be viewed,
270                returns a list of tupples containg time, kind, message,
271                list with empty tupple if nothing found to meet our demands'''
272                jid = jid.lower()
273                jid_id = self.get_jid_id(jid)
274                now = int(float(time.time()))
275                timed_out = now - (timeout * 60) # before that they are too old
276                # so if we ask last 5 lines and we have 2 pending we get
277                # 3 - 8 (we avoid the last 2 lines but we still return 5 asked)
278                self.cur.execute('''
279                        SELECT time, kind, message FROM logs
280                        WHERE jid_id = %d AND kind IN   (%d, %d, %d, %d) AND time > %d
281                        ORDER BY time DESC LIMIT %d OFFSET %d
282                        ''' % (jid_id, constants.KIND_SINGLE_MSG_RECV, constants.KIND_CHAT_MSG_RECV,
283                                constants.KIND_SINGLE_MSG_SENT, constants.KIND_CHAT_MSG_SENT,
284                                timed_out, restore_how_many_rows, pending_how_many)
285                        )
286
287                results = self.cur.fetchall()
288                results.reverse()
289                return results
290       
291        def get_unix_time_from_date(self, year, month, day):
292                # year (fe 2005), month (fe 11), day (fe 25)
293                # returns time in seconds for the second that starts that date since epoch
294                # gimme unixtime from year month day:
295                d = datetime.date(year, month, day)
296                local_time = d.timetuple() # time tupple (compat with time.localtime())
297                start_of_day = int(time.mktime(local_time)) # we have time since epoch baby :)
298                return start_of_day
299       
300        def get_conversation_for_date(self, jid, year, month, day):
301                '''returns contact_name, time, kind, show, message
302                for each row in a list of tupples,
303                returns list with empty tupple if we found nothing to meet our demands'''
304                jid = jid.lower()
305                jid_id = self.get_jid_id(jid)
306               
307                start_of_day = self.get_unix_time_from_date(year, month, day)
308               
309                seconds_in_a_day = 86400 # 60 * 60 * 24
310                last_second_of_day = start_of_day + seconds_in_a_day - 1
311               
312                self.cur.execute('''
313                        SELECT contact_name, time, kind, show, message FROM logs
314                        WHERE jid_id = %d
315                        AND time BETWEEN %d AND %d
316                        ORDER BY time
317                        ''' % (jid_id, start_of_day, last_second_of_day))
318               
319                results = self.cur.fetchall()
320                return results
321
322        def get_search_results_for_query(self, jid, query):
323                '''returns contact_name, time, kind, show, message
324                for each row in a list of tupples,
325                returns list with empty tupple if we found nothing to meet our demands'''
326                jid = jid.lower()
327                jid_id = self.get_jid_id(jid)
328                if False: #query.startswith('SELECT '): # it's SQL query
329                        try:
330                                self.cur.execute(query)
331                        except sqlite.OperationalError, e:
332                                results = [('', '', '', '', str(e))]
333                                return results
334                       
335                else: # user just typed something, we search in message column
336                        like_sql = '%' + query + '%'
337                        self.cur.execute('''
338                                SELECT contact_name, time, kind, show, message, subject FROM logs
339                                WHERE jid_id = ? AND message LIKE ?
340                                ORDER BY time
341                                ''', (jid_id, like_sql))
342
343                results = self.cur.fetchall()
344                return results
345
346        def get_days_with_logs(self, jid, year, month, max_day):
347                '''returns the list of days that have logs (not status messages)'''
348                jid = jid.lower()
349                jid_id = self.get_jid_id(jid)
350                list = []
351
352                # First select all date of month whith logs we want
353                start_of_month = self.get_unix_time_from_date(year, month, 1)
354                seconds_in_a_day = 86400 # 60 * 60 * 24
355                last_second_of_month = start_of_month + (seconds_in_a_day * max_day) - 1
356
357                self.cur.execute('''
358                        SELECT time FROM logs
359                        WHERE jid_id = %d
360                        AND time BETWEEN %d AND %d
361                        AND kind NOT IN (%d, %d)
362                        ORDER BY time
363                        ''' % (jid_id, start_of_month, last_second_of_month,
364                        constants.KIND_STATUS, constants.KIND_GCSTATUS))
365                result = self.cur.fetchall()
366
367                #Copy all interesant time in a temporary table
368                self.cur.execute('CREATE TEMPORARY TABLE blabla(time,INTEGER)') 
369                for line in result: 
370                        self.cur.execute('''
371                                INSERT INTO blabla (time) VALUES (%d)
372                                ''' % (line[0])) 
373
374                #then search in this small temp table for each day
375                for day in xrange(1, max_day): 
376                        start_of_day = self.get_unix_time_from_date(year, month, day) 
377                        last_second_of_day = start_of_day + seconds_in_a_day - 1 
378
379                        # just ask one row to see if we have sth for this date
380                        self.cur.execute('''
381                                SELECT time FROM blabla
382                                WHERE time BETWEEN %d AND %d 
383                                LIMIT 1
384                                ''' % (start_of_day, last_second_of_day)) 
385                        result = self.cur.fetchone() 
386                        if result: 
387                                list[0:0]=[day]
388
389                #Delete temporary table
390                self.cur.execute('DROP TABLE blabla') 
391                result = self.cur.fetchone()
392                return list
393
394        def get_last_date_that_has_logs(self, jid):
395                '''returns last time (in seconds since EPOCH) for which
396                we had logs (excluding statuses)'''
397                jid = jid.lower()
398                jid_id = self.get_jid_id(jid)
399                self.cur.execute('''
400                        SELECT time FROM logs
401                        WHERE jid_id = ?
402                        AND kind NOT IN (?, ?)
403                        ORDER BY time DESC LIMIT 1
404                        ''', (jid_id, constants.KIND_STATUS, constants.KIND_GCSTATUS))
405
406                results = self.cur.fetchone()
407                if results is not None:
408                        result = results[0]
409                else:
410                        result = None
411
412                return result
Note: See TracBrowser for help on using the browser.