def test08Spaces(self): class data(): pass print "Add 'spaceyname ' to check for correct treatment of spacey behaviour" matcher = BestMatch(targets=[('spaceyname ', data())]) print "Search for spaceyname" res = matcher.match('spaceyname ') print res print "solution is '%s'" % res[0] self.assertTrue(len(res)==1 and res[0]=='spaceyname') res = matcher.match('spaceyname') print res print "solution is '%s'" % res[0] self.assertTrue(len(res)==1 and res[0]=='spaceyname') print "Add 'spaceyname ' with aliases 'alias1 ', ' alias2', ' alias3 '" matcher = BestMatch(targets=[['spaceyname ', 'alias1 ',' alias2', ' alias3 ']]) print "Search for spaceyname" res = matcher.match('spaceyname ') print res print "solution is '%s'" % res[0] self.assertTrue(len(res)==1 and res[0]=='spaceyname') print "Search for alias3" res = matcher.match(' alias3 ') print res print "solution is '%s'" % res[0] self.assertTrue(len(res)==1 and res[0]=='spaceyname') res = matcher.match('alias3') print res print "solution is '%s'" % res[0] self.assertTrue(len(res)==1 and res[0]=='spaceyname') print "Add 'spaceyname ' with aliases 'alias1 ', ' alias2', ' alias3 '" matcher = BestMatch(targets=[(['spaceyname ', 'alias1 ',' alias2', ' alias3 '],data())]) print "Search for spaceyname" res = matcher.match('spaceyname') print res print "solution is '%s'" % res[0] self.assertTrue(len(res)==1 and res[0]=='spaceyname')
class TestBestMatch(unittest.TestCase): def setUp(self): self.cityM = BestMatch(city_targets) self.cmdM = BestMatch(command_targets) self.restoM = BestMatch(resto_targets) self.simiM = BestMatch(similar_targets) def test01BasicMatch(self): print print "BASIC MATCHING" print "Find 'join'" res = self.cmdM.match('join') self.assertTrue(len(res) == 1 and res[0] == 'join') print "Find 'j'" res = self.cmdM.match('j') self.assertTrue(len(res) == 1 and res[0] == 'join') print "Find None" res = self.cmdM.match(None) self.assertTrue(len(res) == 0) print "Find ''" res = self.cmdM.match('') self.assertTrue(len(res) == 0) print "Add and find 'help'" self.cmdM.add_target('help') res = self.cmdM.match('help') self.assertTrue(len(res) == 1 and res[0] == 'help') print "Still find 'join'" res = self.cmdM.match('j') self.assertTrue(len(res) == 1 and res[0] == 'join') print "Add 'later' and find 'later'" self.cmdM.add_target('later') res = self.cmdM.match('later') self.assertTrue(len(res) == 1 and res[0] == 'later') print "Find 'l' and get 'later' and 'leave'" res = self.cmdM.match('l') self.assertTrue(set(res) == set(['later', 'leave'])) def test02ExactMatch(self): print print "EXACT MATCHING" print "Find 'bob'" res = self.simiM.match('bob') print res self.assertTrue(len(res) == 1 and res[0] == 'bob') def test03UnAnchored(self): print print "UNANCHORED SEARCH" res = self.cityM.match('field', anchored=False) print "Unanchored search for 'field': %s" % ','.join(res) self.assertTrue(set(res) == set(['springfield', 'westfield'])) def test04PrefixMatch(self): print print "IGNORE PREFIX MATCHING" res = self.restoM.match('panisse', anchored=True) print "Unprefixed search for 'panisse': %s" % ','.join(res) self.assertTrue(len(res) == 0) print "Add prefix 'chez'" self.restoM.add_ignore_prefix('Chez') res = self.restoM.match('panisse', anchored=True) print "Prefixed search for 'panisse': %s" % ','.join(res) self.assertTrue(len(res) == 1 and res[0] == 'chez panisse') res = self.restoM.match('chez', anchored=True) print "Prefixed search for 'chez': %s" % ','.join(res) self.assertTrue(set(res) == set(resto_targets[:3])) print "Add 'hotel' to prefix list" self.restoM.add_ignore_prefix('hotel') res = self.restoM.match('lando', anchored=True) print "Prefixed search for 'lando': %s" % ','.join(res) self.assertTrue(set(res) == set(['chez lando', 'hotel lando'])) print "Add 'burger' to prefix list" self.restoM.add_ignore_prefix('burger') res = self.restoM.match('burger', anchored=True) print "Prefixed search for 'burger': %s" % ','.join(res) self.assertTrue(set(res) == set(resto_targets[-3:])) def test05WithData(self): print print "TARGETS WITH DATA" self.cmdM.targets = zip(command_targets, command_data) print "Retrieve function for data" res = self.cmdM.match('j', with_data=True) self.assertTrue(res[0][1]() == 'join_data') print "Retrieve string" res = self.cmdM.match('le', with_data=True) self.assertTrue(res[0][1] == 'leave_data') print "Retrieve None" res = self.cmdM.match('n', with_data=True) self.assertTrue(res[0][1] is None) print "Retrieve dict" res = self.cmdM.match('cr', with_data=True) self.assertTrue(res[0][1]['key'] == 'value') def test06AddRemoveTargets(self): print print 'Add Buffalo' self.cityM.add_target('buffalo') res = self.cityM.match('buf') self.assertTrue(len(res) == 1 and res[0] == 'buffalo') res = self.cityM.match('new') self.assertTrue(set(res) == set(['newton', 'newberry', 'new orleans'])) print 'Remove Buffalo' self.cityM.remove_target('buffalo') res = self.cityM.match('buF') self.assertTrue(len(res) == 0) print "Add prefix 'new'" self.cityM.add_ignore_prefix('new') res = self.cityM.match('ton') self.assertTrue(len(res) == 1 and res[0] == 'newton') print "Remove prefix 'new'" self.cityM.remove_ignore_prefix('new') res = self.cityM.match('ton') self.assertTrue(len(res) == 0) print "Add target with data. (buffalo, rocks)" self.cityM.add_target(('buffalo', 'rocks')) res = self.cityM.match('BuF', with_data=True) self.assertTrue(len(res) == 1 and res[0] == ('buffalo', 'rocks')) def test07Aliases(self): print print "Add 'boston' with aliases 'the hub', 'beantown'" self.cityM.add_target(['boston', 'the hub', 'beantown']) print "Search for Beantown" res = self.cityM.match('beanTown') print res self.assertTrue(len(res) == 1 and res[0] == 'boston') print "Search for Boston" res = self.cityM.match('boston') self.assertTrue(len(res) == 1 and res[0] == 'boston') print "Add 'redsox country' as alias" self.cityM.add_alias_for_target('boston', 'redsox country') res = self.cityM.match('redsox') self.assertTrue(len(res) == 1 and res[0] == 'boston') print "Remove 'beantown'" self.cityM.remove_alias_for_target('boston', 'beantown') res = self.cityM.match('beantown') self.assertTrue(len(res) == 0) print "Test get aliases" self.assertTrue( set(self.cityM.get_aliases_for_target('boston')) == set( ['redsox country', 'the hub']))
class TestBestMatch(unittest.TestCase): def setUp(self): self.cityM=BestMatch(city_targets) self.cmdM=BestMatch(command_targets) self.restoM=BestMatch(resto_targets) self.simiM=BestMatch(similar_targets) def test01BasicMatch(self): print print "BASIC MATCHING" print "Find 'join'" res=self.cmdM.match('join') self.assertTrue(len(res)==1 and res[0]=='join') print "Find 'j'" res=self.cmdM.match('j') self.assertTrue(len(res)==1 and res[0]=='join') print "Find None" res=self.cmdM.match(None) self.assertTrue(len(res)==0) print "Find ''" res=self.cmdM.match('') self.assertTrue(len(res)==0) print "Add and find 'help'" self.cmdM.add_target('help') res=self.cmdM.match('help') self.assertTrue(len(res)==1 and res[0]=='help') print "Still find 'join'" res=self.cmdM.match('j') self.assertTrue(len(res)==1 and res[0]=='join') print "Add 'later' and find 'later'" self.cmdM.add_target('later') res=self.cmdM.match('later') self.assertTrue(len(res)==1 and res[0]=='later') print "Find 'l' and get 'later' and 'leave'" res=self.cmdM.match('l') self.assertTrue(set(res)==set(['later','leave'])) def test02ExactMatch(self): print print "EXACT MATCHING" print "Find 'bob'" res=self.simiM.match('bob') print res self.assertTrue(len(res)==1 and res[0]=='bob') def test03UnAnchored(self): print print "UNANCHORED SEARCH" res=self.cityM.match('field',anchored=False) print "Unanchored search for 'field': %s" % ','.join(res) self.assertTrue(set(res)==set(['springfield','westfield'])) def test04PrefixMatch(self): print print "IGNORE PREFIX MATCHING" res=self.restoM.match('panisse',anchored=True) print "Unprefixed search for 'panisse': %s" % ','.join(res) self.assertTrue(len(res)==0) print "Add prefix 'chez'" self.restoM.add_ignore_prefix('Chez') res=self.restoM.match('panisse',anchored=True) print "Prefixed search for 'panisse': %s" % ','.join(res) self.assertTrue(len(res)==1 and res[0]=='chez panisse') res=self.restoM.match('chez',anchored=True) print "Prefixed search for 'chez': %s" % ','.join(res) self.assertTrue(set(res)==set(resto_targets[:3])) print "Add 'hotel' to prefix list" self.restoM.add_ignore_prefix('hotel') res=self.restoM.match('lando',anchored=True) print "Prefixed search for 'lando': %s" % ','.join(res) self.assertTrue(set(res)==set(['chez lando','hotel lando'])) print "Add 'burger' to prefix list" self.restoM.add_ignore_prefix('burger') res=self.restoM.match('burger',anchored=True) print "Prefixed search for 'burger': %s" % ','.join(res) self.assertTrue(set(res)==set(resto_targets[-3:])) def test05WithData(self): print print "TARGETS WITH DATA" self.cmdM.targets=zip(command_targets,command_data) print "Retrieve function for data" res=self.cmdM.match('j',with_data=True) self.assertTrue(res[0][1]()=='join_data') print "Retrieve string" res=self.cmdM.match('le',with_data=True) self.assertTrue(res[0][1]=='leave_data') print "Retrieve None" res=self.cmdM.match('n',with_data=True) self.assertTrue(res[0][1] is None) print "Retrieve dict" res=self.cmdM.match('cr',with_data=True) self.assertTrue(res[0][1]['key']=='value') def test06AddRemoveTargets(self): print print 'Add Buffalo' self.cityM.add_target('buffalo') res = self.cityM.match('buf') self.assertTrue(len(res)==1 and res[0]=='buffalo') res = self.cityM.match('new') self.assertTrue(set(res)==set(['newton','newberry','new orleans'])) print 'Remove Buffalo' self.cityM.remove_target('buffalo') res = self.cityM.match('buF') self.assertTrue(len(res)==0) print "Add prefix 'new'" self.cityM.add_ignore_prefix('new') res = self.cityM.match('ton') self.assertTrue(len(res)==1 and res[0]=='newton') print "Remove prefix 'new'" self.cityM.remove_ignore_prefix('new') res = self.cityM.match('ton') self.assertTrue(len(res)==0) print "Add target with data. (buffalo, rocks)" self.cityM.add_target(('buffalo','rocks')) res = self.cityM.match('BuF',with_data=True) self.assertTrue(len(res)==1 and res[0]==('buffalo','rocks')) def test07Aliases(self): print print "Add 'boston' with aliases 'the hub', 'beantown'" self.cityM.add_target(['boston', 'the hub', 'beantown']) print "Search for Beantown" res = self.cityM.match('beanTown') print res self.assertTrue(len(res)==1 and res[0]=='boston') print "Search for Boston" res = self.cityM.match('boston') self.assertTrue(len(res)==1 and res[0]=='boston') print "Add 'redsox country' as alias" self.cityM.add_alias_for_target('boston', 'redsox country') res = self.cityM.match('redsox') self.assertTrue(len(res)==1 and res[0]=='boston') print "Remove 'beantown'" self.cityM.remove_alias_for_target('boston', 'beantown') res = self.cityM.match('beantown') self.assertTrue(len(res)==0) print "Test get aliases" self.assertTrue(set(self.cityM.get_aliases_for_target('boston'))== set(['redsox country', 'the hub'])) def test08Spaces(self): class data(): pass print "Add 'spaceyname ' to check for correct treatment of spacey behaviour" matcher = BestMatch(targets=[('spaceyname ', data())]) print "Search for spaceyname" res = matcher.match('spaceyname ') print res print "solution is '%s'" % res[0] self.assertTrue(len(res)==1 and res[0]=='spaceyname') res = matcher.match('spaceyname') print res print "solution is '%s'" % res[0] self.assertTrue(len(res)==1 and res[0]=='spaceyname') print "Add 'spaceyname ' with aliases 'alias1 ', ' alias2', ' alias3 '" matcher = BestMatch(targets=[['spaceyname ', 'alias1 ',' alias2', ' alias3 ']]) print "Search for spaceyname" res = matcher.match('spaceyname ') print res print "solution is '%s'" % res[0] self.assertTrue(len(res)==1 and res[0]=='spaceyname') print "Search for alias3" res = matcher.match(' alias3 ') print res print "solution is '%s'" % res[0] self.assertTrue(len(res)==1 and res[0]=='spaceyname') res = matcher.match('alias3') print res print "solution is '%s'" % res[0] self.assertTrue(len(res)==1 and res[0]=='spaceyname') print "Add 'spaceyname ' with aliases 'alias1 ', ' alias2', ' alias3 '" matcher = BestMatch(targets=[(['spaceyname ', 'alias1 ',' alias2', ' alias3 '],data())]) print "Search for spaceyname" res = matcher.match('spaceyname') print res print "solution is '%s'" % res[0] self.assertTrue(len(res)==1 and res[0]=='spaceyname')
class App(rapidsms.app.App): def __init__(self, router): rapidsms.app.App.__init__(self, router) # NB: this cannot be called globally # because of depencies between GNUTranslations (a -used here) # and DJangoTranslations (b -used in views) # i.e. initializing b then a is ok, but a then b fails _init_translators() # command target. ToDo--get names from gettext... # needs to be here so that 'self' has meaning. # could also do the hasattr thing when calling instead self.cmd_targets = [ # NOTE: make sure all commands are unicode strings! # Pulaar ([u'naalde', u'naatde', u'tawtude',u'naattugol'], {'lang':'pul','func':self.join}), (u'yettoode', {'lang':'pul','func':self.register_name}), ([u'yaltude',u'iwde'], {'lang':'pul','func':self.leave}), ([u'dallal',u'ballal'], {'lang':'pul','func':self.help}), (u'penngugol', {'lang':'pul','func':self.create_village}), # Wolof ([u'boole', u'yokk', u'duggu'], {'lang':'wol','func':self.join}), ([u'genn', u'génn'], {'lang':'wol','func':self.leave}), ([u'sant', u'tur'], {'lang':'wol','func':self.register_name}), (u'ndimbal', {'lang':'wol','func':self.help}), # Dyuola ([u'unoken', u'ounoken'], {'lang':'dyu','func':self.join}), ([u'karees', u'karees'], {'lang':'dyu','func':self.register_name}), ([u'upur', u'oupour'], {'lang':'dyu','func':self.leave}), (u'rambenom', {'lang':'dyu','func':self.help}), # Soninke (u'ro', {'lang':'snk','func':self.join}), (u'toxo', {'lang':'snk','func':self.register_name}), (u'bagu', {'lang':'snk','func':self.leave}), (u'deema', {'lang':'snk','func':self.help}), (u'taga', {'lang':'snk','func':self.create_village}), # Mandinka (u'koo', {'lang':'mnk','func':self.join}), (u'ntoo', {'lang':'mnk','func':self.register_name}), (u'nbetaamala', {'lang':'mnk','func':self.leave}), (u"n'deemaa", {'lang':'mnk','func':self.help}), # French (u'entrer', {'lang':'fr','func':self.join}), (u'nom', {'lang':'fr','func':self.register_name}), (u'quitter', {'lang':'fr','func':self.leave}), (u'aide', {'lang':'fr','func':self.help}), ([u'créer', u'creer'], {'lang':'fr','func':self.create_village}), (u'enlever', {'lang':'fr','func':self.destroy_community}), (u'langue', {'lang':'fr','func':self.lang}), # English (u'join', {'lang':'en','func':self.join}), (u'name', {'lang':'en','func':self.register_name}), (u'leave', {'lang':'en','func':self.leave}), (u'help', {'lang':'en','func':self.help}), (u'create', {'lang':'en','func':self.create_village}), (u'member', {'lang':'en','func':self.member}), (u'citizens', {'lang':'en','func':self.community_members}), (u'remove', {'lang':'en','func':self.destroy_community}), ] self.cmd_matcher=BestMatch(self.cmd_targets) #villes=[(v.name, v) for v in Village.objects.all()] #self.village_matcher=BestMatch(villes, ignore_prefixes=['keur']) # swap dict so that we send in (name,code) tuples rather than (code,name self.lang_matcher=BestMatch([ (names,code) for code,names in _G['SUPPORTED_LANGS'].items() ]) def __get_village_matcher(self): """ HACK to force reload of names before each match """ villes = [] for v in Village.objects.all(): names = [v.name] + [a.alias for a in v.aliases.all()] villes.append( (names,v) ) return BestMatch(villes, ignore_prefixes=['keur']) def configure(self, **kwargs): try: _G['DEFAULT_LANG'] = kwargs.pop('default_lang') except: pass try: _G['ADMIN_CMD_PWD'] = kwargs.pop('admin_cmd_pwd') except: pass def start(self): self.__loadFixtures() ##################### # Message Lifecycle # ##################### def handle(self, msg): self.__log_incoming_message(msg, villages_for_contact(msg.sender)) self.debug("In handle smsforums: %s" % msg.text) # check permissions if msg.sender.perm_ignore: self.debug('Ignoring sender: %s' % msg.sender.signature) return False if not msg.sender.can_send: self.debug('Sender: %s does no have receive perms' % msg.sender.signature) self.__reply(msg,'inbound-message_rejected') # Ok, we're all good, start processing msg.sender.sent_message_accepted(msg) # # Now we figure out if it's a direct message, a command, or a blast # # ok, this is a little weird, but stay with me. # commands start with '.' '*' or '#'--the cmd markers. e.g. '.join <something>' # addresses are of form cmd_marker address cmd_mark--e.g. '.jeff. hello' # address=None rest=None # check for direct message first m=DM_MESSAGE_MATCHER.match(msg.text) if m is not None: address=m.group(1).strip() rest=m.group(2) if rest is not None: rest=rest.strip() return self.blast_direct(msg,address,rest) # are we a command? m=CMD_MESSAGE_MATCHER.match(msg.text) if m is None: # we are a blast return self.blast(msg) # we must be a command cmd,rest=m.groups() if cmd is None: #user tried to send some sort of command (a message with .,#, or *, but nothing after) self.__reply(msg,"command-not-understood") return True else: cmd=cmd.strip() if rest is not None: rest=rest.strip() # Now match the possible command to ones we know cmd_match=self.cmd_matcher.match(cmd,with_data=True) if len(cmd_match)==0: # no command match self.__reply(msg,"command-not-understood") return True if len(cmd_match)>1: # too many matches! self.__reply(msg, 'command-not-understood %(sug_1)s %(sug_rest)s', \ { 'sug_1':', '.join([t[0] for t in cmd_match[:-1]]), 'sug_rest':cmd_match[-1:][0][0]}) return True # # Ok! We got a real command # cmd,data=cmd_match[0] #arg=msg_text[msg_match.end():] # set the senders default language, if not sent if msg.sender.locale is None: msg.sender.locale=data['lang'] msg.sender.save() return data['func'](msg,arg=rest) def outgoing(self, msg): # TODO # create a ForumMessage class # log messages with associated domain # report on dashboard pass #################### # Command Handlers # #################### def help(self, msg,arg=None): if arg is not None and len(arg)>0: # see if it is a language and send help # for that lang langs=self.lang_matcher.match(arg,with_data=True) if len(langs)==1: self.__reply(msg, "help-with-commands_%s" % langs[0][1]) return True else: # send the list of available langs by passing # to the 'lang' command handler return self.help(msg) self.__reply(msg, "help-with-commands") return True @passwordProtectedCmd def create_village(self, msg, arg=None): self.debug("SMSFORUM:CREATEVILLAGE") if arg is None or len(arg)<1: self.__reply(msg, "create-village-fail_no-village-name") return True else: village = arg if len(Village.objects.filter(name=village)) != 0: self.__reply(msg, "create-village-fail_village-already-exists %(village)s", {'village':village}) return True try: # TODO: add administrator authentication if len(village) > MAX_VILLAGE_NAME_LEN: self.__reply(msg, "create-village-fail_name-too-long %(village)s %(max_char)d", \ {'village':village, 'max_char':MAX_VILLAGE_NAME_LEN} ) return True ville = Village(name=village) ville.save() # self.village_matcher.add_target((village,ville)) self.__reply(msg, "create-village-success %(village)s", {'village':village} ) except: self.debug( traceback.format_exc() ) traceback.print_exc() self.__reply(msg, "internal-error") return True def member(self,msg,arg=None): try: villages=villages_for_contact(msg.sender) if len(villages)==0: self.__reply(msg, "member-fail_not-member-of-village") else: village_names = ', '.join([v.name for v in villages]) txt = "member-success %(village_names)s" if len(villages)>5: villages = villages[0:5] txt = "member-success_long-list %(village_names)s" self.__reply(msg, txt, {"village_names":village_names}) except: traceback.print_exc() self.debug( traceback.format_exc() ) rsp= _st(msg.sender,"internal-error") self.debug(rsp) self.__reply(msg,rsp) return True @passwordProtectedCmd def community_members(self,msg,arg=None): if arg is None or len(arg)==0: self.__reply(msg, "citizens-fail_no-village") return True villes=self.__get_village_matcher().match(arg,with_data=True) if len(villes)==0: self.__reply(msg, "village-not-known %(unknown)s", {'unknown':arg}) return True for name,ville in villes: members=[c.get_signature(max_len=10) for c in \ ville.flatten(klass=Contact)] if len(members)>20: members = members[0:20] txt = 'citizens-success_long-list %(village)s %(citizens)s' else: txt = 'citizens-success %(village)s %(citizens)s' self.__reply(msg, txt, {'village':name, 'citizens':','.join(members)}) return True @passwordProtectedCmd def destroy_community(self,msg,arg=None): if arg is None or len(arg)==0: self.__reply(msg, "remove-fail_no-village") return True try: # EXACT MATCH ONLY! ville=Village.objects.get(name=arg) ville.delete() # self.village_matcher.remove_target(arg) self.__reply(msg, "remove-success %(village)s", {'village': arg}) return True except Exception, e: rsp= _st(msg.sender,"village-not-known %(unknown)s") % {'unknown':arg} self.debug(rsp) self.__reply(msg,rsp) return True
class App(rapidsms.app.App): def __init__(self, router): rapidsms.app.App.__init__(self, router) # NB: this cannot be called globally # because of depencies between GNUTranslations (a -used here) # and DJangoTranslations (b -used in views) # i.e. initializing b then a is ok, but a then b fails _init_translators() # command target. ToDo--get names from gettext... # needs to be here so that 'self' has meaning. # could also do the hasattr thing when calling instead self.cmd_targets = [ # Pulaar (['naalde', 'naatde', 'tawtude'], {'lang':'pul','func':self.join}), ('yettoode', {'lang':'pul','func':self.register_name}), (['yaltude','iwde'], {'lang':'pul','func':self.leave}), (['dallal','ballal'], {'lang':'pul','func':self.help}), # Wolof (['boole', 'yokk', 'duggu'], {'lang':'wol','func':self.join}), (['genn', 'génn'], {'lang':'wol','func':self.leave}), (['sant', 'tur'], {'lang':'wol','func':self.register_name}), ('ndimbal', {'lang':'wol','func':self.help}), # Dyuola (['unoken', 'ounoken'], {'lang':'dyu','func':self.join}), (['karees', 'karees'], {'lang':'dyu','func':self.register_name}), (['upur', 'oupour'], {'lang':'dyu','func':self.leave}), ('rambenom', {'lang':'dyu','func':self.help}), # French ('entrer', {'lang':'fr','func':self.join}), ('nom', {'lang':'fr','func':self.register_name}), ('quitter', {'lang':'fr','func':self.leave}), ('aide', {'lang':'fr','func':self.help}), (['créer', 'creer'], {'lang':'fr','func':self.create_village}), # English ('join', {'lang':'en','func':self.join}), ('name', {'lang':'en','func':self.register_name}), ('leave', {'lang':'en','func':self.leave}), ('language', {'lang':'en','func':self.lang}), ('help', {'lang':'en','func':self.help}), ('create', {'lang':'en','func':self.create_village}), ('member', {'lang':'en','func':self.member}), ('citizens', {'lang':'en','func':self.community_members}), ('remove', {'lang':'en','func':self.destroy_community}), ] self.cmd_matcher=BestMatch(self.cmd_targets) #villes=[(v.name, v) for v in Village.objects.all()] #self.village_matcher=BestMatch(villes, ignore_prefixes=['keur']) # swap dict so that we send in (name,code) tuples rather than (code,name self.lang_matcher=BestMatch([ (names,code) for code,names in _G['SUPPORTED_LANGS'].items() ]) def __get_village_matcher(self): """ HACK to force reload of names before each match """ villes=[(v.name, v) for v in Village.objects.all()] return BestMatch(villes, ignore_prefixes=['keur']) def configure(self, **kwargs): try: _G['DEFAULT_LANG'] = kwargs.pop('default_lang') except: pass try: _G['ADMIN_CMD_PWD'] = kwargs.pop('admin_cmd_pwd') except: pass def start(self): self.__loadFixtures() ##################### # Message Lifecycle # ##################### def handle(self, msg): self.__log_incoming_message(msg, villages_for_contact(msg.sender)) self.debug("In handle smsforums: %s" % msg.text) # check permissions if msg.sender.perm_ignore: self.debug('Ignoring sender: %s' % msg.sender.signature) return False if not msg.sender.can_send: self.debug('Sender: %s does no have receive perms' % msg.sender.signature) self.__reply(msg,'inbound-message_rejected') # Ok, we're all good, start processing msg.sender.sent_message_accepted(msg) # # Now we figure out if it's a direct message, a command, or a blast # # ok, this is a little weird, but stay with me. # commands start with '.' '*' or '#'--the cmd markers. e.g. '.join <something>' # addresses are of form cmd_marker address cmd_mark--e.g. '.jeff. hello' # address=None rest=None # check for direct message first m=DM_MESSAGE_MATCHER.match(msg.text) if m is not None: address=m.group(1).strip() rest=m.group(2) if rest is not None: rest=rest.strip() return self.blast_direct(msg,address,rest) # are we a command? m=CMD_MESSAGE_MATCHER.match(msg.text) if m is None: # we are a blast return self.blast(msg) # we must be a command cmd,rest=m.groups() if cmd is None: #user tried to send some sort of command (a message with .,#, or *, but nothing after) self.__reply(msg,"command-not-understood") return True else: cmd=cmd.strip() if rest is not None: rest=rest.strip() # Now match the possible command to ones we know cmd_match=self.cmd_matcher.match(cmd,with_data=True) if len(cmd_match)==0: # no command match self.__reply(msg,"command-not-understood") return True if len(cmd_match)>1: # too many matches! self.__reply(msg, 'command-not-understood %(sug_1)s %(sug_rest)s', \ { 'sug_1':', '.join([t[0] for t in cmd_match[:-1]]), 'sug_rest':cmd_match[-1:][0][0]}) return True # # Ok! We got a real command # cmd,data=cmd_match[0] #arg=msg_text[msg_match.end():] # set the senders default language, if not sent if msg.sender.locale is None: msg.sender.locale=data['lang'] msg.sender.save() return data['func'](msg,arg=rest) def outgoing(self, msg): # TODO # create a ForumMessage class # log messages with associated domain # report on dashboard pass #################### # Command Handlers # #################### def help(self, msg,arg=None): if arg is not None and len(arg)>0: # see if it is a language and send help # for that lang langs=self.lang_matcher.match(arg,with_data=True) if len(langs)==1: self.__reply(msg, "help-with-commands_%s" % langs[0][1]) return True else: # send the list of available langs by passing # to the 'lang' command handler return self.help(msg) self.__reply(msg, "help-with-commands") return True @passwordProtectedCmd def create_village(self, msg, arg=None): self.debug("SMSFORUM:CREATEVILLAGE") if arg is None or len(arg)<1: self.__reply(msg, "create-village-fail_no-village-name") return True else: village = arg if len(Village.objects.filter(name=village)) != 0: self.__reply(msg, "create-village-fail_village-already-exists %(village)s", {'village':village}) return True try: # TODO: add administrator authentication if len(village) > MAX_VILLAGE_NAME_LEN: self.__reply(msg, "create-village-fail_name-too-long %(village)s %(max_char)d", \ {'village':village, 'max_char':MAX_VILLAGE_NAME_LEN} ) return True ville = Village(name=village) ville.save() # self.village_matcher.add_target((village,ville)) self.__reply(msg, "create-village-success %(village)s", {'village':village} ) except: self.debug( traceback.format_exc() ) traceback.print_exc() self.__reply(msg, "internal-error") return True def member(self,msg,arg=None): try: villages=villages_for_contact(msg.sender) if len(villages)==0: self.__reply(msg, "member-fail_not-member-of-village") else: village_names = ', '.join([v.name for v in villages]) txt = "member-success %(village_names)s" if len(villages)>5: villages = villages[0:5] txt = "member-success_long-list %(village_names)s" self.__reply(msg, txt, {"village_names":village_names}) except: traceback.print_exc() self.debug( traceback.format_exc() ) rsp= _st(msg.sender,"internal-error") self.debug(rsp) self.__reply(msg,rsp) return True @passwordProtectedCmd def community_members(self,msg,arg=None): if arg is None or len(arg)==0: self.__reply(msg, "citizens-fail_no-village") return True villes=self.__get_village_matcher().match(arg,with_data=True) if len(villes)==0: self.__reply(msg, "village-not-known %(unknown)s", {'unknown':arg}) return True for name,ville in villes: members=[c.get_signature(max_len=10) for c in \ ville.flatten(klass=Contact)] if len(members)>20: members = members[0:20] txt = 'citizens-success_long-list %(village)s %(citizens)s' else: txt = 'citizens-success %(village)s %(citizens)s' self.__reply(msg, txt, {'village':name, 'citizens':','.join(members)}) return True @passwordProtectedCmd def destroy_community(self,msg,arg=None): if arg is None or len(arg)==0: self.__reply(msg, "remove-fail_no-village") return True try: # EXACT MATCH ONLY! ville=Village.objects.get(name=arg) ville.delete() # self.village_matcher.remove_target(arg) self.__reply(msg, "remove-success %(village)s", {'village': arg}) return True except: rsp= _st(msg.sender,"village-not-known %(unknown)s") % {'unknown':arg} self.debug(rsp) self.__reply(msg,rsp) return True def register_name(self,msg,arg=None): if arg is None or len(arg)==0: self.__reply(msg,"name-acknowledge %(name)s", {'name':msg.sender.common_name}) return True name=arg try: if len(name) > MAX_CONTACT_NAME_LEN: self.__reply(msg, "name-register-fail_name-too-long %(name)s %(max_char)d", \ {'name':name, 'max_char':MAX_CONTACT_NAME_LEN} ) return True msg.sender.common_name = name msg.sender.save() rsp=_st(msg.sender, "name-register-success %(name)s") % {'name':msg.sender.common_name} self.__reply(msg,rsp) except: traceback.print_exc() self.debug( traceback.format_exc() ) rsp= _st(msg.sender, "internal-error") self.debug(rsp) self.__reply(msg,rsp) return True def join(self,msg,arg=None): if arg is None or len(arg)==0: return self.__suggest_villages(msg) else: village=arg try: matched_villes=self.__get_village_matcher().match(village,with_data=True) # send helpful message if 0 or more than 1 found num_villes=len(matched_villes) # unzip data from names if can if num_villes>0: village_names,villages=zip(*matched_villes) if num_villes==0 or num_villes>1: if num_villes==0: return self.__suggest_villages(msg) else: # use all hit targets rsp=_st(msg.sender, "village-not-found %(suggested)s") % \ {"suggested": ', '.join(village_names)} self.__reply(msg,rsp) return True # ok, here we got just one assert(len(villages)==1) villages[0].add_children(msg.sender) rsp=_st(msg.sender, "join-success %(village)s") % {"village": village_names[0]} self.debug(rsp) self.__reply(msg,rsp) except: traceback.print_exc() self.debug( traceback.format_exc() ) rsp=_st(msg.sender, "internal-error") self.debug(rsp) self.__reply(msg,rsp) return True def blast_direct(self, msg, address, text): """ find the matching people, groups. Consider only matches that return ONE result! Otherwise people might accidentally send messages far wider than expected! The direct messaging to Contacts currently uses 'common_name' which is not guaranteed unique, so if two people have the same name, the returned set will never be unique, and you will not be allowed to send to them. a system that wants to really implement Twitter like DM needs to either enforce uniqueness on common_name or use the 'unique_id' field in stead. Either way people will need to register a 'username' like handle in the field used for the match. """ contacts=[(c.common_name, c) for c in Contact.objects.all()] cont_matcher=BestMatch(targets=contacts) found=MultiMatch(self.__get_village_matcher(),cont_matcher).\ match(address,with_data=True) if len(found)==0: self.__reply( msg, 'direct-blast-fail_recipient-not-found %(recipient)s', {'recipient':address} ) elif len(found)>1: names,objs=zip(*found) self.__reply( msg, 'direct-blast-fail_too-many-recipients %(recip_1)s and %(recip_rest)s', { 'recip_1':names[0], 'recip_rest': ', '.join(names[1:])} ) else: # got one person or village! name,obj=found[0] # found is an array of tuples (name,obj) # prep the outbound message ok,out_text,enc=self.__prep_blast_message(msg,text,[name]) if not ok: # oops, too long, __prep... already responded to # user, so we'll just return return True rsp= _st(msg.sender, "direct-blast-acknowledge %(text)s %(recipient)s") % \ {'recipient':name,'text':out_text} self.debug('REPSONSE TO BLASTER: %s' % rsp) self.__reply(msg,rsp) if isinstance(obj, Village): self.__blast_to_villages([obj],msg.sender,out_text) else: assert(isinstance(obj, Contact)) self.__blast_to_contact(obj,out_text) return True def blast(self, msg): """Takes actual Contact objects""" self.debug("SMSFORUM:BLAST") #find all villages for this sender villes = villages_for_contact(msg.sender) if len(villes)==0: rsp=_st(msg.sender, "blast-fail_not-member-of-any-village") self.debug(rsp) self.__reply(msg,rsp) return True recips=[v.name for v in villes] ok,blast_text,enc=self.__prep_blast_message(msg,msg.text,recips) if not ok: # message was too long, prep already # sent a reply to the sender, so we just # return out return True # respond to sender first because the delay between now # and the last recipient can be long # # TODO: send a follow-up is message sending fails! rsp= _st(msg.sender, "blast-acknowledge %(text)s %(recipients)s") % \ {'recipients':', '.join(recips),'text':msg.text.strip()} self.debug('REPSONSE TO BLASTER: %s' % rsp) self.__reply(msg,rsp) return self.__blast_to_villages(villes,msg.sender,blast_text) def __prep_blast_message(self,msg,out_text,recipients): """ helper function formats blast msg including signature and returns 3-ple of ( bool <message is good to send>, formatted message w/signature, encoding required) If message is NOT good to send 3-ple will be (False, None, encoding) NOTE: If the message is too long this helper sends a reply to the msg.sender, so don't do that again! """ # check for message length, and bounce messages that are too long # # since length depends on encoding, find out if we can send # this GSM (160 chars) or UCS2 (70 chars) # # TODO: factor this somewhere better like the backend since # it's what knows the message size limits... # gsm_enc=True sender_sig=msg.sender.signature try: out_text.encode('gsm') sender_sig.encode('gsm') except: # Either message or sig needs UCS2 encoding gsm_enc=False finally: encoding,max_len=(\ ('gsm',MAX_LATIN_BLAST_LEN) if gsm_enc \ else ('ucs2',MAX_UCS2_BLAST_LEN)) if len(out_text)>max_len: rsp= _st(msg.sender, "blast-fail_message-too-long %(msg_len)d %(max_latin)d %(max_unicode)d") % \ { 'msg_len': len(out_text), 'max_latin': MAX_LATIN_BLAST_LEN, 'max_unicode': MAX_UCS2_BLAST_LEN } self.__reply(msg,rsp) return (False, None, encoding) # ok, we're long enough, lets make the blast text # we replace '%(sender)s' with '%(sender)s' so that # localized strings can put the sender where they want # we then do another subsitution after we pick the send signature blast_tmpl=_st(msg.sender, "blast-message_outgoing %(text)s %(recipients)s %(sender)s") % \ { 'text':out_text, 'recipients':', '.join(recipients), 'sender': '%(sender)s'} #add signature tmpl_len=len(blast_tmpl)-10 # -10 accounts from sig placeholder ('%(sender)s') max_sig=max_len-tmpl_len sig=msg.sender.get_signature(max_len=max_sig,for_message=msg) blast_text = blast_tmpl % {'sender': sig} return (True, blast_text, encoding) def __blast_to_villages(self, villes, sender, text): """Takes actual village objects""" if villes is None or len(villes)==0: return True recipients=set() for ville in villes: recipients.update(ville.flatten(klass=Contact)) # now iterate every member of the group we are broadcasting # to, and queue up the same message to each of them for recipient in recipients: if recipient != sender: self.__blast_to_contact(recipient,text) vnames = ', '.join([v.name for v in villes]) self.debug("success! %(villes)s recvd msg: %(txt)s" % { 'villes':vnames,'txt':text}) return True def __blast_to_contact(self, contact, text): """Returns True is message sent""" if contact.can_receive: self.debug('Blast msg: %s to: %s' % (text,contact.signature)) # TODO: move to lib/pygsm/gsm.py # currently just log messages that are too long # since these are not handled properly in modem self._check_message_length(text) contact.send_to(text) return True else: return False def leave(self,msg,arg=None): self.debug("SMSFORUM:LEAVE: %s" % arg) try: villages=[] if arg is not None and len(arg)>0: village_tupes = self.__get_village_matcher().match(arg, with_data=True) if len(village_tupes)>0: villages = zip(*village_tupes)[1] # the objects else: villages = villages_for_contact(msg.sender) if len(villages)>0: names = list() for ville in villages: ville.remove_children(msg.sender) names.append(ville.name) self.__reply(msg, "leave-success %(villages)s", { "villages": ','.join(names)}) else: if arg is not None and len(arg)>0: self.__reply(msg, "leave-fail_village-not-found %(village)s", {'village':arg}) else: self.__reply(msg, "leave-fail_not-member-of-village") except: # something went wrong - at the # moment, we don't care what traceback.print_exc() self.debug( traceback.format_exc() ) self.__reply(msg, "internal-error") return True def lang(self,msg,arg=None): name=arg self.debug("SMSFORUM:LANG:Current locale: %s" % msg.sender.locale) def _return_all_langs(): # return available langs langs_sorted=[l[0] for l in _G['SUPPORTED_LANGS'].values()] langs_sorted.sort() rsp=_st(msg.sender, "language-set-fail_code-not-understood %(langs)s") % \ { 'langs':', '.join(langs_sorted)} self.__reply(msg,rsp) return True if name is None or len(name)==0: return _return_all_langs() # see if we have that language langs=self.lang_matcher.match(name.strip(),with_data=True) if len(langs)==1: name,code=langs[0] msg.sender.locale=code msg.sender.save() rsp = _st(msg.sender, 'language-set-success %(lang)s') % { 'lang': name } self.__reply(msg,rsp) return True else: # invalid lang code, send them a list return _return_all_langs() # # Private helpers # def __reply(self,msg,reply_text,format_values=None): """ Formats string for response for message's sender the message's associated sender. """ reply_text=_st(msg.sender,reply_text) if format_values is not None: try: reply_text = reply_text % format_values except TypeError: err="Not all format values: %r were used in the string: %s" %\ (format_values, reply_text) self.error(err) # TODO: move to lib/pygsm/gsm.py # currently just log messages that are too long # since these are not handled properly in modem self._check_message_length(reply_text) msg.sender.send_response_to(reply_text) def __suggest_villages(self,msg): """helper to send informative messages""" # pick some names from the DB village_names = [v.name for v in Village.objects.all()[:3]] if len(village_names) == 0: village_names = _st(msg.sender,"village_name") else: village_names=', '.join(village_names) self.__reply(msg,"village-not-found %(suggested)s", {"suggested": village_names}) return True def __loadFixtures(self): pass def __log_incoming_message(self,msg,domains): #TODO: FIX THIS so that it logs for all domains if domains is None or len(domains)==0: return #msg.persistent_msg should never be none if app.logger is used #this is to ensure smsforum does not fail even if logger fails... if hasattr(msg,'persistent_msg'): msg.persistent_msg.domain = domains[0] msg.persistent_msg.save() def _check_message_length(self, text): """ This function DOES NOT belong here - a temporary measure until rapidsms has a good api for backends to speak to router checks message length < 160 if gsm, else <70 if ucs-2/utf16 """ gsm_enc=True try: text.encode('gsm') except: gsm_enc=False finally: encoding,max_len=(\ ('gsm',MAX_LATIN_SMS_LEN) if gsm_enc \ else ('ucs2',MAX_UCS2_SMS_LEN)) if len(text)>max_len: err= ("ERROR: %(encoding)s MESSAGE OF LENGTH '%(msg_len)d' IS TOO LONG. Max is %(max)d.") % \ { 'encoding': encoding, 'msg_len': len(text), 'max': MAX_LATIN_SMS_LEN if encoding=='gsm' else MAX_UCS2_SMS_LEN } self.error(err) return False return True