From f9eece974928caff76bc8caea8c761ad0d7304ce Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 19 Feb 2014 00:13:05 +0100 Subject: [PATCH] Started implementing the Bot functionality. --- src/comms/comms.py | 14 +- src/players/bots.py | 133 ++++++++++++++++++ ...0029_auto__add_field_playerdb_db_is_bot.py | 91 ++++++++++++ src/players/models.py | 2 + src/scripts/scripts.py | 2 +- src/server/amp.py | 2 +- src/server/portal/portalsessionhandler.py | 6 +- src/server/sessionhandler.py | 21 ++- 8 files changed, 261 insertions(+), 10 deletions(-) create mode 100644 src/players/bots.py create mode 100644 src/players/migrations/0029_auto__add_field_playerdb_db_is_bot.py diff --git a/src/comms/comms.py b/src/comms/comms.py index 0928cdd663..5bf2b01599 100644 --- a/src/comms/comms.py +++ b/src/comms/comms.py @@ -3,7 +3,7 @@ Default Typeclass for Comms. See objects.objects for more information on Typeclassing. """ -from src.comms import Msg, TempMsg, ChannelDB +from src.comms import Msg, TempMsg from src.typeclasses.typeclass import TypeClass from src.utils import logger from src.utils.utils import make_iter @@ -14,8 +14,8 @@ class Channel(TypeClass): This is the base class for all Comms. Inherit from this to create different types of communication channels. """ - def __init__(self, dbobj): - super(Channel, self).__init__(dbobj) + + # helper methods, for easy overloading def channel_prefix(self, msg=None, emit=False): """ @@ -107,6 +107,7 @@ class Channel(TypeClass): """ Run at channel creation. """ + pass def pre_join_channel(self, joiner): """ @@ -132,6 +133,7 @@ class Channel(TypeClass): """ Run right after an object or player leaves a channel. """ + pass def pre_send_message(self, msg): """ @@ -146,6 +148,7 @@ class Channel(TypeClass): """ Run after a message is sent to the channel. """ + pass def at_init(self): """ @@ -155,6 +158,7 @@ class Channel(TypeClass): in some way after being created but also after each server restart or reload. """ + pass def distribute_message(self, msg, online=False): """ @@ -165,7 +169,9 @@ class Channel(TypeClass): for player in self.dbobj.db_subscriptions.all(): player = player.typeclass try: - player.msg(msg.message, from_obj=msg.senders) + # note our addition of the from_channel keyword here. This could be checked + # by a custom player.msg() to treat channel-receives differently. + player.msg(msg.message, from_obj=msg.senders, from_channel=self) except AttributeError: try: player.to_external(msg.message, diff --git a/src/players/bots.py b/src/players/bots.py new file mode 100644 index 0000000000..d17f8a852c --- /dev/null +++ b/src/players/bots.py @@ -0,0 +1,133 @@ +""" +Bots are a special child typeclasses of +Player that are controlled by the server. + +""" + +from src.players.player import Player +from src.scripts.script import Script +from src.commands.command import Command +from src.commands.cmdset import CmdSet +from src.commands.cmdhandler import CMD_NOMATCH + +_SESSIONS = None + +class BotStarter(Script): + """ + This non-repeating script has the + sole purpose of kicking its bot + into gear when it is initialized. + """ + def at_script_creation(self): + self.key = "botstarter" + self.desc = "kickstarts bot" + self.persistent = True + self.db.started = False + + def at_start(self): + "Kick bot into gear" + if not self.db.started: + self.obj.start() + self.db.started = False + + def at_server_reload(self): + """ + If server reloads we don't need to start the bot again, + the Portal resync will do that for us. + """ + self.db.started = True + + def at_server_shutdown(self): + "Make sure we are shutdown" + self.db.started = False + + +class CmdBotListen(Command): + """ + This is a catch-all command that absorbs + all input coming into the bot through its + session and pipes it into its execute_cmd + method. + """ + key = CMD_NOMATCH + + def func(self): + text = self.cmdname + self.args + self.obj.execute_cmd(text, sessid=self.sessid) + + +class BotCmdSet(CmdSet): + "Holds the BotListen command" + key = "botcmdset" + def at_cmdset_creation(self): + self.add(CmdBotListen()) + + +class Bot(Player): + """ + A Bot will start itself when the server + starts (it will generally not do so + on a reload - that will be handled by the + normal Portal session resync) + """ + def at_player_creation(self): + """ + Called when the bot is first created. It sets + up the cmdset and the botstarter script + """ + self.cmdset.add_default(BotCmdSet) + script_key = "botstarter_%s" % self.key + self.scripts.add(BotStarter, key=script_key) + self.is_bot = True + + def start(self): + """ + This starts the bot, usually by connecting + to a protocol. + """ + pass + + def msg(self, text=None, from_obj=None, sessid=None, **kwargs): + """ + Evennia -> outgoing protocol + """ + pass + + def execute_cmd(self, raw_string, sessid=None): + """ + Incoming protocol -> Evennia + """ + pass + + +class IRCBot(Bot): + """ + Bot for handling IRC connections + """ + def start(self): + "Start by telling the portal to start a new session" + global _SESSIONS + if not _SESSIONS: + from src.server.sessionhandler import SESSIONS as _SESSIONS + # instruct the server and portal to create a new session + _SESSIONS.start_bot_session("src.server.portal.irc.IRCClient", self.id) + + def connect_to_channel(self, channelname): + """ + Connect the bot to an Evennia channel + """ + pass + + def msg(self, text=None, **kwargs): + """ + Takes text from connected channel (only) + """ + if "from_channel" in kwargs and text: + # a channel receive. This is the only one we deal with + channel = kwargs.pop("from_channel") + ckey = channel.key + text = "[%s] %s" % (ckey, text) + self.dbobj.msg(text=text) + + + def execute_cmd( diff --git a/src/players/migrations/0029_auto__add_field_playerdb_db_is_bot.py b/src/players/migrations/0029_auto__add_field_playerdb_db_is_bot.py new file mode 100644 index 0000000000..2f5c9d8c0c --- /dev/null +++ b/src/players/migrations/0029_auto__add_field_playerdb_db_is_bot.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'PlayerDB.db_is_bot' + db.add_column(u'players_playerdb', 'db_is_bot', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'PlayerDB.db_is_bot' + db.delete_column(u'players_playerdb', 'db_is_bot') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'players.playerdb': { + 'Meta': {'object_name': 'PlayerDB'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'db_attributes': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['typeclasses.Attribute']", 'null': 'True', 'symmetrical': 'False'}), + 'db_cmdset_storage': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_is_bot': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'db_is_connected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'db_lock_storage': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'db_tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['typeclasses.Tag']", 'null': 'True', 'symmetrical': 'False'}), + 'db_typeclass_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'typeclasses.attribute': { + 'Meta': {'object_name': 'Attribute'}, + 'db_attrtype': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'null': 'True', 'blank': 'True'}), + 'db_category': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'db_lock_storage': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'db_model': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'db_strvalue': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'db_value': ('src.utils.picklefield.PickledObjectField', [], {'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + u'typeclasses.tag': { + 'Meta': {'unique_together': "(('db_key', 'db_category'),)", 'object_name': 'Tag', 'index_together': "(('db_key', 'db_category'),)"}, + 'db_category': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'db_index': 'True'}), + 'db_data': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'db_model': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'db_index': 'True'}), + 'db_tagtype': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'db_index': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + } + } + + complete_apps = ['players'] \ No newline at end of file diff --git a/src/players/models.py b/src/players/models.py index 89179fd8e6..51b1716a18 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -95,6 +95,8 @@ class PlayerDB(TypedObject, AbstractUser): # database storage of persistant cmdsets. db_cmdset_storage = models.CharField('cmdset', max_length=255, null=True, help_text="optional python path to a cmdset class. If creating a Character, this will default to settings.CMDSET_CHARACTER.") + # marks if this is a "virtual" bot player object + db_is_bot = models.BooleanField(default=False, verbose_name="is_bot", help_text="Used to identify irc/imc2/rss bots") # Database manager objects = manager.PlayerManager() diff --git a/src/scripts/scripts.py b/src/scripts/scripts.py index d7389d200a..65a7a42c70 100644 --- a/src/scripts/scripts.py +++ b/src/scripts/scripts.py @@ -482,7 +482,7 @@ class Script(ScriptBase): def at_stop(self): """ Called whenever when it's time for this script to stop - (either because is_valid returned False or ) + (either because is_valid returned False or it runs out of iterations) """ pass diff --git a/src/server/amp.py b/src/server/amp.py index ad1f4fc03d..5d1d9740bd 100644 --- a/src/server/amp.py +++ b/src/server/amp.py @@ -473,7 +473,7 @@ class AMPProtocol(amp.AMP): # set a flag in case we are about to shut down soon self.factory.server_restart_mode = True elif operation == SCONN: # server_force_connection (for irc/imc2 etc) - portal_sessionhandler.server_connect(data) + portal_sessionhandler.server_connect(**data) else: raise Exception("operation %(op)s not recognized." % {'op': operation}) return {} diff --git a/src/server/portal/portalsessionhandler.py b/src/server/portal/portalsessionhandler.py index 1188c3f00c..8e54bf960c 100644 --- a/src/server/portal/portalsessionhandler.py +++ b/src/server/portal/portalsessionhandler.py @@ -69,7 +69,7 @@ class PortalSessionHandler(SessionHandler): operation=PDISCONN) - def server_connect(self, protocol_class_path): + def server_connect(self, protocol_path="", uid=None): """ Called by server to force the initialization of a new protocol instance. Server wants this instance to get @@ -83,12 +83,12 @@ class PortalSessionHandler(SessionHandler): in a property sessionhandler. It must have a connectionMade() method, responsible for configuring itself and then calling self.sessionhandler.connect(self) - like any other protocol. + like any other newly connected protocol. """ global _MOD_IMPORT if not _MOD_IMPORT: from src.utils.utils import variable_from_module as _MOD_IMPORT - path, clsname = protocol_class_path.rsplit(".", 1) + path, clsname = protocol_path.rsplit(".", 1) cls = _MOD_IMPORT(path, clsname) protocol = cls() protocol.sessionhandler = self diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index f7ac098457..45b5337dce 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -37,6 +37,7 @@ SDISCONN = chr(5) # server session disconnect SDISCONNALL = chr(6) # server session disconnect all SSHUTD = chr(7) # server shutdown SSYNC = chr(8) # server session sync +SCONN = chr(9) # server portal connection (for bots) # i18n from django.utils.translation import ugettext as _ @@ -256,6 +257,25 @@ class ServerSessionHandler(SessionHandler): # announce the reconnection self.announce_all(_(" ... Server restarted.")) + # server-side access methods + + def start_bot_session(self, protocol_path, uid): + """ + This method allows the server-side to force the Portal to create + a new bot session using the protocol specified by protocol_path, + which should be the full python path to the class, including the + class name, like "src.server.portal.irc.IRCClient". + The new session will use the supplied player-bot uid to + initiate an already logged-in connection. The Portal will + treat this as a normal connection and henceforth so will the + Server. + """ + data = {"protocol_path":protocol_path, + "uid":uid} + self.server.amp_protocol.call_remote_PortalAdmin(0, + operation=SCONN, + data=data) + def portal_shutdown(self): """ Called by server when shutting down the portal. @@ -263,7 +283,6 @@ class ServerSessionHandler(SessionHandler): self.server.amp_protocol.call_remote_PortalAdmin(0, operation=SSHUTD, data="") - # server-side access methods def login(self, session, player, testmode=False): """