class CronSing(object): """ A singleton providing task scheduling facilities. A function may be registered by calling register(function, name) (returning a Task object). A registered function may be scheduled with either: - setTimeout(time, name, params) / doAtTime(time, locality, name, params) - or by calling either method on the Task object returned by register(), omitting the name parameter. Note that params will be pickled for storage in the database. When it comes to be called, the function will be called with an argument unpickled from params (so even if the function needs no arguments it should allow for one eg def foo(_) rather than def foo()). """ def __init__(self): self.delayedcall = None self.fun_dict = {} self.db = Database('Cron') # table for tasks which will be called after a certain amount of time if not self.db.table_exists('cron_delay'): self.db.create_table('cron_delay', ['timestamp', 'reg_name', 'params']) # table for tasks which will be called at a specific time if not self.db.table_exists('cron_datetime'): self.db.create_table( 'cron_datetime', ['datetime', 'locality', 'reg_name', 'params']) def seconds_until(self, td): ds, ss, uss = td.days, td.seconds, td.microseconds return float((uss + (ss + ds * 24 * 3600) * 10**6)) / 10**6 def register(self, function, reg_name, persistent=True): """ Register the callable fun against reg_name. Returns a Task object and allows callable to be scheduled with doAtTime or setTimeout either on self, or on the Task object returned. If persistent is False, any previous reigstrations against reg_name will be deleted before the new function is registered. """ # reg_name is the key we use to access the function - we can then set the # function to be called using setTimeout or doAtTime with regname = name # remove any prior functions with this reg_name if not persistent: self.removeTask(reg_name) self.fun_dict.update({reg_name: function}) return Task(reg_name, self, function) def cancel(self): if self.delayedcall: self.delayedcall.cancel() self.delayedcall = None def do_crons(self): if self.delayedcall: self.delayedcall.cancel() self.delayedcall = reactor.callLater(0, self._do_crons) def _time_left_delay(self, pickleTime): curtime = datetime.datetime.now(timezone('GMT')) dt = cPickle.loads(str(pickleTime)) dt_gmt = dt.astimezone(timezone('GMT')) time_delta = dt_gmt - curtime return self.seconds_until(time_delta) def _time_left_set_time(self, pickleTime, locality): curtime = datetime.datetime.now(timezone('GMT')) dt = cPickle.loads(str(pickleTime)) dt_local = timezone(locality).localize(dt) dt_gmt = dt_local.astimezone(timezone('GMT')) time_delta = dt_gmt - curtime return self.seconds_until(time_delta) def _do_crons(self): self.delayedcall = None # retrieve the information about our two kinds of scheduled tasks from # the database delays = self.db.fetch('cron_delay', ['timestamp', 'reg_name', 'params', 'rowid']) set_times = self.db.fetch( 'cron_datetime', ['datetime', 'reg_name', 'params', 'locality', 'rowid']) # transform the two types of data into a consistant format and combine # data is the raw information we retrieved from self.db crons_d = [{ 'table': 'cron_delay', 'data': data, 'time_left': self._time_left_delay(data['timestamp']) } for data in delays] crons_s = [{ 'table': 'cron_datetime', 'data': data, 'time_left': self._time_left_set_time(data['datetime'], data['locality']) } for data in set_times] crons = crons_d + crons_s shortest = None # run all crons with time_left <= 0, find smallest time_left amongst # others and reschedule ourself to run again after this time for cron in crons: if cron['time_left'] <= 0: # the function is ready to be called # remove the entry from the database and call it self.db.delete(cron['table'], cron['data']) logging.info("Running Cron: {}".format( cron['data']['reg_name'])) params = cPickle.loads(str(cron['data']['params'])) try: self.fun_dict[cron['data']['reg_name']](params) except KeyError: # If there has been a restart we will have lost our fun_dict # If functions have not been re-registered then we will have a problem. logging.error( "Failed to run Cron: {} not in dictionary".format( cron['data']['reg_name'])) else: # update the shortest time left if (shortest is None) or cron['time_left'] < shortest: shortest = cron['time_left'] if not shortest is None: #ie there is another function to be scheduled self.delayedcall = reactor.callLater(shortest, self._do_crons) def doAtTime(self, time, locality, reg_name, params): """ Start a cron job to trigger the specified function ('reg_name') with the specified arguments ('params') at time ('time', 'locality'). """ lTime = timezone(locality).localize(time) gTime = lTime.astimezone(timezone('GMT')) fmt = "Cron task '{}' set for {} ({} GMT)" logging.info(fmt.format(reg_name, lTime, gTime)) t, p = self._pickleTimeParams(time, params) self.db.insert('cron_datetime', { 'datetime': t, 'locality': locality, 'reg_name': reg_name, 'params': p }) self.do_crons() def setTimeout(self, timedelta, reg_name, params): """ Start a cron job to trigger the specified registration ('reg_name') with specified arguments ('params') after delay ('timedelta'). timedelta may either be a datetime.timedelta object, or a real number representing a number of seconds to wait. Negative or 0 values will trigger near immediately. """ if not isinstance(timedelta, datetime.timedelta): timedelta = datetime.timedelta(seconds=timedelta) fmt = 'Cron task "{0}" set to run after {1}' logging.info(fmt.format(reg_name, str(timedelta))) time = datetime.datetime.now(timezone('GMT')) + timedelta t, p = self._pickleTimeParams(time, params) self.db.insert('cron_delay', { 'timestamp': t, 'reg_name': reg_name, 'params': p }) self.do_crons() def removeTask(self, reg_name): """Remove any scheduled tasks registered with reg_name.""" self.db.delete('cron_delay', {'reg_name': reg_name}) self.db.delete('cron_datetime', {'reg_name': reg_name}) self.fun_dict.pop('reg_name', None) def getAtTimes(self): """ Return a string showing the registration names of functions scheduled with doAtTime and the amount of time they will be called in. """ def get_single_string(data): fmt = " name '{}' to run in '{}:{}:{}'" name = data['reg_name'] delay = int( round( self._time_left_set_time(data['datetime'], data['locality']))) return fmt.format(name, delay // 3600, (delay % 3600) // 60, delay % 60) data = self.db.fetch('cron_datetime', ['reg_name', 'locality', 'datetime']) return "Datetime registrations:\n" + '\n'.join( map(get_single_string, data)) def getTimeouts(self): """ Return a string showing the registration names of functions scheduled with setTimeout and the amount of time they will be called in. """ def get_single_string(data): fmt = " name '{}' to run in '{}:{}:{}'" name = data['reg_name'] delay = int(round(self._time_left_delay(data['timestamp']))) return fmt.format(name, delay // 3600, (delay % 3600) // 60, delay % 60) data = self.db.fetch('cron_delay', ['reg_name', 'timestamp']) return "Timeout registrations:\n" + '\n'.join( map(get_single_string, data)) def _pickleTimeParams(self, time, params): return cPickle.dumps(time), cPickle.dumps(params)
class PubPicker(Plugin): name = "pubpicker" def enInit(self): self.venuetype = self.vars.get("venuetype", "pub") if ":" in self.venuetype: self.venuetype, self.pluraltype = self.venuetype.split(":") else: self.pluraltype = self.venuetype + "s" com = self.get('endroid.plugins.command') com.register_both(self.register, ('register', self.venuetype), '<name>', synonyms=(('register', 'new', self.venuetype), ('register', 'a', 'new', self.venuetype))) com.register_both(self.picker, ('pick', 'a', self.venuetype), synonyms=(('pick', self.venuetype), )) com.register_both(self.vote_up, ('vote', self.venuetype, 'up'), '<name>') com.register_both(self.vote_down, ('vote', self.venuetype, 'down'), '<name>') com.register_both(self.alias, ('alias', self.venuetype), '<name> to <alias>') com.register_both(self.list, ('list', self.pluraltype), synonyms=(('list', self.venuetype), )) com.register_both(self.list_aliases, ('list', self.venuetype, 'aliases')) com.register_both(self.rename, ('rename', self.venuetype), '<oldname> to <newname>') self.db = Database(DB_NAME) self.setup_db() self.load_db() def help(self): return "Let EnDroid pick a " + self.venuetype + " for you!" def load_db(self): self.pubs = {} self.aliases = {} for row in self.db.fetch(DB_TABLE, ("name", "score")): self.pubs[row["name"]] = int(row["score"]) for row in self.db.fetch(DB_ALIAS, ("name", "alias")): self.aliases[row["alias"]] = row["name"] def setup_db(self): if not self.db.table_exists(DB_TABLE): self.db.create_table(DB_TABLE, ("name", "score")) if not self.db.table_exists(DB_ALIAS): self.db.create_table(DB_ALIAS, ("name", "alias")) def add_alias(self, pub, alias): if not alias: raise AliasEmptyError('Pub name cannot be empty.') if pub == alias: return # loops are bad :-) if alias in self.aliases: self.db.delete(DB_ALIAS, {"alias": alias}) self.aliases[alias] = pub self.db.insert(DB_ALIAS, {"alias": alias, "name": pub}) self.add_alias(alias, alias.lower()) def resolve_alias(self, alias): seen = set() while alias in self.aliases: if alias in seen: raise AliasError(alias) alias = self.aliases[alias] seen.add(alias) if not alias: raise AliasEmptyError('Pub string cannot be empty.') return alias # command def vote_up(self, msg, pub): if not pub: msg.reply('Pub string should not be empty.') return try: pub = self.resolve_alias(pub) except AliasError as e: return msg.reply(ALIAS_ERROR.format(e)) except AliasEmptyError: return msg.reply('Pub name cannot be empty.') if pub not in self.pubs: self.pubs[pub] = 10 self.db.insert(DB_TABLE, {"name": pub, "score": 10}) self.add_alias(pub, pub.lower()) else: self.pubs[pub] += 1 self.save_pub(pub) # command def vote_down(self, msg, pub): if not pub: msg.reply('Pub string should not be empty.') return try: pub = self.resolve_alias(pub) except AliasError as e: return msg.reply(ALIAS_ERROR.format(e)) except AliasEmptyError: return msg.reply('Pub name cannot be empty.') if pub in self.pubs: self.pubs[pub] = max(self.pubs[pub] - 1, 0) self.save_pub(pub) def rename_pub(self, oldname, newname): if not newname: raise AliasEmptyError('Pub name must not be empty.') self.add_alias(newname, oldname) score = self.pubs[oldname] self.db.delete(DB_TABLE, {"name": oldname}) del self.pubs[oldname] self.pubs[newname] = score self.db.insert(DB_TABLE, {"name": newname, "score": score}) self.add_alias(newname, newname.lower()) def save_pub(self, pub): self.db.update(DB_TABLE, {"score": self.pubs[pub]}, {"name": pub}) self.add_alias(pub, pub.lower()) def pick_a_pub(self): # I imagine this can be done more cheaply... pubs = [] for pub, score in self.pubs.items(): if pub: # if someone enters an empty pub and we pick that pub, # we'll end up reacting as if we didn't know any pubs # if we test for "if not __" rather than "if __ is None" pubs += [pub] * score if pubs: return random.choice(pubs) else: return None # command def register(self, msg, arg): try: self.vote_up(msg, self.resolve_alias(arg)) except AliasError as e: msg.reply(ALIAS_ERROR.format(e)) except AliasEmptyError: msg.reply('Pub name cannot be empty.') # command def picker(self, msg, arg): pub = self.pick_a_pub() if pub is not None: msg.reply("Today, you should definitely go to %s" % pub) else: msg.reply("Unfortunately, I don't seem to know about any pubs " "that anyone wants to go to") # command def alias(self, msg, arg): mf = ALIAS_FOR.match(arg) mt = ALIAS_TO.match(arg) if mf: self.add_alias(mf.group(2), mf.group(1)) elif mt: self.add_alias(mt.group(1), mt.group(2)) else: msg.unhandled() # command def rename(self, msg, arg): mt = ALIAS_TO.match(arg) if mt: try: self.rename_pub(self.resolve_alias(mt.group(1)), mt.group(2)) except AliasError as e: msg.reply(ALIAS_ERROR.format(e)) except AliasEmptyError: msg.reply('Pub name cannot be empty.') else: msg.unhandled() # command def list(self, msg, arg): reply = ["Registered " + self.pluraltype + " (and their scores):"] reply.extend("%s (%d)" % (p, s) for p, s in self.pubs.items()) msg.reply("\n".join(reply)) # command def list_aliases(self, msg, arg): reply = ["Known " + self.venuetype + " aliases:"] reply.extend("%s -> %s" % (a, p) for a, p in self.aliases.items()) msg.reply("\n".join(reply)) def dependencies(self): return ['endroid.plugins.command']
class ReliableSend(CommandPlugin): """Plugin to send messages to a user, storing them if they are offline.""" name = "reliable" help = ("Send a message, storing it to be sent automatically later if the " "recipient is not online. \n " "Commands: reliable message <recipient> <message> to send message " "to given person, storing it if necessary.") def endroid_init(self): self.db = Database(DB_NAME) if not self.db.table_exists(DB_TABLE): self.db.create_table(DB_TABLE, DB_COLUMNS) def _delete_messages(self, recipient): """ Deletes all message that were stored for later sending. Take care with this: it will allow you to delete even messages your plugin didn't send. :param recipient: JID of the person in question """ self.db.delete(DB_TABLE, {'recipient': recipient}) def _send_stored_messages(self, recipient): """ Sends all the stored messages for a given recipient. Note that we delete them from storage afterwards. :param recipient: the JID of the person we want to receive the messages """ cond = {'recipient': recipient} rows = self.db.fetch(DB_TABLE, DB_COLUMNS, cond) for row in rows: message = "Message received from {} on {}:\n{}".format( row['sender'], row['date'], row['text']) self.messages.send_chat(recipient, message) self._delete_messages(recipient) def send_reliably(self, sender, message_text, recipient): """ Sends a message to a user so that they will receive it eventually. If the user is offline, stores the message and sends it when they come online next. :param sender: who sent the message (JID) :param message_text: text of the message :param recipient: who is to receive the message (JID) :return: False if the recipient was already online and so we sent the messages immediately; True if we stored them. """ self.db.insert( DB_TABLE, { 'sender': sender, 'recipient': recipient, 'text': message_text, 'date': time.asctime(time.localtime()) }) if self.rosters.is_online(recipient): self._send_stored_messages(recipient) return False else: cb = lambda: self._send_stored_messages(recipient) self.rosters.register_presence_callback(recipient, callback=cb, available=True) return True @command(helphint="<recipient> <message>", synonyms=('reliable send', )) def reliable_message(self, msg, args): args = args.split() if len(args) < 2: msg.reply( "reliable message <recipient> <message> \t Send a message") return recipient, message = args[0], ' '.join(args[1:]) if not self.send_reliably(msg.sender, message, recipient): msg.reply('{} was already online; I sent the message immediately.'. format(recipient)) else: msg.reply( 'I will send the message to {} when they come online.'.format( recipient))
class SMS(CommandPlugin): """Sends SMS messages to users; exposes relevant functions in Webex.""" class UserNotFoundError(Exception): """When we try to get the number of a user who hasn't given Endroid a number.""" class PersonalSMSLimitError(Exception): """SMS send limit reached; please wait a little to send a text.""" class GlobalSMSLimitError(Exception): """Global SMS send limit reached; please wait a bit to send a text.""" class SMSPeriodSendLimit(Exception): """Personal SMS send limit for this period has been reached.""" class InvalidConfigError(Exception): """Something in the config file is of an invalid format""" class TwilioSMSLengthLimit(Exception): """The SMS message is longer than 1600 characters - don't bother sending it since Twilio won't allow it. """ class InvalidJIDError(Exception): """Raise if JID is of an incorrect format""" class SMSBlock(Exception): """ Raise if you try and send a message to someone who has blocked you from sending messages. """ name = "sms" help = ("Send an SMS to someone. \n" "'sms set number <num>' set your phone number.\n" "'sms whoami' find out what I think your number is.\n" "'sms forget me' make me forget your number.\n" "Aliases: setnum, getnum, delnum.\n" "'sms send <user> <message>' send an SMS to a user.\n" "'sms block <user>' prevent <user> from sending you messages.\n" "'sms unblock <user>' unblock a user.\n" "'sms blocked' display the users you have blocked.\n") dependencies = ['endroid.plugins.ratelimit'] # to allow other plugins access to smslib's errors, we copy them to here SMSError = smslib.SMSError SMSAuthError = smslib.SMSAuthError SMSInvalidNumberError = smslib.SMSInvalidNumberError def endroid_init(self): # set up database, creating it if it doesn't exist if not all([ self.vars["country_code"], self.vars["phone_number"], self.vars["auth_token"], self.vars["twilio_sid"] ]): raise ValueError("Specify country_code, phone_number, auth_token " "and twilio_sid in the Endroid config file.") self._config = { "time_between_sms_resets": 1, "user_bucket_capacity": 3, "user_bucket_fillrate": 0.05, "global_bucket_capacity": 20, "global_bucket_fillrate": 1, "period_limit": 30 } for key in self._config: user_value = self.vars.get(key, "") if user_value: # Check user_value is a number try: a = user_value / 2 self._config[key] = user_value except TypeError: logging.info("{} must be a number".format(key)) self.ratelimit = self.get('endroid.plugins.ratelimit') self.user_buckets = defaultdict(lambda: self.ratelimit.create_bucket( self._config["user_bucket_capacity"], self._config[ "user_bucket_fillrate"])) self.global_bucket = self.ratelimit.create_bucket( self._config["global_bucket_capacity"], self._config["global_bucket_fillrate"]) self.db = Database(DB_NAME) # Create a table to store user's phone numbers if not self.db.table_exists(DB_TABLE): self.db.create_table(DB_TABLE, ("user", "phone")) # Create a table to record the number of SMSs sent per user if not self.db.table_exists(DB_LIMIT_TABLE): self.db.create_table(DB_LIMIT_TABLE, ("user", "texts_sent_this_period")) # Create a table to record any users and a user wants to block if not self.db.table_exists(DB_BLOCK_TABLE): self.db.create_table(DB_BLOCK_TABLE, ("user", "users_blocked")) # logging file stuff self.log = logging.getLogger(__name__) logfile = os.path.expanduser( self.vars.get('logfile', '~/.endroid/sms.log')) self.log.info("Logging SMS activity to {}".format(logfile)) handler = logging.FileHandler(logfile) formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') handler.setFormatter(formatter) self.log.addHandler(handler) self.log.setLevel(logging.INFO) self.cron_name = "SMS_PERIOD_LIMIT" self.cron.register(self._sms_reset_number_sent, self.cron_name) if self.cron_name not in self.cron.getTimeouts(): # Set number sent to reset at some time in the future for the first # time unless there is already a reset time set. # Convert days to seconds time_between = 60 * 60 * 24 * self._config[ "time_between_sms_resets"] self.cron.setTimeout(time_between, self.cron_name, None) def sms_add_sender(self, user): """ Adds someone to the sms database so the number of messages sent is monitored. """ try: self.get_number_sms_sent(user) logging.error("User {} already in database".format(user)) except self.UserNotFoundError: self.db.insert(DB_LIMIT_TABLE, { 'user': user, 'texts_sent_this_period': 0 }) def get_number_sms_sent(self, user): """Returns the number of SMS messages sent by a user in a period.""" results = self.db.fetch(DB_LIMIT_TABLE, ['texts_sent_this_period'], {'user': user}) if not results: raise self.UserNotFoundError return results[0]['texts_sent_this_period'] @command() def sms_blocked(self, msg, arg): users_blocked = self.whos_blocked(msg.sender) if users_blocked: msg.reply("The following users are prevented from sending you " "SMS messages:\n{}".format(", ".join(users_blocked))) else: msg.reply("You haven't blocked anyone at the moment.") @command(helphint="[{sender}]") def sms_block(self, msg, arg): """ Blocks a JID from sending sms messages to this person. msg.sender: this person arg: JID """ user = msg.sender user_to_block = arg.strip() if not user_to_block: msg.reply("You don't appear to have specified a user to block.") return # JID for user_to_block cannot have white space in it if " " in user_to_block: msg.reply( "The user you wish to block can't have whitespace in it.") return people_blocked = self.whos_blocked(user) if user_to_block in people_blocked: msg.reply("User {} already blocked".format(user_to_block)) else: people_blocked.add(user_to_block) self.set_blocked_users(user, people_blocked) msg.reply("{} has been blocked from sending SMS to " "you.".format(user_to_block)) @command(helphint="{sender}") def sms_unblock(self, msg, arg): """ Unblocks the sender from sending SMS messages to the receiver """ user = msg.sender user_to_unblock = arg.strip() if not user_to_unblock: msg.reply("You don't appear to have specified a user to unblock.") return people_blocked = self.whos_blocked(user) if user_to_unblock not in people_blocked: msg.reply("User {} already not blocked".format(user_to_unblock)) else: people_blocked.remove(user_to_unblock) self.set_blocked_users(user, people_blocked) msg.reply("{} has been unblocked from sending SMS to " "you.".format(user_to_unblock)) def set_blocked_users(self, receiver, senders): """Prevent senders from being able to send SMSs to receiver.""" senders_str = ",".join(senders) am_in_db = self.db.count(DB_BLOCK_TABLE, {'user': receiver}) if am_in_db > 0: self.db.update(DB_BLOCK_TABLE, {'users_blocked': senders_str}, {'user': receiver}) else: # This receiver doesn't have an entry in the DB yet self.db.insert(DB_BLOCK_TABLE, { 'user': receiver, 'users_blocked': senders_str }) def whos_blocked(self, receiver): """Which users are not allowed to send SMSs to receiver.""" results = self.db.fetch(DB_BLOCK_TABLE, ['users_blocked'], {'user': receiver}) users_blocked = set() if results: # @@@UNICODE I don't even know where to start... users_blocked_str = results[0]['users_blocked'].encode('ascii') if users_blocked_str: users_blocked = set(users_blocked_str.split(",")) return users_blocked def _sms_reset_number_sent(self, unused_params): """Resets the number of texts sent in the database.""" self.db.empty_table(DB_LIMIT_TABLE) logging.info("Reset the number of SMS messages sent this period " "for all users") self.cron.setTimeout( 60 * 60 * 24 * self._config["time_between_sms_resets"], self.cron_name, None) @command(helphint="{number}", synonyms=("setnum", )) def sms_set_number(self, msg, arg): """ Sets the stored phone number of the sender. """ user = msg.sender number = arg if number[0] == '0': number = self.vars['country_code'] + number[1:] if not PHONE_REGEX.match(number): msg.reply("I don't recognise a number there. Use 11 digits, " "no spaces, or a + followed by 11 to 13 digits. " "I haven't done anything with your request.") return try: self.get_phone_number(user) self.db.update(DB_TABLE, {'phone': number}, {'user': user}) except self.UserNotFoundError: self.db.insert(DB_TABLE, {'user': user, 'phone': number}) msg.reply('Phone number for user {} set to {}.'.format(user, number)) @command(helphint="", synonyms=("getnum", 'sms get number')) def sms_whoami(self, msg, arg): """ Tells the user what Endroid thinks their phone number is. """ user = msg.sender try: msg.reply(self.get_phone_number(user)) return except self.UserNotFoundError: msg.reply("I don't know your number.") @command(helphint="", synonyms=("delnum", "sms forgetme")) def sms_forget_me(self, msg, arg): """ Tells Endroid to clear its stored number for the sender of the message """ user = msg.sender self.db.delete(DB_TABLE, {'user': user}) msg.reply("I've forgotten your phone number.") def number_known(self, user): """ Returns bool(we have a number stored for this JID user). :param user: JID string of user to check :return: True/False if we do/don't know their number. """ try: self.get_phone_number(user) except self.UserNotFoundError: return False return True def get_phone_number(self, user): """ Fetch stored number for a JID; raise UserNotFoundError if it doesn't exibzr resolvest :param user: the JID string of the user to search for :return: a string phone number """ results = self.db.fetch(DB_TABLE, ['phone'], {'user': user}) if not results: raise self.UserNotFoundError(user) return results[0]['phone'] def send_sms(self, sender, jid, message): """ Sends an SMS to the user with the specified JID. Returns a Deferred which acts as a string containing the server's response. Raises self.SMSError or its descendants if there is an error. Checks the user is allowed to send a message and they haven't gone over their allowed SMS messages for this time period and that there are enough tokens in their user bucket and the global bucket. :param sender: hashable object identifying the sender of the SMS :param jid: the user we're sending the SMS to :param message: the message body to send to them :return: output of Twilio, as a Deferred """ self.log.info("Attempting send of SMS from {} to {}.".format( sender, jid)) number_messages = math.ceil(float(len(message)) / 160) if number_messages > 10: raise self.TwilioSMSLengthLimit( "You can only send a message which " "is less than or equal to 1600 " "characters long. Your message is " "{} characters " "long".format(len(message))) try: number_sent = self.get_number_sms_sent(sender) number_sent += number_messages except self.UserNotFoundError: self.sms_add_sender(sender) number_sent = number_messages if sender not in self.whos_blocked(jid): if number_sent <= self._config["period_limit"]: if self.global_bucket.use_token(): if self.user_buckets[sender].use_token(): self.db.update(DB_LIMIT_TABLE, {"texts_sent_this_period": number_sent}, {"user": sender}) number = self.get_phone_number(jid) auth_token = self.vars['auth_token'] endroid_number = self.vars['phone_number'] sid = self.vars['twilio_sid'] response = smslib.send(endroid_number, number, message, sid, auth_token) def log_success(resp): self.log.info("{} to {} succeeded.".format( sender, jid)) return resp def log_failure(fail): self.log.info("{} to {} failed.".format( sender, jid)) return fail response.addCallbacks(log_success, log_failure) return response else: self.log.info( "{} to {} was personally ratelimited.".format( sender, jid)) if number_messages == 1: raise self.PersonalSMSLimitError( 'Personal SMS limit reached') else: raise self.PersonalSMSLimitError( 'Personal SMS limit ' 'will be reached. Your ' 'message will be split ' 'into {} ' 'messages'.format(int(number_messages))) else: self.log.info("{} to {} was globally ratelimited.".format( sender, jid)) raise self.GlobalSMSLimitError('Global SMS limit reached') else: self.log.info("{} has reached their SMS limit " "for the period.".format(sender)) if self._config["period_limit"] <= self.get_number_sms_sent( sender): raise self.SMSPeriodSendLimit("SMS period limit reached.") else: raise self.SMSPeriodSendLimit( "SMS period limit will be reached." " Your message will be split into " "{} messages".format(int(number_messages))) else: self.log.info("{} was blocked from sending sms by " "{}".format(sender, jid)) raise self.SMSBlock("Permission denied.") @command(helphint="{user} {message}") def sms_send(self, msg, args): """ Sends SMS from Endroid with given message body to specified user. """ args_split = args.split(' ') try: to_user = args_split[0] to_send = ' '.join(args_split[1:]) except IndexError: msg.reply('Call me like "sms send [email protected] Hello Patrick!"') return # error if we get an unexpected response from the server def errback(fail): if fail.check(smslib.SMSAuthError): msg.reply( "There was a problem with authentication. Check config.") elif fail.check(smslib.SMSError): msg.reply(fail.getErrorMessage()) else: msg.reply(str(fail)) def callback(response): msg.reply('Message sent!') try: result_deferred = self.send_sms(msg.sender, to_user, to_send) result_deferred.addCallbacks(callback, errback) except self.UserNotFoundError: msg.reply(MESSAGES['user-not-found']) except self.GlobalSMSLimitError: msg.reply( "Endroid has sent its limit of texts - please wait a bit.") except self.PersonalSMSLimitError: msg.reply("You have sent your limit of texts - please wait a bit.") except self.SMSPeriodSendLimit: msg.reply("You have used up your quota of texts for this period.")