mirror of
https://github.com/evennia/evennia.git
synced 2026-03-25 01:06:32 +01:00
Cleanups and bug fixes. Fixed the @unlink command and also made it overally more stable. Resolves issue 161. Added more string conversion routines to handle non-ascii variables being stored in an Attribute. Resolves issue 160.
This commit is contained in:
parent
14db4bea4d
commit
7d30b337d9
27 changed files with 873 additions and 1048 deletions
444
src/comms/imc2.py
Normal file
444
src/comms/imc2.py
Normal file
|
|
@ -0,0 +1,444 @@
|
|||
"""
|
||||
IMC2 client module. Handles connecting to and communicating with an IMC2 server.
|
||||
"""
|
||||
|
||||
from time import time
|
||||
from twisted.application import internet
|
||||
from twisted.internet import protocol
|
||||
from twisted.conch import telnet
|
||||
from django.conf import settings
|
||||
|
||||
from src.utils import logger, create, search, utils
|
||||
from src.server.sessionhandler import SESSIONS
|
||||
from src.scripts.scripts import Script
|
||||
from src.comms.models import Channel, ExternalChannelConnection
|
||||
from src.comms.imc2lib import imc2_packets as pck
|
||||
from src.comms.imc2lib.imc2_trackers import IMC2MudList, IMC2ChanList
|
||||
from src.comms.imc2lib.imc2_listeners import handle_whois_reply
|
||||
|
||||
|
||||
# channel to send info to
|
||||
INFOCHANNEL = Channel.objects.channel_search(settings.CHANNEL_MUDINFO[0])
|
||||
# all linked channel connection
|
||||
IMC2_CHANNELS = []
|
||||
# IMC2 debug mode
|
||||
IMC2_DEBUG = True
|
||||
# Use this instance to keep track of the other games on the network.
|
||||
IMC2_MUDLIST = IMC2MudList()
|
||||
# Tracks the list of available channels on the network.
|
||||
IMC2_CHANLIST = IMC2ChanList()
|
||||
|
||||
#
|
||||
# Helper method
|
||||
#
|
||||
|
||||
def msg_info(message):
|
||||
"""
|
||||
Send info to default info channel
|
||||
"""
|
||||
message = '[%s][IMC2]: %s' % (INFOCHANNEL[0].key, message)
|
||||
try:
|
||||
INFOCHANNEL[0].msg(message)
|
||||
except AttributeError:
|
||||
logger.log_infomsg("MUDinfo (imc2): %s" % message)
|
||||
|
||||
#
|
||||
# Regular scripts
|
||||
#
|
||||
|
||||
class Send_IsAlive(Script):
|
||||
"""
|
||||
Sends periodic keepalives to network neighbors. This lets the other
|
||||
games know that our game is still up and connected to the network. Also
|
||||
provides some useful information about the client game.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
self.key = 'IMC2_Send_IsAlive'
|
||||
self.interval = 900
|
||||
self.desc = "Send an IMC2 is-alive packet"
|
||||
self.persistent = True
|
||||
def at_repeat(self):
|
||||
for channel in IMC2_CHANNELS:
|
||||
channel.send_packet(pck.IMC2PacketIsAlive())
|
||||
def is_valid(self):
|
||||
"Is only valid as long as there are channels to update"
|
||||
return IMC2_CHANNELS
|
||||
|
||||
class Send_Keepalive_Request(Script):
|
||||
"""
|
||||
Event: Sends a keepalive-request to connected games in order to see who
|
||||
is connected.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
self.key = "IMC2_Send_Keepalive_Request"
|
||||
self.interval = 3500
|
||||
self.desc = "Send an IMC2 keepalive-request packet"
|
||||
self.persistent = True
|
||||
def at_repeat(self):
|
||||
for channel in IMC2_CHANNELS:
|
||||
channel.send_packet(pck.IMC2PacketKeepAliveRequest())
|
||||
def is_valid(self):
|
||||
"Is only valid as long as there are channels to update"
|
||||
return IMC2_CHANNELS
|
||||
|
||||
class Prune_Inactive_Muds(Script):
|
||||
"""
|
||||
Prunes games that have not sent is-alive packets for a while. If
|
||||
we haven't heard from them, they're probably not connected or don't
|
||||
implement the protocol correctly. In either case, good riddance to them.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
self.key = "IMC2_Prune_Inactive_Muds"
|
||||
self.interval = 1800
|
||||
self.desc = "Check IMC2 list for inactive games"
|
||||
self.persistent = True
|
||||
self.inactive_threshold = 3599
|
||||
def at_repeat(self):
|
||||
for name, mudinfo in IMC2_MUDLIST.mud_list.items():
|
||||
if time() - mudinfo.last_updated > self.inactive_threshold:
|
||||
del IMC2_MUDLIST.mud_list[name]
|
||||
def is_valid(self):
|
||||
"Is only valid as long as there are channels to update"
|
||||
return IMC2_CHANNELS
|
||||
|
||||
class Sync_Server_Channel_List(Script):
|
||||
"""
|
||||
Re-syncs the network's channel list. This will
|
||||
cause a cascade of reply packets of a certain type
|
||||
from the network. These are handled by the protocol,
|
||||
gradually updating the channel cache.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
self.key = "IMC2_Sync_Server_Channel_List"
|
||||
self.interval = 24 * 3600 # once every day
|
||||
self.desc = "Re-sync IMC2 network channel list"
|
||||
self.persistent = True
|
||||
def at_repeat(self):
|
||||
checked_networks = []
|
||||
for channel in self.IMC2_CHANNELS:
|
||||
network = channel.external_config.split['|'][0]
|
||||
if not network in checked_networks:
|
||||
channel.send_packet(pkg.IMC2PacketIceRefresh())
|
||||
checked_networks.append(network)
|
||||
|
||||
#
|
||||
# IMC2 protocol
|
||||
#
|
||||
|
||||
class IMC2Protocol(telnet.StatefulTelnetProtocol):
|
||||
"""
|
||||
Provides the abstraction for the IMC2 protocol. Handles connection,
|
||||
authentication, and all necessary packets.
|
||||
"""
|
||||
def __init__(self):
|
||||
global IMC2_CHANNELS
|
||||
IMC2_CHANNELS.append(self)
|
||||
self.is_authenticated = False
|
||||
self.auth_type = None
|
||||
self.server_name = None
|
||||
self.network_name = None
|
||||
self.sequence = None
|
||||
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
Triggered after connecting to the IMC2 network.
|
||||
"""
|
||||
self.auth_type = "plaintext"
|
||||
logger.log_infomsg("IMC2: Connected to network server.")
|
||||
logger.log_infomsg("IMC2: Sending authentication packet.")
|
||||
self.send_packet(pck.IMC2PacketAuthPlaintext())
|
||||
|
||||
def send_packet(self, packet):
|
||||
"""
|
||||
Given a sub-class of IMC2Packet, assemble the packet and send it
|
||||
on its way to the IMC2 server.
|
||||
|
||||
Evennia -> IMC2
|
||||
"""
|
||||
if self.sequence:
|
||||
# This gets incremented with every command.
|
||||
self.sequence += 1
|
||||
packet.imc2_protocol = self
|
||||
packet_str = utils.to_str(packet.assemble(self.factory.mudname, self.factory.client_pwd, self.factory.server_pwd))
|
||||
if IMC2_DEBUG:
|
||||
logger.log_infomsg("IMC2: SENT> %s" % packet_str)
|
||||
self.sendLine(packet_str)
|
||||
|
||||
def _parse_auth_response(self, line):
|
||||
"""
|
||||
Parses the IMC2 network authentication packet.
|
||||
"""
|
||||
if self.auth_type == "plaintext":
|
||||
# Plain text passwords.
|
||||
# SERVER Sends: PW <servername> <serverpw> version=<version#> <networkname>
|
||||
|
||||
if IMC2_DEBUG:
|
||||
logger.log_infomsg("IMC2: AUTH< %s" % line)
|
||||
|
||||
line_split = line.split(' ')
|
||||
pw_present = line_split[0] == 'PW'
|
||||
autosetup_present = line_split[0] == 'autosetup'
|
||||
|
||||
if "reject" in line_split:
|
||||
auth_message = "IMC2 server rejected connection."
|
||||
logger.log_infomsg(auth_message)
|
||||
msg_info(auth_message)
|
||||
return
|
||||
|
||||
if pw_present:
|
||||
self.server_name = line_split[1]
|
||||
self.network_name = line_split[4]
|
||||
elif autosetup_present:
|
||||
logger.log_infomsg("IMC2: Autosetup response found.")
|
||||
self.server_name = line_split[1]
|
||||
self.network_name = line_split[3]
|
||||
self.is_authenticated = True
|
||||
self.sequence = int(time())
|
||||
|
||||
# Log to stdout and notify over MUDInfo.
|
||||
auth_message = "Successfully authenticated to the '%s' network." % self.network_name
|
||||
logger.log_infomsg('IMC2: %s' % auth_message)
|
||||
msg_info(auth_message)
|
||||
|
||||
# Ask to see what other MUDs are connected.
|
||||
self.send_packet(pck.IMC2PacketKeepAliveRequest())
|
||||
# IMC2 protocol states that KeepAliveRequests should be followed
|
||||
# up by the requester sending an IsAlive packet.
|
||||
self.send_packet(pck.IMC2PacketIsAlive())
|
||||
# Get a listing of channels.
|
||||
self.send_packet(pck.IMC2PacketIceRefresh())
|
||||
|
||||
def _msg_evennia(self, packet):
|
||||
"""
|
||||
Handle the sending of packet data to Evennia channel
|
||||
(Message from IMC2 -> Evennia)
|
||||
"""
|
||||
conn_name = packet.optional_data.get('channel', None)
|
||||
# If the packet lacks the 'echo' key, don't bother with it.
|
||||
has_echo = packet.optional_data.get('echo', None)
|
||||
if conn_name and has_echo:
|
||||
# The second half of this is the channel name: Server:Channel
|
||||
chan_name = conn_name.split(':', 1)[1]
|
||||
key = "imc2_%s" % conn_name
|
||||
# Look for matching IMC2 channel maps.
|
||||
conns = ExternalChannelConnection.objects.filter(db_external_key=self.factory.key)
|
||||
if not conns:
|
||||
return
|
||||
|
||||
# Format the message to send to local channel.
|
||||
message = '[%s] %s@%s: %s' % (self.factory.evennia_channel, packet.sender, packet.origin, packet.optional_data.get('text'))
|
||||
|
||||
for conn in conns:
|
||||
if conn.channel:
|
||||
conn.to_channel(message)
|
||||
|
||||
def _format_tell(self, packet):
|
||||
"""
|
||||
Handle tells over IMC2 by formatting the text properly
|
||||
"""
|
||||
return "%s@%s IMC tells: %s" % (packet.sender, packet.origin,
|
||||
packet.optional_data.get('text', 'ERROR: No text provided.'))
|
||||
|
||||
def lineReceived(self, line):
|
||||
"""
|
||||
Triggered when text is received from the IMC2 network. Figures out
|
||||
what to do with the packet.
|
||||
IMC2 -> Evennia
|
||||
"""
|
||||
if not self.is_authenticated:
|
||||
self._parse_auth_response(line)
|
||||
else:
|
||||
if IMC2_DEBUG and not 'is-alive' in line:
|
||||
# if IMC2_DEBUG mode is on, print the contents of the packet
|
||||
# to stdout.
|
||||
logger.log_infomsg("PACKET: %s" % line)
|
||||
|
||||
# Parse the packet and encapsulate it for easy access
|
||||
packet = pck.IMC2Packet(self.factory.mudname, packet_str=line)
|
||||
|
||||
if IMC2_DEBUG and packet.packet_type not in ['is-alive', 'keepalive-request']:
|
||||
# Print the parsed packet's __str__ representation.
|
||||
# is-alive and keepalive-requests happen pretty frequently.
|
||||
# Don't bore us with them in stdout.
|
||||
logger.log_infomsg(str(packet))
|
||||
|
||||
|
||||
# Figure out what kind of packet we're dealing with and hand it
|
||||
# off to the correct handler.
|
||||
|
||||
if packet.packet_type == 'is-alive':
|
||||
IMC2_MUDLIST.update_mud_from_packet(packet)
|
||||
elif packet.packet_type == 'keepalive-request':
|
||||
# Don't need to check the destination, we only receive these
|
||||
# packets when they are intended for us.
|
||||
self.send_packet(pck.IMC2PacketIsAlive())
|
||||
elif packet.packet_type == 'ice-msg-b':
|
||||
self._msg_evennia(packet)
|
||||
elif packet.packet_type == 'whois-reply':
|
||||
handle_whois_reply(packet)
|
||||
elif packet.packet_type == 'close-notify':
|
||||
IMC2_MUDLIST.remove_mud_from_packet(packet)
|
||||
elif packet.packet_type == 'ice-update':
|
||||
IMC2_CHANLIST.update_channel_from_packet(packet)
|
||||
elif packet.packet_type == 'ice-destroy':
|
||||
IMC2_CHANLIST.remove_channel_from_packet(packet)
|
||||
elif packet.packet_type == 'tell':
|
||||
player = search.players(packet.target)
|
||||
if not player:
|
||||
return
|
||||
player.msg(self._format_tell(packet))
|
||||
|
||||
def msg_imc2(self, message, from_obj=None):
|
||||
"""
|
||||
Called by Evennia to send a message through the imc2 connection
|
||||
"""
|
||||
conns = ExternalChannelConnection.objects.filter(db_external_key=self.factory.key)
|
||||
if not conns:
|
||||
return
|
||||
if from_obj:
|
||||
if hasattr(from_obj, 'key'):
|
||||
from_name = from_obj.key
|
||||
else:
|
||||
from_name = from_obj
|
||||
else:
|
||||
from_name = self.factory.mudname
|
||||
# send the packet
|
||||
self.send_packet(pck.IMC2PacketIceMsgBroadcasted(self.factory.network, self.factory.channel,
|
||||
from_name, message))
|
||||
|
||||
class IMC2Factory(protocol.ClientFactory):
|
||||
"""
|
||||
Creates instances of the IMC2Protocol. Should really only ever create one
|
||||
in our particular instance. Tied in via src/server.py.
|
||||
"""
|
||||
protocol = IMC2Protocol
|
||||
|
||||
def __init__(self, key, channel, network, port, mudname, client_pwd, server_pwd, evennia_channel):
|
||||
self.key = key
|
||||
self.mudname = mudname
|
||||
self.channel = channel
|
||||
self.pretty_key = "%s:%s/%s (%s)" % (network, port, channel, mudname)
|
||||
self.network = network
|
||||
self.protocol_version = '2'
|
||||
self.client_pwd = client_pwd
|
||||
self.server_pwd = server_pwd
|
||||
self.evennia_channel = evennia_channel
|
||||
|
||||
|
||||
def clientConnectionFailed(self, connector, reason):
|
||||
message = 'Connection failed: %s' % reason.getErrorMessage()
|
||||
msg_info(message)
|
||||
logger.log_errmsg('IMC2: %s' % message)
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
message = 'Connection lost: %s' % reason.getErrorMessage()
|
||||
msg_info(message)
|
||||
logger.log_errmsg('IMC2: %s' % message)
|
||||
|
||||
|
||||
def build_connection_key(channel, imc2_network, imc2_port, imc2_channel, imc2_mudname):
|
||||
"Build an id hash for the connection"
|
||||
if hasattr(channel, 'key'):
|
||||
channel = channel.key
|
||||
return "imc2_%s:%s/%s(%s)<>%s" % (imc2_network, imc2_port, imc2_channel, imc2_mudname, channel)
|
||||
|
||||
def build_service_key(key):
|
||||
return "IMC2:%s" % key
|
||||
|
||||
|
||||
def start_scripts(validate=False):
|
||||
"""
|
||||
Start all the needed scripts
|
||||
"""
|
||||
|
||||
if validate:
|
||||
from src.utils import reloads
|
||||
reloads.reload_scripts()
|
||||
return
|
||||
if not search.scripts("IMC2_Send_IsAlive"):
|
||||
create.create_script(Send_IsAlive)
|
||||
if not search.scripts("IMC2_Send_Keepalive_Request"):
|
||||
create.create_script(Send_Keepalive_Request)
|
||||
if not search.scripts("IMC2_Prune_Inactive_Muds"):
|
||||
create.create_script(Prune_Inactive_Muds)
|
||||
if not search.scripts("IMC2_Sync_Server_Channel_List"):
|
||||
create.create_script(Sync_Server_Channel_List)
|
||||
|
||||
def create_connection(channel, imc2_network, imc2_port, imc2_channel, imc2_mudname, imc2_client_pwd, imc2_server_pwd):
|
||||
"""
|
||||
This will create a new IMC2<->channel connection.
|
||||
"""
|
||||
if not type(channel) == Channel:
|
||||
new_channel = Channel.objects.filter(db_key=channel)
|
||||
if not new_channel:
|
||||
logger.log_errmsg("Cannot attach IMC2<->Evennia: Evennia Channel '%s' not found" % channel)
|
||||
return False
|
||||
channel = new_channel[0]
|
||||
key = build_connection_key(channel, imc2_network, imc2_port, imc2_channel, imc2_mudname)
|
||||
|
||||
old_conns = ExternalChannelConnection.objects.filter(db_external_key=key)
|
||||
if old_conns:
|
||||
return False
|
||||
config = "%s|%s|%s|%s|%s|%s" % (imc2_network, imc2_port, imc2_channel, imc2_mudname, imc2_client_pwd, imc2_server_pwd)
|
||||
# how the channel will be able to contact this protocol
|
||||
send_code = "from src.comms.imc2 import IMC2_CHANNELS\n"
|
||||
send_code += "matched_imc2s = [imc2 for imc2 in IMC2_CHANNELS if imc2.factory.key == '%s']\n" % key
|
||||
send_code += "[imc2.msg_imc2(message, from_obj=from_obj) for imc2 in matched_imc2s]\n"
|
||||
conn = ExternalChannelConnection(db_channel=channel, db_external_key=key, db_external_send_code=send_code,
|
||||
db_external_config=config)
|
||||
conn.save()
|
||||
|
||||
# connect
|
||||
connect_to_imc2(conn)
|
||||
# start scripts (if needed)
|
||||
start_scripts()
|
||||
return True
|
||||
|
||||
def delete_connection(channel, imc2_network, imc2_port, imc2_channel, mudname):
|
||||
"Destroy a connection"
|
||||
if hasattr(channel, 'key'):
|
||||
channel = channel.key
|
||||
|
||||
key = build_connection_key(channel, imc2_network, imc2_port, imc2_channel, mudname)
|
||||
service_key = build_service_key(key)
|
||||
try:
|
||||
conn = ExternalChannelConnection.objects.get(db_external_key=key)
|
||||
except Exception:
|
||||
return False
|
||||
conn.delete()
|
||||
|
||||
try:
|
||||
service = SESSIONS.server.services.getServiceNamed(service_key)
|
||||
except Exception:
|
||||
return True
|
||||
if service.running:
|
||||
SESSIONS.server.services.removeService(service)
|
||||
# validate scripts
|
||||
start_scripts(validate=True)
|
||||
return True
|
||||
|
||||
def connect_to_imc2(connection):
|
||||
"Create the imc instance and connect to the IMC2 network and channel."
|
||||
# get config
|
||||
key = utils.to_str(connection.external_key)
|
||||
service_key = build_service_key(key)
|
||||
imc2_network, imc2_port, imc2_channel, imc2_mudname, imc2_client_pwd, imc2_server_pwd = \
|
||||
[utils.to_str(conf) for conf in connection.external_config.split('|')]
|
||||
# connect
|
||||
imc = internet.TCPClient(imc2_network, int(imc2_port), IMC2Factory(key, imc2_channel, imc2_network, imc2_port, imc2_mudname,
|
||||
imc2_client_pwd, imc2_server_pwd, connection.channel.key))
|
||||
imc.setName(service_key)
|
||||
SESSIONS.server.services.addService(imc)
|
||||
|
||||
def connect_all():
|
||||
"""
|
||||
Activate all imc2 bots.
|
||||
|
||||
Returns a list of (key, TCPClient) tuples for server to properly set services.
|
||||
"""
|
||||
connections = ExternalChannelConnection.objects.filter(db_external_key__startswith='imc2_')
|
||||
for connection in connections:
|
||||
connect_to_imc2(connection)
|
||||
if connections:
|
||||
start_scripts()
|
||||
0
src/comms/imc2lib/__init__.py
Normal file
0
src/comms/imc2lib/__init__.py
Normal file
66
src/comms/imc2lib/imc2_ansi.py
Normal file
66
src/comms/imc2lib/imc2_ansi.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"""
|
||||
ANSI parser - this adds colour to text according to
|
||||
special markup strings.
|
||||
|
||||
This is a IMC2 complacent version.
|
||||
"""
|
||||
|
||||
from src.utils.ansi import ANSIParser, ANSITable
|
||||
|
||||
class IMCANSIParser(ANSIParser):
|
||||
"""
|
||||
This parser is per the IMC2 specification.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.ansi_subs = [
|
||||
# Random
|
||||
(r'~Z', ANSITable.ansi["normal"]),
|
||||
# Dark Grey
|
||||
(r'~D', ANSITable.ansi["hilite"] + ANSITable.ansi["black"]),
|
||||
(r'~z', ANSITable.ansi["hilite"] + ANSITable.ansi["black"]),
|
||||
# Grey/reset
|
||||
(r'~w', ANSITable.ansi["normal"]),
|
||||
(r'~d', ANSITable.ansi["normal"]),
|
||||
(r'~!', ANSITable.ansi["normal"]),
|
||||
# Bold/hilite
|
||||
(r'~L', ANSITable.ansi["hilite"]),
|
||||
# White
|
||||
(r'~W', ANSITable.ansi["normal"] + ANSITable.ansi["hilite"]),
|
||||
# Dark Green
|
||||
(r'~g', ANSITable.ansi["normal"] + ANSITable.ansi["green"]),
|
||||
# Green
|
||||
(r'~G', ANSITable.ansi["hilite"] + ANSITable.ansi["green"]),
|
||||
# Dark magenta
|
||||
(r'~p', ANSITable.ansi["normal"] + ANSITable.ansi["magenta"]),
|
||||
(r'~m', ANSITable.ansi["normal"] + ANSITable.ansi["magenta"]),
|
||||
# Magenta
|
||||
(r'~M', ANSITable.ansi["hilite"] + ANSITable.ansi["magenta"]),
|
||||
(r'~P', ANSITable.ansi["hilite"] + ANSITable.ansi["magenta"]),
|
||||
# Black
|
||||
(r'~x', ANSITable.ansi["normal"] + ANSITable.ansi["black"]),
|
||||
# Cyan
|
||||
(r'~c', ANSITable.ansi["normal"] + ANSITable.ansi["cyan"]),
|
||||
# Dark Yellow (brown)
|
||||
(r'~Y', ANSITable.ansi["hilite"] + ANSITable.ansi["yellow"]),
|
||||
# Yellow
|
||||
(r'~y', ANSITable.ansi["normal"] + ANSITable.ansi["yellow"]),
|
||||
# Dark Blue
|
||||
(r'~B', ANSITable.ansi["normal"] + ANSITable.ansi["blue"]),
|
||||
# Blue
|
||||
(r'~C', ANSITable.ansi["hilite"] + ANSITable.ansi["blue"]),
|
||||
# Dark Red
|
||||
(r'~r', ANSITable.ansi["normal"] + ANSITable.ansi["red"]),
|
||||
# Red
|
||||
(r'~R', ANSITable.ansi["normal"] + ANSITable.ansi["red"]),
|
||||
# Dark Blue
|
||||
(r'~b', ANSITable.ansi["normal"] + ANSITable.ansi["blue"]),
|
||||
## Formatting
|
||||
(r'\\r', ANSITable.ansi["normal"]),
|
||||
(r'\\n', ANSITable.ansi["return"]),
|
||||
]
|
||||
|
||||
def parse_ansi(*args, **kwargs):
|
||||
"""
|
||||
Shortcut to use the IMC2 ANSI parser.
|
||||
"""
|
||||
return ansi.parse_ansi(parser=IMCANSIParser(), *args, **kwargs)
|
||||
16
src/comms/imc2lib/imc2_listeners.py
Normal file
16
src/comms/imc2lib/imc2_listeners.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
This module handles some of the -reply packets like whois-reply.
|
||||
"""
|
||||
from src.objects.models import ObjectDB
|
||||
from src.comms.imc2lib import imc2_ansi
|
||||
|
||||
def handle_whois_reply(packet):
|
||||
try:
|
||||
pobject = ObjectDB.objects.get(id=packet.target)
|
||||
response_text = imc_ansi.parse_ansi(packet.optional_data.get('text',
|
||||
'Unknown'))
|
||||
pobject.emit_to('Whois reply from %s: %s' % (packet.origin,
|
||||
response_text))
|
||||
except Object.DoesNotExist:
|
||||
# No match found for whois sender. Ignore it.
|
||||
pass
|
||||
755
src/comms/imc2lib/imc2_packets.py
Normal file
755
src/comms/imc2lib/imc2_packets.py
Normal file
|
|
@ -0,0 +1,755 @@
|
|||
"""
|
||||
IMC2 packets. These are pretty well documented at:
|
||||
http://www.mudbytes.net/index.php?a=articles&s=imc2_protocol
|
||||
|
||||
"""
|
||||
import shlex
|
||||
from django.conf import settings
|
||||
|
||||
class Lexxer(shlex.shlex):
|
||||
"""
|
||||
A lexical parser for interpreting IMC2 packets.
|
||||
"""
|
||||
def __init__(self, packet_str, posix=True):
|
||||
shlex.shlex.__init__(self, packet_str, posix=True)
|
||||
# Single-quotes are notably not present. This is important!
|
||||
self.quotes = '"'
|
||||
self.commenters = ''
|
||||
# This helps denote what constitutes a continuous token.
|
||||
self.wordchars += "~`!@#$%^&*()-_+=[{]}|\\;:',<.>/?"
|
||||
|
||||
class IMC2Packet(object):
|
||||
"""
|
||||
Base IMC2 packet class. This is generally sub-classed, aside from using it
|
||||
to parse incoming packets from the IMC2 network server.
|
||||
"""
|
||||
def __init__(self, mudname=None, packet_str=None):
|
||||
"""
|
||||
Optionally, parse a packet and load it up.
|
||||
"""
|
||||
# The following fields are all according to the basic packet format of:
|
||||
# <sender>@<origin> <sequence> <route> <packet-type> <target>@<destination> <data...>
|
||||
self.sender = None
|
||||
if not mudname:
|
||||
mudname = settings.SERVERNAME
|
||||
self.origin = mudname
|
||||
self.sequence = None
|
||||
self.route = mudname
|
||||
self.packet_type = None
|
||||
self.target = None
|
||||
self.destination = None
|
||||
# Optional data.
|
||||
self.optional_data = {}
|
||||
# Reference to the IMC2Protocol object doing the sending.
|
||||
self.imc2_protocol = None
|
||||
|
||||
if packet_str:
|
||||
# The lexxer handles the double quotes correctly, unlike just
|
||||
# splitting. Spaces throw things off, so shlex handles it
|
||||
# gracefully, ala POSIX shell-style parsing.
|
||||
lex = Lexxer(packet_str)
|
||||
|
||||
# Token counter.
|
||||
counter = 0
|
||||
for token in lex:
|
||||
if counter == 0:
|
||||
# This is the sender@origin token.
|
||||
sender_origin = token
|
||||
split_sender_origin = sender_origin.split('@')
|
||||
self.sender = split_sender_origin[0].strip()
|
||||
self.origin = split_sender_origin[1]
|
||||
elif counter == 1:
|
||||
# Numeric time-based sequence.
|
||||
self.sequence = token
|
||||
elif counter == 2:
|
||||
# Packet routing info.
|
||||
self.route = token
|
||||
elif counter == 3:
|
||||
# Packet type string.
|
||||
self.packet_type = token
|
||||
elif counter == 4:
|
||||
# Get values for the target and destination attributes.
|
||||
target_destination = token
|
||||
split_target_destination = target_destination.split('@')
|
||||
self.target = split_target_destination[0]
|
||||
try:
|
||||
self.destination = split_target_destination[1]
|
||||
except IndexError:
|
||||
# There is only one element to the target@dest segment
|
||||
# of the packet. Wipe the target and move the captured
|
||||
# value to the destination attrib.
|
||||
self.target = '*'
|
||||
self.destination = split_target_destination[0]
|
||||
elif counter > 4:
|
||||
# Populate optional data.
|
||||
try:
|
||||
key, value = token.split('=', 1)
|
||||
self.optional_data[key] = value
|
||||
except ValueError:
|
||||
# Failed to split on equal sign, disregard.
|
||||
pass
|
||||
# Increment and continue to the next token (if applicable)
|
||||
counter += 1
|
||||
|
||||
def __str__(self):
|
||||
retval = """
|
||||
-- Begin Packet Display --
|
||||
Sender: %s
|
||||
Origin: %s
|
||||
Sequence: %s
|
||||
Route: %s
|
||||
Type: %s
|
||||
Target: %s
|
||||
Destination: %s
|
||||
Data: %s
|
||||
- End Packet Display --""" % (self.sender,
|
||||
self.origin,
|
||||
self.sequence,
|
||||
self.route,
|
||||
self.packet_type,
|
||||
self.target,
|
||||
self.destination,
|
||||
self.optional_data)
|
||||
return retval
|
||||
|
||||
def _get_optional_data_string(self):
|
||||
"""
|
||||
Generates the optional data string to tack on to the end of the packet.
|
||||
"""
|
||||
if self.optional_data:
|
||||
data_string = ''
|
||||
for key, value in self.optional_data.items():
|
||||
# Determine the number of words in this value.
|
||||
words = len(str(value).split(' '))
|
||||
# Anything over 1 word needs double quotes.
|
||||
if words > 1:
|
||||
value = '"%s"' % (value,)
|
||||
data_string += '%s=%s ' % (key, value)
|
||||
return data_string.strip()
|
||||
else:
|
||||
return ''
|
||||
|
||||
def _get_sender_name(self):
|
||||
"""
|
||||
Calculates the sender name to be sent with the packet.
|
||||
"""
|
||||
if self.sender == '*':
|
||||
# Some packets have no sender.
|
||||
return '*'
|
||||
elif str(self.sender).isdigit():
|
||||
return self.sender
|
||||
elif type(self.sender) in [type(u""),type(str())]:
|
||||
#this is used by e.g. IRC where no user object is present.
|
||||
return self.sender.strip().replace(' ', '_')
|
||||
elif self.sender:
|
||||
# Player object.
|
||||
name = self.sender.get_name(fullname=False, show_dbref=False,
|
||||
show_flags=False,
|
||||
no_ansi=True)
|
||||
# IMC2 does not allow for spaces.
|
||||
return name.strip().replace(' ', '_')
|
||||
else:
|
||||
# None value. Do something or other.
|
||||
return 'Unknown'
|
||||
|
||||
def assemble(self, mudname=None, client_pwd=None, server_pwd=None):
|
||||
"""
|
||||
Assembles the packet and returns the ready-to-send string.
|
||||
Note that the arguments are not used, they are there for
|
||||
consistency across all packets.
|
||||
"""
|
||||
self.sequence = self.imc2_protocol.sequence
|
||||
packet = "%s@%s %s %s %s %s@%s %s\n" % (
|
||||
self._get_sender_name(),
|
||||
self.origin,
|
||||
self.sequence,
|
||||
self.route,
|
||||
self.packet_type,
|
||||
self.target,
|
||||
self.destination,
|
||||
self._get_optional_data_string())
|
||||
return packet.strip()
|
||||
|
||||
class IMC2PacketAuthPlaintext(object):
|
||||
"""
|
||||
IMC2 plain-text authentication packet. Auth packets are strangely
|
||||
formatted, so this does not sub-class IMC2Packet. The SHA and plain text
|
||||
auth packets are the two only non-conformers.
|
||||
|
||||
CLIENT Sends:
|
||||
PW <mudname> <clientpw> version=<version#> autosetup <serverpw> (SHA256)
|
||||
|
||||
Optional Arguments( required if using the specified authentication method:
|
||||
(SHA256) The literal string: SHA256. This is sent to notify the server
|
||||
that the MUD is SHA256-Enabled. All future logins from this
|
||||
client will be expected in SHA256-AUTH format if the server
|
||||
supports it.
|
||||
"""
|
||||
def assemble(self, mudname=None, client_pwd=None, server_pwd=None):
|
||||
"""
|
||||
This is one of two strange packets, just assemble the packet manually
|
||||
and go.
|
||||
"""
|
||||
return 'PW %s %s version=2 autosetup %s\n' %(mudname, client_pwd, server_pwd)
|
||||
|
||||
class IMC2PacketKeepAliveRequest(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet is sent by a MUD to trigger is-alive packets from other MUDs.
|
||||
This packet is usually followed by the sending MUD's own is-alive packet.
|
||||
It is used in the filling of a client's MUD list, thus any MUD that doesn't
|
||||
respond with an is-alive isn't marked as online on the sending MUD's mudlist.
|
||||
|
||||
Data:
|
||||
(none)
|
||||
|
||||
Example of a received keepalive-request:
|
||||
*@YourMUD 1234567890 YourMUD!Hub1 keepalive-request *@*
|
||||
|
||||
Example of a sent keepalive-request:
|
||||
*@YourMUD 1234567890 YourMUD keepalive-request *@*
|
||||
"""
|
||||
def __init__(self):
|
||||
super(IMC2PacketKeepAliveRequest, self).__init__()
|
||||
self.sender = '*'
|
||||
self.packet_type = 'keepalive-request'
|
||||
self.target = '*'
|
||||
self.destination = '*'
|
||||
|
||||
class IMC2PacketIsAlive(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet is the reply to a keepalive-request packet. It is responsible
|
||||
for filling a client's mudlist with the information about other MUDs on the
|
||||
network.
|
||||
|
||||
Data:
|
||||
versionid=<string>
|
||||
Where <string> is the text version ID of the client. ("IMC2 4.5 MUD-Net")
|
||||
|
||||
url=<string>
|
||||
Where <string> is the proper URL of the client. (http://www.domain.com)
|
||||
|
||||
host=<string>
|
||||
Where <string> is the telnet address of the MUD. (telnet://domain.com)
|
||||
|
||||
port=<int>
|
||||
Where <int> is the telnet port of the MUD.
|
||||
|
||||
(These data fields are not sent by the MUD, they are added by the server.)
|
||||
networkname=<string>
|
||||
Where <string> is the network name that the MUD/server is on. ("MyNetwork")
|
||||
|
||||
sha256=<int>
|
||||
This is an optional tag that denotes the SHA-256 capabilities of a
|
||||
MUD or server.
|
||||
|
||||
Example of a received is-alive:
|
||||
*@SomeMUD 1234567890 SomeMUD!Hub2 is-alive *@YourMUD versionid="IMC2 4.5 MUD-Net" url="http://www.domain.com" networkname="MyNetwork" sha256=1 host=domain.com port=5500
|
||||
|
||||
Example of a sent is-alive:
|
||||
*@YourMUD 1234567890 YourMUD is-alive *@* versionid="IMC2 4.5 MUD-Net" url="http://www.domain.com" host=domain.com port=5500
|
||||
"""
|
||||
def __init__(self):
|
||||
super(IMC2PacketIsAlive, self).__init__()
|
||||
self.sender = '*'
|
||||
self.packet_type = 'is-alive'
|
||||
self.target = '*'
|
||||
self.destination = '*'
|
||||
self.optional_data = {'versionid': 'Evennia IMC2',
|
||||
'url': '"http://www.evennia.com"',
|
||||
'host': 'test.com',
|
||||
'port': '5555'}
|
||||
|
||||
class IMC2PacketIceRefresh(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet is sent by the MUD to request data about the channels on the
|
||||
network. Servers with channels reply with an ice-update packet for each
|
||||
channel they control. The usual target for this packet is IMC@$.
|
||||
|
||||
Data:
|
||||
(none)
|
||||
|
||||
Example:
|
||||
*@YourMUD 1234567890 YourMUD!Hub1 ice-refresh IMC@$
|
||||
"""
|
||||
def __init__(self):
|
||||
super(IMC2PacketIceRefresh, self).__init__()
|
||||
self.sender = '*'
|
||||
self.packet_type = 'ice-refresh'
|
||||
self.target = 'IMC'
|
||||
self.destination = '$'
|
||||
|
||||
class IMC2PacketIceUpdate(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
A server returns this packet with the data of a channel when prompted with
|
||||
an ice-refresh request.
|
||||
|
||||
Data:
|
||||
channel=<string>
|
||||
The channel's network name in the format of ServerName:ChannelName
|
||||
|
||||
owner=<string>
|
||||
The Name@MUD of the channel's owner
|
||||
|
||||
operators=<string>
|
||||
A space-seperated list of the Channel's operators, in the format of Person@MUD
|
||||
|
||||
policy=<string>
|
||||
The policy is either "open" or "private" with no quotes.
|
||||
|
||||
invited=<string>
|
||||
The space-seperated list of invited User@MUDs, only valid for a
|
||||
"private" channel.
|
||||
|
||||
excluded=<string>
|
||||
The space-seperated list of banned User@MUDs, only valid for "open"
|
||||
channels.
|
||||
|
||||
level=<string> The default level of the channel: Admin, Imp, Imm,
|
||||
Mort, or None
|
||||
|
||||
localname=<string> The suggested local name of the channel.
|
||||
|
||||
Examples:
|
||||
|
||||
Open Policy:
|
||||
ICE@Hub1 1234567890 Hub1!Hub2 ice-update *@YourMUD channel=Hub1:ichat owner=Imm@SomeMUD operators=Other@SomeMUD policy=open excluded="Flamer@badMUD Jerk@dirtyMUD" level=Imm localname=ichat
|
||||
|
||||
Private Policy:
|
||||
ICE@Hub1 1234567890 Hub1!Hub2 ice-update *@YourMUD channel=Hub1:secretchat owner=Imm@SomeMUD operators=Other@SomeMUD policy=private invited="SpecialDude@OtherMUD CoolDude@WeirdMUD" level=Mort localname=schat
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketIceMsgRelayed(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
The -r in this ice-msg packet means it was relayed. This, along with the
|
||||
ice-msg-p packet, are used with private policy channels. The 'r' stands
|
||||
for 'relay'. All incoming channel messages are from ICE@<server>, where
|
||||
<server> is the server hosting the channel.
|
||||
|
||||
Data:
|
||||
realfrom=<string>
|
||||
The User@MUD the message came from.
|
||||
|
||||
channel=<string>
|
||||
The Server:Channel the message is intended to be displayed on.
|
||||
|
||||
text=<string>
|
||||
The message text.
|
||||
|
||||
emote=<int>
|
||||
An integer value designating emotes. 0 for no emote, 1 for an emote,
|
||||
and 2 for a social.
|
||||
|
||||
Examples:
|
||||
ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text="Aha! I got it!" emote=0
|
||||
|
||||
ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text=Ahh emote=0
|
||||
|
||||
ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text="grins evilly." emote=1
|
||||
|
||||
ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text="You@YourMUD grins evilly!" emote=2
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketIceMsgPrivate(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet is sent when a player sends a message to a private channel.
|
||||
This packet should never be seen as incoming to a client. The target of
|
||||
this packet should be IMC@<server> of the server hosting the channel.
|
||||
|
||||
Data:
|
||||
channel=<string>
|
||||
The Server:Channel the message is intended to be displayed on.
|
||||
|
||||
text=<string>
|
||||
The message text.
|
||||
|
||||
emote=<int>
|
||||
An integer value designating emotes. 0 for no emote, 1 for an emote,
|
||||
and 2 for a social.
|
||||
|
||||
echo=<int>
|
||||
Tells the server to echo the message back to the sending MUD. This is only
|
||||
seen on out-going messages.
|
||||
|
||||
Examples:
|
||||
You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text="Ahh! I got it!" emote=0 echo=1
|
||||
You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text=Ahh! emote=0 echo=1
|
||||
You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text="grins evilly." emote=1 echo=1
|
||||
You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text="You@YourMUD grins evilly." emote=2 echo=1
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketIceMsgBroadcasted(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This is the packet used to chat on open policy channels. When sent from a
|
||||
MUD, it is broadcasted across the network. Other MUDs receive it in-tact
|
||||
as it was sent by the originating MUD. The server that hosts the channel
|
||||
sends the packet back to the originating MUD as an 'echo' by removing the
|
||||
"echo=1" and attaching the "sender=Person@MUD" data field.
|
||||
|
||||
Data:
|
||||
channel=<string>
|
||||
The Server:Channel the message is intended to be displayed on.
|
||||
|
||||
text=<string>
|
||||
The message text.
|
||||
|
||||
emote=<int>
|
||||
An integer value designating emotes. 0 for no emote, 1 for an emote,
|
||||
and 2 for a social.
|
||||
|
||||
*echo=<int>
|
||||
This stays on broadcasted messages. It tells the channel's server to
|
||||
relay an echo back.
|
||||
|
||||
*sender=<string>
|
||||
The hosting server replaces "echo=1" with this when sending the echo back
|
||||
to the originating MUD.
|
||||
|
||||
Examples:
|
||||
(See above for emote/social examples as they are pretty much the same)
|
||||
|
||||
Return Echo Packet:
|
||||
You-YourMUD@Hub1 1234567890 Hub1 ice-msg-b *@YourMUD text=Hi! channel=Hub1:ichat sender=You@YourMUD emote=0
|
||||
|
||||
Broadcasted Packet:
|
||||
You@YourMUD 1234567890 YourMUD!Hub1 ice-msg-b *@* channel=Hub1:ichat text=Hi! emote=0 echo=1
|
||||
"""
|
||||
def __init__(self, server, channel, pobject, message):
|
||||
"""
|
||||
Args:
|
||||
server: (String) Server name the channel resides on.
|
||||
channel: (String) Name of the IMC2 channel.
|
||||
pobject: (Object) Object sending the message.
|
||||
message: (String) Message to send.
|
||||
"""
|
||||
super(IMC2PacketIceMsgBroadcasted, self).__init__()
|
||||
self.sender = pobject
|
||||
self.packet_type = 'ice-msg-b'
|
||||
self.target = '*'
|
||||
self.destination = '*'
|
||||
self.optional_data = {'channel': '%s:%s' % (server, channel),
|
||||
'text': message,
|
||||
'emote': 0,
|
||||
'echo': 1}
|
||||
|
||||
class IMC2PacketUserCache(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
Sent by a MUD with a new IMC2-able player or when a player's gender changes,
|
||||
this packet contains only the gender for data. The packet's origination
|
||||
should be the Player@MUD.
|
||||
|
||||
Data:
|
||||
gender=<int> 0 is male, 1 is female, 2 is anything else such as neuter.
|
||||
Will be referred to as "it".
|
||||
|
||||
Example:
|
||||
Dude@someMUD 1234567890 SomeMUD!Hub2!Hub1 user-cache *@* gender=0
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketUserCacheRequest(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
The MUD sends this packet out when making a request for the user-cache
|
||||
information of the user included in the data part of the packet.
|
||||
|
||||
Data:
|
||||
user=<string> The Person@MUD whose data the MUD is seeking.
|
||||
|
||||
Example:
|
||||
*@YourMUD 1234567890 YourMUD user-cache-request *@SomeMUD user=Dude@SomeMUD
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketUserCacheReply(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
A reply to the user-cache-request packet. It contains the user and gender
|
||||
for the user.
|
||||
|
||||
Data:
|
||||
user=<string>
|
||||
The Person@MUD whose data the MUD requested.
|
||||
|
||||
gender=<int>
|
||||
The gender of the Person@MUD in the 'user' field.
|
||||
|
||||
Example:
|
||||
*@someMUD 1234567890 SomeMUD!Hub2!Hub1 user-cache-reply *@YourMUD user=Dude@SomeMUD gender=0
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketTell(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet is used to communicate private messages between users on MUDs
|
||||
across the network.
|
||||
|
||||
Data:
|
||||
text=<string> Message text
|
||||
isreply=<int> Two settings: 1 denotes a reply, 2 denotes a tell social.
|
||||
|
||||
Example:
|
||||
|
||||
Originating:
|
||||
You@YourMUD 1234567890 YourMUD tell Dude@SomeMUD text="Having fun?"
|
||||
|
||||
Reply from Dude:
|
||||
Dude@SomeMUD 1234567890 SomeMUD!Hub1 tell You@YourMUD text="Yeah, this is cool!" isreply=1
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketEmote(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet seems to be sent by servers when notifying the network of a new
|
||||
channel or the destruction of a channel.
|
||||
|
||||
Data:
|
||||
channel=<int>
|
||||
Unsure of what this means. The channel seen in both creation and
|
||||
destruction packets is 15.
|
||||
|
||||
level=<int>
|
||||
I am assuming this is the permission level of the sender. In both
|
||||
creation and destruction messages, this is -1.
|
||||
|
||||
text=<string>
|
||||
This is the message to be sent to the users.
|
||||
|
||||
Examples:
|
||||
ICE@Hub1 1234567890 Hub1 emote *@* channel=15 level=-1 text="the channel called hub1:test has been destroyed by You@YourMUD."
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketRemoteAdmin(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet is used in remote server administration. Please note that
|
||||
SHA-256 Support is *required* for a client to use this feature. The command
|
||||
can vary, in fact this very packet is highly dependant on the server it's
|
||||
being directed to. In most cases, sending the 'list' command will have a
|
||||
remote-admin enabled server send you the list of commands it will accept.
|
||||
|
||||
Data:
|
||||
command=<string>
|
||||
The command being sent to the server for processing.
|
||||
|
||||
data=<string>
|
||||
Data associated with the command. This is not always required.
|
||||
|
||||
hash=<string>
|
||||
The SHA-256 hash that is verified by the server. This hash is generated in
|
||||
the same manner as an authentication packet.
|
||||
|
||||
Example:
|
||||
You@YourMUD 1234567890 YourMUD remote-admin IMC@Hub1 command=list hash=<hash goes here>
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketIceCmd(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
Used for remote channel administration. In most cases, one must be listed
|
||||
as a channel creator on the target server in order to do much with this
|
||||
packet. Other cases include channel operators.
|
||||
|
||||
Data:
|
||||
channel=<string>
|
||||
The target server:channel for the command.
|
||||
|
||||
command=<string>
|
||||
The command to be processed.
|
||||
|
||||
data=<string>
|
||||
Data associated with the command. This is not always required.
|
||||
|
||||
Example:
|
||||
You@YourMUD 1234567890 YourMUD ice-cmd IMC@hub1 channel=hub1:ichat command=list
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketDestroy(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
Sent by a server to indicate the destruction of a channel it hosted.
|
||||
The mud should remove this channel from its local configuration.
|
||||
|
||||
Data:
|
||||
channel=<string> The server:channel being destroyed.
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketWho(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
A seemingly mutli-purpose information-requesting packet. The istats
|
||||
packet currently only works on servers, or at least that's the case on
|
||||
MUD-Net servers. The 'finger' type takes a player name in addition to the
|
||||
type name.
|
||||
|
||||
Example: "finger Dude". The 'who' and 'info' types take no argument.
|
||||
The MUD is responsible for building the reply text sent in the who-reply
|
||||
packet.
|
||||
|
||||
Data:
|
||||
type=<string> Types: who, info, "finger <name>", istats (server only)
|
||||
|
||||
Example:
|
||||
Dude@SomeMUD 1234567890 SomeMUD!Hub1 who *@YourMUD type=who
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketWhoReply(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
The multi-purpose reply to the multi-purpose information-requesting 'who'
|
||||
packet. The MUD is responsible for building the return data, including the
|
||||
format of it. The mud can use the permission level sent in the original who
|
||||
packet to filter the output. The example below is the MUD-Net format.
|
||||
|
||||
Data:
|
||||
text=<string> The formatted reply to a 'who' packet.
|
||||
|
||||
Additional Notes:
|
||||
The example below is for the who list packet. The same construction would
|
||||
go into formatting the other types of who packets.
|
||||
|
||||
Example:
|
||||
*@YourMUD 1234567890 YourMUD who-reply Dude@SomeMUD text="\n\r~R-=< ~WPlayers on YourMUD ~R>=-\n\r ~Y-=< ~Wtelnet://yourmud.domain.com:1234 ~Y>=-\n\r\n\r~B--------------------------------=< ~WPlayers ~B>=---------------------------------\n\r\n\r ~BPlayer ~z<--->~G Mortal the Toy\n\r\n\r~R-------------------------------=< ~WImmortals ~R>=--------------------------------\n\r\n\r ~YStaff ~z<--->~G You the Immortal\n\r\n\r~Y<~W2 Players~Y> ~Y<~WHomepage: http://www.yourmud.com~Y> <~W 2 Max Since Reboot~Y>\n\r~Y<~W3 logins since last reboot on Tue Feb 24, 2004 6:55:59 PM EST~Y>"
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketWhois(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
Sends a request to the network for the location of the specified player.
|
||||
|
||||
Data:
|
||||
level=<int> The permission level of the person making the request.
|
||||
|
||||
Example:
|
||||
You@YourMUD 1234567890 YourMUD whois dude@* level=5
|
||||
"""
|
||||
def __init__(self, pobject, whois_target):
|
||||
super(IMC2PacketWhois, self).__init__()
|
||||
# Use the dbref, it's easier to trace back for the whois-reply.
|
||||
self.sender = pobject.id
|
||||
self.packet_type = 'whois'
|
||||
self.target = whois_target
|
||||
self.destination = '*'
|
||||
self.optional_data = {'level': '5'}
|
||||
|
||||
class IMC2PacketWhoisReply(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
The reply to a whois packet. The MUD is responsible for building and formatting
|
||||
the text sent back to the requesting player, and can use the permission level
|
||||
sent in the original whois packet to filter or block the response.
|
||||
|
||||
Data:
|
||||
text=<string> The whois text.
|
||||
|
||||
Example:
|
||||
*@SomeMUD 1234567890 SomeMUD!Hub1 whois-reply You@YourMUD text="~RIMC Locate: ~YDude@SomeMUD: ~cOnline.\n\r"
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketBeep(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
Sends out a beep packet to the Player@MUD. The client receiving this should
|
||||
then send a bell-character to the target player to 'beep' them.
|
||||
|
||||
Example:
|
||||
You@YourMUD 1234567890 YourMUD beep dude@somemud
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketIceChanWho(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
Sends a request to the specified MUD or * to list all the users listening
|
||||
to the specified channel.
|
||||
|
||||
Data:
|
||||
level=<int>
|
||||
Sender's permission level.
|
||||
|
||||
channel=<string>
|
||||
The server:chan name of the channel.
|
||||
|
||||
lname=<string>
|
||||
The localname of the channel.
|
||||
|
||||
Example:
|
||||
You@YourMUD 1234567890 YourMUD ice-chan-who somemud level=5 channel=Hub1:ichat lname=ichat
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketIceChanWhoReply(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This is the reply packet for an ice-chan-who. The MUD is responsible for
|
||||
creating and formatting the list sent back in the 'list' field. The
|
||||
permission level sent in the original ice-chan-who packet can be used to
|
||||
filter or block the response.
|
||||
|
||||
Data:
|
||||
channel=<string>
|
||||
The server:chan of the requested channel.
|
||||
|
||||
list=<string>
|
||||
The formatted list of local listeners for that MUD.
|
||||
|
||||
Example:
|
||||
*@SomeMUD 1234567890 SomeMUD!Hub1 ice-chan-whoreply You@YourMUD channel=Hub1:ichat list="The following people are listening to ichat on SomeMUD:\n\r\n\rDude\n\r"
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketLaston(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet queries the server the mud is connected to to find out when a
|
||||
specified user was last seen by the network on a public channel.
|
||||
|
||||
Data:
|
||||
username=<string> The user, user@mud, or "all" being queried. Responses
|
||||
to this packet will be sent by the server in the form of a series of tells.
|
||||
|
||||
Example: User@MUD 1234567890 MUD imc-laston SERVER username=somenamehere
|
||||
"""
|
||||
pass
|
||||
|
||||
class IMC2PacketCloseNotify(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet alerts the network when a server or MUD has disconnected. The
|
||||
server hosting the server or MUD is responsible for sending this packet
|
||||
out across the network. Clients need only process the packet to remove the
|
||||
disconnected MUD from their MUD list (or mark it as Disconnected).
|
||||
|
||||
Data:
|
||||
host=<string>
|
||||
The MUD or server that has disconnected from the network.
|
||||
|
||||
Example:
|
||||
*@Hub2 1234567890 Hub2!Hub1 close-notify *@* host=DisconnMUD
|
||||
"""
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
packstr = "Kayle@MW 1234567 MW!Server02!Server01 ice-msg-b *@* channel=Server01:ichat text=\"*they're going woot\" emote=0 echo=1"
|
||||
packstr = "*@Lythelian 1234567 Lythelian!Server01 is-alive *@* versionid=\"Tim's LPC IMC2 client 30-Jan-05 / Dead Souls integrated\" networkname=Mudbytes url=http://dead-souls.net host=70.32.76.142 port=6666 sha256=0"
|
||||
print IMC2Packet(packstr)
|
||||
|
||||
104
src/comms/imc2lib/imc2_trackers.py
Normal file
104
src/comms/imc2lib/imc2_trackers.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"""
|
||||
Certain periodic packets are sent by connected MUDs (is-alive, user-cache,
|
||||
etc). The IMC2 protocol assumes that each connected MUD will capture these and
|
||||
populate/maintain their own lists of other servers connected. This module
|
||||
contains stuff like this.
|
||||
"""
|
||||
from time import time
|
||||
|
||||
class IMC2Mud(object):
|
||||
"""
|
||||
Stores information about other games connected to our current IMC2 network.
|
||||
"""
|
||||
def __init__(self, packet):
|
||||
self.name = packet.origin
|
||||
self.versionid = packet.optional_data.get('versionid', None)
|
||||
self.networkname = packet.optional_data.get('networkname', None)
|
||||
self.url = packet.optional_data.get('url', None)
|
||||
self.host = packet.optional_data.get('host', None)
|
||||
self.port = packet.optional_data.get('port', None)
|
||||
self.sha256 = packet.optional_data.get('sha256', None)
|
||||
# This is used to determine when a Mud has fallen into inactive status.
|
||||
self.last_updated = time()
|
||||
|
||||
class IMC2MudList(object):
|
||||
"""
|
||||
Keeps track of other MUDs connected to the IMC network.
|
||||
"""
|
||||
def __init__(self):
|
||||
# Mud list is stored in a dict, key being the IMC Mud name.
|
||||
self.mud_list = {}
|
||||
|
||||
def get_mud_list(self):
|
||||
"""
|
||||
Returns a sorted list of connected Muds.
|
||||
"""
|
||||
muds = self.mud_list.items()
|
||||
muds.sort()
|
||||
return [value for key, value in muds]
|
||||
|
||||
def update_mud_from_packet(self, packet):
|
||||
"""
|
||||
This grabs relevant info from the packet and stuffs it in the
|
||||
Mud list for later retrieval.
|
||||
"""
|
||||
mud = IMC2Mud(packet)
|
||||
self.mud_list[mud.name] = mud
|
||||
|
||||
def remove_mud_from_packet(self, packet):
|
||||
"""
|
||||
Removes a mud from the Mud list when given a packet.
|
||||
"""
|
||||
mud = IMC2Mud(packet)
|
||||
try:
|
||||
del self.mud_list[mud.name]
|
||||
except KeyError:
|
||||
# No matching entry, no big deal.
|
||||
pass
|
||||
|
||||
class IMC2Channel(object):
|
||||
"""
|
||||
Stores information about channels available on the network.
|
||||
"""
|
||||
def __init__(self, packet):
|
||||
self.localname = packet.optional_data.get('localname', None)
|
||||
self.name = packet.optional_data.get('channel', None)
|
||||
self.level = packet.optional_data.get('level', None)
|
||||
self.owner = packet.optional_data.get('owner', None)
|
||||
self.policy = packet.optional_data.get('policy', None)
|
||||
self.last_updated = time()
|
||||
|
||||
class IMC2ChanList(object):
|
||||
"""
|
||||
Keeps track of other MUDs connected to the IMC network.
|
||||
"""
|
||||
def __init__(self):
|
||||
# Chan list is stored in a dict, key being the IMC Mud name.
|
||||
self.chan_list = {}
|
||||
|
||||
def get_channel_list(self):
|
||||
"""
|
||||
Returns a sorted list of cached channels.
|
||||
"""
|
||||
channels = self.chan_list.items()
|
||||
channels.sort()
|
||||
return [value for key, value in channels]
|
||||
|
||||
def update_channel_from_packet(self, packet):
|
||||
"""
|
||||
This grabs relevant info from the packet and stuffs it in the
|
||||
channel list for later retrieval.
|
||||
"""
|
||||
channel = IMC2Channel(packet)
|
||||
self.chan_list[channel.name] = channel
|
||||
|
||||
def remove_channel_from_packet(self, packet):
|
||||
"""
|
||||
Removes a channel from the Channel list when given a packet.
|
||||
"""
|
||||
channel = IMC2Channel(packet)
|
||||
try:
|
||||
del self.chan_list[channel.name]
|
||||
except KeyError:
|
||||
# No matching entry, no big deal.
|
||||
pass
|
||||
|
|
@ -36,12 +36,12 @@ class IRC_Bot(irc.IRCClient):
|
|||
nickname = property(_get_nickname)
|
||||
|
||||
def signedOn(self):
|
||||
global IRC_CHANNELS
|
||||
self.join(self.factory.channel)
|
||||
|
||||
# This is the first point the protocol is instantiated.
|
||||
# add this protocol instance to the global list so we
|
||||
# can access it later to send data.
|
||||
global IRC_CHANNELS
|
||||
self.join(self.factory.channel)
|
||||
|
||||
IRC_CHANNELS.append(self)
|
||||
#msg_info("Client connecting to %s.'" % (self.factory.channel))
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ class IRC_Bot(irc.IRCClient):
|
|||
"""
|
||||
self.msg(utils.to_str(self.factory.channel), utils.to_str(msg))
|
||||
|
||||
class Factory(protocol.ClientFactory):
|
||||
class IRCbotFactory(protocol.ClientFactory):
|
||||
protocol = IRC_Bot
|
||||
def __init__(self, key, channel, network, port, nickname, evennia_channel):
|
||||
self.key = key
|
||||
|
|
@ -101,9 +101,11 @@ class Factory(protocol.ClientFactory):
|
|||
msg_info(msg)
|
||||
logger.log_errmsg(msg)
|
||||
|
||||
def build_connection_key(irc_network, irc_port, irc_channel, irc_bot_nick):
|
||||
def build_connection_key(channel, irc_network, irc_port, irc_channel, irc_bot_nick):
|
||||
"Build an id hash for the connection"
|
||||
return "irc_%s:%s%s(%s)" % (irc_network, irc_port, irc_channel, irc_bot_nick)
|
||||
if hasattr(channel, 'key'):
|
||||
channel = channel.key
|
||||
return "irc_%s:%s%s(%s)<>%s" % (irc_network, irc_port, irc_channel, irc_bot_nick, channel)
|
||||
|
||||
def build_service_key(key):
|
||||
return "IRCbot:%s" % key
|
||||
|
|
@ -118,7 +120,7 @@ def create_connection(channel, irc_network, irc_port, irc_channel, irc_bot_nick)
|
|||
logger.log_errmsg("Cannot attach IRC<->Evennia: Evennia Channel '%s' not found" % channel)
|
||||
return False
|
||||
channel = new_channel[0]
|
||||
key = build_connection_key(irc_network, irc_port, irc_channel, irc_bot_nick)
|
||||
key = build_connection_key(channel, irc_network, irc_port, irc_channel, irc_bot_nick)
|
||||
|
||||
old_conns = ExternalChannelConnection.objects.filter(db_external_key=key)
|
||||
if old_conns:
|
||||
|
|
@ -136,9 +138,12 @@ def create_connection(channel, irc_network, irc_port, irc_channel, irc_bot_nick)
|
|||
connect_to_irc(conn)
|
||||
return True
|
||||
|
||||
def delete_connection(irc_network, irc_port, irc_channel, irc_bot_nick):
|
||||
def delete_connection(channel, irc_network, irc_port, irc_channel, irc_bot_nick):
|
||||
"Destroy a connection"
|
||||
key = build_connection_key(irc_network, irc_port, irc_channel, irc_bot_nick)
|
||||
if hasattr(channel, 'key'):
|
||||
channel = channel.key
|
||||
|
||||
key = build_connection_key(channel, irc_network, irc_port, irc_channel, irc_bot_nick)
|
||||
service_key = build_service_key(key)
|
||||
try:
|
||||
conn = ExternalChannelConnection.objects.get(db_external_key=key)
|
||||
|
|
@ -161,7 +166,7 @@ def connect_to_irc(connection):
|
|||
service_key = build_service_key(key)
|
||||
irc_network, irc_port, irc_channel, irc_bot_nick = [utils.to_str(conf) for conf in connection.external_config.split('|')]
|
||||
# connect
|
||||
bot = internet.TCPClient(irc_network, int(irc_port), Factory(key, irc_channel, irc_network, irc_port, irc_bot_nick,
|
||||
bot = internet.TCPClient(irc_network, int(irc_port), IRCbotFactory(key, irc_channel, irc_network, irc_port, irc_bot_nick,
|
||||
connection.channel.key))
|
||||
bot.setName(service_key)
|
||||
SESSIONS.server.services.addService(bot)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue