Commits

Victor Gavro committed 471c8b6

initial

Comments (0)

Files changed (15)

+syntax: glob
+
+*.pyc
+Application to send SMS via various SMS Gates API (not email gates).
+
+currently supported backends:
+messagebox.com.ua
+
+You can easilly extend application with another backend, contribute it to project would be nice too.
+
+#TODO: i18n
+#TODO: validation of sms text
+#TODO: PhoneField
+#TODO: отправка больших смс-сообщений?
+from smsgate import settings
+from django.core.exceptions import ImproperlyConfigured
+
+backend = __import__(settings.BACKEND)
+
+def _get_callback_url(callback):
+  if callback == True:
+    if settings.CALLBACK_URL:
+      return settings.CALLBACK_URL
+    else:
+      raise ImproperlyConfigured
+  else:
+    return callback
+
+def send_mass_sms(datatuple,sender=settings.SENDER,callback=False,fail_silently=False):
+  callback_url = _get_callback_url(callback)
+  try:
+    if hasattr(backend,'send_mass_sms'):
+      return backend.send_mass_sms(datatuple,sender=sender,callback_url=callback_url)
+    else:
+      result = []
+      for args in datatuple:
+        result.append(backend.send_sms(*args,sender=sender,callback_url=callback_url))
+      return result
+  except:
+    if not fail_silently:
+      raise
+
+def send_sms(message,recipient_list,sms_ids=None,sender=settings.SENDER,callback=False,fail_silently=False):
+  callback_url = _get_callback_url(callback)
+  try:
+    return backend.send_sms(message,recipient_list,sms_ids=sms_ids,sender=sender,callback_url=callback_url)
+  except:
+    if not fail_silently:
+      raise
+
+def get_sms_status(sms_ids,fail_silently=False):
+  try:
+    return backend.get_sms_status(sms_ids)
+  if not fail_silently:
+    raise
+
+def sms_callback(request):
+  return backend.sms_callback(request)
+
+def sms_admins(message,fail_silently=False):
+  if settings.ADMINS_PHONES:
+    return send_sms(message,settings.ADMINS_PHONES,fail_silently=fail_silently)
+
+def sms_managers(message,fail_silently=False):
+  if settings.MANAGERS_PHONES:
+    return send_sms(message,settings.MANAGERS_PHONES,fail_silently=fail_silently)

backends/__init__.py

Empty file added.

backends/mssgbox_com/__init__.py

Empty file added.

backends/mssgbox_com/constants.py

+from smsgate.constants import *
+
+ERROR_CODES = {
+  '1': 'Authentication failed',
+  '2': 'Unknown username',
+  '3': 'Invalid password',
+  '4': 'Invalid or missing API ID',
+  '5': 'Invalid or expired session ID',
+  '6': 'Account locked',
+  '7': 'IP Lockdown violation',
+  '101': 'Invalid or missing parameters',
+  '102': 'Unknown message UID',
+  '103': 'Missing message UID',
+  '104': 'Invalid destination address',
+  '105': 'Invalid source address',
+  '106': 'Empty message',
+  '107': 'Invalid sender id',
+  '108': 'Invalid delivery time',
+  '120': 'Cannot route message',
+  '121': 'Destination mobile number blocked',
+  '201': 'No credit left',
+  '202': 'Max allowed credit',
+  '301': 'Internal error',
+}
+
+STATUS_CODES = {
+  '1': (STATUS_CANCELED, 'Message unknown'),
+  '2': (STATUS_QUEUED, 'Message queued'),
+  '3': (STATUS_DELIVERED, 'Delivered to gateway'),
+  '4': (STATUS_DELIVERED, 'Recieved by recipient'),
+  '5': (STATUS_UNDELIVERED,'Error with message'),
+  '6': (STATUS_CANCELED, 'User cancelled message delivery'),
+  '7': (STATUS_UNDELIVERED, 'Error delivering message'),
+  '8': (STATUS_SENT, 'OK'),
+  '9': (STATUS_UNDELIVERED, 'Routing error'),
+  '10': (STATUS_EXPIRED, 'Message expired'),
+  '11': (STATUS_QUEUED, 'Message queued for later delivery'),
+  '12': (STATUS_CANCELED, 'Out of credit'),
+}

backends/mssgbox_com/http_v1.py

+from smsgate.utils import http_request
+from smsgate.exceptions import SmsGateError,SmsGateUnknownResponse
+from smsgate.settings import *
+from smsgate.constants import *
+from smsgate.backends.mssgbox_com.constants import *
+from django.core.exceptions import ImproperlyConfigured
+import re
+
+COMMAND_URL = 'http://api.mssgbox.com/http/v1/%s.do'
+AUTH_CREDITALS = {
+  'api_id': API_ID,
+  'login': LOGIN,
+  'password': PASSWORD,
+}
+if None in AUTH_CREDITALS.values():
+  raise ImproperlyConfigured('You must specify API_ID,LOGIN,PASSWORD to work with this module')
+
+def send_command(command,authenticate=True,**params):
+  if authenticate and not params.get('session_id',None):
+    params.update(AUTH_CREDITALS)
+  page = http_request(COMMAND_URL % command, params)
+  return page.code,page.read()
+
+def parse(response,regexp):
+  result = re.findall(regexp,response[1])
+  if not result:
+    raise SmsGateUnknownResponse(response)
+  return result.groups()
+
+def auth():
+  '''Returns session_id for mass sms sending.'''
+  match = parse(send_command('auth'),'(OK|ERR): ([0-9a-f]+)')[0]
+  if match[0] == 'ERR':
+    raise SmsGateError(match[1],ERROR_CODES.get(match[1],'Unknown code: %s' % match[1]))
+  return match[1]
+
+def sendMsg(message,recipient_list,sender,**options):
+  options.update({'to': recipient_list,'text': message,'from': sender,})
+  regexp = '(ERR|ID): ([0-9a-f]+) To: (\d+)' if len(recipient_list) > 1 else '(ERR|ID): ([0-9a-f]+)'
+  match_all = parse(send_command('sendMsg',**options),regexp)
+
+  result = []
+  for match in match_all:
+    #TODO Assuming that recipient statuses returned with same order as we posted them
+    if match[0] == 'ERR':
+      result.append((None,STATUS_CANCELED,ERROR_CODES.get(match[1],'Unknown code: %s' % match[1])))
+    else:
+      result.append((match[1],STATUS_SENT,None))
+  return tuple(result)
+
+def queryMsg(sms_id,**options):
+   response = send_command('queryMsg',uid=sms_id,**options)
+   match = parse(response,'(Status|ERR): (\d+)')[0]
+   if match[0] == 'ERR':
+     return None,ERROR_CODES.get(match[1],'Unknown code: %s' % match[1])
+   return STATUS_CODES.get(match[1],(None,'Unknown status: %s' % match[1]))
+
+def get_sms_status(sms_ids):
+  kwargs = {}
+  if len(sms_ids) > 1:
+    kwargs['session_id'] = auth()
+  result = []
+  for sms_id in sms_ids:
+    result.append(queryMsg(sms_id,**kwargs))
+  return result
+
+def send_sms(message,recipient_list,sms_ids,sender,callback_url):
+  return sendMsg(message,recipient_list,sender,msg_callback=bool(callback_url))
+
+def send_mass_sms(datatuple,sender,callback_url):
+  session_id = auth()
+  for message,recipient_list,sms_ids in datatuple:
+    yield sendMsg(message,recipient_list,sender,msg_callback=bool(callback_url),session_id=session_id)
+#Status only for models - to save into db messages that was\n't
+STATUS_LOCAL_QUEUED = 1
+#Message queued on server
+STATUS_QUEUED = 1
+#Message was not sent, and never will be
+STATUS_CANCELED = 2
+#Message is sent ok, but status unknown
+STATUS_SENT = 3
+#Message delivered to recipient
+STATUS_DELIVERED = 4
+#Message undelivered to recipient
+STATUS_UNDELIVERED = 5
+#Message was sent, but requesting status expired
+STATUS_EXPIRED = 6
+
+class SmsGateError(StandardError):
+  pass
+
+class SmsGateUnknownResponse(SmsGateError):
+  pass
+
+class SmsGateUnknownCode(SmsGateError):
+  pass
+from django.db import models
+from smsgate import settings
+
+def validate_text(value):
+  return True
+
+class SmsBody(models.Model):
+  text = models.TextField(u'Текст SMS',max_length=settings.TEXT_MAX_LENGTH,validators=[validate_text,])
+  sender  = models.CharField(max_length=32,default=settings.SENDER)
+
+  def __unicode__(self):
+    return self.text
+
+  def get_phones(self):
+    return [x[0] for x in self.recipients.values_list('phone')]
+
+  def send(self):
+    '''returns count of sent messages'''
+    result = settings.api.send_message(self.get_phones(),self.text,self.from)
+    sent_count = 0
+    for phone,sms_id,status,status_message in result:
+      if sms_id: sent_count += 1
+      sms = self.recipients.get(phone=phone)
+      sms.sms_id,sms.status,sms.status_message = sms_id,status,status_message
+      sms.save()
+    self.save()
+    return sent_count
+
+  @classmethod
+  def send_all(cls):
+    for obj in cls.objects.get(time_sent__isnull=True):
+      obj.send()
+
+class Sms(models.Model):
+  STATUS_CHOICES = (
+    (STATUS_QUEUED,'В очереди на отправку'),
+    (STATUS_SENT, 'Отправлено'),
+    (STATUS_DELIVERED, 'Доставлено'),
+    (STATUS_UNDELIVERED, 'Не доставлено'),
+    (STATUS_EXPIRED, u'Срок доставки истек'),
+    (STATUS_CANCELED, u'Отменено'),
+  )
+  phone = models.PhoneField()
+  sms_id = models.CharField(max_length=32,editable=False)
+  sms_count = models.SmallIntegerField('Количество смс', editable=False)
+  status = models.SmallIntegerField(u'Состояние',choices=STATUS_CHOICES,default=STATUS_SENT,editable=False)
+  time_queued = models.DateTimeField(null=True,blank=True,editable=False)
+  time_status = models.DateTimeField(null=True,blank=True,editable=False)
+  time_sent = models.DateTimeField(null=True,blank=True,editable=False)
+  status_message = models.CharField(u'Сообщение',max_length=256,null=True,blank=True,editable=False)
+  body = models.ForeignKey(SmsBody,editable=False,related_name='recipients')
+
+  class Meta:
+    unique_together = (('phone','body'),)
+    ordering = ('-time_queued')
+
+  def __unicode__(self):
+    return u'%s %s' % (self.phone,self.sms_id)
+
+  def get_status_all(cls):
+    today,now = date.today(),datetime.now()
+    messages = {}
+    for obj in cls.objects.filter(status=cls.SENT):
+      if ((today - obj.time_sent.date()).days > settings.GET_STATUS_MAX_DAYS):
+        obj.status = cls.STATUS_EXPIRED
+      else:
+        messages[obj.sms_id].append(sms_id)
+
+from smsgate import settings
+from smsgate import send_sms, send_mass_sms
+from django.db import models
+from datetime import datetime
+
+class SmsBody(models.Model):
+  text = models.TextField('SMS text',validators=[validate_text,])
+  sender = models.CharField(max_length=32,default=settings.SENDER)
+
+  def __unicode__(self):
+    return self.text
+
+  def send(self,recipients_list=()):
+    if not recipients_list:
+      raise ValueError('You must specify recipients to send.')
+    result = []
+    for recipient in recipients_list:
+      Sms(body=self,recipient=recipient)
+
+    #TODO send to recipients_list with save - or resend to active recipients
+    pass
+
+class SmsEntity(models.Model):
+  STATUS_CHOICES = (
+    (STATUS_QUEUED,'В очереди на отправку'),
+    (STATUS_CANCELED, u'Отменено'),
+    (STATUS_SENT, 'Отправлено'),
+    (STATUS_DELIVERED, 'Доставлено'),
+    (STATUS_UNDELIVERED, 'Не доставлено'),
+    (STATUS_EXPIRED, u'Срок доставки истек'),
+  )
+  recipient = PhoneField()
+  sms_id = models.CharField(max_length=32,editable=False)
+  status = models.SmallIntegerField(choices=STATUS_CHOICES,default=STATUS_QUEUED,editable=False)
+  queued_time = models.DateTimeField(default=datetime.now,editable=False)
+  status_time = models.DateTimeField(null=True,blank=True,editable=False)
+  sent_time = models.DateTimeField(null=True,blank=True,editable=False)
+  status_message = models.CharField(max_length=64,null=True,blank=True,editable=False)
+  sms = models.ForeignKey(Sms,editable=False,related_name='recipients')
+
+  class Meta:
+    unique_together = (('phone','sms'),)
+    ordering = ('-queued_time')
+
+  @classmethod
+  def update_status_all(cls):
+    today = date.today()
+    sms_ids = []
+    for obj in cls.objects.filter(status=STATUS_SENT):
+      if ((today - obj.time_sent.date()).days > settings.STATUS_MAX_DAYS):
+        obj.status = STATUS_EXPIRED
+      else:
+        sms_ids.append(obj.id)
+    statuses = get_sms_status(sms_ids)
+    #TODO sort by SMS statuses and update
+
+  @classmethod
+  def send_all(cls):
+    #TODO
+    for obj in cls.objects.filter(sms_id=None,status=STATUS_QUEUED):
+      pass
+
+from django.conf import settings
+
+def _get(name,default):
+  return getattr(settings,'SMSGATE_' + name, default)
+
+#Authentication information for usage by backends
+LOGIN = _get('LOGIN',None)
+PASSWORD = _get('PASSWORD',None)
+API_ID = _get('API_ID',None)
+
+#Please specify existing backend or your backend module, for example 'myapp.sms_backend'
+BACKEND = _get('BACKEND','smsgate.backends.mssgbox_com.http_v1')
+
+#Send sms on saving sms in database. You may turn it off to send messages by cron.
+SEND_ON_SAVE = _get('SEND_ON_SAVE',True)
+
+#Callback url for recieving sms statuses by smsgates
+#Do not forget to include it to your urls.py
+#Note, that on some backends CALLBACK_URL configured on sms-gate server side and isn\'t sent, in that case you may set CALLBACK_URL=True
+CALLBACK_URL = _get('CALLBACK_URL',None)
+
+STATUS_MAX_DAYS = 60
+
+#Default sender
+SENDER = _get('SENDER','django-smsgate')
+
+#Specify connection settings
+HTTP_PROXY = _get('HTTP_PROXY',None)
+
+HTTP_TIMEOUT = _get('HTTP_TIMEOUT',None)
+#Set timeouts only for Python 2.6+
+from sys import version_info
+if version_info <= (2,6):
+  HTTP_TIMEOUT = None
+
+ADMINS_PHONES = _get('ADMINS_PHONES',())
+MANAGERS_PHONES _get('MANAGERS_PHONES',())
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+    def test_basic_addition(self):
+        """
+        Tests that 1 + 1 always equals 2.
+        """
+        self.failUnlessEqual(1 + 1, 2)
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+
+from smsgate import settings
+import urllib
+import urllib2
+
+def _urlencode(data):
+  '''Small workaround for urlencode to recieve dict and nested tuples'''
+  post_encode = []
+  items = post.items() if hasattr(post,'items') else post
+  for key,value in items:
+    if hasattr(value,'__iter__'):
+      for subvalue in value:
+        post_encode.append((key,subvalue))
+    else:
+      post_encode.append((key,value))
+  return urllib.urlencode(post_encode)
+
+def http_request(url,params,method='POST',extra_handlers=[]):
+  #TODO http_request helper currently supports only POST method
+  if method != 'POST': raise NotImplementedError
+  handlers = [urllib2.HTTPHandler(), urllib2.HTTPSHandler(),] + extra_handlers
+  opener = urllib2.build_opener(*handlers)
+  if settings.HTTP_PROXY:
+    opener.add_handler(urllib2.ProxyHandler(settings.HTTP_PROXY))
+  request = urllib2.Request(url,_urlencode(params))
+
+  kwargs = {}
+  if settings.HTTP_TIMEOUT:
+    kwargs['timeout'] = settings.HTTP_TIMEOUT
+  page = opener.open(request,**kwargs)
+  return page
+
+# Create your views here.