Translated using Weblate.
[wammu.git] / Wammu / Utils.py
blob0cde4075e3696e963c00e67d6a8fe7a76e372a47
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 - 2010 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 import base64
34 try:
35 import grp
36 HAVE_GRP = True
37 except ImportError:
38 HAVE_GRP = False
39 import Wammu.Locales
40 from Wammu.Locales import StrConv
42 import Wammu
43 if Wammu.gammu_error == None:
44 import gammu
47 def GetItemType(txt):
48 if txt == '':
49 return None
50 elif txt[-8:] == 'DATETIME' or txt == 'Date' or txt == 'LastModified' or txt == 'LAST_MODIFIED':
51 return 'datetime'
52 elif txt[-4:] == 'DATE':
53 return 'date'
54 elif txt in ['TEXT', 'DESCRIPTION', 'LOCATION', 'LUID'] or txt[:4] == 'Text':
55 return 'text'
56 elif txt == 'PHONE' or txt[:6] == 'Number':
57 return 'phone'
58 elif txt == 'CONTACTID':
59 return 'contact'
60 elif txt == 'PRIVATE' or txt == 'Private' or txt == 'COMPLETED':
61 return 'bool'
62 elif txt == 'Category' or txt == 'CATEGORY':
63 return 'category'
64 elif txt == 'PictureID' or txt == 'RingtoneID' or txt == 'RingtoneFileSystemID':
65 return 'id'
66 elif txt == 'Photo':
67 return 'photo'
68 else:
69 return 'number'
71 def SearchLocation(lst, loc, second = None):
72 result = -1
73 for i in range(len(lst)):
74 if second != None:
75 if not lst[i][second[0]] == second[1]:
76 continue
77 if type(lst[i]['Location']) == type(loc):
78 if loc == lst[i]['Location']:
79 result = i
80 break
81 else:
82 if str(loc) in lst[i]['Location'].split(', '):
83 result = i
84 break
85 return result
87 def MatchesText(item, match, num):
88 testkeys = ['Value', 'Text', 'Number']
89 for x in item:
90 if type(item) == dict:
91 val = item[x]
92 else:
93 val = x
94 if type(val) in (str, unicode):
95 if match.search(val) != None:
96 return True
97 elif num is not None and type(val) == int and num == val:
98 return True
99 elif type(val) == list:
100 for i in range(len(val)):
101 for key in testkeys:
102 try:
103 val2 = val[i][key]
104 if type(val2) in (str, unicode):
105 if match.search(val2) != None:
106 return True
107 elif num is not None and type(val2) == int and num == val2:
108 return True
109 except KeyError:
110 # Ignore not found keys
111 pass
112 return False
115 def SearchItem(lst, item):
116 for i in range(len(lst)):
117 if item == lst[i]:
118 return i
119 return -1
121 def GrabNumberPrefix(number, prefixes):
122 l = len(number)
123 if l == 0 or number[0] != '+':
124 return None
125 i = 2
126 while not number[:i] in prefixes:
127 i += 1
128 if i > l:
129 return None
130 return number[:i]
132 '''Prefix for making international numbers'''
133 NumberPrefix = ''
135 NumberStrip = re.compile('^([#*]\d+[#*])?(\\+?.*)$')
137 def NormalizeNumber(number):
139 Attempts to create international number from anything it receives.
140 It does strip any network prefixes and attempts to properly add
141 international prefix. However this is a bit tricky, as there are
142 many ways which can break this.
144 # Strip magic prefixes (like no CLIR)
145 nbmatch = NumberStrip.match(number)
146 resnumber = nbmatch.group(2)
147 # If we stripped whole number, return original
148 if len(resnumber) == 0:
149 return number
150 # Handle 00 prefix same as +
151 if resnumber[0:2] == '00':
152 resnumber = '+' + resnumber[2:]
153 # Detect numbers with international prefix and without +
154 # This can be national number in some countries (eg. US)
155 if len(NumberPrefix) > 0 and NumberPrefix[0] == '+' and resnumber[:len(NumberPrefix) - 1] == NumberPrefix[1:]:
156 resnumber = '+' + resnumber
157 # Add international prefix
158 if resnumber[0] != '+':
159 resnumber = NumberPrefix + resnumber
160 return resnumber
162 def SearchNumber(lst, number):
163 for i in range(len(lst)):
164 for x in lst[i]['Entries']:
165 if GetItemType(x['Type']) == 'phone' and NormalizeNumber(number) == NormalizeNumber(x['Value']):
166 return i
167 return -1
169 def GetContactLink(lst, i, txt):
170 return StrConv('<a href="memory://%s/%d">%s</a> (%s)' % (lst[i]['MemoryType'], lst[i]['Location'], lst[i]['Name'], txt))
172 def GetNumberLink(lst, number):
173 i = SearchNumber(lst, number)
174 if i == -1:
175 return StrConv(number)
176 return GetContactLink(lst, i, number)
178 def GetTypeString(type, value, values, linkphone = True):
180 Returns string for entry in data dictionary. Formats it according to
181 knowledge of format types.
183 t = GetItemType(type)
184 if t == 'contact':
185 i = SearchLocation(values['contact']['ME'], value)
186 if i == -1:
187 return '%d' % value
188 else:
189 return GetContactLink([] + values['contact']['ME'], i, str(value))
190 elif linkphone and t == 'phone':
191 return StrConv(GetNumberLink([] + values['contact']['ME'] + values['contact']['SM'], value))
192 elif t == 'id':
193 v = hex(value)
194 if v[-1] == 'L':
195 v = v[:-1]
196 return v
197 elif t == 'photo':
198 return '<wxp module="Wammu.Image" class="EncodedBitmap">' + \
199 '<param name="image" value="' + \
200 base64.b64encode(value) + \
201 '">' + \
202 '</wxp>'
203 else:
204 return StrConv(value)
206 def ParseMemoryEntry(entry, config = None):
207 first = ''
208 last = ''
209 name = ''
210 nickname = ''
211 formalname = ''
212 company = ''
213 number = ''
214 number_result = ''
215 name_result = ''
216 date = None
217 for i in entry['Entries']:
218 if i['Type'] == 'Text_Name':
219 name = i['Value']
220 elif i['Type'] == 'Text_FirstName':
221 first = i['Value']
222 if i['Type'] == 'Text_LastName':
223 last = i['Value']
224 if i['Type'] == 'Text_NickName':
225 nickname = i['Value']
226 if i['Type'] == 'Text_FormalName':
227 formalname = i['Value']
228 if i['Type'] == 'Date':
229 # Store date olny if it is more recent
230 # This can happen in multiple call records
231 if date is None:
232 date = i['Value']
233 else:
234 if i['Value'] > date:
235 date = i['Value']
236 if i['Type'] == 'Text_Company':
237 company = i['Value']
238 if i['Type'] == 'Number_General':
239 number_result = i['Value']
240 elif i['Type'][:7] == 'Number_':
241 number = i['Value']
243 if config is None:
244 format = 'auto'
245 else:
246 format = config.Read('/Wammu/NameFormat')
248 if format == 'custom':
249 name_result = config.Read('/Wammu/NameFormatString') % {
250 'Name' : name,
251 'FirstName' : first,
252 'LastName' : last,
253 'NickName' : nickname,
254 'FormalName' : formalname,
255 'Company' : company,
257 else:
258 if name != '':
259 name_result = name
260 elif first != '':
261 if last != '':
262 if format == 'auto-first-last':
263 name_result = '%s %s' % (first, last)
264 else:
265 name_result = '%s, %s' % (last, first)
266 else:
267 name_result = first
268 elif last != '':
269 name_result = last
270 elif nickname != '':
271 name_result = nickname
272 elif formalname != '':
273 name_result = formalname
274 else:
275 name_result = ''
277 if name_result == '':
278 if company != '':
279 name_result = company
280 else:
281 if company != '':
282 name_result = '%s (%s)' % (name_result, company)
284 if number_result == '':
285 number_result = number
287 entry['Number'] = number_result
288 entry['Name'] = name_result
289 entry['Synced'] = False
290 entry['Date'] = date
292 return entry
294 def ParseTodo(entry):
295 dt = ''
296 text = ''
297 completed = ''
298 for i in entry['Entries']:
299 if i['Type'] == 'END_DATETIME':
300 dt = str(i['Value'])
301 elif i['Type'] == 'TEXT':
302 text = i['Value']
303 elif i['Type'] == 'COMPLETED':
304 if i['Value']:
305 completed = _('Yes')
306 else:
307 completed = _('No')
308 entry['Completed'] = completed
309 entry['Text'] = text
310 entry['Date'] = dt
311 entry['Synced'] = False
312 return entry
314 def ParseCalendar(entry):
315 start = ''
316 end = ''
317 text = ''
318 description = ''
319 tone_alarm = None
320 silent_alarm = None
321 recurrence = None
322 for i in entry['Entries']:
323 if i['Type'] == 'END_DATETIME':
324 end = str(i['Value'])
325 elif i['Type'] == 'START_DATETIME':
326 start = str(i['Value'])
327 elif i['Type'] == 'TONE_ALARM_DATETIME':
328 tone_alarm = _('enabled (tone)')
329 elif i['Type'] == 'SILENT_ALARM_DATETIME':
330 silent_alarm = _('enabled (silent)')
331 elif i['Type'] == 'TEXT':
332 text = i['Value']
333 elif i['Type'] == 'DESCRIPTION':
334 description = i['Value']
335 elif i['Type'] == 'REPEAT_MONTH':
336 recurrence = _('yearly')
337 elif i['Type'] == 'REPEAT_DAY':
338 recurrence = _('monthly')
339 elif i['Type'] == 'REPEAT_FREQUENCY':
340 if i['Value'] == 1:
341 recurrence = _('daily')
342 elif i['Value'] == 2:
343 recurrence = _('biweekly')
344 elif (i['Type'] == 'REPEAT_DAYOFWEEK'):
345 if i['Value'] == 1:
346 recurrence = _('weekly on monday')
347 elif i['Value'] == 2:
348 recurrence = _('weekly on tuesday')
349 elif i['Value'] == 3:
350 recurrence = _('weekly on wednesday')
351 elif i['Value'] == 4:
352 recurrence = _('weekly on thursday')
353 elif i['Value'] == 5:
354 recurrence = _('weekly on friday')
355 elif i['Value'] == 6:
356 recurrence = _('weekly on saturday')
357 elif i['Value'] == 7:
358 recurrence = _('weekly on sunday')
360 if tone_alarm is not None:
361 entry['Alarm'] = tone_alarm
362 elif silent_alarm is not None:
363 entry['Alarm'] = silent_alarm
364 else:
365 entry['Alarm'] = _('disabled')
367 if recurrence is None:
368 entry['Recurrence'] = _('nonrecurring')
369 else:
370 entry['Recurrence'] = recurrence
372 if text == '':
373 entry['Text'] = description
374 elif description == '':
375 entry['Text'] = text
376 else:
377 entry['Text'] = '%s (%s)' % (text, description)
379 entry['Start'] = start
380 entry['End'] = end
381 entry['Synced'] = False
382 return entry
384 def ParseMessage(msg, parseinfo = False):
385 txt = ''
386 loc = ''
387 msg['Folder'] = msg['SMS'][0]['Folder']
388 msg['State'] = msg['SMS'][0]['State']
389 msg['Number'] = msg['SMS'][0]['Number']
390 msg['Name'] = msg['SMS'][0]['Name']
391 msg['DateTime'] = msg['SMS'][0]['DateTime']
392 if parseinfo:
393 for i in msg['SMSInfo']['Entries']:
394 if i['Buffer'] != None:
395 txt = txt + i['Buffer']
396 else:
397 for i in msg['SMS']:
398 txt = txt + i['Text']
399 for i in msg['SMS']:
400 if loc != '':
401 loc = loc + ', '
402 loc = loc + str(i['Location'])
403 try:
404 StrConv(txt)
405 msg['Text'] = txt
406 except:
407 s2 = ''
408 for x in txt:
409 if x in string.printable:
410 s2 += x
411 msg['Text'] = s2
412 msg['Location'] = loc
413 msg['Synced'] = False
414 return msg
416 def ProcessMessages(list, synced):
417 read = []
418 unread = []
419 sent = []
420 unsent = []
421 data = gammu.LinkSMS(list)
423 for x in data:
424 i = {}
425 v = gammu.DecodeSMS(x)
426 i['SMS'] = x
427 if v != None:
428 i['SMSInfo'] = v
429 ParseMessage(i, (v != None))
430 i['Synced'] = synced
431 if i['State'] == 'Read':
432 read.append(i)
433 elif i['State'] == 'UnRead':
434 unread.append(i)
435 elif i['State'] == 'Sent':
436 sent.append(i)
437 elif i['State'] == 'UnSent':
438 unsent.append(i)
440 return {'read':read, 'unread':unread, 'sent':sent, 'unsent':unsent}
442 def FormatError(txt, info, gammu_config = None):
443 if info['Code'] == gammu.Errors['ERR_NOTSUPPORTED']:
444 message = _('Your phone doesn\'t support this function.')
445 elif info['Code'] == gammu.Errors['ERR_NOTIMPLEMENTED']:
446 message = _('This function is not implemented for your phone. If you want help with implementation please contact authors.')
447 elif info['Code'] == gammu.Errors['ERR_SECURITYERROR']:
448 message = _('Your phone asks for PIN.')
449 elif info['Code'] == gammu.Errors['ERR_FULL']:
450 message = _('Memory is full, try deleting some entries.')
451 elif info['Code'] == gammu.Errors['ERR_CANCELED']:
452 message = _('Communication canceled by phone, did you press cancel on phone?')
453 elif info['Code'] == gammu.Errors['ERR_EMPTY']:
454 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.')
455 elif info['Code'] == gammu.Errors['ERR_INSIDEPHONEMENU']:
456 message = _('Please close opened menu in phone and retry, data can not be accessed while you have opened them.')
457 elif info['Code'] == gammu.Errors['ERR_TIMEOUT']:
458 message = _('Timeout while trying to communicate with phone. Maybe phone is not connected (for cable) or out of range (for Bluetooth or IrDA).')
459 elif info['Code'] == gammu.Errors['ERR_DEVICENOTEXIST']:
460 if gammu_config is None:
461 message = _('Device for communication with phone does not exist. Maybe you don\'t have phone plugged or your configuration is wrong.')
462 else:
463 message = _('Device "%s" for communication with phone does not exist. Maybe you don\'t have phone plugged or your configuration is wrong.') % gammu_config['Device']
464 elif info['Code'] == gammu.Errors['ERR_DEVICENOPERMISSION']:
465 if sys.platform[:5] == 'linux':
466 message_group = ' ' + _('Maybe you need to be member of some group to have acces to device.')
467 else:
468 message_group = ''
469 if gammu_config is None:
470 message = _('Can not access device for communication with phone.')
471 else:
472 check = CheckDeviceNode(gammu_config['Device'])
473 if check[0] == -2:
474 message = check[3]
475 message_group = ''
476 else:
477 message = _('Can not access device "%s" for communication with phone.') % gammu_config['Device']
478 if message_group != '':
479 message += ' ' + message_group
480 elif info['Code'] == gammu.Errors['ERR_NOSIM']:
481 message = _('Can not access SIM card. Please check whether it is properly inserted in phone and/or try to reboot the phone by removing battery.')
482 else:
483 message = '%s %s\n%s %s\n%s %d' % (_('Description:'), StrConv(info['Text']), _('Function:'), info['Where'], _('Error code:'), info['Code'])
484 return StrConv(txt + '\n\n' + message)
486 def FixupMaskedEdit(edit):
487 # XXX: this is not clean way of reseting to system colour, but I don't know better.
488 bgc = wx.SystemSettings.GetColour(wx.SYS_COLOUR_LISTBOX)
489 fgc = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)
490 setattr(edit, '_validBackgroundColour', bgc)
491 setattr(edit, '_foregroundColour', fgc)
493 def GetWebsiteLang():
494 (loc, charset) = locale.getdefaultlocale()
495 try:
496 if loc[:2].lower() == 'cs':
497 return 'cs.'
498 return ''
499 except TypeError:
500 return ''
502 def DBUSServiceAvailable(bus, interface, try_start_service=False):
503 try:
504 import dbus
505 except ImportError:
506 return False
507 if try_start_service:
508 try:
509 bus.start_service_by_name(interface)
510 except dbus.exceptions.DBusException:
511 print 'Failed to start DBus service %s' % interface
512 obj = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')
513 dbus_iface = dbus.Interface(obj, 'org.freedesktop.DBus')
514 avail = dbus_iface.ListNames()
515 return interface in avail
518 def CheckDeviceNode(curdev):
520 Checks whether it makes sense to perform searching on this device and
521 possibly warns user about misconfigurations.
523 Returns tuple of 4 members:
524 - error code (0 = ok, -1 = device does not exits, -2 = no permissions)
525 - log text
526 - error dialog title
527 - error dialog text
529 if sys.platform == 'win32':
530 try:
531 import win32file
532 if curdev[:3] == 'COM':
533 try:
534 win32file.QueryDosDevice(curdev)
535 return (0, '', '', '')
536 except:
537 return (-1,
538 _('Device %s does not exist!') % curdev,
539 _('Error opening device'),
540 _('Device %s does not exist!') % curdev
542 except ImportError:
543 return (0, '', '', '')
544 if not os.path.exists(curdev):
545 return (-1,
546 _('Device %s does not exist!') % curdev,
547 _('Error opening device'),
548 _('Device %s does not exist!') % curdev
550 if not os.access(curdev, os.R_OK) or not os.access(curdev, os.W_OK):
551 gid = os.stat(curdev).st_gid
552 try:
553 group = grp.getgrgid(gid)[0]
554 except:
555 group = str(gid)
556 return (-2,
557 _('You don\'t have permissions for %s device!') % curdev,
558 _('Error opening device'),
559 (_('You don\'t have permissions for %s device!') % curdev) +
560 ' ' +
561 (_('Maybe you need to be member of %s group.') % group)
563 return (0, '', '', '')
565 def CompatConfig(cfg):
567 Adjust configuration for possible changes in Gammu history.
570 # 1.27.0 changed handling of boolean options
571 # Pre 1.27.0 used strings
572 if tuple(map(int, gammu.Version()[1].split('.'))) < (1, 27, 0):
573 if cfg['SyncTime']:
574 cfg['SyncTime'] = 'yes'
575 else:
576 cfg['SyncTime'] = 'no'
577 if cfg['LockDevice']:
578 cfg['LockDevice'] = 'yes'
579 else:
580 cfg['LockDevice'] = 'no'
581 if cfg['StartInfo']:
582 cfg['StartInfo'] = 'yes'
583 else:
584 cfg['StartInfo'] = 'no'
585 # 1.27.0 accepted only numbers
586 if tuple(map(int, gammu.Version()[1].split('.'))) == (1, 27, 0):
587 if cfg['SyncTime']:
588 cfg['SyncTime'] = 1
589 else:
590 cfg['SyncTime'] = 0
591 if cfg['LockDevice']:
592 cfg['LockDevice'] = 1
593 else:
594 cfg['LockDevice'] = 0
595 if cfg['StartInfo']:
596 cfg['StartInfo'] = 1
597 else:
598 cfg['StartInfo'] = 0
600 # Older versions did not use model auto
601 if cfg['Model'] == 'auto':
602 cfg['Model'] = ''
604 return cfg