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 --- .../Avatar/Concierge/ConciergeModule.cs | 604 +++++++++++++++++++++ 1 file changed, 604 insertions(+) create mode 100644 OpenSim/Region/OptionalModules/Avatar/Concierge/ConciergeModule.cs (limited to 'OpenSim/Region/OptionalModules/Avatar/Concierge/ConciergeModule.cs') diff --git a/OpenSim/Region/OptionalModules/Avatar/Concierge/ConciergeModule.cs b/OpenSim/Region/OptionalModules/Avatar/Concierge/ConciergeModule.cs new file mode 100644 index 0000000..bb46b11 --- /dev/null +++ b/OpenSim/Region/OptionalModules/Avatar/Concierge/ConciergeModule.cs @@ -0,0 +1,604 @@ +/* + * 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.IO; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using log4net; +using Nini.Config; +using Nwc.XmlRpc; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Framework.Servers; +using OpenSim.Region.Framework.Interfaces; +using OpenSim.Region.Framework.Scenes; +using OpenSim.Region.CoreModules.Avatar.Chat; + +namespace OpenSim.Region.OptionalModules.Avatar.Concierge +{ + public class ConciergeModule : ChatModule, IRegionModule + { + private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + private const int DEBUG_CHANNEL = 2147483647; + + private List _scenes = new List(); + private List _conciergedScenes = new List(); + private Dictionary> _sceneAttendees = + new Dictionary>(); + private Dictionary _attendeeNames = + new Dictionary(); + + private bool _replacingChatModule = false; + + private IConfig _config; + + private string _whoami = "conferencier"; + private Regex _regions = null; + private string _welcomes = null; + private int _conciergeChannel = 42; + private string _announceEntering = "{0} enters {1} (now {2} visitors in this region)"; + private string _announceLeaving = "{0} leaves {1} (back to {2} visitors in this region)"; + private string _xmlRpcPassword = String.Empty; + private string _brokerURI = String.Empty; + + internal object _syncy = new object(); + + #region IRegionModule Members + public override void Initialise(Scene scene, IConfigSource config) + { + try + { + if ((_config = config.Configs["Concierge"]) == null) + { + //_log.InfoFormat("[Concierge]: no configuration section [Concierge] in OpenSim.ini: module not configured"); + return; + } + + if (!_config.GetBoolean("enabled", false)) + { + //_log.InfoFormat("[Concierge]: module disabled by OpenSim.ini configuration"); + return; + } + } + catch (Exception) + { + _log.Info("[Concierge]: module not configured"); + return; + } + + // check whether ChatModule has been disabled: if yes, + // then we'll "stand in" + try + { + if (config.Configs["Chat"] == null) + { + _replacingChatModule = false; + } + else + { + _replacingChatModule = !config.Configs["Chat"].GetBoolean("enabled", true); + } + } + catch (Exception) + { + _replacingChatModule = false; + } + _log.InfoFormat("[Concierge] {0} ChatModule", _replacingChatModule ? "replacing" : "not replacing"); + + + // take note of concierge channel and of identity + _conciergeChannel = config.Configs["Concierge"].GetInt("concierge_channel", _conciergeChannel); + _whoami = _config.GetString("whoami", "conferencier"); + _welcomes = _config.GetString("welcomes", _welcomes); + _announceEntering = _config.GetString("announce_entering", _announceEntering); + _announceLeaving = _config.GetString("announce_leaving", _announceLeaving); + _xmlRpcPassword = _config.GetString("password", _xmlRpcPassword); + _brokerURI = _config.GetString("broker", _brokerURI); + + _log.InfoFormat("[Concierge] reporting as \"{0}\" to our users", _whoami); + + // calculate regions Regex + if (_regions == null) + { + string regions = _config.GetString("regions", String.Empty); + if (!String.IsNullOrEmpty(regions)) + { + _regions = new Regex(@regions, RegexOptions.Compiled | RegexOptions.IgnoreCase); + } + } + + scene.CommsManager.HttpServer.AddXmlRPCHandler("concierge_update_welcome", XmlRpcUpdateWelcomeMethod, false); + + lock (_syncy) + { + if (!_scenes.Contains(scene)) + { + _scenes.Add(scene); + + if (_regions == null || _regions.IsMatch(scene.RegionInfo.RegionName)) + _conciergedScenes.Add(scene); + + // subscribe to NewClient events + scene.EventManager.OnNewClient += OnNewClient; + + // subscribe to *Chat events + scene.EventManager.OnChatFromWorld += OnChatFromWorld; + if (!_replacingChatModule) + scene.EventManager.OnChatFromClient += OnChatFromClient; + scene.EventManager.OnChatBroadcast += OnChatBroadcast; + + // subscribe to agent change events + scene.EventManager.OnMakeRootAgent += OnMakeRootAgent; + scene.EventManager.OnMakeChildAgent += OnMakeChildAgent; + } + } + _log.InfoFormat("[Concierge]: initialized for {0}", scene.RegionInfo.RegionName); + } + + public override void PostInitialise() + { + } + + public override void Close() + { + } + + public override string Name + { + get { return "ConciergeModule"; } + } + + public override bool IsSharedModule + { + get { return true; } + } + + #endregion + + #region ISimChat Members + public override void OnChatBroadcast(Object sender, OSChatMessage c) + { + if (_replacingChatModule) + { + // distribute chat message to each and every avatar in + // the region + base.OnChatBroadcast(sender, c); + } + + // TODO: capture logic + return; + } + + public override void OnChatFromClient(Object sender, OSChatMessage c) + { + if (_replacingChatModule) + { + // replacing ChatModule: need to redistribute + // ChatFromClient to interested subscribers + c = FixPositionOfChatMessage(c); + + Scene scene = (Scene)c.Scene; + scene.EventManager.TriggerOnChatFromClient(sender, c); + + if (_conciergedScenes.Contains(c.Scene)) + { + // when we are replacing ChatModule, we treat + // OnChatFromClient like OnChatBroadcast for + // concierged regions, effectively extending the + // range of chat to cover the whole + // region. however, we don't do this for whisper + // (got to have some privacy) + if (c.Type != ChatTypeEnum.Whisper) + { + base.OnChatBroadcast(sender, c); + return; + } + } + + // redistribution will be done by base class + base.OnChatFromClient(sender, c); + } + + // TODO: capture chat + return; + } + + public override void OnChatFromWorld(Object sender, OSChatMessage c) + { + if (_replacingChatModule) + { + if (_conciergedScenes.Contains(c.Scene)) + { + // when we are replacing ChatModule, we treat + // OnChatFromClient like OnChatBroadcast for + // concierged regions, effectively extending the + // range of chat to cover the whole + // region. however, we don't do this for whisper + // (got to have some privacy) + if (c.Type != ChatTypeEnum.Whisper) + { + base.OnChatBroadcast(sender, c); + return; + } + } + + base.OnChatFromWorld(sender, c); + } + return; + } + #endregion + + + public override void OnNewClient(IClientAPI client) + { + client.OnLogout += OnClientLoggedOut; + + if (_replacingChatModule) + client.OnChatFromClient += OnChatFromClient; + } + + + + public void OnClientLoggedOut(IClientAPI client) + { + client.OnLogout -= OnClientLoggedOut; + client.OnConnectionClosed -= OnClientLoggedOut; + + if (_conciergedScenes.Contains(client.Scene)) + { + _log.DebugFormat("[Concierge]: {0} logs off from {1}", client.Name, client.Scene.RegionInfo.RegionName); + RemoveFromAttendeeList(client.AgentId, client.Name, client.Scene); + AnnounceToAgentsRegion(client.Scene, String.Format(_announceLeaving, client.Name, client.Scene.RegionInfo.RegionName, + _sceneAttendees[client.Scene].Count)); + UpdateBroker(client.Scene); + } + } + + + public void OnMakeRootAgent(ScenePresence agent) + { + if (_conciergedScenes.Contains(agent.Scene)) + { + _log.DebugFormat("[Concierge]: {0} enters {1}", agent.Name, agent.Scene.RegionInfo.RegionName); + AddToAttendeeList(agent.UUID, agent.Name, agent.Scene); + WelcomeAvatar(agent, agent.Scene); + AnnounceToAgentsRegion(agent.Scene, String.Format(_announceEntering, agent.Name, agent.Scene.RegionInfo.RegionName, + _sceneAttendees[agent.Scene].Count)); + UpdateBroker(agent.Scene); + } + } + + + public void OnMakeChildAgent(ScenePresence agent) + { + if (_conciergedScenes.Contains(agent.Scene)) + { + _log.DebugFormat("[Concierge]: {0} leaves {1}", agent.Name, agent.Scene.RegionInfo.RegionName); + RemoveFromAttendeeList(agent.UUID, agent.Name, agent.Scene); + AnnounceToAgentsRegion(agent.Scene, String.Format(_announceLeaving, agent.Name, agent.Scene.RegionInfo.RegionName, + _sceneAttendees[agent.Scene].Count)); + UpdateBroker(agent.Scene); + } + } + + protected void AddToAttendeeList(UUID agentID, string name, Scene scene) + { + lock (_sceneAttendees) + { + if (!_sceneAttendees.ContainsKey(scene)) + _sceneAttendees[scene] = new List(); + + List attendees = _sceneAttendees[scene]; + if (!attendees.Contains(agentID)) + { + attendees.Add(agentID); + _attendeeNames[agentID] = name; + } + } + } + + protected void RemoveFromAttendeeList(UUID agentID, String name, IScene scene) + { + lock (_sceneAttendees) + { + if (!_sceneAttendees.ContainsKey(scene)) + { + _log.WarnFormat("[Concierge]: attendee list missing for region {0}", scene.RegionInfo.RegionName); + return; + } + + List attendees = _sceneAttendees[scene]; + if (!attendees.Contains(agentID)) + { + _log.WarnFormat("[Concierge]: avatar {0} must have sneaked in to region {1} earlier", + name, scene.RegionInfo.RegionName); + return; + } + + attendees.Remove(agentID); + _attendeeNames.Remove(agentID); + } + } + + protected void UpdateBroker(IScene scene) + { + if (String.IsNullOrEmpty(_brokerURI)) + return; + + string uri = String.Format(_brokerURI, scene.RegionInfo.RegionName, scene.RegionInfo.RegionID); + + // get attendee list for the scene + List attendees; + lock (_sceneAttendees) + { + if (!_sceneAttendees.ContainsKey(scene)) + { + _log.DebugFormat("[Concierge]: attendee list missing for region {0}", scene.RegionInfo.RegionName); + return; + } + + attendees = _sceneAttendees[scene]; + } + + // create XML sniplet + StringBuilder list = new StringBuilder(); + if (0 == attendees.Count) + { + list.Append(String.Format("", + scene.RegionInfo.RegionName, scene.RegionInfo.RegionID, + DateTime.UtcNow.ToString("s"))); + } + else + { + list.Append(String.Format("\n", + attendees.Count, scene.RegionInfo.RegionName, + scene.RegionInfo.RegionID, + DateTime.UtcNow.ToString("s"))); + foreach (UUID uuid in attendees) + { + string name = _attendeeNames[uuid]; + list.Append(String.Format(" \n", name, uuid)); + } + list.Append(""); + } + string payload = list.ToString(); + + // post via REST to broker + HttpWebRequest updatePost = WebRequest.Create(uri) as HttpWebRequest; + updatePost.Method = "POST"; + updatePost.ContentType = "text/xml"; + updatePost.ContentLength = payload.Length; + updatePost.UserAgent = "OpenSim.Concierge"; + + try + { + StreamWriter payloadStream = new StreamWriter(updatePost.GetRequestStream()); + payloadStream.Write(payload); + payloadStream.Close(); + + updatePost.BeginGetResponse(UpdateBrokerDone, updatePost); + _log.DebugFormat("[Concierge] async broker POST to {0} started", uri); + } + catch (WebException we) + { + _log.ErrorFormat("[Concierge] async broker POST to {0} failed: {1}", uri, we.Status); + } + } + + private void UpdateBrokerDone(IAsyncResult result) + { + HttpWebRequest updatePost = null; + try + { + updatePost = result.AsyncState as HttpWebRequest; + using (HttpWebResponse response = updatePost.EndGetResponse(result) as HttpWebResponse) + { + _log.DebugFormat("[Concierge] broker update: status {0}", response.StatusCode); + } + } + catch (WebException we) + { + string uri = updatePost.RequestUri.OriginalString; + _log.ErrorFormat("[Concierge] broker update to {0} failed with status {1}", uri, we.Status); + if (null != we.Response) + { + using (HttpWebResponse resp = we.Response as HttpWebResponse) + { + _log.ErrorFormat("[Concierge] response from {0} status code: {1}", uri, resp.StatusCode); + _log.ErrorFormat("[Concierge] response from {0} status desc: {1}", uri, resp.StatusDescription); + _log.ErrorFormat("[Concierge] response from {0} server: {1}", uri, resp.Server); + + if (resp.ContentLength > 0) + { + StreamReader content = new StreamReader(resp.GetResponseStream()); + _log.ErrorFormat("[Concierge] response from {0} content: {1}", uri, content.ReadToEnd()); + content.Close(); + } + } + } + } + } + + protected void WelcomeAvatar(ScenePresence agent, Scene scene) + { + // welcome mechanics: check whether we have a welcomes + // directory set and wether there is a region specific + // welcome file there: if yes, send it to the agent + if (!String.IsNullOrEmpty(_welcomes)) + { + string[] welcomes = new string[] { + Path.Combine(_welcomes, agent.Scene.RegionInfo.RegionName), + Path.Combine(_welcomes, "DEFAULT")}; + foreach (string welcome in welcomes) + { + if (File.Exists(welcome)) + { + try + { + string[] welcomeLines = File.ReadAllLines(welcome); + foreach (string l in welcomeLines) + { + AnnounceToAgent(agent, String.Format(l, agent.Name, scene.RegionInfo.RegionName, _whoami)); + } + } + catch (IOException ioe) + { + _log.ErrorFormat("[Concierge]: run into trouble reading welcome file {0} for region {1} for avatar {2}: {3}", + welcome, scene.RegionInfo.RegionName, agent.Name, ioe); + } + catch (FormatException fe) + { + _log.ErrorFormat("[Concierge]: welcome file {0} is malformed: {1}", welcome, fe); + } + } + return; + } + _log.DebugFormat("[Concierge]: no welcome message for region {0}", scene.RegionInfo.RegionName); + } + } + + static private Vector3 PosOfGod = new Vector3(128, 128, 9999); + + // protected void AnnounceToAgentsRegion(Scene scene, string msg) + // { + // ScenePresence agent = null; + // if ((client.Scene is Scene) && (client.Scene as Scene).TryGetAvatar(client.AgentId, out agent)) + // AnnounceToAgentsRegion(agent, msg); + // else + // _log.DebugFormat("[Concierge]: could not find an agent for client {0}", client.Name); + // } + + protected void AnnounceToAgentsRegion(IScene scene, string msg) + { + OSChatMessage c = new OSChatMessage(); + c.Message = msg; + c.Type = ChatTypeEnum.Say; + c.Channel = 0; + c.Position = PosOfGod; + c.From = _whoami; + c.Sender = null; + c.SenderUUID = UUID.Zero; + c.Scene = scene; + + if (scene is Scene) + (scene as Scene).EventManager.TriggerOnChatBroadcast(this, c); + } + + protected void AnnounceToAgent(ScenePresence agent, string msg) + { + OSChatMessage c = new OSChatMessage(); + c.Message = msg; + c.Type = ChatTypeEnum.Say; + c.Channel = 0; + c.Position = PosOfGod; + c.From = _whoami; + c.Sender = null; + c.SenderUUID = UUID.Zero; + c.Scene = agent.Scene; + + agent.ControllingClient.SendChatMessage(msg, (byte) ChatTypeEnum.Say, PosOfGod, _whoami, UUID.Zero, + (byte)ChatSourceType.Object, (byte)ChatAudibleLevel.Fully); + } + + private static void checkStringParameters(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 string parameter {0}", p)); + if (String.IsNullOrEmpty((string)requestData[p])) + throw new Exception(String.Format("parameter {0} is empty", p)); + } + } + + public XmlRpcResponse XmlRpcUpdateWelcomeMethod(XmlRpcRequest request) + { + _log.Info("[Concierge]: processing UpdateWelcome request"); + XmlRpcResponse response = new XmlRpcResponse(); + Hashtable responseData = new Hashtable(); + + try + { + Hashtable requestData = (Hashtable)request.Params[0]; + checkStringParameters(request, new string[] { "password", "region", "welcome" }); + + // check password + if (!String.IsNullOrEmpty(_xmlRpcPassword) && + (string)requestData["password"] != _xmlRpcPassword) throw new Exception("wrong password"); + + if (String.IsNullOrEmpty(_welcomes)) + throw new Exception("welcome templates are not enabled, ask your OpenSim operator to set the \"welcomes\" option in the [Concierge] section of OpenSim.ini"); + + string msg = (string)requestData["welcome"]; + if (String.IsNullOrEmpty(msg)) + throw new Exception("empty parameter \"welcome\""); + + string regionName = (string)requestData["region"]; + IScene scene = _scenes.Find(delegate(IScene s) { return s.RegionInfo.RegionName == regionName; }); + if (scene == null) + throw new Exception(String.Format("unknown region \"{0}\"", regionName)); + + if (!_conciergedScenes.Contains(scene)) + throw new Exception(String.Format("region \"{0}\" is not a concierged region.", regionName)); + + string welcome = Path.Combine(_welcomes, regionName); + if (File.Exists(welcome)) + { + _log.InfoFormat("[Concierge]: UpdateWelcome: updating existing template \"{0}\"", welcome); + string welcomeBackup = String.Format("{0}~", welcome); + if (File.Exists(welcomeBackup)) + File.Delete(welcomeBackup); + File.Move(welcome, welcomeBackup); + } + File.WriteAllText(welcome, msg); + + responseData["success"] = "true"; + response.Value = responseData; + } + catch (Exception e) + { + _log.InfoFormat("[Concierge]: UpdateWelcome failed: {0}", e.Message); + + responseData["success"] = "false"; + responseData["error"] = e.Message; + + response.Value = responseData; + } + _log.Debug("[Concierge]: done processing UpdateWelcome request"); + return response; + } + } +} -- cgit v1.1