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

Revision 8640, 19.6 kB (checked in by roidelapluie, 15 months ago)

0.11.2: [michael] fix bug when searching text containing a ' in history
window. fixes #3091

  • 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                        row = self.cur.fetchone()
173                        if row:
174                                return row[0]
175                # oh! a new jid :), we add it now
176                if typestr == 'ROOM':
177                        typ = constants.JID_ROOM_TYPE
178                else:
179                        typ = constants.JID_NORMAL_TYPE
180                try:
181                        self.cur.execute('INSERT INTO jids (jid, type) VALUES (?, ?)', (jid,
182                                typ))
183                except sqlite.IntegrityError, e:
184                        # Jid already in DB, maybe added by another instance. re-read DB
185                        self.get_jids_already_in_db()
186                        return self.get_jid_id(jid, typestr)
187                try:
188                        self.con.commit()
189                except sqlite.OperationalError, e:
190                        print >> sys.stderr, str(e)
191                jid_id = self.cur.lastrowid
192                self.jids_already_in.append(jid)
193                return jid_id
194       
195        def convert_human_values_to_db_api_values(self, kind, show):
196                '''coverts from string style to constant ints for db'''
197                if kind == 'status':
198                        kind_col = constants.KIND_STATUS
199                elif kind == 'gcstatus':
200                        kind_col = constants.KIND_GCSTATUS
201                elif kind == 'gc_msg':
202                        kind_col = constants.KIND_GC_MSG
203                elif kind == 'single_msg_recv':
204                        kind_col = constants.KIND_SINGLE_MSG_RECV
205                elif kind == 'single_msg_sent':
206                        kind_col = constants.KIND_SINGLE_MSG_SENT
207                elif kind == 'chat_msg_recv':
208                        kind_col = constants.KIND_CHAT_MSG_RECV
209                elif kind == 'chat_msg_sent':
210                        kind_col = constants.KIND_CHAT_MSG_SENT
211                elif kind == 'error':
212                        kind_col = constants.KIND_ERROR
213
214                if show == 'online':
215                        show_col = constants.SHOW_ONLINE
216                elif show == 'chat':
217                        show_col = constants.SHOW_CHAT
218                elif show == 'away':
219                        show_col = constants.SHOW_AWAY
220                elif show == 'xa':
221                        show_col = constants.SHOW_XA
222                elif show == 'dnd':
223                        show_col = constants.SHOW_DND
224                elif show == 'offline':
225                        show_col = constants.SHOW_OFFLINE
226                elif show is None:
227                        show_col = None
228                else: # invisible in GC when someone goes invisible
229                        # it's a RFC violation .... but we should not crash
230                        show_col = 'UNKNOWN'
231               
232                return kind_col, show_col
233
234        def convert_human_transport_type_to_db_api_values(self, type_):
235                '''converts from string style to constant ints for db'''
236                if type_ == 'aim':
237                        return constants.TYPE_AIM
238                if type_ == 'gadu-gadu':
239                        return constants.TYPE_GG
240                if type_ == 'http-ws':
241                        return constants.TYPE_HTTP_WS
242                if type_ == 'icq':
243                        return constants.TYPE_ICQ
244                if type_ == 'msn':
245                        return constants.TYPE_MSN
246                if type_ == 'qq':
247                        return constants.TYPE_QQ
248                if type_ == 'sms':
249                        return constants.TYPE_SMS
250                if type_ == 'smtp':
251                        return constants.TYPE_SMTP
252                if type_ in ('tlen', 'x-tlen'):
253                        return constants.TYPE_TLEN
254                if type_ == 'yahoo':
255                        return constants.TYPE_YAHOO
256                if type_ == 'newmail':
257                        return constants.TYPE_NEWMAIL
258                if type_ == 'rss':
259                        return constants.TYPE_RSS
260                if type_ == 'weather':
261                        return constants.TYPE_WEATHER
262                return None
263
264        def convert_api_values_to_human_transport_type(self, type_id):
265                '''converts from constant ints for db to string style'''
266                if type_id == constants.TYPE_AIM:
267                        return 'aim'
268                if type_id == constants.TYPE_GG:
269                        return 'gadu-gadu'
270                if type_id == constants.TYPE_HTTP_WS:
271                        return 'http-ws'
272                if type_id == constants.TYPE_ICQ:
273                        return 'icq'
274                if type_id == constants.TYPE_MSN:
275                        return 'msn'
276                if type_id == constants.TYPE_QQ:
277                        return 'qq'
278                if type_id == constants.TYPE_SMS:
279                        return 'sms'
280                if type_id == constants.TYPE_SMTP:
281                        return 'smtp'
282                if type_id == constants.TYPE_TLEN:
283                        return 'tlen'
284                if type_id == constants.TYPE_YAHOO:
285                        return 'yahoo'
286                if type_id == constants.TYPE_NEWMAIL:
287                        return 'newmail'
288                if type_id == constants.TYPE_RSS:
289                        return 'rss'
290                if type_id == constants.TYPE_WEATHER:
291                        return 'weather'
292
293        def commit_to_db(self, values, write_unread = False):
294                #print 'saving', values
295                sql = 'INSERT INTO logs (jid_id, contact_name, time, kind, show, message, subject) VALUES (?, ?, ?, ?, ?, ?, ?)'
296                try:
297                        self.cur.execute(sql, values)
298                except sqlite.OperationalError, e:
299                        raise exceptions.PysqliteOperationalError(str(e))
300                message_id = None
301                try:
302                        self.con.commit()
303                        if write_unread:
304                                message_id = self.cur.lastrowid
305                except sqlite.OperationalError, e:
306                        print >> sys.stderr, str(e)
307                if message_id:
308                        self.insert_unread_events(message_id, values[0])
309                return message_id
310       
311        def insert_unread_events(self, message_id, jid_id):
312                ''' add unread message with id: message_id'''
313                sql = 'INSERT INTO unread_messages VALUES (%d, %d)' % (message_id, jid_id)
314                self.cur.execute(sql)
315                try:
316                        self.con.commit()
317                except sqlite.OperationalError, e:
318                        print >> sys.stderr, str(e)
319       
320        def set_read_messages(self, message_ids):
321                ''' mark all messages with ids in message_ids as read'''
322                ids = ','.join([str(i) for i in message_ids])
323                sql = 'DELETE FROM unread_messages WHERE message_id IN (%s)' % ids
324                self.cur.execute(sql)
325                try:
326                        self.con.commit()
327                except sqlite.OperationalError, e:
328                        print >> sys.stderr, str(e)
329       
330        def get_unread_msgs_for_jid(self, jid):
331                ''' get unread messages for jid '''
332                if not jid:
333                        return
334                jid = jid.lower()
335                jid_id = self.get_jid_id(jid)
336                all_messages = []
337                try:
338                        self.cur.execute(
339                                'SELECT message_id from unread_messages WHERE jid_id = %d' % jid_id)
340                        results = self.cur.fetchall()
341                except:
342                        pass
343
344                for message in results:
345                        msg_id = message[0]
346                        self.cur.execute('''
347                                SELECT log_line_id, message, time, subject FROM logs
348                                WHERE jid_id = %d AND log_line_id = %d
349                                ''' % (jid_id, msg_id)
350                                )
351                        results = self.cur.fetchall()
352                        all_messages.append(results[0])
353                return all_messages
354               
355        def write(self, kind, jid, message = None, show = None, tim = None,
356        subject = None):
357                '''write a row (status, gcstatus, message etc) to logs database
358                kind can be status, gcstatus, gc_msg, (we only recv for those 3),
359                single_msg_recv, chat_msg_recv, chat_msg_sent, single_msg_sent
360                we cannot know if it is pm or normal chat message, we try to guess
361                see jid_is_from_pm() which is called by get_jid_id()
362               
363                we analyze jid and store it as follows:
364                jids.jid text column will hold JID if TC-related, room_jid if GC-related,
365                ROOM_JID/nick if pm-related.'''
366
367                if self.jids_already_in == []: # only happens if we just created the db
368                        self.open_db()
369
370                jid = jid.lower()
371                contact_name_col = None # holds nickname for kinds gcstatus, gc_msg
372                # message holds the message unless kind is status or gcstatus,
373                # then it holds status message
374                message_col = message
375                subject_col = subject
376                if tim:
377                        time_col = int(float(time.mktime(tim)))
378                else:
379                        time_col = int(float(time.time()))
380               
381                kind_col, show_col = self.convert_human_values_to_db_api_values(kind,
382                        show)
383               
384                write_unread = False
385               
386                # now we may have need to do extra care for some values in columns
387                if kind == 'status': # we store (not None) time, jid, show, msg
388                        # status for roster items
389                        jid_id = self.get_jid_id(jid)
390                        if show is None: # show is None (xmpp), but we say that 'online'
391                                show_col = constants.SHOW_ONLINE
392
393                elif kind == 'gcstatus':
394                        # status in ROOM (for pm status see status)
395                        if show is None: # show is None (xmpp), but we say that 'online'
396                                show_col = constants.SHOW_ONLINE
397                        jid, nick = jid.split('/', 1)
398                        jid_id = self.get_jid_id(jid, 'ROOM') # re-get jid_id for the new jid
399                        contact_name_col = nick
400
401                elif kind == 'gc_msg':
402                        if jid.find('/') != -1: # if it has a /
403                                jid, nick = jid.split('/', 1)
404                        else:
405                                # it's server message f.e. error message
406                                # when user tries to ban someone but he's not allowed to
407                                nick = None
408                        jid_id = self.get_jid_id(jid, 'ROOM') # re-get jid_id for the new jid
409                        contact_name_col = nick
410                else:
411                        jid_id = self.get_jid_id(jid)
412                        if kind == 'chat_msg_recv':
413                                write_unread = True
414               
415                if show_col == 'UNKNOWN': # unknown show, do not log
416                        return
417                       
418                values = (jid_id, contact_name_col, time_col, kind_col, show_col,
419                        message_col, subject_col)
420                return self.commit_to_db(values, write_unread)
421               
422        def get_last_conversation_lines(self, jid, restore_how_many_rows,
423                pending_how_many, timeout, account):
424                '''accepts how many rows to restore and when to time them out (in minutes)
425                (mark them as too old) and number of messages that are in queue
426                and are already logged but pending to be viewed,
427                returns a list of tupples containg time, kind, message,
428                list with empty tupple if nothing found to meet our demands'''
429                jid = jid.lower()
430                jid_id = self.get_jid_id(jid)
431                where_sql = self._build_contact_where(account, jid)
432               
433                now = int(float(time.time()))
434                timed_out = now - (timeout * 60) # before that they are too old
435                # so if we ask last 5 lines and we have 2 pending we get
436                # 3 - 8 (we avoid the last 2 lines but we still return 5 asked)
437                self.cur.execute('''
438                        SELECT time, kind, message FROM logs
439                        WHERE (%s) AND kind IN (%d, %d, %d, %d, %d) AND time > %d
440                        ORDER BY time DESC LIMIT %d OFFSET %d
441                        ''' % (where_sql, constants.KIND_SINGLE_MSG_RECV,
442                                constants.KIND_CHAT_MSG_RECV, constants.KIND_SINGLE_MSG_SENT,
443                                constants.KIND_CHAT_MSG_SENT, constants.KIND_ERROR,
444                                timed_out, restore_how_many_rows, pending_how_many)
445                        )
446
447                results = self.cur.fetchall()
448                results.reverse()
449                return results
450       
451        def get_unix_time_from_date(self, year, month, day):
452                # year (fe 2005), month (fe 11), day (fe 25)
453                # returns time in seconds for the second that starts that date since epoch
454                # gimme unixtime from year month day:
455                d = datetime.date(year, month, day)
456                local_time = d.timetuple() # time tupple (compat with time.localtime())
457                start_of_day = int(time.mktime(local_time)) # we have time since epoch baby :)
458                return start_of_day
459       
460        def get_conversation_for_date(self, jid, year, month, day, account):
461                '''returns contact_name, time, kind, show, message
462                for each row in a list of tupples,
463                returns list with empty tupple if we found nothing to meet our demands'''
464                jid = jid.lower()
465                jid_id = self.get_jid_id(jid)
466                where_sql = self._build_contact_where(account, jid)
467
468                start_of_day = self.get_unix_time_from_date(year, month, day)
469                seconds_in_a_day = 86400 # 60 * 60 * 24
470                last_second_of_day = start_of_day + seconds_in_a_day - 1
471               
472                self.cur.execute('''
473                        SELECT contact_name, time, kind, show, message FROM logs
474                        WHERE (%s)
475                        AND time BETWEEN %d AND %d
476                        ORDER BY time
477                        ''' % (where_sql, start_of_day, last_second_of_day))
478               
479                results = self.cur.fetchall()
480                return results
481
482        def get_search_results_for_query(self, jid, query, account):
483                '''returns contact_name, time, kind, show, message
484                for each row in a list of tupples,
485                returns list with empty tupple if we found nothing to meet our demands'''
486                jid = jid.lower()
487                jid_id = self.get_jid_id(jid)
488
489                if False: #query.startswith('SELECT '): # it's SQL query (FIXME)
490                        try:
491                                self.cur.execute(query)
492                        except sqlite.OperationalError, e:
493                                results = [('', '', '', '', str(e))]
494                                return results
495                       
496                else: # user just typed something, we search in message column
497                        where_sql = self._build_contact_where(account, jid)
498                        like_sql = '%' + query.replace("'", "''") + '%'
499                        self.cur.execute('''
500                                SELECT contact_name, time, kind, show, message, subject FROM logs
501                                WHERE (%s) AND message LIKE '%s'
502                                ORDER BY time
503                                ''' % (where_sql, like_sql))
504
505                results = self.cur.fetchall()
506                return results
507
508        def get_days_with_logs(self, jid, year, month, max_day, account):
509                '''returns the list of days that have logs (not status messages)'''
510                jid = jid.lower()
511                jid_id = self.get_jid_id(jid)
512                days_with_logs = []
513                where_sql = self._build_contact_where(account, jid)
514               
515                # First select all date of month whith logs we want
516                start_of_month = self.get_unix_time_from_date(year, month, 1)
517                seconds_in_a_day = 86400 # 60 * 60 * 24
518                last_second_of_month = start_of_month + (seconds_in_a_day * max_day) - 1
519
520                self.cur.execute('''
521                        SELECT time FROM logs
522                        WHERE (%s)
523                        AND time BETWEEN %d AND %d
524                        AND kind NOT IN (%d, %d)
525                        ORDER BY time
526                        ''' % (where_sql, start_of_month, last_second_of_month,
527                        constants.KIND_STATUS, constants.KIND_GCSTATUS))
528                result = self.cur.fetchall()
529
530                # Copy all interesting times in a temporary table
531                self.cur.execute('CREATE TEMPORARY TABLE blabla(time,INTEGER)') 
532                for line in result: 
533                        self.cur.execute('''
534                                INSERT INTO blabla (time) VALUES (%d)
535                                ''' % (line[0])) 
536
537                # then search in this small temp table for each day
538                for day in xrange(1, max_day + 1):  # count from 1 to 28 or to 30 or to 31
539                        start_of_day = self.get_unix_time_from_date(year, month, day) 
540                        last_second_of_day = start_of_day + seconds_in_a_day - 1 
541
542                        # just ask one row to see if we have sth for this date
543                        self.cur.execute('''
544                                SELECT time FROM blabla
545                                WHERE time BETWEEN %d AND %d 
546                                LIMIT 1
547                                ''' % (start_of_day, last_second_of_day)) 
548                        result = self.cur.fetchone() 
549                        if result: 
550                                days_with_logs[0:0]=[day]
551
552                # Delete temporary table
553                self.cur.execute('DROP TABLE blabla') 
554                result = self.cur.fetchone()
555                return days_with_logs
556
557        def get_last_date_that_has_logs(self, jid, account = None, is_room = False):
558                '''returns last time (in seconds since EPOCH) for which
559                we had logs (excluding statuses)'''
560                jid = jid.lower()
561       
562                where_sql = ''
563                if not is_room:
564                        where_sql = self._build_contact_where(account, jid)
565                else:
566                        jid_id = self.get_jid_id(jid, 'ROOM')
567                        where_sql = 'jid_id = %s' % jid_id     
568                self.cur.execute('''
569                        SELECT MAX(time) FROM logs
570                        WHERE (%s)
571                        AND kind NOT IN (%d, %d)
572                        ''' % (where_sql, constants.KIND_STATUS, constants.KIND_GCSTATUS))
573
574                results = self.cur.fetchone()
575                if results is not None:
576                        result = results[0]
577                else:
578                        result = None
579
580                return result
581
582        def _build_contact_where(self, account, jid):
583                '''build the where clause for a jid, including metacontacts
584                jid(s) if any'''
585                where_sql = ''   
586                # will return empty list if jid is not associated with
587                # any metacontacts
588                family = gajim.contacts.get_metacontacts_family(account, jid)
589                if family:
590                        for user in family:
591                                jid_id = self.get_jid_id(user['jid'])
592                                where_sql += 'jid_id = %s' % jid_id
593                                if user != family[-1]:
594                                        where_sql += ' OR '
595                else: # if jid was not associated with metacontacts
596                        jid_id = self.get_jid_id(jid)
597                        where_sql = 'jid_id = %s' % jid_id
598                return where_sql
599
600        def save_transport_type(self, jid, type_):
601                '''save the type of the transport in DB'''
602                type_id = self.convert_human_transport_type_to_db_api_values(type_)
603                if not type_id:
604                        # unknown type
605                        return
606                self.cur.execute(
607                        'SELECT type from transports_cache WHERE transport = "%s"' % jid)
608                results = self.cur.fetchall()
609                if results:
610                        result = results[0][0]
611                        if result == type_id:
612                                return
613                        self.cur.execute(
614                                'UPDATE transports_cache SET type = %d WHERE transport = "%s"' % (type_id,
615                                        jid))
616                        try:
617                                self.con.commit()
618                        except sqlite.OperationalError, e:
619                                print >> sys.stderr, str(e)
620                        return
621                self.cur.execute(
622                        'INSERT INTO transports_cache VALUES ("%s", %d)' % (jid, type_id))
623                try:
624                        self.con.commit()
625                except sqlite.OperationalError, e:
626                        print >> sys.stderr, str(e)
627
628        def get_transports_type(self):
629                '''return all the type of the transports in DB'''
630                self.cur.execute(
631                        'SELECT * from transports_cache')
632                results = self.cur.fetchall()
633                if not results:
634                        return {}
635                answer = {}
636                for result in results:
637                        answer[result[0]] = self.convert_api_values_to_human_transport_type(result[1])
638                return answer
Note: See TracBrowser for help on using the browser.