Exemple #1
0
class PassTheBomb(CommandPlugin):
    help = "Pass the bomb game for EnDroid"
    bombs = defaultdict(set)  # users : set of bombs
    users = dict()  # user strings : User objects

    def endroid_init(self):
        self.db = Database(DB_NAME)
        if not self.db.table_exists(DB_TABLE):
            self.db.create_table(DB_TABLE, ('user', 'kills'))

        # make a local copy of the registration database
        data = self.db.fetch(DB_TABLE, ['user', 'kills'])
        for dct in data:
            self.users[dct['user']] = User(dct['user'], dct['kills'])

    def cmd_furl_umbrella(self, msg, arg):
        """This is how a user enters the game - allows them to be targeted
        and to create and throw bombs"""
        user = msg.sender
        if not self.get_shielded(user):
            msg.reply("Your umbrella is already furled!")
        else:
            if self.get_registered(user):
                self.users[user].shield = False
            else:  # they are not - register them
                self.db.insert(DB_TABLE, {'user': user, 'kills': 0})
                self.users[user] = User(user, kills=0, shield=False)
            msg.reply("You furl your umbrella!")

    cmd_furl_umbrella.helphint = ("Furl your umbrella to participate in the "
                                  "noble game of pass the bomb!")

    def cmd_unfurl_umbrella(self, msg, arg):
        """A user with an unfurled umbrella cannot create or receive bombs"""
        user = msg.sender
        if self.get_shielded(user):
            msg.reply("Your umbrella is already unfurled!")
        else:
            # to get user must not have been shielded ie they must have furled
            # so they will be in the database
            self.users[user].shield = True
            msg.reply("You unfurl your umbrella! No bomb can reach you now!")

    cmd_unfurl_umbrella.helphint = ("Unfurl your umbrella to cower from the "
                                    "rain of boms!")

    def cmd_bomb(self, msg, arg):
        """Create a bomb with a specified timer.

        eg: 'bomb 1.5' for a 1.5 second fuse

        """

        holder = msg.sender
        if self.get_shielded(holder):
            return msg.reply("Your sense of honour insists that you furl your "
                             "umbrella before lighting the fuse")
        # otherwise get a time from the first word of arg
        try:
            time = float(arg.split(' ', 1)[0])
            # make a new bomb and throw it to its creator
            Bomb(msg.sender, time, self).throw(msg.sender)
            msg.reply("Sniggering evilly, you light the fuse...")
        # provision for a failure to read a time float...
        except ValueError:
            msg.reply("You struggle with the matches")

    cmd_bomb.helphint = ("Light the fuse!")

    def cmd_throw(self, msg, arg):
        """Throw a bomb to a user, eg: 'throw [email protected]'"""
        target = arg.split(' ')[0]
        # we need a bomb to thrown
        if not self.bombs[msg.sender]:
            msg.reply("You idly throw your hat, wishing you had something "
                      "rounder, heavier and with more smoking fuses.")
        # need our umbrella to be furled
        elif self.get_shielded(msg.sender):
            msg.reply("You notice that your unfurled umbrella would hinder "
                      "your throw.")
        # check that target is online
        elif not target in self.usermanagement.get_available_users():
            msg.reply("You look around but cannot spot your target")
        elif self.get_shielded(target):  # target registered/vulnerable?
            msg.reply(
                "You see your target hunkered down under their umbrella. "
                "No doubt a bomb would have little effect on that "
                "monstrosity.")
        else:
            self.bombs[msg.sender].pop().throw(target)
            msg.reply("You throw the bomb!")
            self.messagehandler.send_chat(target, "A bomb lands by your feet!")

    cmd_throw.helphint = ("Throw a bomb!")

    def cmd_kills(self, msg, arg):
        kills = self.get_kills(msg.sender)
        nick = self.usermanagement.get_nickname(msg.sender, self.place_name)
        level = self.get_level(kills)

        text = "{} the {} has {} kill".format(nick, level, kills)
        text += ("" if kills == 1 else "s")
        msg.reply(text)

    cmd_kills.helphint = ("Receive and gloat over you score!")

    def register_kill(self, user):
        kills = self.get_kills(user)
        # change the value of 'kills' to kills+1 in the row where 'user' = user
        self.users[user].kills += 1
        self.db.update(DB_TABLE, {'kills': kills + 1}, {'user': user})

    def get_kills(self, user):
        return self.users[user].kills if user in self.users else 0

    def get_shielded(self, user):
        return self.users[user].shield if user in self.users else True

    def get_registered(self, user):
        return user in self.users

    @staticmethod
    def get_level(kills):
        if kills < 5:
            level = 'novice'
        elif kills < 15:
            level = 'apprentice'
        elif kills < 35:
            level = 'journeyman'
        elif kills < 65:
            level = 'expert'
        elif kills < 100:
            level = 'master'
        else:
            level = 'grand-master'
        return level
Exemple #2
0
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)
Exemple #3
0
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']
Exemple #4
0
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))
Exemple #5
0
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.")