Package common :: Module logger
[hide private]
[frames] | no frames]

Source Code for Module common.logger

   1  # -*- coding:utf-8 -*- 
   2  ## src/common/logger.py 
   3  ## 
   4  ## Copyright (C) 2003-2010 Yann Leboulanger <asterix AT lagaule.org> 
   5  ## Copyright (C) 2004-2005 Vincent Hanquez <tab AT snarc.org> 
   6  ## Copyright (C) 2005-2006 Nikos Kouremenos <kourem AT gmail.com> 
   7  ## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com> 
   8  ## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org> 
   9  ## Copyright (C) 2007 Tomasz Melcer <liori AT exroot.org> 
  10  ##                    Julien Pivotto <roidelapluie AT gmail.com> 
  11  ## 
  12  ## This file is part of Gajim. 
  13  ## 
  14  ## Gajim is free software; you can redistribute it and/or modify 
  15  ## it under the terms of the GNU General Public License as published 
  16  ## by the Free Software Foundation; version 3 only. 
  17  ## 
  18  ## Gajim is distributed in the hope that it will be useful, 
  19  ## but WITHOUT ANY WARRANTY; without even the implied warranty of 
  20  ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 
  21  ## GNU General Public License for more details. 
  22  ## 
  23  ## You should have received a copy of the GNU General Public License 
  24  ## along with Gajim. If not, see <http://www.gnu.org/licenses/>. 
  25  ## 
  26   
  27  """ 
  28  This module allows to access the on-disk database of logs 
  29  """ 
  30   
  31  import os 
  32  import sys 
  33  import time 
  34  import datetime 
  35  from gzip import GzipFile 
  36  from cStringIO import StringIO 
  37   
  38  import exceptions 
  39  import gajim 
  40   
  41  import sqlite3 as sqlite 
  42   
  43  import configpaths 
  44  LOG_DB_PATH = configpaths.gajimpaths['LOG_DB'] 
  45  LOG_DB_FOLDER, LOG_DB_FILE = os.path.split(LOG_DB_PATH) 
  46  CACHE_DB_PATH = configpaths.gajimpaths['CACHE_DB'] 
  47   
48 -class Constants:
49 - def __init__(self):
50 ( 51 self.JID_NORMAL_TYPE, 52 self.JID_ROOM_TYPE 53 ) = range(2) 54 55 ( 56 self.KIND_STATUS, 57 self.KIND_GCSTATUS, 58 self.KIND_GC_MSG, 59 self.KIND_SINGLE_MSG_RECV, 60 self.KIND_CHAT_MSG_RECV, 61 self.KIND_SINGLE_MSG_SENT, 62 self.KIND_CHAT_MSG_SENT, 63 self.KIND_ERROR 64 ) = range(8) 65 66 ( 67 self.SHOW_ONLINE, 68 self.SHOW_CHAT, 69 self.SHOW_AWAY, 70 self.SHOW_XA, 71 self.SHOW_DND, 72 self.SHOW_OFFLINE 73 ) = range(6) 74 75 ( 76 self.TYPE_AIM, 77 self.TYPE_GG, 78 self.TYPE_HTTP_WS, 79 self.TYPE_ICQ, 80 self.TYPE_MSN, 81 self.TYPE_QQ, 82 self.TYPE_SMS, 83 self.TYPE_SMTP, 84 self.TYPE_TLEN, 85 self.TYPE_YAHOO, 86 self.TYPE_NEWMAIL, 87 self.TYPE_RSS, 88 self.TYPE_WEATHER, 89 self.TYPE_MRIM, 90 ) = range(14) 91 92 ( 93 self.SUBSCRIPTION_NONE, 94 self.SUBSCRIPTION_TO, 95 self.SUBSCRIPTION_FROM, 96 self.SUBSCRIPTION_BOTH, 97 ) = range(4)
98 99 constants = Constants() 100
101 -class Logger:
102 - def __init__(self):
103 self.jids_already_in = [] # holds jids that we already have in DB 104 self.con = None 105 106 if not os.path.exists(LOG_DB_PATH): 107 # this can happen only the first time (the time we create the db) 108 # db is not created here but in src/common/checks_paths.py 109 return 110 self.init_vars() 111 if not os.path.exists(CACHE_DB_PATH): 112 # this can happen cache database is not present when gajim is launched 113 # db will be created in src/common/checks_paths.py 114 return 115 self.attach_cache_database()
116
117 - def close_db(self):
118 if self.con: 119 self.con.close() 120 self.con = None 121 self.cur = None
122
123 - def open_db(self):
124 self.close_db() 125 126 # FIXME: sqlite3_open wants UTF8 strings. So a path with 127 # non-ascii chars doesn't work. See #2812 and 128 # http://lists.initd.org/pipermail/pysqlite/2005-August/000134.html 129 back = os.getcwd() 130 os.chdir(LOG_DB_FOLDER) 131 132 # if locked, wait up to 20 sec to unlock 133 # before raise (hopefully should be enough) 134 135 self.con = sqlite.connect(LOG_DB_FILE, timeout=20.0, 136 isolation_level='IMMEDIATE') 137 os.chdir(back) 138 self.cur = self.con.cursor() 139 self.set_synchronous(False)
140
141 - def attach_cache_database(self):
142 try: 143 self.cur.execute("ATTACH DATABASE '%s' AS cache" % CACHE_DB_PATH) 144 except sqlite.Error, e: 145 gajim.log.debug("Failed to attach cache database: %s" % str(e))
146
147 - def set_synchronous(self, sync):
148 try: 149 if sync: 150 self.cur.execute("PRAGMA synchronous = NORMAL") 151 else: 152 self.cur.execute("PRAGMA synchronous = OFF") 153 except sqlite.Error, e: 154 gajim.log.debug("Failed to set_synchronous(%s): %s" % (sync, str(e)))
155
156 - def init_vars(self):
157 self.open_db() 158 self.get_jids_already_in_db()
159
160 - def simple_commit(self, sql_to_commit):
161 """ 162 Helper to commit 163 """ 164 self.cur.execute(sql_to_commit) 165 try: 166 self.con.commit() 167 except sqlite.OperationalError, e: 168 print >> sys.stderr, str(e)
169
170 - def get_jids_already_in_db(self):
171 try: 172 self.cur.execute('SELECT jid FROM jids') 173 # list of tupples: [(u'aaa@bbb',), (u'cc@dd',)] 174 rows = self.cur.fetchall() 175 except sqlite.DatabaseError: 176 raise exceptions.DatabaseMalformed 177 self.jids_already_in = [] 178 for row in rows: 179 # row[0] is first item of row (the only result here, the jid) 180 if row[0] == '': 181 # malformed jid, ignore line 182 pass 183 else: 184 self.jids_already_in.append(row[0])
185
186 - def get_jids_in_db(self):
187 return self.jids_already_in
188
189 - def jid_is_from_pm(self, jid):
190 """ 191 If jid is gajim@conf/nkour it's likely a pm one, how we know gajim@conf 192 is not a normal guy and nkour is not his resource? we ask if gajim@conf 193 is already in jids (with type room jid) this fails if user disables 194 logging for room and only enables for pm (so higly unlikely) and if we 195 fail we do not go chaos (user will see the first pm as if it was message 196 in room's public chat) and after that all okay 197 """ 198 if jid.find('/') > -1: 199 possible_room_jid = jid.split('/', 1)[0] 200 return self.jid_is_room_jid(possible_room_jid) 201 else: 202 # it's not a full jid, so it's not a pm one 203 return False
204
205 - def jid_is_room_jid(self, jid):
206 self.cur.execute('SELECT jid_id FROM jids WHERE jid=? AND type=?', 207 (jid, constants.JID_ROOM_TYPE)) 208 row = self.cur.fetchone() 209 if row is None: 210 return False 211 else: 212 return True
213
214 - def get_jid_id(self, jid, typestr=None):
215 """ 216 jids table has jid and jid_id logs table has log_id, jid_id, 217 contact_name, time, kind, show, message so to ask logs we need jid_id 218 that matches our jid in jids table this method wants jid and returns the 219 jid_id for later sql-ing on logs typestr can be 'ROOM' or anything else 220 depending on the type of JID and is only needed to be specified when the 221 JID is new in DB 222 """ 223 if jid.find('/') != -1: # if it has a / 224 jid_is_from_pm = self.jid_is_from_pm(jid) 225 if not jid_is_from_pm: # it's normal jid with resource 226 jid = jid.split('/', 1)[0] # remove the resource 227 if jid in self.jids_already_in: # we already have jids in DB 228 self.cur.execute('SELECT jid_id FROM jids WHERE jid=?', [jid]) 229 row = self.cur.fetchone() 230 if row: 231 return row[0] 232 # oh! a new jid :), we add it now 233 if typestr == 'ROOM': 234 typ = constants.JID_ROOM_TYPE 235 else: 236 typ = constants.JID_NORMAL_TYPE 237 try: 238 self.cur.execute('INSERT INTO jids (jid, type) VALUES (?, ?)', (jid, 239 typ)) 240 self.con.commit() 241 except sqlite.IntegrityError, e: 242 # Jid already in DB, maybe added by another instance. re-read DB 243 self.get_jids_already_in_db() 244 return self.get_jid_id(jid, typestr) 245 except sqlite.OperationalError, e: 246 raise exceptions.PysqliteOperationalError(str(e)) 247 jid_id = self.cur.lastrowid 248 self.jids_already_in.append(jid) 249 return jid_id
250
251 - def convert_human_values_to_db_api_values(self, kind, show):
252 """ 253 Convert from string style to constant ints for db 254 """ 255 if kind == 'status': 256 kind_col = constants.KIND_STATUS 257 elif kind == 'gcstatus': 258 kind_col = constants.KIND_GCSTATUS 259 elif kind == 'gc_msg': 260 kind_col = constants.KIND_GC_MSG 261 elif kind == 'single_msg_recv': 262 kind_col = constants.KIND_SINGLE_MSG_RECV 263 elif kind == 'single_msg_sent': 264 kind_col = constants.KIND_SINGLE_MSG_SENT 265 elif kind == 'chat_msg_recv': 266 kind_col = constants.KIND_CHAT_MSG_RECV 267 elif kind == 'chat_msg_sent': 268 kind_col = constants.KIND_CHAT_MSG_SENT 269 elif kind == 'error': 270 kind_col = constants.KIND_ERROR 271 272 if show == 'online': 273 show_col = constants.SHOW_ONLINE 274 elif show == 'chat': 275 show_col = constants.SHOW_CHAT 276 elif show == 'away': 277 show_col = constants.SHOW_AWAY 278 elif show == 'xa': 279 show_col = constants.SHOW_XA 280 elif show == 'dnd': 281 show_col = constants.SHOW_DND 282 elif show == 'offline': 283 show_col = constants.SHOW_OFFLINE 284 elif show is None: 285 show_col = None 286 else: # invisible in GC when someone goes invisible 287 # it's a RFC violation .... but we should not crash 288 show_col = 'UNKNOWN' 289 290 return kind_col, show_col
291
293 """ 294 Convert from string style to constant ints for db 295 """ 296 if type_ == 'aim': 297 return constants.TYPE_AIM 298 if type_ == 'gadu-gadu': 299 return constants.TYPE_GG 300 if type_ == 'http-ws': 301 return constants.TYPE_HTTP_WS 302 if type_ == 'icq': 303 return constants.TYPE_ICQ 304 if type_ == 'msn': 305 return constants.TYPE_MSN 306 if type_ == 'qq': 307 return constants.TYPE_QQ 308 if type_ == 'sms': 309 return constants.TYPE_SMS 310 if type_ == 'smtp': 311 return constants.TYPE_SMTP 312 if type_ in ('tlen', 'x-tlen'): 313 return constants.TYPE_TLEN 314 if type_ == 'yahoo': 315 return constants.TYPE_YAHOO 316 if type_ == 'newmail': 317 return constants.TYPE_NEWMAIL 318 if type_ == 'rss': 319 return constants.TYPE_RSS 320 if type_ == 'weather': 321 return constants.TYPE_WEATHER 322 if type_ == 'mrim': 323 return constants.TYPE_MRIM 324 return None
325
327 """ 328 Convert from constant ints for db to string style 329 """ 330 if type_id == constants.TYPE_AIM: 331 return 'aim' 332 if type_id == constants.TYPE_GG: 333 return 'gadu-gadu' 334 if type_id == constants.TYPE_HTTP_WS: 335 return 'http-ws' 336 if type_id == constants.TYPE_ICQ: 337 return 'icq' 338 if type_id == constants.TYPE_MSN: 339 return 'msn' 340 if type_id == constants.TYPE_QQ: 341 return 'qq' 342 if type_id == constants.TYPE_SMS: 343 return 'sms' 344 if type_id == constants.TYPE_SMTP: 345 return 'smtp' 346 if type_id == constants.TYPE_TLEN: 347 return 'tlen' 348 if type_id == constants.TYPE_YAHOO: 349 return 'yahoo' 350 if type_id == constants.TYPE_NEWMAIL: 351 return 'newmail' 352 if type_id == constants.TYPE_RSS: 353 return 'rss' 354 if type_id == constants.TYPE_WEATHER: 355 return 'weather' 356 if type_id == constants.TYPE_MRIM: 357 return 'mrim'
358
360 """ 361 Convert from string style to constant ints for db 362 """ 363 if sub == 'none': 364 return constants.SUBSCRIPTION_NONE 365 if sub == 'to': 366 return constants.SUBSCRIPTION_TO 367 if sub == 'from': 368 return constants.SUBSCRIPTION_FROM 369 if sub == 'both': 370 return constants.SUBSCRIPTION_BOTH
371
373 """ 374 Convert from constant ints for db to string style 375 """ 376 if sub == constants.SUBSCRIPTION_NONE: 377 return 'none' 378 if sub == constants.SUBSCRIPTION_TO: 379 return 'to' 380 if sub == constants.SUBSCRIPTION_FROM: 381 return 'from' 382 if sub == constants.SUBSCRIPTION_BOTH: 383 return 'both'
384
385 - def commit_to_db(self, values, write_unread=False):
386 sql = '''INSERT INTO logs (jid_id, contact_name, time, kind, show, 387 message, subject) VALUES (?, ?, ?, ?, ?, ?, ?)''' 388 try: 389 self.cur.execute(sql, values) 390 except sqlite.DatabaseError: 391 raise exceptions.DatabaseMalformed 392 except sqlite.OperationalError, e: 393 raise exceptions.PysqliteOperationalError(str(e)) 394 message_id = None 395 try: 396 self.con.commit() 397 if write_unread: 398 message_id = self.cur.lastrowid 399 except sqlite.OperationalError, e: 400 print >> sys.stderr, str(e) 401 if message_id: 402 self.insert_unread_events(message_id, values[0]) 403 return message_id
404
405 - def insert_unread_events(self, message_id, jid_id):
406 """ 407 Add unread message with id: message_id 408 """ 409 sql = 'INSERT INTO unread_messages VALUES (%d, %d, 0)' % (message_id, 410 jid_id) 411 self.simple_commit(sql)
412
413 - def set_read_messages(self, message_ids):
414 """ 415 Mark all messages with ids in message_ids as read 416 """ 417 ids = ','.join([str(i) for i in message_ids]) 418 sql = 'DELETE FROM unread_messages WHERE message_id IN (%s)' % ids 419 self.simple_commit(sql)
420
421 - def set_shown_unread_msgs(self, msg_id):
422 """ 423 Mark unread message as shown un GUI 424 """ 425 sql = 'UPDATE unread_messages SET shown = 1 where message_id = %s' % \ 426 msg_id 427 self.simple_commit(sql)
428
430 """ 431 Set shown field to False in unread_messages table 432 """ 433 sql = 'UPDATE unread_messages SET shown = 0' 434 self.simple_commit(sql)
435
436 - def get_unread_msgs(self):
437 """ 438 Get all unread messages 439 """ 440 all_messages = [] 441 try: 442 self.cur.execute( 443 'SELECT message_id, shown from unread_messages') 444 results = self.cur.fetchall() 445 except Exception: 446 pass 447 for message in results: 448 msg_id = message[0] 449 shown = message[1] 450 # here we get infos for that message, and related jid from jids table 451 # do NOT change order of SELECTed things, unless you change function(s) 452 # that called this function 453 self.cur.execute(''' 454 SELECT logs.log_line_id, logs.message, logs.time, logs.subject, 455 jids.jid 456 FROM logs, jids 457 WHERE logs.log_line_id = %d AND logs.jid_id = jids.jid_id 458 ''' % msg_id 459 ) 460 results = self.cur.fetchall() 461 if len(results) == 0: 462 # Log line is no more in logs table. remove it from unread_messages 463 self.set_read_messages([msg_id]) 464 continue 465 all_messages.append(results[0] + (shown,)) 466 return all_messages
467
468 - def write(self, kind, jid, message=None, show=None, tim=None, subject=None):
469 """ 470 Write a row (status, gcstatus, message etc) to logs database 471 472 kind can be status, gcstatus, gc_msg, (we only recv for those 3), 473 single_msg_recv, chat_msg_recv, chat_msg_sent, single_msg_sent we cannot 474 know if it is pm or normal chat message, we try to guess see 475 jid_is_from_pm() 476 477 We analyze jid and store it as follows: 478 jids.jid text column will hold JID if TC-related, room_jid if GC-related, 479 ROOM_JID/nick if pm-related. 480 """ 481 482 if self.jids_already_in == []: # only happens if we just created the db 483 self.open_db() 484 485 contact_name_col = None # holds nickname for kinds gcstatus, gc_msg 486 # message holds the message unless kind is status or gcstatus, 487 # then it holds status message 488 message_col = message 489 subject_col = subject 490 if tim: 491 time_col = int(float(time.mktime(tim))) 492 else: 493 time_col = int(float(time.time())) 494 495 kind_col, show_col = self.convert_human_values_to_db_api_values(kind, 496 show) 497 498 write_unread = False 499 500 # now we may have need to do extra care for some values in columns 501 if kind == 'status': # we store (not None) time, jid, show, msg 502 # status for roster items 503 try: 504 jid_id = self.get_jid_id(jid) 505 except exceptions.PysqliteOperationalError, e: 506 raise exceptions.PysqliteOperationalError(str(e)) 507 if show is None: # show is None (xmpp), but we say that 'online' 508 show_col = constants.SHOW_ONLINE 509 510 elif kind == 'gcstatus': 511 # status in ROOM (for pm status see status) 512 if show is None: # show is None (xmpp), but we say that 'online' 513 show_col = constants.SHOW_ONLINE 514 jid, nick = jid.split('/', 1) 515 try: 516 # re-get jid_id for the new jid 517 jid_id = self.get_jid_id(jid, 'ROOM') 518 except exceptions.PysqliteOperationalError, e: 519 raise exceptions.PysqliteOperationalError(str(e)) 520 contact_name_col = nick 521 522 elif kind == 'gc_msg': 523 if jid.find('/') != -1: # if it has a / 524 jid, nick = jid.split('/', 1) 525 else: 526 # it's server message f.e. error message 527 # when user tries to ban someone but he's not allowed to 528 nick = None 529 try: 530 # re-get jid_id for the new jid 531 jid_id = self.get_jid_id(jid, 'ROOM') 532 except exceptions.PysqliteOperationalError, e: 533 raise exceptions.PysqliteOperationalError(str(e)) 534 contact_name_col = nick 535 else: 536 try: 537 jid_id = self.get_jid_id(jid) 538 except exceptions.PysqliteOperationalError, e: 539 raise exceptions.PysqliteOperationalError(str(e)) 540 if kind == 'chat_msg_recv': 541 if not self.jid_is_from_pm(jid): 542 # Save in unread table only if it's not a pm 543 write_unread = True 544 545 if show_col == 'UNKNOWN': # unknown show, do not log 546 return 547 548 values = (jid_id, contact_name_col, time_col, kind_col, show_col, 549 message_col, subject_col) 550 return self.commit_to_db(values, write_unread)
551
552 - def get_last_conversation_lines(self, jid, restore_how_many_rows, 553 pending_how_many, timeout, account):
554 """ 555 Accept how many rows to restore and when to time them out (in minutes) 556 (mark them as too old) and number of messages that are in queue and are 557 already logged but pending to be viewed, returns a list of tupples 558 containg time, kind, message, list with empty tupple if nothing found to 559 meet our demands 560 """ 561 try: 562 self.get_jid_id(jid) 563 except exceptions.PysqliteOperationalError, e: 564 # Error trying to create a new jid_id. This means there is no log 565 return [] 566 where_sql = self._build_contact_where(account, jid) 567 568 now = int(float(time.time())) 569 timed_out = now - (timeout * 60) # before that they are too old 570 # so if we ask last 5 lines and we have 2 pending we get 571 # 3 - 8 (we avoid the last 2 lines but we still return 5 asked) 572 try: 573 self.cur.execute(''' 574 SELECT time, kind, message FROM logs 575 WHERE (%s) AND kind IN (%d, %d, %d, %d, %d) AND time > %d 576 ORDER BY time DESC LIMIT %d OFFSET %d 577 ''' % (where_sql, constants.KIND_SINGLE_MSG_RECV, 578 constants.KIND_CHAT_MSG_RECV, constants.KIND_SINGLE_MSG_SENT, 579 constants.KIND_CHAT_MSG_SENT, constants.KIND_ERROR, 580 timed_out, restore_how_many_rows, pending_how_many) 581 ) 582 583 results = self.cur.fetchall() 584 except sqlite.DatabaseError: 585 raise exceptions.DatabaseMalformed 586 results.reverse() 587 return results
588
589 - def get_unix_time_from_date(self, year, month, day):
590 # year (fe 2005), month (fe 11), day (fe 25) 591 # returns time in seconds for the second that starts that date since epoch 592 # gimme unixtime from year month day: 593 d = datetime.date(year, month, day) 594 local_time = d.timetuple() # time tupple (compat with time.localtime()) 595 # we have time since epoch baby :) 596 start_of_day = int(time.mktime(local_time)) 597 return start_of_day
598
599 - def get_conversation_for_date(self, jid, year, month, day, account):
600 """ 601 Return contact_name, time, kind, show, message, subject 602 603 For each row in a list of tupples, returns list with empty tupple if we 604 found nothing to meet our demands 605 """ 606 try: 607 self.get_jid_id(jid) 608 except exceptions.PysqliteOperationalError, e: 609 # Error trying to create a new jid_id. This means there is no log 610 return [] 611 where_sql = self._build_contact_where(account, jid) 612 613 start_of_day = self.get_unix_time_from_date(year, month, day) 614 seconds_in_a_day = 86400 # 60 * 60 * 24 615 last_second_of_day = start_of_day + seconds_in_a_day - 1 616 617 self.cur.execute(''' 618 SELECT contact_name, time, kind, show, message, subject FROM logs 619 WHERE (%s) 620 AND time BETWEEN %d AND %d 621 ORDER BY time 622 ''' % (where_sql, start_of_day, last_second_of_day)) 623 624 results = self.cur.fetchall() 625 return results
626
627 - def get_search_results_for_query(self, jid, query, account):
628 """ 629 Returns contact_name, time, kind, show, message 630 631 For each row in a list of tupples, returns list with empty tupple if we 632 found nothing to meet our demands 633 """ 634 try: 635 self.get_jid_id(jid) 636 except exceptions.PysqliteOperationalError, e: 637 # Error trying to create a new jid_id. This means there is no log 638 return [] 639 640 if False: # query.startswith('SELECT '): # it's SQL query (FIXME) 641 try: 642 self.cur.execute(query) 643 except sqlite.OperationalError, e: 644 results = [('', '', '', '', str(e))] 645 return results 646 647 else: # user just typed something, we search in message column 648 where_sql = self._build_contact_where(account, jid) 649 like_sql = '%' + query.replace("'", "''") + '%' 650 self.cur.execute(''' 651 SELECT contact_name, time, kind, show, message, subject FROM logs 652 WHERE (%s) AND message LIKE '%s' 653 ORDER BY time 654 ''' % (where_sql, like_sql)) 655 656 results = self.cur.fetchall() 657 return results
658
659 - def get_days_with_logs(self, jid, year, month, max_day, account):
660 """ 661 Return the list of days that have logs (not status messages) 662 """ 663 try: 664 self.get_jid_id(jid) 665 except exceptions.PysqliteOperationalError, e: 666 # Error trying to create a new jid_id. This means there is no log 667 return [] 668 days_with_logs = [] 669 where_sql = self._build_contact_where(account, jid) 670 671 # First select all date of month whith logs we want 672 start_of_month = self.get_unix_time_from_date(year, month, 1) 673 seconds_in_a_day = 86400 # 60 * 60 * 24 674 last_second_of_month = start_of_month + (seconds_in_a_day * max_day) - 1 675 676 # Select times and 'floor' them to time 0:00 677 # (by dividing, they are integers) 678 # and take only one of the same values (distinct) 679 # Now we have timestamps of time 0:00 of every day with logs 680 self.cur.execute(''' 681 SELECT DISTINCT time/(86400)*86400 FROM logs 682 WHERE (%s) 683 AND time BETWEEN %d AND %d 684 AND kind NOT IN (%d, %d) 685 ORDER BY time 686 ''' % (where_sql, start_of_month, last_second_of_month, 687 constants.KIND_STATUS, constants.KIND_GCSTATUS)) 688 result = self.cur.fetchall() 689 690 # convert timestamps to day of month 691 for line in result: 692 days_with_logs[0:0]=[time.gmtime(line[0])[2]] 693 694 return days_with_logs
695
696 - def get_last_date_that_has_logs(self, jid, account=None, is_room=False):
697 """ 698 Return last time (in seconds since EPOCH) for which we had logs 699 (excluding statuses) 700 """ 701 where_sql = '' 702 if not is_room: 703 where_sql = self._build_contact_where(account, jid) 704 else: 705 try: 706 jid_id = self.get_jid_id(jid, 'ROOM') 707 except exceptions.PysqliteOperationalError, e: 708 # Error trying to create a new jid_id. This means there is no log 709 return None 710 where_sql = 'jid_id = %s' % jid_id 711 self.cur.execute(''' 712 SELECT MAX(time) FROM logs 713 WHERE (%s) 714 AND kind NOT IN (%d, %d) 715 ''' % (where_sql, constants.KIND_STATUS, constants.KIND_GCSTATUS)) 716 717 results = self.cur.fetchone() 718 if results is not None: 719 result = results[0] 720 else: 721 result = None 722 return result
723
724 - def get_room_last_message_time(self, jid):
725 """ 726 Return FASTLY last time (in seconds since EPOCH) for which we had logs 727 for that room from rooms_last_message_time table 728 """ 729 try: 730 jid_id = self.get_jid_id(jid, 'ROOM') 731 except exceptions.PysqliteOperationalError, e: 732 # Error trying to create a new jid_id. This means there is no log 733 return None 734 where_sql = 'jid_id = %s' % jid_id 735 self.cur.execute(''' 736 SELECT time FROM rooms_last_message_time 737 WHERE (%s) 738 ''' % (where_sql)) 739 740 results = self.cur.fetchone() 741 if results is not None: 742 result = results[0] 743 else: 744 result = None 745 return result
746
747 - def set_room_last_message_time(self, jid, time):
748 """ 749 Set last time (in seconds since EPOCH) for which we had logs for that 750 room in rooms_last_message_time table 751 """ 752 jid_id = self.get_jid_id(jid, 'ROOM') 753 # jid_id is unique in this table, create or update : 754 sql = 'REPLACE INTO rooms_last_message_time VALUES (%d, %d)' % \ 755 (jid_id, time) 756 self.simple_commit(sql)
757
758 - def _build_contact_where(self, account, jid):
759 """ 760 Build the where clause for a jid, including metacontacts jid(s) if any 761 """ 762 where_sql = '' 763 # will return empty list if jid is not associated with 764 # any metacontacts 765 family = gajim.contacts.get_metacontacts_family(account, jid) 766 if family: 767 for user in family: 768 try: 769 jid_id = self.get_jid_id(user['jid']) 770 except exceptions.PysqliteOperationalError, e: 771 continue 772 where_sql += 'jid_id = %s' % jid_id 773 if user != family[-1]: 774 where_sql += ' OR ' 775 else: # if jid was not associated with metacontacts 776 jid_id = self.get_jid_id(jid) 777 where_sql = 'jid_id = %s' % jid_id 778 return where_sql
779
780 - def save_transport_type(self, jid, type_):
781 """ 782 Save the type of the transport in DB 783 """ 784 type_id = self.convert_human_transport_type_to_db_api_values(type_) 785 if not type_id: 786 # unknown type 787 return 788 self.cur.execute( 789 'SELECT type from transports_cache WHERE transport = "%s"' % jid) 790 results = self.cur.fetchall() 791 if results: 792 result = results[0][0] 793 if result == type_id: 794 return 795 sql = 'UPDATE transports_cache SET type = %d WHERE transport = "%s"' %\ 796 (type_id, jid) 797 self.simple_commit(sql) 798 return 799 sql = 'INSERT INTO transports_cache VALUES ("%s", %d)' % (jid, type_id) 800 self.simple_commit(sql)
801
802 - def get_transports_type(self):
803 """ 804 Return all the type of the transports in DB 805 """ 806 self.cur.execute( 807 'SELECT * from transports_cache') 808 results = self.cur.fetchall() 809 if not results: 810 return {} 811 answer = {} 812 for result in results: 813 answer[result[0]] = self.convert_api_values_to_human_transport_type( 814 result[1]) 815 return answer
816 817 # A longer note here: 818 # The database contains a blob field. Pysqlite seems to need special care for 819 # such fields. 820 # When storing, we need to convert string into buffer object (1). 821 # When retrieving, we need to convert it back to a string to decompress it. 822 # (2) 823 # GzipFile needs a file-like object, StringIO emulates file for plain strings
824 - def iter_caps_data(self):
825 """ 826 Iterate over caps cache data stored in the database 827 828 The iterator values are pairs of (node, ver, ext, identities, features): 829 identities == {'category':'foo', 'type':'bar', 'name':'boo'}, 830 features being a list of feature namespaces. 831 """ 832 # get data from table 833 # the data field contains binary object (gzipped data), this is a hack 834 # to get that data without trying to convert it to unicode 835 try: 836 self.cur.execute('SELECT hash_method, hash, data FROM caps_cache;') 837 except sqlite.OperationalError: 838 # might happen when there's no caps_cache table yet 839 # -- there's no data to read anyway then 840 return 841 842 # list of corrupted entries that will be removed 843 to_be_removed = [] 844 for hash_method, hash_, data in self.cur: 845 # for each row: unpack the data field 846 # (format: (category, type, name, category, type, name, ... 847 # ..., 'FEAT', feature1, feature2, ...).join(' ')) 848 # NOTE: if there's a need to do more gzip, put that to a function 849 try: 850 data = GzipFile(fileobj=StringIO(str(data))).read().decode( 851 'utf-8').split('\0') 852 except IOError: 853 # This data is corrupted. It probably contains non-ascii chars 854 to_be_removed.append((hash_method, hash_)) 855 continue 856 i = 0 857 identities = list() 858 features = list() 859 while i < (len(data) - 3) and data[i] != 'FEAT': 860 category = data[i] 861 type_ = data[i + 1] 862 lang = data[i + 2] 863 name = data[i + 3] 864 identities.append({'category': category, 'type': type_, 865 'xml:lang': lang, 'name': name}) 866 i += 4 867 i+=1 868 while i < len(data): 869 features.append(data[i]) 870 i += 1 871 872 # yield the row 873 yield hash_method, hash_, identities, features 874 for hash_method, hash_ in to_be_removed: 875 sql = '''DELETE FROM caps_cache WHERE hash_method = "%s" AND 876 hash = "%s"''' % (hash_method, hash_) 877 self.simple_commit(sql)
878
879 - def add_caps_entry(self, hash_method, hash_, identities, features):
880 data = [] 881 for identity in identities: 882 # there is no FEAT category 883 if identity['category'] == 'FEAT': 884 return 885 data.extend((identity.get('category'), identity.get('type', ''), 886 identity.get('xml:lang', ''), identity.get('name', ''))) 887 data.append('FEAT') 888 data.extend(features) 889 data = '\0'.join(data) 890 # if there's a need to do more gzip, put that to a function 891 string = StringIO() 892 gzip = GzipFile(fileobj=string, mode='w') 893 data = data.encode('utf-8') # the gzip module can't handle unicode objects 894 gzip.write(data) 895 gzip.close() 896 data = string.getvalue() 897 self.cur.execute(''' 898 INSERT INTO caps_cache ( hash_method, hash, data, last_seen ) 899 VALUES (?, ?, ?, ?); 900 ''', (hash_method, hash_, buffer(data), int(time.time()))) 901 # (1) -- note above 902 try: 903 self.con.commit() 904 except sqlite.OperationalError, e: 905 print >> sys.stderr, str(e)
906
907 - def update_caps_time(self, method, hash_):
908 sql = '''UPDATE caps_cache SET last_seen = %d 909 WHERE hash_method = "%s" and hash = "%s"''' % \ 910 (int(time.time()), method, hash_) 911 self.simple_commit(sql)
912
913 - def clean_caps_table(self):
914 """ 915 Remove caps which was not seen for 3 months 916 """ 917 sql = '''DELETE FROM caps_cache WHERE last_seen < %d''' % \ 918 int(time.time() - 3*30*24*3600) 919 self.simple_commit(sql)
920
921 - def replace_roster(self, account_name, roster_version, roster):
922 """ 923 Replace current roster in DB by a new one 924 925 accout_name is the name of the account to change. 926 roster_version is the version of the new roster. 927 roster is the new version. 928 """ 929 # First we must reset roster_version value to ensure that the server 930 # sends back all the roster at the next connexion if the replacement 931 # didn't work properly. 932 gajim.config.set_per('accounts', account_name, 'roster_version', '') 933 934 account_jid = gajim.get_jid_from_account(account_name) 935 account_jid_id = self.get_jid_id(account_jid) 936 937 # Delete old roster 938 self.remove_roster(account_jid) 939 940 # Fill roster tables with the new roster 941 for jid in roster: 942 self.add_or_update_contact(account_jid, jid, roster[jid]['name'], 943 roster[jid]['subscription'], roster[jid]['ask'], 944 roster[jid]['groups'], commit=False) 945 self.con.commit() 946 947 # At this point, we are sure the replacement works properly so we can 948 # set the new roster_version value. 949 gajim.config.set_per('accounts', account_name, 'roster_version', 950 roster_version)
951
952 - def del_contact(self, account_jid, jid):
953 """ 954 Remove jid from account_jid roster 955 """ 956 try: 957 account_jid_id = self.get_jid_id(account_jid) 958 jid_id = self.get_jid_id(jid) 959 except exceptions.PysqliteOperationalError, e: 960 raise exceptions.PysqliteOperationalError(str(e)) 961 self.cur.execute( 962 'DELETE FROM roster_group WHERE account_jid_id=? AND jid_id=?', 963 (account_jid_id, jid_id)) 964 self.cur.execute( 965 'DELETE FROM roster_entry WHERE account_jid_id=? AND jid_id=?', 966 (account_jid_id, jid_id)) 967 self.con.commit()
968
969 - def add_or_update_contact(self, account_jid, jid, name, sub, ask, groups, 970 commit=True):
971 """ 972 Add or update a contact from account_jid roster 973 """ 974 if sub == 'remove': 975 self.del_contact(account_jid, jid) 976 return 977 978 try: 979 account_jid_id = self.get_jid_id(account_jid) 980 jid_id = self.get_jid_id(jid) 981 except exceptions.PysqliteOperationalError, e: 982 raise exceptions.PysqliteOperationalError(str(e)) 983 984 # Update groups information 985 # First we delete all previous groups information 986 self.cur.execute( 987 'DELETE FROM roster_group WHERE account_jid_id=? AND jid_id=?', 988 (account_jid_id, jid_id)) 989 # Then we add all new groups information 990 for group in groups: 991 self.cur.execute('INSERT INTO roster_group VALUES(?, ?, ?)', 992 (account_jid_id, jid_id, group)) 993 994 if name is None: 995 name = '' 996 997 self.cur.execute('REPLACE INTO roster_entry VALUES(?, ?, ?, ?, ?)', 998 (account_jid_id, jid_id, name, 999 self.convert_human_subscription_values_to_db_api_values(sub), 1000 bool(ask))) 1001 if commit: 1002 self.con.commit()
1003
1004 - def get_roster(self, account_jid):
1005 """ 1006 Return the accound_jid roster in NonBlockingRoster format 1007 """ 1008 data = {} 1009 account_jid_id = self.get_jid_id(account_jid) 1010 1011 # First we fill data with roster_entry informations 1012 self.cur.execute(''' 1013 SELECT j.jid, re.jid_id, re.name, re.subscription, re.ask 1014 FROM roster_entry re, jids j 1015 WHERE re.account_jid_id=? AND j.jid_id=re.jid_id''', (account_jid_id,)) 1016 for jid, jid_id, name, subscription, ask in self.cur: 1017 data[jid] = {} 1018 if name: 1019 data[jid]['name'] = name 1020 else: 1021 data[jid]['name'] = None 1022 data[jid]['subscription'] = \ 1023 self.convert_db_api_values_to_human_subscription_values( 1024 subscription) 1025 data[jid]['groups'] = [] 1026 data[jid]['resources'] = {} 1027 if ask: 1028 data[jid]['ask'] = 'subscribe' 1029 else: 1030 data[jid]['ask'] = None 1031 data[jid]['id'] = jid_id 1032 1033 # Then we add group for roster entries 1034 for jid in data: 1035 self.cur.execute(''' 1036 SELECT group_name FROM roster_group 1037 WHERE account_jid_id=? AND jid_id=?''', 1038 (account_jid_id, data[jid]['id'])) 1039 for (group_name,) in self.cur: 1040 data[jid]['groups'].append(group_name) 1041 del data[jid]['id'] 1042 1043 return data
1044
1045 - def remove_roster(self, account_jid):
1046 """ 1047 Remove all entry from account_jid roster 1048 """ 1049 account_jid_id = self.get_jid_id(account_jid) 1050 1051 self.cur.execute('DELETE FROM roster_entry WHERE account_jid_id=?', 1052 (account_jid_id,)) 1053 self.cur.execute('DELETE FROM roster_group WHERE account_jid_id=?', 1054 (account_jid_id,)) 1055 self.con.commit()
1056