404 lines
13 KiB
Python
404 lines
13 KiB
Python
"""
|
|
Bots are a special child typeclasses of
|
|
Player that are controlled by the server.
|
|
|
|
"""
|
|
from __future__ import print_function
|
|
|
|
from django.conf import settings
|
|
from evennia.players.players import DefaultPlayer
|
|
from evennia.scripts.scripts import DefaultScript
|
|
from evennia.commands.command import Command
|
|
from evennia.commands.cmdset import CmdSet
|
|
from evennia.utils import search
|
|
|
|
_IDLE_TIMEOUT = settings.IDLE_TIMEOUT
|
|
|
|
_SESSIONS = None
|
|
|
|
|
|
# Bot helper utilities
|
|
|
|
class BotStarter(DefaultScript):
|
|
"""
|
|
This non-repeating script has the
|
|
sole purpose of kicking its bot
|
|
into gear when it is initialized.
|
|
|
|
"""
|
|
def at_script_creation(self):
|
|
"""
|
|
Called once, when script is created.
|
|
|
|
"""
|
|
self.key = "botstarter"
|
|
self.desc = "bot start/keepalive"
|
|
self.persistent = True
|
|
self.db.started = False
|
|
if _IDLE_TIMEOUT > 0:
|
|
# call before idle_timeout triggers
|
|
self.interval = int(max(60, _IDLE_TIMEOUT * 0.90))
|
|
self.start_delay = True
|
|
|
|
def at_start(self):
|
|
"""
|
|
Kick bot into gear.
|
|
|
|
"""
|
|
if not self.db.started:
|
|
self.player.start()
|
|
self.db.started = True
|
|
|
|
def at_repeat(self):
|
|
"""
|
|
Called self.interval seconds to keep connection. We cannot use
|
|
the IDLE command from inside the game since the system will
|
|
not catch it (commands executed from the server side usually
|
|
has no sessions). So we update the idle counter manually here
|
|
instead. This keeps the bot getting hit by IDLE_TIMEOUT.
|
|
|
|
"""
|
|
global _SESSIONS
|
|
if not _SESSIONS:
|
|
from evennia.server.sessionhandler import SESSIONS as _SESSIONS
|
|
for session in _SESSIONS.sessions_from_player(self.player):
|
|
session.update_session_counters(idle=True)
|
|
|
|
def at_server_reload(self):
|
|
"""
|
|
If server reloads we don't need to reconnect the protocol
|
|
again, this is handled by the portal reconnect mechanism.
|
|
|
|
"""
|
|
self.db.started = True
|
|
|
|
def at_server_shutdown(self):
|
|
"""
|
|
Make sure we are shutdown.
|
|
|
|
"""
|
|
self.db.started = False
|
|
|
|
|
|
class CmdBotListen(Command):
|
|
"""
|
|
This is a command that absorbs input aimed specifically at the
|
|
bot. The session must prepend its data with bot_data_in for this
|
|
to trigger.
|
|
|
|
"""
|
|
key = "bot_data_in"
|
|
def func(self):
|
|
"Relay to typeclass"
|
|
self.obj.execute_cmd(self.args.strip(), sessid=self.sessid)
|
|
|
|
class BotCmdSet(CmdSet):
|
|
"""
|
|
Holds the BotListen command
|
|
|
|
"""
|
|
key = "botcmdset"
|
|
def at_cmdset_creation(self):
|
|
self.add(CmdBotListen())
|
|
|
|
|
|
# Bot base class
|
|
|
|
class Bot(DefaultPlayer):
|
|
"""
|
|
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 basetype_setup(self):
|
|
"""
|
|
This sets up the basic properties for the bot.
|
|
|
|
"""
|
|
# the text encoding to use.
|
|
self.db.encoding = "utf-8"
|
|
# A basic security setup
|
|
lockstring = "examine:perm(Wizards);edit:perm(Wizards);delete:perm(Wizards);boot:perm(Wizards);msg:false()"
|
|
self.locks.add(lockstring)
|
|
# set the basics of being a bot
|
|
self.cmdset.add_default(BotCmdSet)
|
|
script_key = "%s" % self.key
|
|
self.scripts.add(BotStarter, key=script_key)
|
|
self.is_bot = True
|
|
|
|
def start(self, **kwargs):
|
|
"""
|
|
This starts the bot, whatever that may mean.
|
|
|
|
"""
|
|
pass
|
|
|
|
def msg(self, text=None, from_obj=None, sessid=None, **kwargs):
|
|
"""
|
|
Evennia -> outgoing protocol
|
|
|
|
"""
|
|
super(Bot, self).msg(text=text, from_obj=from_obj, sessid=sessid, **kwargs)
|
|
|
|
def execute_cmd(self, raw_string, sessid=None):
|
|
"""
|
|
Incoming protocol -> Evennia
|
|
|
|
"""
|
|
super(Bot, self).msg(raw_string, sessid=sessid)
|
|
|
|
def at_server_shutdown(self):
|
|
"""
|
|
We need to handle this case manually since the shutdown may be
|
|
a reset.
|
|
|
|
"""
|
|
for session in self.get_all_sessions():
|
|
session.sessionhandler.disconnect(session)
|
|
|
|
|
|
# Bot implementations
|
|
|
|
# IRC
|
|
|
|
class IRCBot(Bot):
|
|
"""
|
|
Bot for handling IRC connections.
|
|
|
|
"""
|
|
def start(self, ev_channel=None, irc_botname=None, irc_channel=None, irc_network=None, irc_port=None):
|
|
"""
|
|
Start by telling the portal to start a new session.
|
|
|
|
Args:
|
|
ev_channel (str): Key of the Evennia channel to connect to.
|
|
irc_botname (str): Name of bot to connect to irc channel. If
|
|
not set, use `self.key`.
|
|
irc_channel (str): Name of channel on the form `#channelname`.
|
|
irc_network (str): URL of the IRC network, like `irc.freenode.net`.
|
|
irc_port (str): Port number of the irc network, like `6667`.
|
|
|
|
"""
|
|
global _SESSIONS
|
|
if not _SESSIONS:
|
|
from evennia.server.sessionhandler import SESSIONS as _SESSIONS
|
|
|
|
# if keywords are given, store (the BotStarter script
|
|
# will not give any keywords, so this should normally only
|
|
# happen at initialization)
|
|
if irc_botname:
|
|
self.db.irc_botname = irc_botname
|
|
elif not self.db.irc_botname:
|
|
self.db.irc_botname = self.key
|
|
if ev_channel:
|
|
# connect to Evennia channel
|
|
channel = search.channel_search(ev_channel)
|
|
if not channel:
|
|
raise RuntimeError("Evennia Channel '%s' not found." % ev_channel)
|
|
channel = channel[0]
|
|
channel.connect(self)
|
|
self.db.ev_channel = channel
|
|
if irc_channel:
|
|
self.db.irc_channel = irc_channel
|
|
if irc_network:
|
|
self.db.irc_network = irc_network
|
|
if irc_port:
|
|
self.db.irc_port = irc_port
|
|
|
|
# instruct the server and portal to create a new session with
|
|
# the stored configuration
|
|
configdict = {"uid":self.dbid,
|
|
"botname": self.db.irc_botname,
|
|
"channel": self.db.irc_channel ,
|
|
"network": self.db.irc_network,
|
|
"port": self.db.irc_port}
|
|
_SESSIONS.start_bot_session("evennia.server.portal.irc.IRCBotFactory", configdict)
|
|
|
|
def msg(self, text=None, **kwargs):
|
|
"""
|
|
Takes text from connected channel (only).
|
|
|
|
Args:
|
|
text (str, optional): Incoming text from channel.
|
|
|
|
Kwargs:
|
|
from_channel (str): dbid of a channel this text originated from.
|
|
from_obj (str): dbid of an object sending this text.
|
|
|
|
"""
|
|
if not self.ndb.ev_channel and self.db.ev_channel:
|
|
# cache channel lookup
|
|
self.ndb.ev_channel = self.db.ev_channel
|
|
if "from_channel" in kwargs and text and self.ndb.ev_channel.dbid == kwargs["from_channel"]:
|
|
if "from_obj" not in kwargs or kwargs["from_obj"] != [self.id]:
|
|
text = "bot_data_out %s" % text
|
|
super(IRCBot, self).msg(text=text)
|
|
|
|
def execute_cmd(self, text=None, sessid=None):
|
|
"""
|
|
Take incoming data and send it to connected channel. This is
|
|
triggered by the CmdListen command in the BotCmdSet.
|
|
|
|
Args:
|
|
text (str, optional): Command string.
|
|
sessid (int, optional): Session id responsible for this
|
|
command.
|
|
|
|
"""
|
|
if not self.ndb.ev_channel and self.db.ev_channel:
|
|
# cache channel lookup
|
|
self.ndb.ev_channel = self.db.ev_channel
|
|
if self.ndb.ev_channel:
|
|
self.ndb.ev_channel.msg(text, senders=self.id)
|
|
|
|
# RSS
|
|
|
|
class RSSBot(Bot):
|
|
"""
|
|
An RSS relayer. The RSS protocol itself runs a ticker to update
|
|
its feed at regular intervals.
|
|
|
|
"""
|
|
def start(self, ev_channel=None, rss_url=None, rss_rate=None):
|
|
"""
|
|
Start by telling the portal to start a new RSS session
|
|
|
|
Args:
|
|
ev_channel (str): Key of the Evennia channel to connect to.
|
|
rss_url (str): Full URL to the RSS feed to subscribe to.
|
|
rss_update_rate (int): How often for the feedreader to update.
|
|
|
|
Raises:
|
|
RuntimeError: If `ev_channel` does not exist.
|
|
|
|
"""
|
|
global _SESSIONS
|
|
if not _SESSIONS:
|
|
from evennia.server.sessionhandler import SESSIONS as _SESSIONS
|
|
|
|
if ev_channel:
|
|
# connect to Evennia channel
|
|
channel = search.channel_search(ev_channel)
|
|
if not channel:
|
|
raise RuntimeError("Evennia Channel '%s' not found." % ev_channel)
|
|
channel = channel[0]
|
|
self.db.ev_channel = channel
|
|
if rss_url:
|
|
self.db.rss_url = rss_url
|
|
if rss_rate:
|
|
self.db.rss_rate = rss_rate
|
|
# instruct the server and portal to create a new session with
|
|
# the stored configuration
|
|
configdict = {"uid": self.dbid,
|
|
"url": self.db.rss_url,
|
|
"rate": self.db.rss_rate}
|
|
_SESSIONS.start_bot_session("evennia.server.portal.rss.RSSBotFactory", configdict)
|
|
|
|
def execute_cmd(self, text=None, sessid=None):
|
|
"""
|
|
Echo RSS input to connected channel
|
|
|
|
"""
|
|
if not self.ndb.ev_channel and self.db.ev_channel:
|
|
# cache channel lookup
|
|
self.ndb.ev_channel = self.db.ev_channel
|
|
if self.ndb.ev_channel:
|
|
self.ndb.ev_channel.msg(text, senders=self.id)
|
|
|
|
# IMC2 (not tested currently)
|
|
|
|
class IMC2Bot(Bot):
|
|
"""
|
|
IMC2 Bot
|
|
|
|
"""
|
|
def start(self, ev_channel=None, imc2_network=None, imc2_mudname=None,
|
|
imc2_port=None, imc2_client_pwd=None, imc2_server_pwd=None):
|
|
"""
|
|
Start by telling the portal to start a new session
|
|
|
|
Args:
|
|
ev_channel (str, optional): Key of the Evennia channel to connect to.
|
|
imc2_network (str, optional): IMC2 network name.
|
|
imc2_mudname (str, optional): Registered mudname (if not given, use settings.SERVERNAME).
|
|
imc2_port (int, optional): Port number of the IMC2 network.
|
|
imc2_client_pwd (str, optional): Client password registered with IMC2 network.
|
|
imc2_server_pwd (str, optional): Server password registered with IMC2 network.
|
|
|
|
Raises:
|
|
RuntimeError: If `ev_channel` was not found.
|
|
|
|
"""
|
|
global _SESSIONS
|
|
if not _SESSIONS:
|
|
from evennia.server.sessionhandler import SESSIONS as _SESSIONS
|
|
if ev_channel:
|
|
# connect to Evennia channel
|
|
channel = search.channel_search(ev_channel)
|
|
if not channel:
|
|
raise RuntimeError("Evennia Channel '%s' not found." % ev_channel)
|
|
channel = channel[0]
|
|
channel.connect(self)
|
|
self.db.ev_channel = channel
|
|
if imc2_network:
|
|
self.db.imc2_network = imc2_network
|
|
if imc2_port:
|
|
self.db.imc2_port = imc2_port
|
|
if imc2_mudname:
|
|
self.db.imc2_mudname = imc2_mudname
|
|
elif not self.db.imc2_mudname:
|
|
self.db.imc2_mudname = settings.SERVERNAME
|
|
# storing imc2 passwords in attributes - a possible
|
|
# security issue?
|
|
if imc2_server_pwd:
|
|
self.db.imc2_server_pwd = imc2_server_pwd
|
|
if imc2_client_pwd:
|
|
self.db.imc2_client_pwd = imc2_client_pwd
|
|
|
|
configdict = {"uid": self.dbid,
|
|
"mudname": self.db.imc2_mudname,
|
|
"network": self.db.imc2_network,
|
|
"port": self.db.imc2_port,
|
|
"client_pwd": self.db.client_pwd,
|
|
"server_pwd": self.db.server_pwd}
|
|
|
|
_SESSIONS.start_bot_session("evennia.server.portal.imc2.IMC2BotFactory", configdict)
|
|
|
|
def msg(self, text=None, **kwargs):
|
|
"""
|
|
Takes text from connected channel (only).
|
|
|
|
Args:
|
|
text (str): Message from channel.
|
|
|
|
Kwargs:
|
|
from_channel (str): dbid of a channel this text originated from.
|
|
from_obj (str): dbid of an object sending this text.
|
|
|
|
"""
|
|
if not self.ndb.ev_channel and self.db.ev_channel:
|
|
# cache channel lookup
|
|
self.ndb.ev_channel = self.db.ev_channel
|
|
if "from_channel" in kwargs and text and self.ndb.ev_channel.dbid == kwargs["from_channel"]:
|
|
if "from_obj" not in kwargs or kwargs["from_obj"] != [self.id]:
|
|
text = "bot_data_out %s" % text
|
|
self.msg(text=text)
|
|
|
|
def execute_cmd(self, text=None, sessid=None):
|
|
"""
|
|
Relay incoming data to connected channel.
|
|
|
|
Args:
|
|
text (str, optional): Command string.
|
|
sessid (int, optional): Session id responsible for this
|
|
command.
|
|
|
|
"""
|
|
if not self.ndb.ev_channel and self.db.ev_channel:
|
|
# cache channel lookup
|
|
self.ndb.ev_channel = self.db.ev_channel
|
|
if self.ndb.ev_channel:
|
|
self.ndb.ev_channel.msg(text, senders=self.id)
|
|
|