Prepare for release, mention why there is still no Windows version.
[wammu.git] / Wammu / Utils.py
blobb0ed9d2100ee1aaf726d2afcb8a777265aef6297
1 # -*- coding: UTF-8 -*-
2 # vim: expandtab sw=4 ts=4 sts=4:
3 '''
4 Wammu - Phone manager
5 Misc functions like charset conversion, entries parsers,..
6 '''
7 __author__ = 'Michal Čihař'
8 __email__ = 'michal@cihar.com'
9 __license__ = '''
10 Copyright © 2003 - 2008 Michal Čihař
12 This program is free software; you can redistribute it and/or modify it
13 under the terms of the GNU General Public License version 2 as published by
14 the Free Software Foundation.
16 This program is distributed in the hope that it will be useful, but WITHOUT
17 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
18 FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
19 more details.
21 You should have received a copy of the GNU General Public License along with
22 this program; if not, write to the Free Software Foundation, Inc.,
23 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
24 '''
26 import codecs
27 import locale
28 import sys
29 import re
30 import wx
31 import string
32 import os
33 try:
34 import grp
35 HAVE_GRP = True
36 except ImportError:
37 HAVE_GRP = False
38 import Wammu.Locales
39 from Wammu.Locales import StrConv
41 import Wammu
42 if Wammu.gammu_error == None:
43 import gammu
46 def GetItemType(txt):
47 if txt == '':
48 return None
49 elif txt[-8:] == 'DATETIME' or txt == 'Date' or txt == 'LastModified' or txt == 'LAST_MODIFIED':
50 return 'datetime'
51 elif txt[-4:] == 'DATE':
52 return 'date'
53 elif txt in ['TEXT', 'DESCRIPTION', 'LOCATION', 'LUID'] or txt[:4] == 'Text':
54 return 'text'
55 elif txt == 'PHONE' or txt[:6] == 'Number':
56 return 'phone'
57 elif txt == 'CONTACTID':
58 return 'contact'
59 elif txt == 'PRIVATE' or txt == 'Private' or txt == 'COMPLETED':
60 return 'bool'
61 elif txt == 'Category' or txt == 'CATEGORY':
62 return 'category'
63 elif txt == 'PictureID' or txt == 'RingtoneID' or txt == 'RingtoneFileSystemID':
64 return 'id'
65 else:
66 return 'number'
68 def SearchLocation(lst, loc, second = None):
69 result = -1
70 for i in range(len(lst)):
71 if second != None:
72 if not lst[i][second[0]] == second[1]:
73 continue
74 if type(lst[i]['Location']) == type(loc):
75 if loc == lst[i]['Location']:
76 result = i
77 break
78 else:
79 if str(loc) in lst[i]['Location'].split(', '):
80 result = i
81 break
82 return result
84 def MatchesText(item, match, num):
85 testkeys = ['Value', 'Text', 'Number']
86 for x in item:
87 if type(item) == dict:
88 val = item[x]
89 else:
90 val = x
91 if type(val) in (str, unicode):
92 if match.search(val) != None:
93 return True
94 elif num is not None and type(val) == int and num == val:
95 return True
96 elif type(val) == list:
97 for i in range(len(val)):
98 for key in testkeys:
99 try:
100 val2 = val[i][key]
101 if type(val2) in (str, unicode):
102 if match.search(val2) != None:
103 return True
104 elif num is not None and type(val2) == int and num == val2:
105 return True
106 except KeyError:
107 # Ignore not found keys
108 pass
109 return False
112 def SearchItem(lst, item):
113 for i in range(len(lst)):
114 if item == lst[i]:
115 return i
116 return -1
118 def GrabNumberPrefix(number, prefixes):
119 l = len(number)
120 if l == 0 or number[0] != '+':
121 return None
122 i = 2
123 while not number[:i] in prefixes:
124 i += 1
125 if i > l:
126 return None
127 return number[:i]
129 '''Prefix for making international numbers'''
130 NumberPrefix = ''
132 NumberStrip = re.compile('^([#*]\d+[#*])?(\\+?.*)$')
134 def NormalizeNumber(number):
136 Attempts to create international number from anything it receives.
137 It does strip any network prefixes and attempts to properly add
138 international prefix. However this is a bit tricky, as there are
139 many ways which can break this.
141 # Strip magic prefixes (like no CLIR)
142 nbmatch = NumberStrip.match(number)
143 resnumber = nbmatch.group(2)
144 # If we stripped whole number, return original
145 if len(resnumber) == 0:
146 return number
147 # Handle 00 prefix same as +
148 if resnumber[0:2] == '00':
149 resnumber = '+' + resnumber[2:]
150 # Detect numbers with international prefix and without +
151 # This can be national number in some countries (eg. US)
152 if NumberPrefix[0] == '+' and resnumber[:len(NumberPrefix) - 1] == NumberPrefix[1:]:
153 resnumber = '+' + resnumber
154 # Add international prefix
155 if resnumber[0] != '+':
156 resnumber = NumberPrefix + resnumber
157 return resnumber
159 def SearchNumber(lst, number):
160 for i in range(len(lst)):
161 for x in lst[i]['Entries']:
162 if GetItemType(x['Type']) == 'phone' and NormalizeNumber(number) == NormalizeNumber(x['Value']):
163 return i
164 return -1
166 def GetContactLink(lst, i, txt):
167 return StrConv('<a href="memory://%s/%d">%s</a> (%s)' % (lst[i]['MemoryType'], lst[i]['Location'], lst[i]['Name'], txt))
169 def GetNumberLink(lst, number):
170 i = SearchNumber(lst, number)
171 if i == -1:
172 return StrConv(number)
173 return GetContactLink(lst, i, number)
175 def GetTypeString(type, value, values, linkphone = True):
177 Returns string for entry in data dictionary. Formats it according to
178 knowledge of format types.
180 t = GetItemType(type)
181 if t == 'contact':
182 i = SearchLocation(values['contact']['ME'], value)
183 if i == -1:
184 return '%d' % value
185 else:
186 return GetContactLink([] + values['contact']['ME'], i, str(value))
187 elif linkphone and t == 'phone':
188 return StrConv(GetNumberLink([] + values['contact']['ME'] + values['contact']['SM'], value))
189 elif t == 'id':
190 v = hex(value)
191 if v[-1] == 'L':
192 v = v[:-1]
193 return v
194 else:
195 return StrConv(value)
197 def ParseMemoryEntry(entry, config = None):
198 first = ''
199 last = ''
200 name = ''
201 nickname = ''
202 formalname = ''
203 company = ''
204 number = ''
205 number_result = ''
206 name_result = ''
207 date = None
208 for i in entry['Entries']:
209 if i['Type'] == 'Text_Name':
210 name = i['Value']
211 elif i['Type'] == 'Text_FirstName':
212 first = i['Value']
213 if i['Type'] == 'Text_LastName':
214 last = i['Value']
215 if i['Type'] == 'Text_NickName':
216 nickname = i['Value']
217 if i['Type'] == 'Text_FormalName':
218 formalname = i['Value']
219 if i['Type'] == 'Date':
220 date = i['Value']
221 if i['Type'] == 'Text_Company':
222 company = i['Value']
223 if i['Type'] == 'Number_General':
224 number_result = i['Value']
225 elif i['Type'][:7] == 'Number_':
226 number = i['Value']
228 if config is None:
229 format = 'auto'
230 else:
231 format = config.Read('/Wammu/NameFormat')
233 if format == 'custom':
234 name_result = config.Read('/Wammu/NameFormatString') % {
235 'Name' : name,
236 'FirstName' : first,
237 'LastName' : last,
238 'NickName' : nickname,
239 'FormalName' : formalname,
240 'Company' : company,
242 else:
243 if name != '':
244 name_result = name
245 elif first != '':
246 if last != '':
247 if format == 'auto-first-last':
248 name_result = '%s %s' % (first, last)
249 else:
250 name_result = '%s, %s' % (last, first)
251 else:
252 name_result = first
253 elif last != '':
254 name_result = last
255 elif nickname != '':
256 name_result = nickname
257 elif formalname != '':
258 name_result = formalname
259 else:
260 name_result = ''
262 if name_result == '':
263 if company != '':
264 name_result = company
265 else:
266 if company != '':
267 name_result = '%s (%s)' % (name_result, company)
269 if number_result == '':
270 number_result = number
272 entry['Number'] = number_result
273 entry['Name'] = name_result
274 entry['Synced'] = False
275 entry['Date'] = date
277 return entry
279 def ParseTodo(entry):
280 dt = ''
281 text = ''
282 completed = ''
283 for i in entry['Entries']:
284 if i['Type'] == 'END_DATETIME':
285 dt = str(i['Value'])
286 elif i['Type'] == 'TEXT':
287 text = i['Value']
288 elif i['Type'] == 'COMPLETED':
289 if i['Value']:
290 completed = _('Yes')
291 else:
292 completed = _('No')
293 entry['Completed'] = completed
294 entry['Text'] = text
295 entry['Date'] = dt
296 entry['Synced'] = False
297 return entry
299 def ParseCalendar(entry):
300 start = ''
301 end = ''
302 text = ''
303 description = ''
304 tone_alarm = None
305 silent_alarm = None
306 recurrence = None
307 for i in entry['Entries']:
308 if i['Type'] == 'END_DATETIME':
309 end = str(i['Value'])
310 elif i['Type'] == 'START_DATETIME':
311 start = str(i['Value'])
312 elif i['Type'] == 'TONE_ALARM_DATETIME':
313 tone_alarm = _('enabled (tone)')
314 elif i['Type'] == 'SILENT_ALARM_DATETIME':
315 silent_alarm = _('enabled (silent)')
316 elif i['Type'] == 'TEXT':
317 text = i['Value']
318 elif i['Type'] == 'DESCRIPTION':
319 description = i['Value']
320 elif i['Type'] == 'REPEAT_MONTH':
321 recurrence = _('yearly')
322 elif i['Type'] == 'REPEAT_DAY':
323 recurrence = _('monthly')
324 elif i['Type'] == 'REPEAT_FREQUENCY':
325 if i['Value'] == 1:
326 recurrence = _('daily')
327 elif i['Value'] == 2:
328 recurrence = _('biweekly')
329 elif (i['Type'] == 'REPEAT_DAYOFWEEK'):
330 if i['Value'] == 1:
331 recurrence = _('weekly on monday')
332 elif i['Value'] == 2:
333 recurrence = _('weekly on tuesday')
334 elif i['Value'] == 3:
335 recurrence = _('weekly on wednesday')
336 elif i['Value'] == 4:
337 recurrence = _('weekly on thursday')
338 elif i['Value'] == 5:
339 recurrence = _('weekly on friday')
340 elif i['Value'] == 6:
341 recurrence = _('weekly on saturday')
342 elif i['Value'] == 7:
343 recurrence = _('weekly on sunday')
345 if tone_alarm is not None:
346 entry['Alarm'] = tone_alarm
347 elif silent_alarm is not None:
348 entry['Alarm'] = silent_alarm
349 else:
350 entry['Alarm'] = _('disabled')
352 if recurrence is None:
353 entry['Recurrence'] = _('nonrecurring')
354 else:
355 entry['Recurrence'] = recurrence
357 if text == '':
358 entry['Text'] = description
359 elif description == '':
360 entry['Text'] = text
361 else:
362 entry['Text'] = '%s (%s)' % (text, description)
364 entry['Start'] = start
365 entry['End'] = end
366 entry['Synced'] = False
367 return entry
369 def ParseMessage(msg, parseinfo = False):
370 txt = ''
371 loc = ''
372 msg['Folder'] = msg['SMS'][0]['Folder']
373 msg['State'] = msg['SMS'][0]['State']
374 msg['Number'] = msg['SMS'][0]['Number']
375 msg['Name'] = msg['SMS'][0]['Name']
376 msg['DateTime'] = msg['SMS'][0]['DateTime']
377 if parseinfo:
378 for i in msg['SMSInfo']['Entries']:
379 if i['Buffer'] != None:
380 txt = txt + i['Buffer']
381 else:
382 for i in msg['SMS']:
383 txt = txt + i['Text']
384 for i in msg['SMS']:
385 if loc != '':
386 loc = loc + ', '
387 loc = loc + str(i['Location'])
388 try:
389 tmp = StrConv(txt)
390 msg['Text'] = txt
391 except:
392 s2 = ''
393 for x in txt:
394 if x in string.printable:
395 s2 += x
396 msg['Text'] = s2
397 msg['Location'] = loc
398 msg['Synced'] = False
399 return msg
401 def ProcessMessages(list, synced):
402 read = []
403 unread = []
404 sent = []
405 unsent = []
406 data = gammu.LinkSMS(list)
408 for x in data:
409 i = {}
410 v = gammu.DecodeSMS(x)
411 i['SMS'] = x
412 if v != None:
413 i['SMSInfo'] = v
414 ParseMessage(i, (v != None))
415 i['Synced'] = synced
416 if i['State'] == 'Read':
417 read.append(i)
418 elif i['State'] == 'UnRead':
419 unread.append(i)
420 elif i['State'] == 'Sent':
421 sent.append(i)
422 elif i['State'] == 'UnSent':
423 unsent.append(i)
425 return {'read':read, 'unread':unread, 'sent':sent, 'unsent':unsent}
427 def FormatError(txt, info):
428 if info['Code'] == gammu.Errors['ERR_NOTSUPPORTED']:
429 message = _('Your phone doesn\'t support this function.')
430 elif info['Code'] == gammu.Errors['ERR_NOTIMPLEMENTED']:
431 message = _('This function is not implemented for your phone. If you want help with implementation please contact authors.')
432 elif info['Code'] == gammu.Errors['ERR_SECURITYERROR']:
433 message = _('Your phone asks for PIN.')
434 elif info['Code'] == gammu.Errors['ERR_FULL']:
435 message = _('Memory is full, try deleting some entries.')
436 elif info['Code'] == gammu.Errors['ERR_CANCELED']:
437 message = _('Communication canceled by phone, did you press cancel on phone?')
438 elif info['Code'] == gammu.Errors['ERR_EMPTY']:
439 message = _('Empty entry received. This usually should not happen and most likely is caused by bug in phone firmware or in Gammu/Wammu.\n\nIf you miss some entry, please contact Gammu/Wammu authors.')
440 elif info['Code'] == gammu.Errors['ERR_INSIDEPHONEMENU']:
441 message = _('Please close opened menu in phone and retry, data can not be accessed while you have opened them.')
442 elif info['Code'] == gammu.Errors['ERR_TIMEOUT']:
443 message = _('Timeout while trying to communicate with phone. Maybe phone is not connected (for cable) or out of range (for bluetooth or IrDA).')
444 elif info['Code'] == gammu.Errors['ERR_DEVICENOTEXIST']:
445 message = _('Device for communication with phone does not exist. Maybe you don\'t have phone plugged or your configuration is wrong.')
446 elif info['Code'] == gammu.Errors['ERR_DEVICENOPERMISSION']:
447 message = _('Can not access device for communication with phone.')
448 if sys.platform == 'linux2':
449 message += ' ' + _('Maybe you need to be member of some group to have acces to device.')
450 else:
451 message = '%s %s\n%s %s\n%s %d' % (_('Description:'), StrConv(info['Text']), _('Function:'), info['Where'], _('Error code:'), info['Code'])
452 return StrConv(txt + '\n\n' + message)
454 def FixupMaskedEdit(edit):
455 # XXX: this is not clean way of reseting to system colour, but I don't know better.
456 bgc = wx.SystemSettings.GetColour(wx.SYS_COLOUR_LISTBOX)
457 fgc = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)
458 setattr(edit, '_validBackgroundColour', bgc)
459 setattr(edit, '_foregroundColour', fgc)
461 def GetWebsiteLang():
462 (loc, charset) = locale.getdefaultlocale()
463 try:
464 if loc[:2].lower() == 'cs':
465 return 'cz.'
466 return ''
467 except TypeError:
468 return ''
470 def DBUSServiceAvailable(bus, interface, try_start_service=False):
471 try:
472 import dbus
473 except ImportError:
474 return False
475 if try_start_service:
476 try:
477 bus.start_service_by_name(interface)
478 except dbus.exceptions.DBusException:
479 print 'Failed to start DBus service %s' % interface
480 obj = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')
481 dbus_iface = dbus.Interface(obj, 'org.freedesktop.DBus')
482 avail = dbus_iface.ListNames()
483 return interface in avail
486 def CheckDeviceNode(curdev):
488 Checks whether it makes sense to perform searching on this device and
489 possibly warns user about misconfigurations.
491 Returns tuple of 4 members:
492 - error code (0 = ok, -1 = device does not exits, -2 = no permissions)
493 - log text
494 - error dialog title
495 - error dialog text
497 if sys.platform == 'win32':
498 try:
499 import win32file
500 if curdev[:3] == 'COM':
501 try:
502 win32file.QueryDosDevice(curdev)
503 return (0, '', '', '')
504 except:
505 return (-1,
506 _('Device %s does not exist!') % curdev,
507 _('Error opening device'),
508 _('Device %s does not exist!') % curdev
510 except ImportError:
511 return (0, '', '', '')
512 if not os.path.exists(curdev):
513 return (-1,
514 _('Device %s does not exist!') % curdev,
515 _('Error opening device'),
516 _('Device %s does not exist!') % curdev
518 if not os.access(curdev, os.R_OK) or not os.access(curdev, os.W_OK):
519 gid = os.stat(curdev).st_gid
520 if HAVE_GRP:
521 group = grp.getgrgid(gid)[0]
522 else:
523 group = str(gid)
524 return (-2,
525 _('You don\'t have permissions for %s device!') % curdev,
526 _('Error opening device'),
527 (_('You don\'t have permissions for %s device!') % curdev) +
528 ' ' +
529 (_('Maybe you need to be member of %s group.') % group)
531 return (0, '', '', '')