├── MANIFEST.in ├── CHANGES.txt ├── .gitignore ├── setup.py ├── LICENSE.txt ├── README.txt └── smsapi.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | recursive-include docs *.txt -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | v0.1, 2010 -- initial release based on SOAP 2 | v0.2, 2012-12-18 -- rewrite for HTTPS API, SMS sending, subaccounts manipulation, sender manipulation 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | *.orig 4 | *.pyc 5 | *.rej 6 | .installed.cfg 7 | __minitage__* 8 | bin/* 9 | develop-eggs 10 | downloads 11 | eggs 12 | log 13 | parts 14 | project/media/uploads 15 | tmp 16 | settings_local.py 17 | .pypirc 18 | test.py 19 | 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from distutils.core import setup 4 | 5 | setup( 6 | name='python-smsapi', 7 | version='0.2', 8 | license='LICENSE.txt', 9 | description='Client library for SMSAPI.pl API.', 10 | long_description=open('README.txt').read(), 11 | author='ELCODO', 12 | author_email='info@elcodo.pl', 13 | url='https://github.com/elcodo/python-smsapi', 14 | py_modules=['smsapi', ], 15 | keywords=['api', 'smsapi.pl', 'sms'], 16 | classifiers=[ 17 | "Programming Language :: Python", 18 | "Development Status :: 3 - Alpha", 19 | "Environment :: Other Environment", 20 | "Intended Audience :: Developers", 21 | "License :: OSI Approved :: BSD License", 22 | "Operating System :: OS Independent", 23 | "Topic :: Software Development :: Libraries :: Python Modules", 24 | "Topic :: Internet :: WWW/HTTP", 25 | ], 26 | ) 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | OSI - The BSD License (http://www.opensource.org/licenses/bsd-license.php) 2 | 3 | Copyright (c) 2012, Grzegorz Bialy, ELCODO.pl 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 11 | * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND ANY 14 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY 17 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | ============= 2 | python-smsapi 3 | ============= 4 | 5 | Python client library for SMSAPI.pl. 6 | 7 | Currently allows: 8 | 9 | * SMS sending 10 | * User sender (caller ID) manipulation (add, delete, check) 11 | * User subaccounts management 12 | * Address Book manipulation - adding, listing and deleting numbers and groups (suds library needed for SOAP API) 13 | 14 | Author 15 | ------ 16 | 17 | * Grzegorz Biały (https://github.com/grzegorzbialy/) 18 | * ELCODO (http://elcodo.pl) 19 | 20 | Install 21 | ------- 22 | You can do one of the following: 23 | 24 | * python setup.py install 25 | * copy smsapi.py to anywhere to your PYTHONPATH (e.g. your project directory) 26 | 27 | Requirements 28 | ------------ 29 | 30 | * Python 2.6+ 31 | * (optional) suds library for SOAP API - AddressBook, etc. 32 | 33 | Usage 34 | ----- 35 | 36 | *Init and get points (credits) quantity*:: 37 | 38 | from smsapi import SmsApi 39 | 40 | username = "" 41 | password = "" 42 | 43 | sms = SmsApi(username, password) 44 | 45 | total_points = sms.get_points()['points'] 46 | print "You have %s points left" % total_points 47 | 48 | 49 | *SMS sending*:: 50 | 51 | # Send SMS message to +48123456789 - fill sender field "SENDER" and message with "MESSAGE" 52 | sms = sms.send_sms( 53 | recipient="48123456789", 54 | sender_name="SENDER", 55 | message="MESSAGE", 56 | eco=False, 57 | ) 58 | 59 | # print sms 60 | # expected result: 61 | # {'cost': '0.1650', 'id': , 'status': "OK"} 62 | 63 | *Address Book*:: 64 | 65 | # add group called "Test Group" 66 | group_id = ab.add_group(u"Test Group") 67 | 68 | # add test number 69 | number = ab.add_number(u"48123456789", u"Test Number", group_id) 70 | 71 | # get all groups and assign numbers to it 72 | groups_and_numbers = {} 73 | groups = ab.get_groups() 74 | for g in groups: 75 | groups_and_numbers[g['name']] = ab.get_numbers(g['id']) 76 | 77 | # print groups_and_numbers 78 | # expected result: 79 | # {u'Test Group': [{'group_id': , 'name': Test Number, 'number': 48123456789}]} 80 | 81 | 82 | License 83 | ------- 84 | OSI - The BSD License (http://www.opensource.org/licenses/bsd-license.php) 85 | 86 | Copyright (c) 2012, Grzegorz Bialy, ELCODO.pl 87 | All rights reserved. 88 | 89 | Redistribution and use in source and binary forms, with or without 90 | modification, are permitted provided that the following conditions are met: 91 | 92 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 93 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 94 | * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 95 | 96 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND ANY 97 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 98 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 99 | DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY 100 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 101 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 102 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 103 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 104 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 105 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /smsapi.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # 3 | # python-smsapi - Python bindings for the smsapi.pl HTTPS API 4 | # 5 | # Copyright (c) 2012, Grzegorz Bialy, ELCODO.pl 6 | # All rights reserved. 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions are met: 10 | # * Redistributions of source code must retain the above copyright 11 | # notice, this list of conditions and the following disclaimer. 12 | # * Redistributions in binary form must reproduce the above copyright 13 | # notice, this list of conditions and the following disclaimer in the 14 | # documentation and/or other materials provided with the distribution. 15 | # * Neither the name of the author nor the names of its contributors may 16 | # be used to endorse or promote products derived from this software 17 | # without specific prior written permission. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS``AS IS'' AND ANY 20 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY 23 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | import hashlib 31 | import httplib 32 | import json 33 | import urllib 34 | import time 35 | 36 | try: 37 | from suds import WebFault 38 | from suds.client import Client 39 | except: 40 | pass 41 | 42 | 43 | __all__ = ['SmsApi'] 44 | 45 | VERSION = '0.2' 46 | 47 | API_URL = "ssl.smsapi.pl" 48 | WSDL_URL = "https://ssl.smsapi.pl/api/soap/v2/webservices?WSDL" 49 | 50 | 51 | class SmsApiException(Exception): 52 | 53 | def __init__(self, message, args=[]): 54 | Exception.__init__(self, message) 55 | self.args = args 56 | 57 | def __unicode__(self): 58 | return u"%s" % self.message 59 | 60 | 61 | class SmsApiBaseObject(object): 62 | 63 | def __init__(self, username, password): 64 | """Set username and password""" 65 | self.username = username 66 | self.password = hashlib.md5(password).hexdigest() 67 | 68 | try: 69 | self.soap_client = Client(WSDL_URL) 70 | self.soap_user = self._prepare_soap_user() 71 | except NameError: 72 | self.soap_client = None 73 | self.soap_user = None 74 | 75 | def _prepare_soap_user(self): 76 | """Prepare User object for SOAP authentication""" 77 | user = self.soap_client.factory.create('Client') 78 | user.username = self.username 79 | user.password = self.password 80 | return user 81 | 82 | def send_command(self, namespace, params={}): 83 | params['username'] = self.username 84 | params['password'] = self.password 85 | 86 | headers = { 87 | 'User-Agent': 'python-smsapi v%s' % VERSION, 88 | 'Accept': 'text/plain', 89 | 'Content-type': 'application/x-www-form-urlencoded', 90 | } 91 | 92 | h = httplib.HTTPSConnection(API_URL) 93 | h.request( 94 | "POST", 95 | u"/%s.do" % namespace, 96 | urllib.urlencode(params), 97 | headers=headers, 98 | ) 99 | response = h.getresponse() 100 | return response.read() 101 | 102 | def send_soap_command(self, command, args=[]): 103 | func = getattr(self.soap_client.service, command) 104 | 105 | try: 106 | response = func(*args) 107 | except WebFault, e: 108 | raise SmsApiException(e) 109 | 110 | if u"result" in response and response[u"result"] != 0: 111 | raise SmsApiException(response[u"description"]) 112 | 113 | return response 114 | 115 | 116 | class SmsApi(SmsApiBaseObject): 117 | 118 | def get_user(self, username): 119 | """Get account subuser details""" 120 | response = self.send_command(namespace="user", params={ 121 | 'get_user': username, 122 | 'format': 'json', 123 | }) 124 | try: 125 | data = json.loads(response) 126 | return { 127 | 'username': data['username'], 128 | 'limit': data['limit'], 129 | 'month_limit': data['month_limit'], 130 | 'allow_senders': True if data['senders'] == 1 else False, 131 | 'allow_phonebook': True if data['phonebook'] == 1 else False, 132 | 'is_active': True if data['active'] == 1 else False, 133 | 'info': data['info'], 134 | } 135 | except ValueError: 136 | raise SmsApiException(response) 137 | 138 | def add_user(self, username, password, password_api=None, limit=None, 139 | month_limit=None, allow_senders=False, allow_phonebook=False, 140 | is_active=False, info=None): 141 | """Add account subuser""" 142 | params = { 143 | 'add_user': username, 144 | 'pass': hashlib.md5(password).hexdigest(), 145 | } 146 | if password_api: 147 | params['pass_api'] = hashlib.md5(password_api).md5() 148 | if limit: 149 | params['limit'] = limit 150 | if month_limit: 151 | params['month_limit'] 152 | if allow_senders: 153 | params['senders'] = 1 154 | if allow_phonebook: 155 | params['phonebook'] = 1 156 | if is_active: 157 | params['active'] = 1 158 | if info: 159 | params['info'] = info 160 | 161 | response = self.send_command(namespace="user", params=params) 162 | if not response.startswith(u"OK"): 163 | raise SmsApiException(response) 164 | return True 165 | 166 | def edit_user(self, username, password=None, password_api=None, limit=None, 167 | month_limit=None, allow_senders=False, allow_phonebook=False, 168 | is_active=False, info=None): 169 | """Edit account subuser""" 170 | params = { 171 | 'set_user': username, 172 | } 173 | if password: 174 | params['pass'] = hashlib.md5(password).hexdigest() 175 | if password_api: 176 | params['pass_api'] = hashlib.md5(password_api).md5() 177 | if limit: 178 | params['limit'] = limit 179 | if month_limit: 180 | params['month_limit'] 181 | if allow_senders: 182 | params['senders'] = 1 183 | if allow_phonebook: 184 | params['phonebook'] = 1 185 | if is_active: 186 | params['active'] = 1 187 | if info: 188 | params['info'] = info 189 | 190 | response = self.send_command(namespace="user", params=params) 191 | if not response.startswith(u"OK"): 192 | raise SmsApiException(response) 193 | return True 194 | 195 | def get_points(self): 196 | """Get user points""" 197 | response = self.send_command(namespace="user", params={ 198 | 'credits': 1, 199 | 'details': 1, 200 | }) 201 | response = response.replace(u"Points: ", u"").split(u";") 202 | return { 203 | 'points': float(response[0]), 204 | 'pro': float(response[1]), 205 | 'eco': float(response[2]), 206 | 'mms': float(response[3]), 207 | 'vms_gsm': float(response[4]), 208 | 'vms_land': float(response[5]), 209 | } 210 | 211 | def get_senders(self): 212 | """Get sender (caller ID) list""" 213 | response = self.send_command(namespace="sender", params={ 214 | 'list': 1, 215 | 'format': 'json', 216 | }) 217 | try: 218 | return json.loads(response) 219 | except ValueError: 220 | raise SmsApiException(response) 221 | 222 | def get_sender_status(self, name): 223 | response = self.send_command(namespace="sender", params={ 224 | 'status': name, 225 | 'format': 'json', 226 | }) 227 | try: 228 | return json.loads(response) 229 | except ValueError: 230 | raise SmsApiException(response) 231 | 232 | def add_sender(self, name): 233 | response = self.send_command(namespace="sender", params={ 234 | 'add': name, 235 | }) 236 | if response != u"OK": 237 | raise SmsApiException(response) 238 | return True 239 | 240 | def delete_sender(self, name): 241 | """Delete sender""" 242 | response = self.send_command(namespace="sender", params={ 243 | 'delete': name, 244 | }) 245 | if not response.startswith(u"OK"): 246 | raise SmsApiException(response) 247 | return True 248 | 249 | def set_default_sender(self, name): 250 | """Set default sender to name""" 251 | response = self.send_command(namespace="sender", params={ 252 | 'default': name, 253 | }) 254 | if response != "OK": 255 | raise SmsApiException(response) 256 | return True 257 | 258 | def send_sms(self, message, sender_name=None, recipient=None, group=None, 259 | flash=False, test=False, get_details=False, date=None, 260 | data_coding=False, idx=None, check_idx=False, eco=False, 261 | nounicode=False, normalize=False, fast=False, partner_id=None, 262 | max_parts=None, expiration_date=None): 263 | params = { 264 | 'message': message.encode('utf-8'), 265 | 'encoding': 'utf-8', 266 | } 267 | if sender_name: 268 | params['from'] = sender_name 269 | if recipient: 270 | params['to'] = recipient 271 | if group: 272 | params['group'] = group 273 | if flash: 274 | params['flash'] = 1 275 | if test: 276 | params['test'] = 1 277 | if get_details: 278 | params['details'] = 1 279 | if date: 280 | params['date_validate'] = 1 # Always perform date check 281 | params['date'] = time.mktime(date) 282 | if data_coding: 283 | params['datacoding'] = u"bin" 284 | if idx: 285 | params['idx'] = idx 286 | if check_idx: 287 | params['check_idx'] = 1 288 | if eco: 289 | params['eco'] = 1 290 | if nounicode: 291 | params['nounicode'] = 1 292 | if normalize: 293 | params['normalize'] = 1 294 | if fast: 295 | params['fast'] = 1 296 | if partner_id: 297 | params['partner_id'] = partner_id 298 | if max_parts and int(max_parts) <= 6: 299 | params['max_parts'] = max_parts 300 | if expiration_date: 301 | params['expiration_date'] = time.mktime(expiration_date) 302 | response = self.send_command(namespace="sms", params=params) 303 | if not response.startswith(u"OK"): 304 | raise SmsApiException(response) 305 | 306 | # Check if message was sent to multiple recipients and return list 307 | if u"," in recipient: 308 | data_list = response.split(u";") 309 | data = [] 310 | for message in data_list: 311 | message = message.split(u":") 312 | data.append({ 313 | 'id': message[1], 314 | 'cost': message[2], 315 | 'status': message[0], 316 | }) 317 | return data 318 | 319 | data = response.split(u":") 320 | return [{ 321 | 'id': data[1], 322 | 'cost': data[2], 323 | 'status': data[0], 324 | }] 325 | 326 | def delete_scheduled_sms(self, id): 327 | """ 328 | Delete scheduled SMS message. 329 | 330 | Returns True if message was deleted and False when ID doesn't exists. 331 | """ 332 | response = self.send_command(namespace="sms", params={ 333 | 'sch_del': id, 334 | }) 335 | if response != "OK": 336 | return False 337 | return True 338 | 339 | 340 | class SmsApiAddressBook(SmsApiBaseObject): 341 | """Address Book contains numbers and groups of numbers""" 342 | 343 | def __init__(self, *args, **kwargs): 344 | super(SmsApiAddressBook, self).__init__(*args, **kwargs) 345 | 346 | if not self.soap_client: 347 | raise SmsApiException(u"Suds not installed. Cannot use SmsApiAddressBook.") 348 | 349 | def get_groups(self): 350 | """Return list of groups""" 351 | response = self.send_soap_command("get_groups", [ 352 | self.username, 353 | self.password, 354 | ]) 355 | if not "groups" in response: 356 | return None 357 | retlist = [] 358 | for g in response['groups']: 359 | retlist.append({ 360 | 'id': int(g['id']), 361 | 'name': unicode(g['name']), 362 | 'description': unicode(g['info']), 363 | 'number_count': int(g['num_count']), 364 | }) 365 | return retlist 366 | 367 | def add_group(self, name, description=u""): 368 | """Add group. Return ID (int) if added, Exception otherwise""" 369 | response = self.send_soap_command("add_group", [self.soap_user, name, description]) 370 | try: 371 | return response['group_id'] 372 | except KeyError: 373 | pass 374 | raise SmsApiException("Couldn't add group") 375 | 376 | def delete_group(self, id): 377 | """Delete group by given id. Returns True if deleted or error dict""" 378 | response = self.send_soap_command("delete_group", [self.soap_user, int(id)]) 379 | return { 380 | 'code': response['result'], 381 | 'description': response['description'], 382 | } 383 | 384 | def get_numbers(self, group_id=-1): 385 | """Return list of numbers""" 386 | response = self.send_soap_command("get_numbers", [self.soap_user, group_id]) 387 | if "numbers" not in response: 388 | return [] 389 | retlist = [] 390 | for n in response["numbers"]: 391 | retlist.append({ 392 | 'name': n["name"], 393 | 'number': n["number"], 394 | 'group_id': n["group_id"], 395 | }) 396 | return retlist 397 | 398 | def add_number(self, number, name, group_id=-1): 399 | """Add number""" 400 | num = self.soap_client.factory.create('Number') 401 | num.number = str(number) 402 | num.name = name 403 | num.group_id = group_id 404 | 405 | self.send_soap_command("add_number", [self.soap_user, num]) 406 | return True 407 | 408 | def delete_number(self, number, group_id=-1): 409 | """Delete number""" 410 | self.send_soap_command("delete_number", [self.soap_user, number, group_id]) 411 | return True 412 | --------------------------------------------------------------------------------