diff --git a/OpenSim/Region/Environment/Modules/Avatar/Chat/ChannelState.cs b/OpenSim/Region/Environment/Modules/Avatar/Chat/ChannelState.cs new file mode 100644 index 0000000000..7a3eadf50e --- /dev/null +++ b/OpenSim/Region/Environment/Modules/Avatar/Chat/ChannelState.cs @@ -0,0 +1,570 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSim Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.RegularExpressions; +using log4net; +using Nini.Config; +using OpenSim.Framework; +using OpenSim.Region.Environment.Interfaces; +using OpenSim.Region.Environment.Scenes; + +namespace OpenSim.Region.Environment.Modules.Avatar.Chat +{ + + // An instance of this class exists for each unique combination of + // IRC chat interface characteristics, as determined by the supplied + // configuration file. + + internal class ChannelState + { + + private static readonly ILog m_log = + LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + private static Regex arg = new Regex(@"\[[^\[\]]*\]"); + private static int _idk_ = 0; + + // These are the IRC Connector configurable parameters with hard-wired + // default values (retained for compatability). + + internal string Server = null; + internal string IrcChannel = null; + internal string BaseNickname = "OSimBot"; + internal uint Port = 6667; + internal string User = "USER OpenSimBot 8 * :I'm an OpenSim to IRC bot"; + + internal bool ClientReporting = true; + internal bool RelayPrivateChannels = false; + internal int RelayChannel = 1; + + // Connector agnostic parameters. These values are NOT shared with the + // connector and do not differentiate at an IRC level + + internal string PrivateMessageFormat = "PRIVMSG {0} :<{2}> {1} {3}"; + internal string NoticeMessageFormat = "PRIVMSG {0} :<{2}> {3}"; + internal int RelayChannelOut = -1; + internal bool RandomizeNickname = true; + internal string AccessPassword = "badkitty"; + internal bool CommandsEnabled = false; + internal int CommandChannel = -1; + internal int ConnectDelay = 10; + internal int PingDelay = 15; + internal string DefaultZone = "Sim"; + + // IRC connector reference + + internal XIRCConnector irc = null; + + internal int idn = _idk_++; + + // List of regions dependent upon this connection + + internal List clientregions = new List(); + + // Needed by OpenChannel + + internal ChannelState() + { + } + + // This constructor is used by the Update* methods. A copy of the + // existing channel state is created, and distinguishing characteristics + // are copied across. + + internal ChannelState(ChannelState model) + { + Server = model.Server; + IrcChannel = model.IrcChannel; + Port = model.Port; + BaseNickname = model.BaseNickname; + RandomizeNickname = model.RandomizeNickname; + User = model.User; + CommandsEnabled = model.CommandsEnabled; + CommandChannel = model.CommandChannel; + RelayPrivateChannels = model.RelayPrivateChannels; + RelayChannelOut = model.RelayChannelOut; + RelayChannel = model.RelayChannel; + PrivateMessageFormat = model.PrivateMessageFormat; + NoticeMessageFormat = model.NoticeMessageFormat; + ClientReporting = model.ClientReporting; + AccessPassword = model.AccessPassword; + DefaultZone = model.DefaultZone; + ConnectDelay = model.ConnectDelay; + PingDelay = model.PingDelay; + } + + // Read the configuration file, performing variable substitution and any + // necessary aliasing. See accompanying documentation for how this works. + // If you don't need variables, then this works exactly as before. + // If either channel or server are not specified, the request fails. + + internal static void OpenChannel(RegionState rs, IConfig config) + { + + // Create a new instance of a channel. This may not actually + // get used if an equivalent channel already exists. + + ChannelState cs = new ChannelState(); + + // Read in the configuration file and filter everything for variable + // subsititution. + + m_log.DebugFormat("[IRC-Channel-{0}] Initial request by Region {1} to connect to IRC", cs.idn, rs.Region); + + cs.Server = Substitute(rs, config.GetString("server", null)); + m_log.DebugFormat("[IRC-Channel-{0}] Server : <{1}>", cs.idn, cs.Server); + cs.IrcChannel = Substitute(rs, config.GetString("channel", null)); + m_log.DebugFormat("[IRC-Channel-{0}] IrcChannel : <{1}>", cs.idn, cs.IrcChannel); + cs.Port = Convert.ToUInt32(Substitute(rs, config.GetString("port", Convert.ToString(cs.Port)))); + m_log.DebugFormat("[IRC-Channel-{0}] Port : <{1}>", cs.idn, cs.Port); + cs.BaseNickname = Substitute(rs, config.GetString("nick", cs.BaseNickname)); + m_log.DebugFormat("[IRC-Channel-{0}] BaseNickname : <{1}>", cs.idn, cs.BaseNickname); + cs.RandomizeNickname = Convert.ToBoolean(Substitute(rs, config.GetString("randomize_nick", Convert.ToString(cs.RandomizeNickname)))); + m_log.DebugFormat("[IRC-Channel-{0}] RandomizeNickname : <{1}>", cs.idn, cs.RandomizeNickname); + cs.RandomizeNickname = Convert.ToBoolean(Substitute(rs, config.GetString("nicknum", Convert.ToString(cs.RandomizeNickname)))); + m_log.DebugFormat("[IRC-Channel-{0}] RandomizeNickname : <{1}>", cs.idn, cs.RandomizeNickname); + cs.User = Substitute(rs, config.GetString("username", cs.User)); + m_log.DebugFormat("[IRC-Channel-{0}] User : <{1}>", cs.idn, cs.User); + cs.CommandsEnabled = Convert.ToBoolean(Substitute(rs, config.GetString("commands_enabled", Convert.ToString(cs.CommandsEnabled)))); + m_log.DebugFormat("[IRC-Channel-{0}] CommandsEnabled : <{1}>", cs.idn, cs.CommandsEnabled); + cs.CommandChannel = Convert.ToInt32(Substitute(rs, config.GetString("commandchannel", Convert.ToString(cs.CommandChannel)))); + m_log.DebugFormat("[IRC-Channel-{0}] CommandChannel : <{1}>", cs.idn, cs.CommandChannel); + cs.CommandChannel = Convert.ToInt32(Substitute(rs, config.GetString("command_channel", Convert.ToString(cs.CommandChannel)))); + m_log.DebugFormat("[IRC-Channel-{0}] CommandChannel : <{1}>", cs.idn, cs.CommandChannel); + cs.RelayPrivateChannels = Convert.ToBoolean(Substitute(rs, config.GetString("relay_private_channels", Convert.ToString(cs.RelayPrivateChannels)))); + m_log.DebugFormat("[IRC-Channel-{0}] RelayPrivateChannels : <{1}>", cs.idn, cs.RelayPrivateChannels); + cs.RelayPrivateChannels = Convert.ToBoolean(Substitute(rs, config.GetString("useworldcomm", Convert.ToString(cs.RelayPrivateChannels)))); + m_log.DebugFormat("[IRC-Channel-{0}] RelayPrivateChannels : <{1}>", cs.idn, cs.RelayPrivateChannels); + cs.RelayChannelOut = Convert.ToInt32(Substitute(rs, config.GetString("relay_private_channel_out", Convert.ToString(cs.RelayChannelOut)))); + m_log.DebugFormat("[IRC-Channel-{0}] RelayChannelOut : <{1}>", cs.idn, cs.RelayChannelOut); + cs.RelayChannel = Convert.ToInt32(Substitute(rs, config.GetString("relay_private_channel_in", Convert.ToString(cs.RelayChannel)))); + m_log.DebugFormat("[IRC-Channel-{0}] RelayChannel : <{1}>", cs.idn, cs.RelayChannel); + cs.RelayChannel = Convert.ToInt32(Substitute(rs, config.GetString("inchannel", Convert.ToString(cs.RelayChannel)))); + m_log.DebugFormat("[IRC-Channel-{0}] RelayChannel : <{1}>", cs.idn, cs.RelayChannel); + cs.PrivateMessageFormat = Substitute(rs, config.GetString("msgformat", cs.PrivateMessageFormat)); + m_log.DebugFormat("[IRC-Channel-{0}] PrivateMessageFormat : <{1}>", cs.idn, cs.PrivateMessageFormat); + cs.NoticeMessageFormat = Substitute(rs, config.GetString("noticeformat", cs.NoticeMessageFormat)); + m_log.DebugFormat("[IRC-Channel-{0}] NoticeMessageFormat : <{1}>", cs.idn, cs.NoticeMessageFormat); + cs.ClientReporting = Convert.ToInt32(Substitute(rs, config.GetString("verbosity", cs.ClientReporting?"1":"0"))) > 0; + m_log.DebugFormat("[IRC-Channel-{0}] ClientReporting : <{1}>", cs.idn, cs.ClientReporting); + cs.ClientReporting = Convert.ToBoolean(Substitute(rs, config.GetString("report_clients", Convert.ToString(cs.ClientReporting)))); + m_log.DebugFormat("[IRC-Channel-{0}] ClientReporting : <{1}>", cs.idn, cs.ClientReporting); + cs.AccessPassword = Substitute(rs, config.GetString("access_password", cs.AccessPassword)); + m_log.DebugFormat("[IRC-Channel-{0}] AccessPassword : <{1}>", cs.idn, cs.AccessPassword); + cs.DefaultZone = Substitute(rs, config.GetString("fallback_region", cs.DefaultZone)); + m_log.DebugFormat("[IRC-Channel-{0}] DefaultZone : <{1}>", cs.idn, cs.DefaultZone); + cs.ConnectDelay = Convert.ToInt32(Substitute(rs, config.GetString("connect_delay", Convert.ToString(cs.ConnectDelay)))); + m_log.DebugFormat("[IRC-Channel-{0}] ConnectDelay : <{1}>", cs.idn, cs.ConnectDelay); + cs.PingDelay = Convert.ToInt32(Substitute(rs, config.GetString("ping_delay", Convert.ToString(cs.PingDelay)))); + m_log.DebugFormat("[IRC-Channel-{0}] PingDelay : <{1}>", cs.idn, cs.PingDelay); + + // Fail if fundamental information is still missing + + if (cs.Server == null || cs.IrcChannel == null || cs.BaseNickname == null || cs.User == null) + throw new Exception(String.Format("[IRC-Channel-{0}] Invalid configuration for region {1}", cs.idn, rs.Region)); + + m_log.InfoFormat("[IRC-Channel-{0}] Configuration for Region {1} is valid", cs.idn, rs.Region); + m_log.InfoFormat("[IRC-Channel-{0}] Server = {1}", cs.idn, cs.Server); + m_log.InfoFormat("[IRC-Channel-{0}] Channel = {1}", cs.idn, cs.IrcChannel); + m_log.InfoFormat("[IRC-Channel-{0}] Port = {1}", cs.idn, cs.Port); + m_log.InfoFormat("[IRC-Channel-{0}] Nickname = {1}", cs.idn, cs.BaseNickname); + m_log.InfoFormat("[IRC-Channel-{0}] User = {1}", cs.idn, cs.User); + + // Set the channel state for this region + + rs.cs = Integrate(rs, cs); + + } + + // An initialized channel state instance is passed in. If an identical + // channel state instance already exists, then the existing instance + // is used to replace the supplied value. + // If the instance matches with respect to IRC, then the underlying + // IRCConnector is assigned to the supplied channel state and the + // updated value is returned. + // If there is no match, then the supplied instance is completed by + // creating and assigning an instance of an IRC connector. + + private static ChannelState Integrate(RegionState rs, ChannelState p_cs) + { + + ChannelState cs = p_cs; + + // Check to see if we have an existing server/channel setup that can be used + // In the absence of variable substitution this will always resolve to the + // same ChannelState instance, and the table will only contains a single + // entry, so the performance considerations for the existing behavior are + // zero. Only the IRC connector is shared, the ChannelState still contains + // values that, while independent of the IRC connetion, do still distinguish + // this region's behavior. + + foreach (ChannelState xcs in XIRCBridgeModule.m_channels) + { + if (cs.IsAPerfectMatchFor(xcs)) + { + m_log.DebugFormat("[IRC-Channel-{0}] Channel state matched", cs.idn); + cs = xcs; + break; + } + if (cs.IsAConnectionMatchFor(xcs)) + { + m_log.DebugFormat("[IRC-Channel-{0}] Channel matched", cs.idn); + cs.irc = xcs.irc; + break; + } + } + + // No entry was found, so this is going to be a new entry. + + if (cs.irc == null) + { + + m_log.DebugFormat("[IRC-Channel-{0}] New channel required", cs.idn); + + if ((cs.irc = new XIRCConnector(cs)) != null) + { + XIRCBridgeModule.m_channels.Add(cs); + + m_log.InfoFormat("[IRC-Channel-{0}] New channel initialized for {1}, nick: {2}, commands {3}, private channels {4}", + cs.idn, rs.Region, cs.DefaultZone, + cs.CommandsEnabled ? "enabled" : "not enabled", + cs.RelayPrivateChannels ? "relayed" : "not relayed"); + } + else + { + string txt = String.Format("[IRC-Channel-{0}] Region {1} failed to connect to channel {2} on server {3}:{4}", + cs.idn, rs.Region, cs.IrcChannel, cs.Server, cs.Port); + m_log.Error(txt); + throw new Exception(txt); + } + } + else + { + m_log.InfoFormat("[IRC-Channel-{0}] Region {1} reusing existing connection to channel {2} on server {3}:{4}", + cs.idn, rs.Region, cs.IrcChannel, cs.Server, cs.Port); + } + + m_log.InfoFormat("[IRC-Channel-{0}] Region {1} connected to channel {2} on server {3}:{4}", + cs.idn, rs.Region, cs.IrcChannel, cs.Server, cs.Port); + + // We're finally ready to commit ourselves + + return cs; + + } + + // These routines allow differentiating changes to + // the underlying channel state. If necessary, a + // new channel state will be created. + + internal ChannelState UpdateServer(RegionState rs, string server) + { + RemoveRegion(rs); + ChannelState cs = new ChannelState(this); + cs.Server = server; + cs = Integrate(rs, cs); + cs.AddRegion(rs); + return cs; + } + + internal ChannelState UpdatePort(RegionState rs, string port) + { + RemoveRegion(rs); + ChannelState cs = new ChannelState(this); + cs.Port = Convert.ToUInt32(port); + cs = Integrate(rs, cs); + cs.AddRegion(rs); + return cs; + } + + internal ChannelState UpdateChannel(RegionState rs, string channel) + { + RemoveRegion(rs); + ChannelState cs = new ChannelState(this); + cs.IrcChannel = channel; + cs = Integrate(rs, cs); + cs.AddRegion(rs); + return cs; + } + + internal ChannelState UpdateNickname(RegionState rs, string nickname) + { + RemoveRegion(rs); + ChannelState cs = new ChannelState(this); + cs.BaseNickname = nickname; + cs = Integrate(rs, cs); + cs.AddRegion(rs); + return cs; + } + + internal ChannelState UpdateClientReporting(RegionState rs, string cr) + { + RemoveRegion(rs); + ChannelState cs = new ChannelState(this); + cs.ClientReporting = Convert.ToBoolean(cr); + cs = Integrate(rs, cs); + cs.AddRegion(rs); + return cs; + } + + internal ChannelState UpdateRelayIn(RegionState rs, string channel) + { + RemoveRegion(rs); + ChannelState cs = new ChannelState(this); + cs.RelayChannel = Convert.ToInt32(channel); + cs = Integrate(rs, cs); + cs.AddRegion(rs); + return cs; + } + + internal ChannelState UpdateRelayOut(RegionState rs, string channel) + { + RemoveRegion(rs); + ChannelState cs = new ChannelState(this); + cs.RelayChannelOut = Convert.ToInt32(channel); + cs = Integrate(rs, cs); + cs.AddRegion(rs); + return cs; + } + + // Determine whether or not this is a 'new' channel. Only those + // attributes that uniquely distinguish an IRC connection should + // be included here (and only those attributes should really be + // in the ChannelState structure) + + private bool IsAConnectionMatchFor(ChannelState cs) + { + return ( + Server == cs.Server && + IrcChannel == cs.IrcChannel && + Port == cs.Port && + BaseNickname == cs.BaseNickname && + User == cs.User + ); + } + + // This level of obsessive matching allows us to produce + // a minimal overhead int he case of a server which does + // need to differentiate IRC at a region level. + + private bool IsAPerfectMatchFor(ChannelState cs) + { + return ( IsAConnectionMatchFor(cs) && + + RelayChannelOut == cs.RelayChannelOut && + PrivateMessageFormat == cs.PrivateMessageFormat && + NoticeMessageFormat == cs.NoticeMessageFormat && + RandomizeNickname == cs.RandomizeNickname && + AccessPassword == cs.AccessPassword && + CommandsEnabled == cs.CommandsEnabled && + CommandChannel == cs.CommandChannel && + DefaultZone == cs.DefaultZone && + RelayPrivateChannels == cs.RelayPrivateChannels && + RelayChannel == cs.RelayChannel && + ClientReporting == cs.ClientReporting + ); + } + + // This function implements the variable substitution mechanism + // for the configuration values. Each string read from the + // configuration file is scanned for '[...]' enclosures. Each + // one that is found is replaced by either a runtime variable + // (%xxx) or an existing configuration key. When no further + // substitution is possible, the remaining string is returned + // to the caller. This allows for arbitrarily nested + // enclosures. + + private static string Substitute(RegionState rs, string instr) + { + + string result = instr; + + if (result == null || result.Length == 0) + return result; + + // Repeatedly scan the string until all possible + // substitutions have been performed. + + while (arg.IsMatch(result)) + { + + string vvar = arg.Match(result).ToString(); + string var = vvar.Substring(1,vvar.Length-2).Trim(); + + switch (var.ToLower()) + { + case "%region" : + result = result.Replace(vvar, rs.Region); + break; + case "%host" : + result = result.Replace(vvar, rs.Host); + break; + case "%master1" : + result = result.Replace(vvar, rs.MA1); + break; + case "%master2" : + result = result.Replace(vvar, rs.MA2); + break; + case "%locx" : + result = result.Replace(vvar, rs.LocX); + break; + case "%locy" : + result = result.Replace(vvar, rs.LocY); + break; + case "%k" : + result = result.Replace(vvar, rs.IDK); + break; + default : + result = result.Replace(vvar, rs.config.GetString(var,var)); + break; + } + } + + return result; + + } + + public void Close() + { + + m_log.InfoFormat("[IRC-Channel-{0}] Closing channel <{1} to server <{2}:{3}>", + idn, IrcChannel, Server, Port); + + m_log.InfoFormat("[IRC-Channel-{0}] There are {1} active clients", + idn, clientregions.Count); + + irc.Close(); + + } + + public void Open() + { + m_log.InfoFormat("[IRC-Channel-{0}] Opening channel <{1} to server <{2}:{3}>", + idn, IrcChannel, Server, Port); + + irc.Open(); + + } + + // These are called by each region that attaches to this channel. The call affects + // only the relationship of the region with the channel. Not the channel to IRC + // relationship (unless it is closed and we want it open). + + public void Open(RegionState rs) + { + AddRegion(rs); + Open(); + } + + public void Close(RegionState rs) + { + RemoveRegion(rs); + } + + // Add a client region to this channel if it is not already known + + public void AddRegion(RegionState rs) + { + m_log.InfoFormat("[IRC-Channel-{0}] Adding region {1} to channel <{2} to server <{3}:{4}>", + idn, rs.Region, IrcChannel, Server, Port); + if (!clientregions.Contains(rs)) + { + clientregions.Add(rs); + lock (irc) irc.depends++; + } + } + + // Remove a client region from the channel. If this is the last + // region, then clean up the channel. The connector will clean itself + // up if it finds itself about to be GC'd. + + public void RemoveRegion(RegionState rs) + { + + m_log.InfoFormat("[IRC-Channel-{0}] Removing region {1} from channel <{2} to server <{3}:{4}>", + idn, rs.Region, IrcChannel, Server, Port); + + if (clientregions.Contains(rs)) + { + clientregions.Remove(rs); + lock (irc) irc.depends--; + } + + } + + // This function is lifted from the IRCConnector because it + // contains information that is not differentiating from an + // IRC point-of-view. + + public static void OSChat(XIRCConnector p_irc, OSChatMessage c, bool cmsg) + { + + // m_log.DebugFormat("[IRC-OSCHAT] from {0}:{1}", p_irc.Server, p_irc.IrcChannel); + + try + { + + // Scan through the set of unique channel configuration for those + // that belong to this connector. And then forward the message to + // all regions known to those channels. + // Note that this code is responsible for completing some of the + // settings for the inbound OSChatMessage + + foreach (ChannelState cs in XIRCBridgeModule.m_channels) + { + if ( p_irc == cs.irc) + { + + // This non-IRC differentiator moved to here + + if (cmsg && !cs.ClientReporting) + continue; + + // This non-IRC differentiator moved to here + + c.Channel = (cs.RelayPrivateChannels ? cs.RelayChannel : 0); + + foreach (RegionState region in cs.clientregions) + { + region.OSChat(cs.irc, c); + } + + } + } + + } + catch (Exception ex) + { + m_log.ErrorFormat("[IRC-OSCHAT]: BroadcastSim Exception: {0}", ex.Message); + m_log.Debug(ex); + } + } + } +} diff --git a/OpenSim/Region/Environment/Modules/Avatar/Chat/IRCBridgeModule.cs b/OpenSim/Region/Environment/Modules/Avatar/Chat/IRCBridgeModule.cs index ba993a06b9..f1aa75676e 100644 --- a/OpenSim/Region/Environment/Modules/Avatar/Chat/IRCBridgeModule.cs +++ b/OpenSim/Region/Environment/Modules/Avatar/Chat/IRCBridgeModule.cs @@ -242,7 +242,7 @@ namespace OpenSim.Region.Environment.Modules.Avatar.Chat break; } } - catch(Exception ex) + catch (Exception ex) { m_log.DebugFormat("[IRC] error processing in-world command channel input: {0}", ex); } diff --git a/OpenSim/Region/Environment/Modules/Avatar/Chat/RegionState.cs b/OpenSim/Region/Environment/Modules/Avatar/Chat/RegionState.cs new file mode 100644 index 0000000000..cf2154fb7f --- /dev/null +++ b/OpenSim/Region/Environment/Modules/Avatar/Chat/RegionState.cs @@ -0,0 +1,435 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSim Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.Reflection; +using log4net; +using Nini.Config; +using OpenSim.Framework; +using OpenSim.Region.Environment.Interfaces; +using OpenSim.Region.Environment.Scenes; + +namespace OpenSim.Region.Environment.Modules.Avatar.Chat +{ + // An instance of this class exists for every active region + + internal class RegionState + { + + private static readonly ILog m_log = + LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + private static readonly OpenMetaverse.Vector3 CenterOfRegion = new OpenMetaverse.Vector3(128, 128, 20); + private const int DEBUG_CHANNEL = 2147483647; + + private static int _idk_ = 0; + + // Runtime variables; these values are assigned when the + // IrcState is created and remain constant thereafter. + + internal string Region = String.Empty; + internal string Host = String.Empty; + internal string LocX = String.Empty; + internal string LocY = String.Empty; + internal string MA1 = String.Empty; + internal string MA2 = String.Empty; + internal string IDK = String.Empty; + + // System values - used only be the IRC classes themselves + + internal ChannelState cs = null; // associated IRC configuration + internal Scene scene = null; // associated scene + internal IConfig config = null; // configuration file reference + internal bool enabled = true; + + // This list is used to keep track of who is here, and by + // implication, who is not. + + internal List clients = new List(); + + // Setup runtime variable values + + public RegionState(Scene p_scene, IConfig p_config) + { + + scene = p_scene; + config = p_config; + + Region = scene.RegionInfo.RegionName; + Host = scene.RegionInfo.ExternalHostName; + LocX = Convert.ToString(scene.RegionInfo.RegionLocX); + LocY = Convert.ToString(scene.RegionInfo.RegionLocY); + MA1 = scene.RegionInfo.MasterAvatarFirstName; + MA2 = scene.RegionInfo.MasterAvatarLastName; + IDK = Convert.ToString(_idk_++); + + // OpenChannel conditionally establishes a connection to the + // IRC server. The request will either succeed, or it will + // throw an exception. + + ChannelState.OpenChannel(this, config); + + // Connect channel to world events + + scene.EventManager.OnChatFromWorld += OnSimChat; + scene.EventManager.OnChatFromClient += OnSimChat; + scene.EventManager.OnMakeRootAgent += OnMakeRootAgent; + scene.EventManager.OnMakeChildAgent += OnMakeChildAgent; + + m_log.InfoFormat("[IRC-Region {0}] Initialization complete", Region); + + } + + // Auto cleanup when abandoned + + ~RegionState() + { + if (cs != null) + cs.RemoveRegion(this); + } + + // Called by PostInitialize after all regions have been created + + public void Open() + { + cs.Open(this); + enabled = true; + } + + // Called by IRCBridgeModule.Close immediately prior to unload + + public void Close() + { + enabled = false; + cs.Close(this); + } + + // The agent has disconnected, cleanup associated resources + + private void OnClientLoggedOut(IClientAPI client) + { + try + { + if (clients.Contains(client)) + { + if (enabled && (cs.irc.Enabled) && (cs.irc.Connected) && (cs.ClientReporting)) + { + m_log.InfoFormat("[IRC-Region {0}]: {1} has left", Region, client.Name); + cs.irc.PrivMsg(cs.NoticeMessageFormat, cs.irc.Nick, Region, String.Format("{0} has left", client.Name)); + } + client.OnLogout -= OnClientLoggedOut; + client.OnConnectionClosed -= OnClientLoggedOut; + clients.Remove(client); + } + } + catch (Exception ex) + { + m_log.ErrorFormat("[IRC-Region {0}]: ClientLoggedOut exception: {1}", Region, ex.Message); + m_log.Debug(ex); + } + } + + // This event indicates that the agent has left the building. We should treat that the same + // as if the agent has logged out (we don't want cross-region noise - or do we?) + + private void OnMakeChildAgent(ScenePresence presence) + { + + IClientAPI client = presence.ControllingClient; + + try + { + if (clients.Contains(client)) + { + if (enabled && (cs.irc.Enabled) && (cs.irc.Connected) && (cs.ClientReporting)) + { + string clientName = String.Format("{0} {1}", presence.Firstname, presence.Lastname); + m_log.DebugFormat("[IRC-Region {0}] {1} has left", Region, clientName); + cs.irc.PrivMsg(cs.NoticeMessageFormat, cs.irc.Nick, Region, String.Format("{0} has left", clientName)); + } + client.OnLogout -= OnClientLoggedOut; + client.OnConnectionClosed -= OnClientLoggedOut; + clients.Remove(client); + } + } + catch (Exception ex) + { + m_log.ErrorFormat("[IRC-Region {0}]: MakeChildAgent exception: {1}", Region, ex.Message); + m_log.Debug(ex); + } + + } + + // An agent has entered the region (from another region). Add the client to the locally + // known clients list + + private void OnMakeRootAgent(ScenePresence presence) + { + + IClientAPI client = presence.ControllingClient; + + try + { + if (!clients.Contains(client)) + { + client.OnLogout += OnClientLoggedOut; + client.OnConnectionClosed += OnClientLoggedOut; + clients.Add(client); + if (enabled && (cs.irc.Enabled) && (cs.irc.Connected) && (cs.ClientReporting)) + { + string clientName = String.Format("{0} {1}", presence.Firstname, presence.Lastname); + m_log.DebugFormat("[IRC-Region {0}] {1} has arrived", Region, clientName); + cs.irc.PrivMsg(cs.NoticeMessageFormat, cs.irc.Nick, Region, String.Format("{0} has arrived", clientName)); + } + } + } + catch (Exception ex) + { + m_log.ErrorFormat("[IRC-Region {0}]: MakeRootAgent exception: {1}", Region, ex.Message); + m_log.Debug(ex); + } + + } + + // This handler detects chat events int he virtual world. + + public void OnSimChat(Object sender, OSChatMessage msg) + { + + // early return if this comes from the IRC forwarder + + if (cs.irc.Equals(sender)) return; + + // early return if nothing to forward + + if (msg.Message.Length == 0) return; + + // check for commands coming from avatars or in-world + // object (if commands are enabled) + + if (cs.CommandsEnabled && msg.Channel == cs.CommandChannel) + { + + m_log.DebugFormat("[IRC-Region {0}] command on channel {1}: {2}", Region, msg.Channel, msg.Message); + + string[] messages = msg.Message.Split(' '); + string command = messages[0].ToLower(); + + try + { + switch (command) + { + + // These commands potentially require a change in the + // underlying ChannelState. + + case "server": + cs.Close(this); + cs = cs.UpdateServer(this, messages[1]); + cs.Open(this); + break; + case "port": + cs.Close(this); + cs = cs.UpdatePort(this, messages[1]); + cs.Open(this); + break; + case "channel": + cs.Close(this); + cs = cs.UpdateChannel(this, messages[1]); + cs.Open(this); + break; + case "nick": + cs.Close(this); + cs = cs.UpdateNickname(this, messages[1]); + cs.Open(this); + break; + + // These may also (but are less likely) to require a + // change in ChannelState. + + case "client-reporting": + cs = cs.UpdateClientReporting(this, messages[1]); + break; + case "in-channel": + cs = cs.UpdateRelayIn(this, messages[1]); + break; + case "out-channel": + cs = cs.UpdateRelayOut(this, messages[1]); + break; + + // These are all taken to be temporary changes in state + // so the underlying connector remains intact. But note + // that with regions sharing a connector, there could + // be interference. + + case "close": + enabled = false; + cs.Close(this); + break; + + case "connect": + enabled = true; + cs.Open(this); + break; + + case "reconnect": + enabled = true; + cs.Close(this); + cs.Open(this); + break; + + // This one is harmless as far as we can judge from here. + // If it is not, then the complaints will eventually make + // that evident. + + default: + m_log.DebugFormat("[IRC-Region {0}] Forwarding unrecognized command to IRC : {1}", + Region, msg.Message); + cs.irc.Send(msg.Message); + break; + } + } + catch (Exception ex) + { + m_log.WarnFormat("[IRC-Region {0}] error processing in-world command channel input: {1}", + Region, ex.Message); + m_log.Debug(ex); + } + + return; + + } + + // The command channel remains enabled, even if we have otherwise disabled the IRC + // interface. + + if (!enabled) + return; + + // drop all messages coming in on a private channel, + // except if we are relaying private channels, in which + // case we drop if the private channel is not the + // configured m_relayChannelOut + + if (cs.RelayPrivateChannels) + { + if (msg.Channel != 0 && msg.Channel != DEBUG_CHANNEL && msg.Channel != cs.RelayChannelOut) + { + m_log.DebugFormat("[IRC-Region {0}] dropping message {1} on channel {2}", Region, msg, msg.Channel); + return; + } + } + else if (msg.Channel != 0 && msg.Channel != DEBUG_CHANNEL) + { + m_log.DebugFormat("[IRC-Region {0}] dropping message {1} on channel {2}", Region, msg, msg.Channel); + return; + } + + ScenePresence avatar = null; + + string fromName = msg.From; + + if (msg.Sender != null) + { + avatar = scene.GetScenePresence(msg.Sender.AgentId); + if (avatar != null) fromName = avatar.Name; + } + + if (!cs.irc.Connected) + { + m_log.WarnFormat("[IRC-Region {0}] IRCConnector not connected: dropping message from {1}", Region, fromName); + return; + } + + m_log.DebugFormat("[IRC-Region {0}] heard on channel {1} : {2}", Region, msg.Channel, msg.Message); + + if (null != avatar) + { + string txt = msg.Message; + if (txt.StartsWith("/me ")) + txt = String.Format("{0} {1}", fromName, msg.Message.Substring(4)); + + cs.irc.PrivMsg(cs.PrivateMessageFormat, fromName, Region, txt); + } + else + { + //Message came from an object + char[] splits = { ',' }; + string[] tokens = msg.Message.Split(splits,3); // This is certainly wrong + + if (tokens.Length == 3) + { + if (tokens[0] == cs.AccessPassword) // This is my really simple check + { + m_log.DebugFormat("[IRC-Region {0}] message from object {1}, {2}", Region, tokens[0], tokens[1]); + cs.irc.PrivMsg(cs.PrivateMessageFormat, tokens[1], scene.RegionInfo.RegionName, tokens[2]); + } + else + { + m_log.WarnFormat("[IRC-Region {0}] prim security key mismatch <{1}> not <{2}>", Region, tokens[0], cs.AccessPassword); + } + } + } + } + + // This method gives the region an opportunity to interfere with + // message delivery. For now we just enforce the enable/disable + // flag. + + internal void OSChat(Object irc, OSChatMessage msg) + { + if (enabled) + { + // m_log.DebugFormat("[IRC-OSCHAT] Region {0} being sent message", region.Region); + msg.Scene = scene; + scene.EventManager.TriggerOnChatBroadcast(irc, msg); + } + } + + // This supports any local message traffic that might be needed in + // support of command processing. At present there is none. + + internal void LocalChat(string msg) + { + if (enabled) + { + OSChatMessage osm = new OSChatMessage(); + osm.From = "IRC Agent"; + osm.Message = msg; + osm.Type = ChatTypeEnum.Region; + osm.Position = CenterOfRegion; + osm.Sender = null; + osm.SenderUUID = OpenMetaverse.UUID.Zero; // Hmph! Still? + osm.Channel = 0; + OSChat(this, osm); + } + } + + } + +} diff --git a/OpenSim/Region/Environment/Modules/Avatar/Chat/XIRCBridgeModule.cs b/OpenSim/Region/Environment/Modules/Avatar/Chat/XIRCBridgeModule.cs new file mode 100644 index 0000000000..d7abc1951b --- /dev/null +++ b/OpenSim/Region/Environment/Modules/Avatar/Chat/XIRCBridgeModule.cs @@ -0,0 +1,280 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSim Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using log4net; +using Nini.Config; +using Nwc.XmlRpc; +using OpenSim.Framework; +using OpenSim.Region.Environment.Interfaces; +using OpenSim.Region.Environment.Scenes; + +namespace OpenSim.Region.Environment.Modules.Avatar.Chat +{ + + public class XIRCBridgeModule : IRegionModule + { + + private static readonly ILog m_log = + LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + internal static bool configured = false; + internal static bool enabled = false; + internal static IConfig m_config = null; + + internal static List m_regions = new List(); + internal static List m_channels = new List(); + + internal static string password = String.Empty; + + #region IRegionModule Members + + public string Name + { + get { return "XIRCBridgeModule"; } + } + + public bool IsSharedModule + { + get { return true; } + } + + public void Initialise(Scene scene, IConfigSource config) + { + + // Do a once-only scan of the configuration file to make + // sure it's basically intact. + + if (!configured) + { + + configured = true; + + try + { + if ((m_config = config.Configs["XIRC"]) == null) + { + m_log.InfoFormat("[XIRC-Bridge] module not configured"); + return; + } + + if (!m_config.GetBoolean("enabled", false)) + { + m_log.InfoFormat("[XIRC-Bridge] module disabled in configuration"); + return; + } + } + catch (Exception e) + { + m_log.ErrorFormat("[XIRC-Bridge] configuration failed : {0}", e.Message); + return; + } + + enabled = true; + + if(config.Configs["RemoteAdmin"] != null) + { + password = config.Configs["RemoteAdmin"].GetString("access_password", password); + scene.CommsManager.HttpServer.AddXmlRPCHandler("xirc_admin", XmlRpcAdminMethod, false); + } + + } + + // Iff the IRC bridge is enabled, then each new region may be + // connected to IRC. But it should NOT be obligatory (and it + // is not). + + if (enabled) + { + try + { + m_log.InfoFormat("[XIRC-Bridge] Connecting region {0}", scene.RegionInfo.RegionName); + m_regions.Add(new RegionState(scene, m_config)); + } + catch (Exception e) + { + m_log.WarnFormat("[XIRC-Bridge] Region {0} not connected to IRC : {1}", scene.RegionInfo.RegionName, e.Message); + m_log.Debug(e); + } + } + else + { + m_log.WarnFormat("[XIRC-Bridge] Not enabled. Connect for region {0} ignored", scene.RegionInfo.RegionName); + } + + } + + // Called after all region modules have been loaded. + // Iff the IRC bridge is enabled, then start all of the + // configured channels. The set of channels is a side + // effect of RegionState creation. + + public void PostInitialise() + { + + if (!enabled) + return; + + foreach (RegionState region in m_regions) + { + m_log.InfoFormat("[XIRC-Bridge] Opening connection for {0}:{1} on IRC server {2}:{3}", + region.Region, region.cs.BaseNickname, region.cs.Server, region.cs.IrcChannel); + try + { + region.Open(); + } + catch (Exception e) + { + m_log.ErrorFormat("[XIRC-Bridge] Open failed for {0}:{1} on IRC server {2}:{3} : {4}", + region.Region, region.cs.BaseNickname, region.cs.Server, region.cs.IrcChannel, + e.Message); + } + } + + } + + // Called immediately before the region module is unloaded. Close all + // associated channels. + + public void Close() + { + + if (!enabled) + return; + + // Stop each of the region sessions + + foreach (RegionState region in m_regions) + { + m_log.InfoFormat("[XIRC-Bridge] Closing connection for {0}:{1} on IRC server {2}:{3}", + region.Region, region.cs.BaseNickname, region.cs.Server, region.cs.IrcChannel); + try + { + region.Close(); + } + catch (Exception e) + { + m_log.ErrorFormat("[XIRC-Bridge] Close failed for {0}:{1} on IRC server {2}:{3} : {4}", + region.Region, region.cs.BaseNickname, region.cs.Server, region.cs.IrcChannel, + e.Message); + } + } + + // Perform final cleanup of the channels (they now have no active clients) + + foreach (ChannelState channel in m_channels) + { + m_log.InfoFormat("[XIRC-Bridge] Closing connection for {0} on IRC server {1}:{2}", + channel.BaseNickname, channel.Server, channel.IrcChannel); + try + { + channel.Close(); + } + catch (Exception e) + { + m_log.ErrorFormat("[XIRC-Bridge] Close failed for {0} on IRC server {1}:{2} : {3}", + channel.BaseNickname, channel.Server, channel.IrcChannel, + e.Message); + } + } + + } + + #endregion + + public XmlRpcResponse XmlRpcAdminMethod(XmlRpcRequest request) + { + + m_log.Info("[XIRC-Bridge]: XML RPC Admin Entry"); + + XmlRpcResponse response = new XmlRpcResponse(); + Hashtable responseData = new Hashtable(); + + try + { + + Hashtable requestData = (Hashtable)request.Params[0]; + bool found = false; + string region = String.Empty; + + if(password != String.Empty) + { + if(!requestData.ContainsKey("password")) + throw new Exception("Invalid request"); + if(requestData["password"] != password) + throw new Exception("Invalid request"); + } + + if(!requestData.ContainsKey("region")) + throw new Exception("No region name specified"); + + foreach(RegionState rs in m_regions) + { + if(rs.Region == region) + { + responseData["server"] = rs.cs.Server; + responseData["port"] = rs.cs.Port; + responseData["user"] = rs.cs.User; + responseData["channel"] = rs.cs.IrcChannel; + responseData["enabled"] = rs.cs.irc.Enabled; + responseData["connected"] = rs.cs.irc.Connected; + responseData["nickname"] = rs.cs.irc.Nick; + found = true; + break; + } + } + + if(!found) throw new Exception(String.Format("Region <{0}> not found", region)); + + responseData["success"] = true; + + } + catch (Exception e) + { + m_log.InfoFormat("[XIRC-Bridge] XML RPC Admin request failed : {0}", e.Message); + + responseData["success"] = "false"; + responseData["error"] = e.Message; + + } + finally + { + response.Value = responseData; + } + + m_log.Debug("[XIRC-Bridge]: XML RPC Admin Exit"); + + return response; + + } + + } + +} diff --git a/OpenSim/Region/Environment/Modules/Avatar/Chat/XIRCConnector.cs b/OpenSim/Region/Environment/Modules/Avatar/Chat/XIRCConnector.cs new file mode 100644 index 0000000000..b9203a8385 --- /dev/null +++ b/OpenSim/Region/Environment/Modules/Avatar/Chat/XIRCConnector.cs @@ -0,0 +1,842 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSim Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Timers; +using System.Collections.Generic; +using System.IO; +using System.Net.Sockets; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading; +using OpenMetaverse; +using log4net; +using Nini.Config; +using OpenSim.Framework; +using OpenSim.Region.Environment.Interfaces; +using OpenSim.Region.Environment.Scenes; + +namespace OpenSim.Region.Environment.Modules.Avatar.Chat +{ + public class XIRCConnector + { + + #region Global (static) state + + private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + // Local constants + + private static readonly Vector3 CenterOfRegion = new Vector3(128, 128, 20); + private static readonly char[] CS_SPACE = { ' ' }; + + private const int WD_INTERVAL = 1000; // base watchdog interval + private static int PING_PERIOD = 15; // WD intervals per PING + private static int ICCD_PERIOD = 10; // WD intervals between Connects + + private static int _idk_ = 0; // core connector identifier + private static int _pdk_ = 0; // ping interval counter + private static int _icc_ = 0; // IRC connect counter + + // List of configured connectors + + private static List m_connectors = new List(); + + // Watchdog state + + private static System.Timers.Timer m_watchdog = null; + + static XIRCConnector() + { + m_log.DebugFormat("[IRC-Connector]: Static initialization started"); + m_watchdog = new System.Timers.Timer(WD_INTERVAL); + m_watchdog.Elapsed += new ElapsedEventHandler(WatchdogHandler); + m_watchdog.AutoReset = true; + m_watchdog.Start(); + m_log.DebugFormat("[IRC-Connector]: Static initialization complete"); + } + + #endregion + + #region Instance state + + // Connector identity + + internal int idn = _idk_++; + + // How many regions depend upon this connection + // This count is updated by the ChannelState object and reflects the sum + // of the region clients associated with the set of associated channel + // state instances. That's why it cannot be managed here. + + internal int depends = 0; + + // Working threads + + private Thread m_listener = null; + + private Object msyncConnect = new Object(); + + internal bool m_randomizeNick = true; // add random suffix + internal string m_baseNick = null; // base name for randomizing + internal string m_nick = null; // effective nickname + + public string Nick // Public property + { + get { return m_nick; } + set { m_nick = value; } + } + + private bool m_enabled = false; // connector enablement + public bool Enabled + { + get { return m_enabled; } + } + + private bool m_connected = false; // connection status + public bool Connected + { + get { return m_connected; } + } + + private string m_ircChannel; // associated channel id + public string IrcChannel + { + get { return m_ircChannel; } + set { m_ircChannel = value; } + } + + private uint m_port = 6667; // session port + public uint Port + { + get { return m_port; } + set { m_port = value; } + } + + private string m_server = null; // IRC server name + public string Server + { + get { return m_server; } + set { m_server = value; } + } + + private string m_user = "USER OpenSimBot 8 * :I'm an OpenSim to IRC bot"; + public string User + { + get { return m_user; } + } + + // Network interface + + private TcpClient m_tcp; + private NetworkStream m_stream = null; + private StreamReader m_reader; + private StreamWriter m_writer; + + // Channel characteristic info (if available) + + internal string usermod = String.Empty; + internal string chanmod = String.Empty; + internal string version = String.Empty; + internal bool motd = false; + + #endregion + + #region connector instance management + + internal XIRCConnector(ChannelState cs) + { + + // Prepare network interface + + m_tcp = null; + m_writer = null; + m_reader = null; + + // Setup IRC session parameters + + m_server = cs.Server; + m_baseNick = cs.BaseNickname; + m_randomizeNick = cs.RandomizeNickname; + m_ircChannel = cs.IrcChannel; + m_port = (uint) cs.Port; + m_user = cs.User; + + if (m_watchdog == null) + { + // Non-differentiating + + ICCD_PERIOD = cs.ConnectDelay; + PING_PERIOD = cs.PingDelay; + + // Smaller values are not reasonable + + if (ICCD_PERIOD < 5) + ICCD_PERIOD = 5; + + if (PING_PERIOD < 5) + PING_PERIOD = 5; + + _icc_ = ICCD_PERIOD; // get started right away! + + } + + // The last line of defense + + if (m_server == null || m_baseNick == null || m_ircChannel == null || m_user == null) + throw new Exception("Invalid connector configuration"); + + // Generate an initial nickname if randomizing is enabled + + if (m_randomizeNick) + { + m_nick = m_baseNick + Util.RandomClass.Next(1, 99); + } + + // Add the newly created connector to the known connectors list + + m_connectors.Add(this); + + m_log.InfoFormat("[IRC-Connector-{0}]: Initialization complete", idn); + + } + + ~XIRCConnector() + { + m_watchdog.Stop(); + Close(); + } + + // Mark the connector as connectable. Harmless if already enabled. + + public void Open() + { + if (!m_enabled) + { + + m_connectors.Add(this); + m_enabled = true; + + if (!Connected) + { + Connect(); + } + + } + } + + // Only close the connector if the dependency count is zero. + + public void Close() + { + + m_log.InfoFormat("[IRC-Connector-{0}] Closing", idn); + + lock (msyncConnect) + { + + if ((depends == 0) && Enabled) + { + + m_enabled = false; + + if (Connected) + { + m_log.DebugFormat("[IRC-Connector-{0}] Closing interface", idn); + + // Cleanup the IRC session + + try + { + m_writer.WriteLine(String.Format("QUIT :{0} to {1} wormhole to {2} closing", + m_nick, m_ircChannel, m_server)); + m_writer.Flush(); + } + catch (Exception) {} + + + m_connected = false; + + try { m_writer.Close(); } catch (Exception) {} + try { m_reader.Close(); } catch (Exception) {} + try { m_stream.Close(); } catch (Exception) {} + try { m_tcp.Close(); } catch (Exception) {} + + } + + m_connectors.Remove(this); + + } + } + + m_log.InfoFormat("[IRC-Connector-{0}] Closed", idn); + + } + + #endregion + + #region session management + + // Connect to the IRC server. A connector should always be connected, once enabled + + public void Connect() + { + + if (!m_enabled) + return; + + // Delay until next WD cycle if this is too close to the last start attempt + + while (_icc_ < ICCD_PERIOD) + return; + + m_log.DebugFormat("[IRC-Connector-{0}]: Connection request for {1} on {2}:{3}", idn, m_nick, m_server, m_ircChannel); + + lock (msyncConnect) + { + + _icc_ = 0; + + try + { + if (m_connected) return; + + m_connected = true; + + m_tcp = new TcpClient(m_server, (int)m_port); + m_stream = m_tcp.GetStream(); + m_reader = new StreamReader(m_stream); + m_writer = new StreamWriter(m_stream); + + m_log.InfoFormat("[IRC-Connector-{0}]: Connected to {1}:{2}", idn, m_server, m_port); + + m_listener = new Thread(new ThreadStart(ListenerRun)); + m_listener.Name = "IRCConnectorListenerThread"; + m_listener.IsBackground = true; + m_listener.Start(); + ThreadTracker.Add(m_listener); + + // This is the message order recommended by RFC 2812 + + m_writer.WriteLine(String.Format("NICK {0}", m_nick)); + m_writer.Flush(); + m_writer.WriteLine(m_user); + m_writer.Flush(); + m_writer.WriteLine(String.Format("JOIN {0}", m_ircChannel)); + m_writer.Flush(); + + m_log.InfoFormat("[IRC-Connector-{0}]: {1} has joined {2}", idn, m_nick, m_ircChannel); + m_log.InfoFormat("[IRC-Connector-{0}] Connected", idn); + + } + catch (Exception e) + { + m_log.ErrorFormat("[IRC-Connector-{0}] cannot connect {1} to {2}:{3}: {4}", + idn, m_nick, m_server, m_port, e.Message); + m_connected = false; + } + + } + + return; + + } + + // Reconnect is used to force a re-cycle of the IRC connection. Should generally + // be a transparent event + + public void Reconnect() + { + + m_log.DebugFormat("[IRC-Connector-{0}]: Reconnect request for {1} on {2}:{3}", idn, m_nick, m_server, m_ircChannel); + + // Don't do this if a Connect is in progress... + + lock (msyncConnect) + { + + if (m_connected) + { + m_log.InfoFormat("[IRC-Connector-{0}] Resetting connector", idn); + + // Mark as disconnected. This will allow the listener thread + // to exit if still in-flight. + + + // The listener thread is not aborted - it *might* actually be + // the thread that is running the Reconnect! Instead just close + // the socket and it will disappear of its own accord, once this + // processing is completed. + + try { m_writer.Close(); } catch (Exception) {} + try { m_reader.Close(); } catch (Exception) {} + try { m_tcp.Close(); } catch (Exception) {} + + m_connected = false; + + } + + } + + Connect(); + + } + + #endregion + + #region Outbound (to-IRC) message handlers + + public void PrivMsg(string pattern, string from, string region, string msg) + { + + m_log.DebugFormat("[IRC-Connector-{0}] PrivMsg to IRC from {1}: <{2}>", idn, from, + String.Format(pattern, m_ircChannel, from, region, msg)); + + // One message to the IRC server + + try + { + m_writer.WriteLine(pattern, m_ircChannel, from, region, msg); + m_writer.Flush(); + m_log.DebugFormat("[IRC-Connector-{0}]: PrivMsg from {1} in {2}: {3}", idn, from, region, msg); + } + catch (IOException) + { + m_log.ErrorFormat("[IRC-Connector-{0}]: PrivMsg I/O Error: disconnected from IRC server", idn); + Reconnect(); + } + catch (Exception ex) + { + m_log.ErrorFormat("[IRC-Connector-{0}]: PrivMsg exception : {1}", idn, ex.Message); + m_log.Debug(ex); + } + + } + + public void Send(string msg) + { + + m_log.DebugFormat("[IRC-Connector-{0}] Send to IRC : <{1}>", idn, msg); + + try + { + m_writer.WriteLine(msg); + m_writer.Flush(); + m_log.DebugFormat("[IRC-Connector-{0}] Sent command string: {1}", idn, msg); + } + catch (IOException) + { + m_log.ErrorFormat("[IRC-Connector-{0}] Disconnected from IRC server.(Send)", idn); + Reconnect(); + } + catch (Exception ex) + { + m_log.ErrorFormat("[IRC-Connector-{0}] Send exception trap: {0}", idn, ex.Message); + m_log.Debug(ex); + } + + } + + #endregion + + public void ListenerRun() + { + string inputLine; + + try + { + while (m_enabled && m_connected) + { + + if ((inputLine = m_reader.ReadLine()) == null) + throw new Exception("Listener input socket closed"); + + // m_log.Info("[IRCConnector]: " + inputLine); + + if (inputLine.Contains("PRIVMSG")) + { + + Dictionary data = ExtractMsg(inputLine); + + // Any chat ??? + if (data != null) + { + + OSChatMessage c = new OSChatMessage(); + c.Message = data["msg"]; + c.Type = ChatTypeEnum.Region; + c.Position = CenterOfRegion; + c.From = data["nick"]; + c.Sender = null; + c.SenderUUID = UUID.Zero; + + // Is message "\001ACTION foo bar\001"? + // Then change to: "/me foo bar" + + if ((1 == c.Message[0]) && c.Message.Substring(1).StartsWith("ACTION")) + c.Message = String.Format("/me {0}", c.Message.Substring(8, c.Message.Length - 9)); + + ChannelState.OSChat(this, c, false); + + } + + } + else + { + ProcessIRCCommand(inputLine); + } + } + } + catch (Exception /*e*/) + { + // m_log.ErrorFormat("[IRC-Connector-{0}]: ListenerRun exception trap: {1}", idn, e.Message); + // m_log.Debug(e); + } + + if (m_enabled) Reconnect(); + + } + + private Dictionary ExtractMsg(string input) + { + //examines IRC commands and extracts any private messages + // which will then be reboadcast in the Sim + + // m_log.InfoFormat("[IRC-Connector-{0}]: ExtractMsg: {1}", idn, input); + + Dictionary result = null; + string regex = @":(?[\w-]*)!(?\S*) PRIVMSG (?\S+) :(?.*)"; + Regex RE = new Regex(regex, RegexOptions.Multiline); + MatchCollection matches = RE.Matches(input); + + // Get some direct matches $1 $4 is a + if ((matches.Count == 0) || (matches.Count != 1) || (matches[0].Groups.Count != 5)) + { + // m_log.Info("[IRCConnector]: Number of matches: " + matches.Count); + // if (matches.Count > 0) + // { + // m_log.Info("[IRCConnector]: Number of groups: " + matches[0].Groups.Count); + // } + return null; + } + + result = new Dictionary(); + result.Add("nick", matches[0].Groups[1].Value); + result.Add("user", matches[0].Groups[2].Value); + result.Add("channel", matches[0].Groups[3].Value); + result.Add("msg", matches[0].Groups[4].Value); + + return result; + } + + public void BroadcastSim(string sender, string format, params string[] args) + { + try + { + OSChatMessage c = new OSChatMessage(); + c.From = sender; + c.Message = String.Format(format, args); + c.Type = ChatTypeEnum.Region; // ChatTypeEnum.Say; + c.Position = CenterOfRegion; + c.Sender = null; + c.SenderUUID = UUID.Zero; + + ChannelState.OSChat(this, c, true); + + } + catch (Exception ex) // IRC gate should not crash Sim + { + m_log.ErrorFormat("[IRC-Connector-{0}]: BroadcastSim Exception Trap: {1}\n{2}", idn, ex.Message, ex.StackTrace); + } + } + + #region IRC Command Handlers + + public void ProcessIRCCommand(string command) + { + + string[] commArgs; + string c_server = m_server; + + string pfx = String.Empty; + string cmd = String.Empty; + string parms = String.Empty; + + // ":" indicates that a prefix is present + // There are NEVER more than 17 real + // fields. A parameter that starts with + // ":" indicates that the remainder of the + // line is a single parameter value. + + commArgs = command.Split(CS_SPACE,2); + + if (commArgs[0].StartsWith(":")) + { + pfx = commArgs[0].Substring(1); + commArgs = commArgs[1].Split(CS_SPACE,2); + } + + cmd = commArgs[0]; + parms = commArgs[1]; + + // m_log.DebugFormat("[IRC-Connector-{0}] prefix = <{1}> cmd = <{2}>", idn, pfx, cmd); + + switch (cmd) + { + + // Messages 001-004 are always sent + // following signon. + + case "001" : // Welcome ... + case "002" : // Server information + case "003" : // Welcome ... + break; + case "004" : // Server information + m_log.DebugFormat("[IRC-Connector-{0}] parms = <{1}>", idn, parms); + commArgs = parms.Split(CS_SPACE); + c_server = commArgs[1]; + m_server = c_server; + version = commArgs[2]; + usermod = commArgs[3]; + chanmod = commArgs[4]; + break; + case "005" : // Server information + break; + case "042" : + case "250" : + case "251" : + case "252" : + case "254" : + case "255" : + case "265" : + case "266" : + case "332" : // Subject + case "333" : // Subject owner (?) + case "353" : // Name list + case "366" : // End-of-Name list marker + case "372" : // MOTD body + case "375" : // MOTD start + m_log.InfoFormat("[IRC-Connector-{0}] {1}", idn, parms.Split(CS_SPACE,2)[1]); + break; + case "376" : // MOTD end + m_log.InfoFormat("[IRC-Connector-{0}] {1}", idn, parms.Split(CS_SPACE,2)[1]); + motd = true; + break; + case "451" : // Not registered + break; + case "433" : // Nickname in use + // Gen a new name + m_nick = m_baseNick + Util.RandomClass.Next(1, 99); + m_log.ErrorFormat("[IRC-Connector-{0}]: IRC SERVER reports NicknameInUse, trying {1}", idn, m_nick); + // Retry + m_writer.WriteLine(String.Format("NICK {0}", m_nick)); + m_writer.Flush(); + m_writer.WriteLine(m_user); + m_writer.Flush(); + m_writer.WriteLine(String.Format("JOIN {0}", m_ircChannel)); + m_writer.Flush(); + break; + case "NOTICE" : + m_log.WarnFormat("[IRC-Connector-{0}] {1}", idn, parms.Split(CS_SPACE,2)[1]); + break; + case "ERROR" : + m_log.ErrorFormat("[IRC-Connector-{0}] {1}", idn, parms.Split(CS_SPACE,2)[1]); + if (parms.Contains("reconnect too fast")) + ICCD_PERIOD++; + break; + case "PING" : + m_log.DebugFormat("[IRC-Connector-{0}] parms = <{1}>", idn, parms); + m_writer.WriteLine(String.Format("PONG {0}", parms)); + m_writer.Flush(); + break; + case "PONG" : + break; + case "JOIN": + m_log.DebugFormat("[IRC-Connector-{0}] parms = <{1}>", idn, parms); + eventIrcJoin(pfx, cmd, parms); + break; + case "PART": + m_log.DebugFormat("[IRC-Connector-{0}] parms = <{1}>", idn, parms); + eventIrcPart(pfx, cmd, parms); + break; + case "MODE": + m_log.DebugFormat("[IRC-Connector-{0}] parms = <{1}>", idn, parms); + eventIrcMode(pfx, cmd, parms); + break; + case "NICK": + m_log.DebugFormat("[IRC-Connector-{0}] parms = <{1}>", idn, parms); + eventIrcNickChange(pfx, cmd, parms); + break; + case "KICK": + m_log.DebugFormat("[IRC-Connector-{0}] parms = <{1}>", idn, parms); + eventIrcKick(pfx, cmd, parms); + break; + case "QUIT": + m_log.DebugFormat("[IRC-Connector-{0}] parms = <{1}>", idn, parms); + eventIrcQuit(pfx, cmd, parms); + break; + default : + m_log.DebugFormat("[IRC-Connector-{0}] Command '{1}' ignored, parms = {2}", idn, cmd, parms); + break; + } + + // m_log.DebugFormat("[IRC-Connector-{0}] prefix = <{1}> cmd = <{2}> complete", idn, pfx, cmd); + + } + + public void eventIrcJoin(string prefix, string command, string parms) + { + string[] args = parms.Split(CS_SPACE,2); + string IrcUser = prefix.Split('!')[0]; + string IrcChannel = args[0]; + + if (IrcChannel.StartsWith(":")) + IrcChannel = IrcChannel.Substring(1); + + m_log.DebugFormat("[IRC-Connector-{0}] Event: IRCJoin {1}:{2}", idn, m_server, m_ircChannel); + BroadcastSim(IrcUser, "/me joins {0}", IrcChannel); + } + + public void eventIrcPart(string prefix, string command, string parms) + { + string[] args = parms.Split(CS_SPACE,2); + string IrcUser = prefix.Split('!')[0]; + string IrcChannel = args[0]; + + m_log.DebugFormat("[IRC-Connector-{0}] Event: IRCPart {1}:{2}", idn, m_server, m_ircChannel); + BroadcastSim(IrcUser, "/me parts {0}", IrcChannel); + } + + public void eventIrcMode(string prefix, string command, string parms) + { + string[] args = parms.Split(CS_SPACE,2); + string UserMode = args[1]; + + m_log.DebugFormat("[IRC-Connector-{0}] Event: IRCMode {1}:{2}", idn, m_server, m_ircChannel); + if (UserMode.Substring(0, 1) == ":") + { + UserMode = UserMode.Remove(0, 1); + } + } + + public void eventIrcNickChange(string prefix, string command, string parms) + { + string[] args = parms.Split(CS_SPACE,2); + string UserOldNick = prefix.Split('!')[0]; + string UserNewNick = args[0].Remove(0, 1); + + m_log.DebugFormat("[IRC-Connector-{0}] Event: IRCNickChange {1}:{2}", idn, m_server, m_ircChannel); + BroadcastSim(UserOldNick, "/me is now known as {0}", UserNewNick); + } + + public void eventIrcKick(string prefix, string command, string parms) + { + string[] args = parms.Split(CS_SPACE,3); + string UserKicker = prefix.Split('!')[0]; + string IrcChannel = args[0]; + string UserKicked = args[1]; + string KickMessage = args[2]; + + m_log.DebugFormat("[IRC-Connector-{0}] Event: IRCKick {1}:{2}", idn, m_server, m_ircChannel); + BroadcastSim(UserKicker, "/me kicks kicks {0} off {1} saying \"{2}\"", UserKicked, IrcChannel, KickMessage); + + if (UserKicked == m_nick) + { + BroadcastSim(m_nick, "Hey, that was me!!!"); + } + + } + + public void eventIrcQuit(string prefix, string command, string parms) + { + string IrcUser = prefix.Split('!')[0]; + string QuitMessage = parms; + + m_log.DebugFormat("[IRC-Connector-{0}] Event: IRCQuit {1}:{2}", idn, m_server, m_ircChannel); + BroadcastSim(IrcUser, "/me quits saying \"{0}\"", QuitMessage); + } + + #endregion + + #region Connector Watch Dog + + // A single watch dog monitors extant connectors and makes sure that they + // are re-connected as necessary. If a connector IS connected, then it is + // pinged, but only if a PING period has elapsed. + + protected static void WatchdogHandler(Object source, ElapsedEventArgs args) + { + + // m_log.InfoFormat("[IRC-Watchdog] Status scan"); + + _pdk_ = (_pdk_+1)%PING_PERIOD; // cycle the ping trigger + _icc_++; // increment the inter-consecutive-connect-delay counter + + foreach (XIRCConnector connector in m_connectors) + { + if (connector.Enabled) + { + if (!connector.Connected) + { + try + { + // m_log.DebugFormat("[IRC-Watchdog] Connecting {1}:{2}", connector.idn, connector.m_server, connector.m_ircChannel); + connector.Connect(); + } + catch (Exception e) + { + m_log.ErrorFormat("[IRC-Watchdog] Exception on connector {0}: {1} ", connector.idn, e.Message); + } + } + else + { + if (_pdk_ == 0) + { + try + { + connector.m_writer.WriteLine(String.Format("PING :{0}", connector.m_server)); + connector.m_writer.Flush(); + } + catch (Exception /*e*/) + { + // m_log.ErrorFormat("[IRC-PingRun] Exception on connector {0}: {1} ", connector.idn, e.Message); + // m_log.Debug(e); + connector.Reconnect(); + } + } + } + } + } + + // m_log.InfoFormat("[IRC-Watchdog] Status scan completed"); + + } + + #endregion + + } +} diff --git a/OpenSim/Region/Environment/Modules/Avatar/Concierge/ConciergeModule.cs b/OpenSim/Region/Environment/Modules/Avatar/Concierge/ConciergeModule.cs index 52e743964a..0acb611ca3 100644 --- a/OpenSim/Region/Environment/Modules/Avatar/Concierge/ConciergeModule.cs +++ b/OpenSim/Region/Environment/Modules/Avatar/Concierge/ConciergeModule.cs @@ -428,16 +428,6 @@ namespace OpenSim.Region.Environment.Modules.Avatar.Concierge } } -// private static void checkIntegerParams(XmlRpcRequest request, string[] param) -// { -// Hashtable requestData = (Hashtable) request.Params[0]; -// foreach (string p in param) -// { -// if (!requestData.Contains(p)) -// throw new Exception(String.Format("missing integer parameter {0}", p)); -// } -// } - public XmlRpcResponse XmlRpcUpdateWelcomeMethod(XmlRpcRequest request) { _log.Info("[Concierge]: processing UpdateWelcome request"); diff --git a/OpenSim/Region/Environment/Modules/World/Archiver/ArchiveReadRequest.cs b/OpenSim/Region/Environment/Modules/World/Archiver/ArchiveReadRequest.cs index b001b426b2..c3d017adee 100644 --- a/OpenSim/Region/Environment/Modules/World/Archiver/ArchiveReadRequest.cs +++ b/OpenSim/Region/Environment/Modules/World/Archiver/ArchiveReadRequest.cs @@ -36,6 +36,7 @@ using System.IO; using System.IO.Compression; using System.Reflection; using System.Xml; +using System.Net; using OpenMetaverse; using log4net; @@ -70,7 +71,8 @@ namespace OpenSim.Region.Environment.Modules.World.Archiver { TarArchiveReader archive = new TarArchiveReader( - new GZipStream(new FileStream(m_loadPath, FileMode.Open), CompressionMode.Decompress)); + new GZipStream(GetStream(m_loadPath), CompressionMode.Decompress)); + //AssetsDearchiver dearchiver = new AssetsDearchiver(m_scene.AssetCache); List serialisedSceneObjects = new List(); @@ -284,5 +286,65 @@ namespace OpenSim.Region.Environment.Modules.World.Archiver return true; } + + /// + /// Resolve path to a working FileStream + /// + + private Stream GetStream(string path) + { + try + { + if (File.Exists(path)) + { + return new FileStream(path, FileMode.Open); + } + else + { + Uri uri = new Uri(path); // throw exception if not valid URI + if (uri.Scheme == "file") + { + return new FileStream(uri.AbsolutePath, FileMode.Open); + } + else + { + if (uri.Scheme != "http") + throw new Exception(String.Format("Unsupported URI scheme ({0})", path)); + + // OK, now we know we have an HTTP URI to work with + + return URIFetch(uri); + + } + } + } + catch (Exception e) + { + throw new Exception(String.Format("Unable to create file input stream for {0}: {1}", path, e)); + } + } + + private static Stream URIFetch(Uri uri) + { + + HttpWebRequest request = (HttpWebRequest) WebRequest.Create(uri); + + // request.Credentials = credentials; + + request.ContentLength = 0; + + WebResponse response = request.GetResponse(); + Stream file = response.GetResponseStream(); + + if (response.ContentType != "application/x-oar") + throw new Exception(String.Format("{0} does not identify an OAR file", uri.ToString())); + + if (response.ContentLength == 0) + throw new Exception(String.Format("{0} returned an empty file", uri.ToString())); + + return new BufferedStream(file, (int) response.ContentLength); + + } + } } diff --git a/bin/OpenSim.ini.example b/bin/OpenSim.ini.example index 5517da5021..32944ab558 100644 --- a/bin/OpenSim.ini.example +++ b/bin/OpenSim.ini.example @@ -448,6 +448,11 @@ msgformat = "PRIVMSG {0} : {3} - {1} of {2}" ;for : - from : ;msgformat = "PRIVMSG {0} : {3} - from {1}" +; work-in-progress IRCBridge capabable of supporting multiple IRC channels +; [XIRC] +; enabled = false + + ; Uncomment the following for experiment in world versioning ; support. This is so experimental at this point that I'm not going ; to explain it further, you'll need to read the source code