#!/usr/bin/env python # -*- encoding: UTF8 -*- # author: Philipp Klaus, philipp.klaus →AT→ gmail.com # This file is part of python-sipgate-xmlrpc. # # python-sipgate-xmlrpc is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # python-sipgate-xmlrpc is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with python-sipgate-xmlrpc. If not, see . ##################################################################### ###### This the most important file of the project: ####### ###### It contains the classe api, which ####### ###### implements the XML-RPC communication with the ####### ###### Sipgate API. ####### #from time import time from sys import stderr from xmlrpclib import ServerProxy, Fault, ProtocolError, ResponseError from exceptions import TypeError from socket import error as socket_error import re VERSION = "0.9.2" NAME = "%s - python-sipgate-xmlrpc/sipgate.py" VENDOR = "https://github.com/pklaus/python-sipgate-xmlrpc" ### ------- Here comes the most important piece of code: the api class with magic methods ----- class api (ServerProxy): def __init__ (self, username=False, password=False, prog_name=False, verbose=False): if not (username and password and prog_name): raise SipgateAPIException('To use the class sipgate.api you must provide, username, password and a program name.') address = SIPGATE_API_URL % {'username':username, 'password':password} ### The super() call would be more modern but it doesn't work with the current Python version yet. #super(api, self).__init__(address, verbose=debug) ServerProxy.__init__(self, address,verbose=verbose) ### It is considered good practice to Identify the client talking to the server: self.ClientIdentify({ "ClientName" : NAME % prog_name, "ClientVersion" : VERSION, "ClientVendor" : VENDOR }) def __getattr__(self,name): return _Method(self.__request, name) def __request (self, methodname, params): if methodname.replace(API_PREFIX,'') not in VALID_METHODS: stderr.write( UNKNOWN_METHOD_MESSAGE % { 'method': methodname.replace(API_PREFIX,''), 'api_prefix': API_PREFIX, 'api_version': SIPGATE_API_DOC_V, 'api_date': SIPGATE_API_DOC_D } ) if len(params)>0 and not type(params[0]) is dict: raise TypeError(DICT_AS_PARAM_MESSAGE % methodname.replace(API_PREFIX,'')) method_function = ServerProxy.__getattr__(self,methodname) try: result = method_function(params[0] if len(params)>0 and type(params[0]) is dict else dict()) # cast the result dictionary to a SipgateResponse (custom dictionary): result = SipgateResponse(result) except Fault, e: raise SipgateAPIFault(e.faultCode, e.faultString) except ProtocolError, e: raise SipgateAPIProtocolError(e.url, e.errcode, e.errmsg, e.headers) except socket_error, (value,message): raise SipgateAPISocketError(value, message) return result ## class SipgateResponse(dict): def __init__(self, response_dict): try: self.StatusCode, self.StatusString = int(response_dict['StatusCode']), response_dict['StatusString'] self.success = self.StatusCode == 200 except: raise TypeError(RESPONSE_NOT_A_DICTIONARY % response_dict) dict.__init__(self, response_dict) class _Method: # With the help of this class the api class does not # need to state explicitly the possible XML-RPC calls. def __init__(self, send, name): self.__send = send self.__name = API_PREFIX+name def __call__(self, *args): return self.__send(self.__name, args) ### ------ now we define the exceptions that could occur ------ class SipgateAPIException(Exception): pass class SipgateAPIFault(Fault, SipgateAPIException): # As this inherits from xmlrpclib.Fault it also has the # attributes faultCode and faultString. pass class SipgateAPIProtocolError(ProtocolError, SipgateAPIException): # As this inherits from xmlrpclib.ProtocolError it also has the # attributes errcode and errmsg. pass class SipgateAPISocketError(socket_error, SipgateAPIException): # As this inherits from socket.error it also has the # attributes . pass ### ------ This section contains message strings ------- UNKNOWN_METHOD_MESSAGE = "The method '%(method)s' for the API prefix '%(api_prefix)s' " + \ "was called. This method, however, is currently not documented for the Sipgate API " + \ "v%(api_version)s (%(api_date)s). Let's try but I've warned you.\n" DICT_AS_PARAM_MESSAGE = 'Please specify a dictionary as function call parameter for api.%s().' RESPONSE_NOT_A_DICTIONARY = 'The response "%s" does not seem to be a response from the ' + \ 'Sipgate XML-RPC API.' ### ------ This section contains constants of the Sipgate XML-RPC API ------- # This constant represents the version of the currently implemented Sipgate API # ans is taken from the API description PDF: SIPGATE_API_DOC_V = '1.06' SIPGATE_API_DOC_D = 'August 21, 2007' # Sipgate basic and plus accounts must use this API URL: SIPGATE_API_URL = "https://%(username)s:%(password)s@samurai.sipgate.net/RPC2" # Sipgate one and team have a different URL: api.sipgate.net. # see API_PREFIX = 'samurai.' VALID_METHODS = [ 'AccountStatementGet', 'BalanceGet', 'ClientIdentify', 'HistoryGetByDate', 'ItemizedEntriesGet', 'OwnUriListGet', 'PhonebookEntryGet', 'PhonebookListGet', 'RecommendedIntervalGet', 'ServerdataGet', 'SessionClose', 'SessionInitiate', 'SessionInitiateMulti', 'SessionStatusGet', 'TosListGet', 'TosListGet', 'UmSummaryGet', 'UserdataGreetingGet', 'UserdataSipGet', ] SERVER_STATUS_CODES = { ### From Table A.1 and A.2 of the API docu: general server status codes 200: 'Method success', 400: 'Method not supported', 401: 'Request denied (no reason specified)', 402: 'Internal error', 403: 'Invalid arguments', 404: 'Resources exceeded (this MUST not be used to indicate parameters in error)', 405: 'Invalid parameter name', 406: 'Invalid parameter type', 407: 'Invalid parameter value', 408: 'Attempt to set a non-writable parameter', 409: 'Notification request rejected.', 410: 'Parameter exceeds maximum size.', 411: 'Missing parameter.', 412: 'Too many requests.', 500: 'Date out of range.', 501: 'Uri does not belong to user.', 502: 'Unknown type of service.', 503: 'Selected payment method failed.', 504: 'Selected currency not supported.', 505: 'Amount exceeds limit.', 506: 'Malformed SIP URI.', 507: 'URI not in list.', 508: 'Format is not valid E.164.', 509: 'Unknown status.', 510: 'Unknown ID.', 511: 'Invalid timevalue.', 512: 'Referenced session not found.', 513: 'Only single default per TOS allowed.', 514: 'Malformed VCARD format.', 515: 'Malformed PID format.', 516: 'Presence information not available.', 517: 'Invalid label name.', 518: 'Label not assigned.', 519: 'Label doesn’t exist.', 520: 'Parameter includes invalid characters.', 521: 'Bad password. (Rejected due to security concerns.)', 522: 'Malformed timezone format.', 523: 'Delay exceeds limit.', 524: 'Requested VPN type not available.', 525: 'Requested TOS not available.', 526: 'Unified messaging not available.', 527: 'URI not available for registration.', } TYPE_OF_SERVICE = { 'fax': 'pages', # fax transmission 'text': 'characters', # text message (e.g. "SMS") 'video': 'seconds', # video communication 'voice': 'seconds', # voice communication } class helpers (object): @staticmethod def FQTN(phone_number, default_country_code): """ Assures phone numbers are in the form of a E164 Fully Qualified Telephone Number without the leading + sign. The alternative would be the Python port of Google's libphonenumber: https://github.com/daviddrysdale/python-phonenumbers """ phone_number = phone_number.replace(' ','').replace('-','').replace('+','').replace('/','') ## number starting with 00 (so it's an international format) if re.compile("^00[1-9][0-9]*$").match(phone_number): return phone_number[2:] ## number starting with your country code (so it was already a FQTN): if re.compile("^"+default_country_code+"[1-9][0-9]*$").match(phone_number): return phone_number if re.compile("^0[1-9]*$").match(phone_number): return default_country_code+phone_number[1:] if re.compile("^[1-9]*$").match(phone_number): return phone_number raise TypeError("Couldn't parse this phone number: "+phone_number)