From 180be7de07014aa33bc6066f12a0819b731c1c9d Mon Sep 17 00:00:00 2001 From: Dr Scofield Date: Tue, 10 Feb 2009 13:10:57 +0000 Subject: this is step 2 of 2 of the OpenSim.Region.Environment refactor. NOTHING has been deleted or moved off to forge at this point. what has happened is that OpenSim.Region.Environment.Modules has been split in two: - OpenSim.Region.CoreModules: all those modules that are either directly or indirectly referenced from other OpenSim packages, or that provide functionality that the OpenSim developer community considers core functionality: CoreModules/Agent/AssetTransaction CoreModules/Agent/Capabilities CoreModules/Agent/TextureDownload CoreModules/Agent/TextureSender CoreModules/Agent/TextureSender/Tests CoreModules/Agent/Xfer CoreModules/Avatar/AvatarFactory CoreModules/Avatar/Chat/ChatModule CoreModules/Avatar/Combat CoreModules/Avatar/Currency/SampleMoney CoreModules/Avatar/Dialog CoreModules/Avatar/Friends CoreModules/Avatar/Gestures CoreModules/Avatar/Groups CoreModules/Avatar/InstantMessage CoreModules/Avatar/Inventory CoreModules/Avatar/Inventory/Archiver CoreModules/Avatar/Inventory/Transfer CoreModules/Avatar/Lure CoreModules/Avatar/ObjectCaps CoreModules/Avatar/Profiles CoreModules/Communications/Local CoreModules/Communications/REST CoreModules/Framework/EventQueue CoreModules/Framework/InterfaceCommander CoreModules/Hypergrid CoreModules/InterGrid CoreModules/Scripting/DynamicTexture CoreModules/Scripting/EMailModules CoreModules/Scripting/HttpRequest CoreModules/Scripting/LoadImageURL CoreModules/Scripting/VectorRender CoreModules/Scripting/WorldComm CoreModules/Scripting/XMLRPC CoreModules/World/Archiver CoreModules/World/Archiver/Tests CoreModules/World/Estate CoreModules/World/Land CoreModules/World/Permissions CoreModules/World/Serialiser CoreModules/World/Sound CoreModules/World/Sun CoreModules/World/Terrain CoreModules/World/Terrain/DefaultEffects CoreModules/World/Terrain/DefaultEffects/bin CoreModules/World/Terrain/DefaultEffects/bin/Debug CoreModules/World/Terrain/Effects CoreModules/World/Terrain/FileLoaders CoreModules/World/Terrain/FloodBrushes CoreModules/World/Terrain/PaintBrushes CoreModules/World/Terrain/Tests CoreModules/World/Vegetation CoreModules/World/Wind CoreModules/World/WorldMap - OpenSim.Region.OptionalModules: all those modules that are not core modules: OptionalModules/Avatar/Chat/IRC-stuff OptionalModules/Avatar/Concierge OptionalModules/Avatar/Voice/AsterixVoice OptionalModules/Avatar/Voice/SIPVoice OptionalModules/ContentManagementSystem OptionalModules/Grid/Interregion OptionalModules/Python OptionalModules/SvnSerialiser OptionalModules/World/NPC OptionalModules/World/TreePopulator --- .../OptionalModules/Avatar/Chat/ChannelState.cs | 628 +++++++++++++++ .../OptionalModules/Avatar/Chat/IRCBridgeModule.cs | 219 +++++ .../OptionalModules/Avatar/Chat/IRCConnector.cs | 887 +++++++++++++++++++++ .../OptionalModules/Avatar/Chat/RegionState.cs | 424 ++++++++++ 4 files changed, 2158 insertions(+) create mode 100644 OpenSim/Region/OptionalModules/Avatar/Chat/ChannelState.cs create mode 100644 OpenSim/Region/OptionalModules/Avatar/Chat/IRCBridgeModule.cs create mode 100644 OpenSim/Region/OptionalModules/Avatar/Chat/IRCConnector.cs create mode 100644 OpenSim/Region/OptionalModules/Avatar/Chat/RegionState.cs (limited to 'OpenSim/Region/OptionalModules/Avatar/Chat') diff --git a/OpenSim/Region/OptionalModules/Avatar/Chat/ChannelState.cs b/OpenSim/Region/OptionalModules/Avatar/Chat/ChannelState.cs new file mode 100644 index 0000000..167f0cc --- /dev/null +++ b/OpenSim/Region/OptionalModules/Avatar/Chat/ChannelState.cs @@ -0,0 +1,628 @@ +/* + * 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.Framework.Interfaces; +using OpenSim.Region.Framework.Scenes; + +namespace OpenSim.Region.OptionalModules.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; + private static int DEBUG_CHANNEL = 2147483647; + + // These are the IRC Connector configurable parameters with hard-wired + // default values (retained for compatability). + + internal string Server = null; + internal string Password = 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 RelayChat = true; + internal bool RelayPrivateChannels = false; + internal int RelayChannel = 1; + internal List ValidInWorldChannels = new List(); + + // 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 bool CommandsEnabled = false; + internal int CommandChannel = -1; + internal int ConnectDelay = 10; + internal int PingDelay = 15; + internal string DefaultZone = "Sim"; + + internal string _accessPassword = String.Empty; + internal Regex AccessPasswordRegex = null; + internal string AccessPassword + { + get { return _accessPassword; } + set + { + _accessPassword = value; + AccessPasswordRegex = new Regex(String.Format(@"^{0},\s*(?[^,]+),\s*(?.+)$", _accessPassword), + RegexOptions.Compiled); + } + } + + + + // IRC connector reference + + internal IRCConnector 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; + Password = model.Password; + IrcChannel = model.IrcChannel; + Port = model.Port; + BaseNickname = model.BaseNickname; + RandomizeNickname = model.RandomizeNickname; + User = model.User; + CommandsEnabled = model.CommandsEnabled; + CommandChannel = model.CommandChannel; + RelayChat = model.RelayChat; + RelayPrivateChannels = model.RelayPrivateChannels; + RelayChannelOut = model.RelayChannelOut; + RelayChannel = model.RelayChannel; + ValidInWorldChannels = model.ValidInWorldChannels; + 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.Password = Substitute(rs, config.GetString("password", null)); + // probably not a good idea to put a password in the log file + 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.RelayChat = Convert.ToBoolean(Substitute(rs, config.GetString("relay_chat", Convert.ToString(cs.RelayChat)))); + m_log.DebugFormat("[IRC-Channel-{0}] RelayChat : <{1}>", cs.idn, cs.RelayChat); + 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.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); + cs.AccessPassword = Substitute(rs, config.GetString("access_password", cs.AccessPassword)); + m_log.DebugFormat("[IRC-Channel-{0}] AccessPassword : <{1}>", cs.idn, cs.AccessPassword); + + + // 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 + + if (cs.RelayChat) + { + cs.ValidInWorldChannels.Add(0); + cs.ValidInWorldChannels.Add(DEBUG_CHANNEL); + } + + if (cs.RelayPrivateChannels) + cs.ValidInWorldChannels.Add(cs.RelayChannelOut); + + 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. + + lock (IRCBridgeModule.m_channels) + { + + foreach (ChannelState xcs in IRCBridgeModule.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 IRCConnector(cs)) != null) + { + + IRCBridgeModule.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} associated with 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 && + RelayChat == cs.RelayChat && + 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. + + // m_log.DebugFormat("[IRC-Channel] Parse[1]: {0}", result); + + 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; + } + // m_log.DebugFormat("[IRC-Channel] Parse[2]: {0}", result); + } + + // m_log.DebugFormat("[IRC-Channel] Parse[3]: {0}", result); + 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(); + } + + // Close is called to ensure that the IRC session is terminated if this is the + // only client. + + public void Close(RegionState rs) + { + RemoveRegion(rs); + lock (IRCBridgeModule.m_channels) + { + if (clientregions.Count == 0) + { + Close(); + IRCBridgeModule.m_channels.Remove(this); + m_log.InfoFormat("[IRC-Channel-{0}] Region {1} is last user of channel <{2}> to server <{3}:{4}>", + idn, rs.Region, IrcChannel, Server, Port); + m_log.InfoFormat("[IRC-Channel-{0}] Removed", idn); + } + } + } + + // 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(IRCConnector 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 + + lock (IRCBridgeModule.m_channels) + { + foreach (ChannelState cs in IRCBridgeModule.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/OptionalModules/Avatar/Chat/IRCBridgeModule.cs b/OpenSim/Region/OptionalModules/Avatar/Chat/IRCBridgeModule.cs new file mode 100644 index 0000000..0facc14 --- /dev/null +++ b/OpenSim/Region/OptionalModules/Avatar/Chat/IRCBridgeModule.cs @@ -0,0 +1,219 @@ +/* + * 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.Framework.Interfaces; +using OpenSim.Region.Framework.Scenes; + +namespace OpenSim.Region.OptionalModules.Avatar.Chat +{ + public class IRCBridgeModule : 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_channels = new List(); + internal static List m_regions = new List(); + + internal static string password = String.Empty; + + internal RegionState region = null; + + #region IRegionModule Members + + public string Name + { + get { return "IRCBridgeModule"; } + } + + public bool IsSharedModule + { + get { return false; } + } + + 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["IRC"]) == null) + { + m_log.InfoFormat("[IRC-Bridge] module not configured"); + return; + } + + if (!m_config.GetBoolean("enabled", false)) + { + m_log.InfoFormat("[IRC-Bridge] module disabled in configuration"); + return; + } + } + catch (Exception e) + { + m_log.ErrorFormat("[IRC-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("irc_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). + // We have to do ALL of the startup here because PostInitialize + // is not called when a region gets created in-flight from the + // command line. + + if (enabled) + { + try + { + m_log.InfoFormat("[IRC-Bridge] Connecting region {0}", scene.RegionInfo.RegionName); + region = new RegionState(scene, m_config); + lock (m_regions) m_regions.Add(region); + region.Open(); + } + catch (Exception e) + { + m_log.WarnFormat("[IRC-Bridge] Region {0} not connected to IRC : {1}", scene.RegionInfo.RegionName, e.Message); + m_log.Debug(e); + } + } + else + { + m_log.WarnFormat("[IRC-Bridge] Not enabled. Connect for region {0} ignored", scene.RegionInfo.RegionName); + } + } + + // This module can be called in-flight in which case PostInitialize + // is not called following Initialize. So no use is made of this + // call. + + public void PostInitialise() + { + } + + // Called immediately before the region module is unloaded. Cleanup + // the region. + + public void Close() + { + if (!enabled) + return; + + region.Close(); + lock (m_regions) m_regions.Remove(region); + } + + #endregion + + public static XmlRpcResponse XmlRpcAdminMethod(XmlRpcRequest request) + { + m_log.Info("[IRC-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 ((string)requestData["password"] != password) + throw new Exception("Invalid request"); + } + + if (!requestData.ContainsKey("region")) + throw new Exception("No region name specified"); + region = (string)requestData["region"]; + + foreach (RegionState rs in m_regions) + { + if (rs.Region == region) + { + responseData["server"] = rs.cs.Server; + responseData["port"] = (int)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("[IRC-Bridge] XML RPC Admin request failed : {0}", e.Message); + + responseData["success"] = "false"; + responseData["error"] = e.Message; + } + finally + { + response.Value = responseData; + } + + m_log.Debug("[IRC-Bridge]: XML RPC Admin Exit"); + + return response; + } + } +} diff --git a/OpenSim/Region/OptionalModules/Avatar/Chat/IRCConnector.cs b/OpenSim/Region/OptionalModules/Avatar/Chat/IRCConnector.cs new file mode 100644 index 0000000..f5c324d --- /dev/null +++ b/OpenSim/Region/OptionalModules/Avatar/Chat/IRCConnector.cs @@ -0,0 +1,887 @@ +/* + * 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.Framework.Interfaces; +using OpenSim.Region.Framework.Scenes; + +namespace OpenSim.Region.OptionalModules.Avatar.Chat +{ + public class IRCConnector + { + + #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 L_TIMEOUT = 25; // Login time out interval + + 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 IRCConnector() + { + 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 + private bool m_pending = false; // login disposition + private int m_timeout = L_TIMEOUT; // login timeout counter + 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_password = null; + public string Password + { + get { return m_password; } + set { m_password = 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 IRCConnector(ChannelState cs) + { + + // Prepare network interface + + m_tcp = null; + m_writer = null; + m_reader = null; + + // Setup IRC session parameters + + m_server = cs.Server; + m_password = cs.Password; + m_baseNick = cs.BaseNickname; + m_randomizeNick = cs.RandomizeNickname; + m_ircChannel = cs.IrcChannel; + m_port = 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); + + } + + ~IRCConnector() + { + 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_pending = true; + m_timeout = L_TIMEOUT; + + 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 + if (m_password != null) + m_writer.WriteLine(String.Format("PASS {0}", m_password)); + 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 asked to join {2}", idn, m_nick, m_ircChannel); + + } + 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; + m_pending = 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; + m_pending = 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); + } + + // This is potentially circular, but harmless if so. + // The connection is marked as not connected the first time + // through reconnect. + + if (m_enabled) Reconnect(); + + } + + private Regex RE = new Regex(@":(?[\w-]*)!(?\S*) PRIVMSG (?\S+) :(?.*)", + RegexOptions.Multiline); + + 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; + 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}] [{1}] parms = <{2}>", idn, cmd, 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}] {2}", idn, cmd, parms.Split(CS_SPACE,2)[1]); + break; + case "376" : // MOTD end + m_log.InfoFormat("[IRC-Connector-{0}] [{1}] {2}", idn, cmd, 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}]: [{1}] IRC SERVER reports NicknameInUse, trying {2}", idn, cmd, 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 "479" : // Bad channel name, etc. This will never work, so disable the connection + m_log.ErrorFormat("[IRC-Connector-{0}] [{1}] {2}", idn, cmd, parms.Split(CS_SPACE,2)[1]); + m_log.ErrorFormat("[IRC-Connector-{0}] [{1}] Connector disabled", idn, cmd); + m_enabled = false; + m_connected = false; + m_pending = false; + break; + case "NOTICE" : + m_log.WarnFormat("[IRC-Connector-{0}] [{1}] {2}", idn, cmd, parms.Split(CS_SPACE,2)[1]); + break; + case "ERROR" : + m_log.ErrorFormat("[IRC-Connector-{0}] [{1}] {2}", idn, cmd, parms.Split(CS_SPACE,2)[1]); + if (parms.Contains("reconnect too fast")) + ICCD_PERIOD++; + m_pending = false; + Reconnect(); + break; + case "PING" : + m_log.DebugFormat("[IRC-Connector-{0}] [{1}] parms = <{2}>", idn, cmd, parms); + m_writer.WriteLine(String.Format("PONG {0}", parms)); + m_writer.Flush(); + break; + case "PONG" : + break; + case "JOIN": + if (m_pending) + { + m_log.InfoFormat("[IRC-Connector-{0}] [{1}] Connected", idn, cmd); + m_pending = false; + } + m_log.DebugFormat("[IRC-Connector-{0}] [{1}] parms = <{2}>", idn, cmd, parms); + eventIrcJoin(pfx, cmd, parms); + break; + case "PART": + m_log.DebugFormat("[IRC-Connector-{0}] [{1}] parms = <{2}>", idn, cmd, parms); + eventIrcPart(pfx, cmd, parms); + break; + case "MODE": + m_log.DebugFormat("[IRC-Connector-{0}] [{1}] parms = <{2}>", idn, cmd, parms); + eventIrcMode(pfx, cmd, parms); + break; + case "NICK": + m_log.DebugFormat("[IRC-Connector-{0}] [{1}] parms = <{2}>", idn, cmd, parms); + eventIrcNickChange(pfx, cmd, parms); + break; + case "KICK": + m_log.DebugFormat("[IRC-Connector-{0}] [{1}] parms = <{2}>", idn, cmd, parms); + eventIrcKick(pfx, cmd, parms); + break; + case "QUIT": + m_log.DebugFormat("[IRC-Connector-{0}] [{1}] parms = <{2}>", idn, cmd, 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 (IRCConnector 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 (connector.m_pending) + { + if (connector.m_timeout == 0) + { + m_log.ErrorFormat("[IRC-Watchdog] Login timed-out for connector {0}, reconnecting", connector.idn); + connector.Reconnect(); + } + else + connector.m_timeout--; + } + + 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/OptionalModules/Avatar/Chat/RegionState.cs b/OpenSim/Region/OptionalModules/Avatar/Chat/RegionState.cs new file mode 100644 index 0000000..6733120 --- /dev/null +++ b/OpenSim/Region/OptionalModules/Avatar/Chat/RegionState.cs @@ -0,0 +1,424 @@ +/* + * 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.Framework.Interfaces; +using OpenSim.Region.Framework.Scenes; + +namespace OpenSim.Region.OptionalModules.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 + // of the module for this region. This happens when the region + // is being removed or the server is terminating. The IRC + // BridgeModule will remove the region from the region list + // when control returns. + + 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 messages unless they are on a valid in-world + // channel as configured in the ChannelState + + if (!cs.ValidInWorldChannels.Contains(msg.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 && cs.RelayChat && (msg.Channel == 0 || msg.Channel == DEBUG_CHANNEL)) + { + 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); + return; + } + + if (null == avatar && cs.RelayPrivateChannels && null != cs.AccessPassword && + msg.Channel == cs.RelayChannelOut) + { + Match m = cs.AccessPasswordRegex.Match(msg.Message); + if (null != m) + { + m_log.DebugFormat("[IRC] relaying message from {0}: {1}", m.Groups["avatar"].ToString(), + m.Groups["message"].ToString()); + cs.irc.PrivMsg(cs.PrivateMessageFormat, m.Groups["avatar"].ToString(), + scene.RegionInfo.RegionName, m.Groups["message"].ToString()); + } + } + } + + // 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); + } + } + + } + +} -- cgit v1.1