diff options
Diffstat (limited to '')
-rw-r--r-- | OpenSim/Region/Environment/Modules/Avatar/Chat/IRCBridgeModule.cs | 393 | ||||
-rw-r--r-- | OpenSim/Region/Environment/Modules/Avatar/Chat/IRCConnector.cs | 702 |
2 files changed, 0 insertions, 1095 deletions
diff --git a/OpenSim/Region/Environment/Modules/Avatar/Chat/IRCBridgeModule.cs b/OpenSim/Region/Environment/Modules/Avatar/Chat/IRCBridgeModule.cs deleted file mode 100644 index 482f469..0000000 --- a/OpenSim/Region/Environment/Modules/Avatar/Chat/IRCBridgeModule.cs +++ /dev/null | |||
@@ -1,393 +0,0 @@ | |||
1 | /* | ||
2 | * Copyright (c) Contributors, http://opensimulator.org/ | ||
3 | * See CONTRIBUTORS.TXT for a full list of copyright holders. | ||
4 | * | ||
5 | * Redistribution and use in source and binary forms, with or without | ||
6 | * modification, are permitted provided that the following conditions are met: | ||
7 | * * Redistributions of source code must retain the above copyright | ||
8 | * notice, this list of conditions and the following disclaimer. | ||
9 | * * Redistributions in binary form must reproduce the above copyright | ||
10 | * notice, this list of conditions and the following disclaimer in the | ||
11 | * documentation and/or other materials provided with the distribution. | ||
12 | * * Neither the name of the OpenSim Project nor the | ||
13 | * names of its contributors may be used to endorse or promote products | ||
14 | * derived from this software without specific prior written permission. | ||
15 | * | ||
16 | * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY | ||
17 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
19 | * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY | ||
20 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | ||
23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
26 | */ | ||
27 | |||
28 | using System; | ||
29 | using System.Collections.Generic; | ||
30 | using System.IO; | ||
31 | using System.Net.Sockets; | ||
32 | using System.Reflection; | ||
33 | using System.Text.RegularExpressions; | ||
34 | using System.Threading; | ||
35 | using OpenMetaverse; | ||
36 | using log4net; | ||
37 | using Nini.Config; | ||
38 | using OpenSim.Framework; | ||
39 | using OpenSim.Region.Environment.Interfaces; | ||
40 | using OpenSim.Region.Environment.Scenes; | ||
41 | |||
42 | namespace OpenSim.Region.Environment.Modules.Avatar.Chat | ||
43 | { | ||
44 | public class IRCBridgeModule : IRegionModule | ||
45 | { | ||
46 | private static readonly ILog m_log = | ||
47 | LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); | ||
48 | |||
49 | private const int DEBUG_CHANNEL = 2147483647; | ||
50 | |||
51 | |||
52 | private IRCConnector m_irc = null; | ||
53 | |||
54 | private string m_last_leaving_user = null; | ||
55 | private string m_last_new_user = null; | ||
56 | private List<Scene> m_scenes = new List<Scene>(); | ||
57 | private List<int> m_validInWorldChannels = new List<int>(); | ||
58 | |||
59 | internal object m_syncInit = new object(); | ||
60 | internal object m_syncLogout = new object(); | ||
61 | |||
62 | private IConfig m_config; | ||
63 | private string m_defaultzone = null; | ||
64 | private bool m_commandsEnabled = false; | ||
65 | private int m_commandChannel = -1; | ||
66 | private bool m_relayPrivateChannels = false; | ||
67 | private int m_relayChannelOut = -1; | ||
68 | private bool m_clientReporting = true; | ||
69 | private bool m_relayChat = true; | ||
70 | private Regex m_accessPasswordRe = null; | ||
71 | |||
72 | #region IRegionModule Members | ||
73 | |||
74 | public void Initialise(Scene scene, IConfigSource config) | ||
75 | { | ||
76 | try | ||
77 | { | ||
78 | if ((m_config = config.Configs["OIRC"]) == null) | ||
79 | { | ||
80 | m_log.InfoFormat("[IRC] module not configured"); | ||
81 | return; | ||
82 | } | ||
83 | |||
84 | if (!m_config.GetBoolean("enabled", false)) | ||
85 | { | ||
86 | m_log.InfoFormat("[IRC] module disabled in configuration"); | ||
87 | return; | ||
88 | } | ||
89 | } | ||
90 | catch (Exception) | ||
91 | { | ||
92 | m_log.Info("[IRC] module not configured"); | ||
93 | return; | ||
94 | } | ||
95 | |||
96 | m_commandsEnabled = m_config.GetBoolean("commands_enabled", m_commandsEnabled); | ||
97 | m_commandChannel = m_config.GetInt("commandchannel", m_commandChannel); // compat | ||
98 | m_commandChannel = m_config.GetInt("command_channel", m_commandChannel); | ||
99 | |||
100 | m_relayPrivateChannels = m_config.GetBoolean("relay_private_channels", m_relayPrivateChannels); | ||
101 | m_relayChannelOut = m_config.GetInt("relay_private_channel_out", m_relayChannelOut); | ||
102 | m_relayChat = m_config.GetBoolean("relay_chat", m_relayChat); | ||
103 | |||
104 | m_clientReporting = m_config.GetBoolean("report_clients", m_clientReporting); | ||
105 | |||
106 | if (m_accessPasswordRe == null) | ||
107 | { | ||
108 | string pass = config.Configs["IRC"].GetString("access_password", String.Empty); | ||
109 | m_accessPasswordRe = new Regex(String.Format(@"^{0},(?<avatar>[^,]+),(?<message>.+)$", pass), | ||
110 | RegexOptions.Compiled); | ||
111 | } | ||
112 | |||
113 | if (m_relayChat) | ||
114 | { | ||
115 | m_validInWorldChannels.Add(0); | ||
116 | m_validInWorldChannels.Add(DEBUG_CHANNEL); | ||
117 | } | ||
118 | |||
119 | if (m_relayPrivateChannels) | ||
120 | m_validInWorldChannels.Add(m_relayChannelOut); | ||
121 | |||
122 | |||
123 | lock (m_syncInit) | ||
124 | { | ||
125 | |||
126 | if (!m_scenes.Contains(scene)) | ||
127 | { | ||
128 | m_scenes.Add(scene); | ||
129 | scene.EventManager.OnNewClient += OnNewClient; | ||
130 | scene.EventManager.OnChatFromWorld += OnSimChat; | ||
131 | scene.EventManager.OnChatFromClient += OnSimChat; | ||
132 | scene.EventManager.OnMakeRootAgent += OnMakeRootAgent; | ||
133 | scene.EventManager.OnMakeChildAgent += OnMakeChildAgent; | ||
134 | } | ||
135 | |||
136 | try | ||
137 | { | ||
138 | m_defaultzone = config.Configs["IRC"].GetString("fallback_region", "Sim"); | ||
139 | } | ||
140 | catch (Exception) | ||
141 | { | ||
142 | } | ||
143 | |||
144 | // setup IRC Relay | ||
145 | if (m_irc == null) | ||
146 | { | ||
147 | m_irc = new IRCConnector(config); | ||
148 | } | ||
149 | m_irc.AddScene(scene); | ||
150 | |||
151 | m_log.InfoFormat("[IRC] initialized for {0}, nick: {1}, commands {2}, private channels {3}", | ||
152 | scene.RegionInfo.RegionName, m_defaultzone, | ||
153 | m_commandsEnabled ? "enabled" : "not enabled", | ||
154 | m_relayPrivateChannels ? "relayed" : "not relayed"); | ||
155 | } | ||
156 | } | ||
157 | |||
158 | public void PostInitialise() | ||
159 | { | ||
160 | if (null == m_irc || !m_irc.Enabled) return; | ||
161 | m_irc.Start(); | ||
162 | } | ||
163 | |||
164 | public void Close() | ||
165 | { | ||
166 | if (null != m_irc) | ||
167 | { | ||
168 | m_irc.Close(); | ||
169 | m_log.Info("[IRC] closed connection to IRC server"); | ||
170 | } | ||
171 | } | ||
172 | |||
173 | public string Name | ||
174 | { | ||
175 | get { return "IRCBridgeModule"; } | ||
176 | } | ||
177 | |||
178 | public bool IsSharedModule | ||
179 | { | ||
180 | get { return true; } | ||
181 | } | ||
182 | |||
183 | #endregion | ||
184 | |||
185 | #region ISimChat Members | ||
186 | |||
187 | public void OnSimChat(Object sender, OSChatMessage c) | ||
188 | { | ||
189 | // early return if nothing to forward | ||
190 | if (c.Message.Length == 0) return; | ||
191 | |||
192 | // early return if this comes from the IRC forwarder | ||
193 | if (m_irc.Equals(sender)) return; | ||
194 | |||
195 | m_log.DebugFormat("[IRC] heard on channel {0}: {1}", c.Channel, c.Message); | ||
196 | |||
197 | // check for commands coming from avatars or in-world | ||
198 | // object (if commands are enabled) | ||
199 | if (m_commandsEnabled && c.Channel == m_commandChannel) | ||
200 | { | ||
201 | string[] messages = c.Message.Split(' '); | ||
202 | string command = messages[0].ToLower(); | ||
203 | |||
204 | try | ||
205 | { | ||
206 | switch (command) | ||
207 | { | ||
208 | case "channel": | ||
209 | m_irc.IrcChannel = messages[1]; | ||
210 | break; | ||
211 | case "close": | ||
212 | m_irc.Close(); | ||
213 | break; | ||
214 | case "connect": | ||
215 | m_irc.Connect(); | ||
216 | break; | ||
217 | case "nick": | ||
218 | m_irc.Nick = messages[1]; | ||
219 | break; | ||
220 | case "port": | ||
221 | m_irc.Port = Convert.ToUInt32(messages[1]); | ||
222 | break; | ||
223 | case "reconnect": | ||
224 | m_irc.Reconnect(); | ||
225 | break; | ||
226 | case "server": | ||
227 | m_irc.Server = messages[1]; | ||
228 | break; | ||
229 | case "client-reporting": | ||
230 | m_irc.ClientReporting = Convert.ToBoolean(messages[1]); | ||
231 | |||
232 | break; | ||
233 | case "in-channel": | ||
234 | m_irc.RelayChannel = Convert.ToInt32(messages[1]); | ||
235 | break; | ||
236 | case "out-channel": | ||
237 | m_relayChannelOut = Convert.ToInt32(messages[1]); | ||
238 | break; | ||
239 | |||
240 | default: | ||
241 | m_irc.Send(c.Message); | ||
242 | break; | ||
243 | } | ||
244 | } | ||
245 | catch (Exception ex) | ||
246 | { | ||
247 | m_log.DebugFormat("[IRC] error processing in-world command channel input: {0}", ex); | ||
248 | } | ||
249 | } | ||
250 | |||
251 | // drop messages if their channel is not on the valid | ||
252 | // in-world channel list | ||
253 | if (!m_validInWorldChannels.Contains(c.Channel)) | ||
254 | { | ||
255 | m_log.DebugFormat("[IRC] dropping message {0} on channel {1}", c, c.Channel); | ||
256 | return; | ||
257 | } | ||
258 | |||
259 | ScenePresence avatar = null; | ||
260 | Scene scene = (Scene)c.Scene; | ||
261 | |||
262 | if (scene == null) | ||
263 | scene = m_scenes[0]; | ||
264 | |||
265 | string fromName = c.From; | ||
266 | |||
267 | if (c.Sender != null) | ||
268 | { | ||
269 | avatar = scene.GetScenePresence(c.Sender.AgentId); | ||
270 | if (avatar != null) fromName = avatar.Name; | ||
271 | } | ||
272 | |||
273 | if (!m_irc.Connected) | ||
274 | { | ||
275 | m_log.WarnFormat("[IRC] IRCConnector not connected: dropping message from {0}", fromName); | ||
276 | return; | ||
277 | } | ||
278 | |||
279 | if (null != avatar && m_relayChat) | ||
280 | { | ||
281 | string msg = c.Message; | ||
282 | if (msg.StartsWith("/me ")) | ||
283 | msg = String.Format("{0} {1}", fromName, c.Message.Substring(4)); | ||
284 | |||
285 | m_irc.PrivMsg(fromName, scene.RegionInfo.RegionName, msg); | ||
286 | return; | ||
287 | } | ||
288 | |||
289 | if (null == avatar && m_relayPrivateChannels) | ||
290 | { | ||
291 | Match m; | ||
292 | if (m_accessPasswordRe != null && | ||
293 | (m = m_accessPasswordRe.Match(c.Message)) != null) | ||
294 | { | ||
295 | m_log.DebugFormat("[IRC] relaying message from {0}: {1}", m.Groups["avatar"].ToString(), | ||
296 | m.Groups["message"].ToString()); | ||
297 | m_irc.PrivMsg(m.Groups["avatar"].ToString(), scene.RegionInfo.RegionName, | ||
298 | m.Groups["message"].ToString()); | ||
299 | } | ||
300 | return; | ||
301 | } | ||
302 | } | ||
303 | #endregion | ||
304 | |||
305 | public void OnNewClient(IClientAPI client) | ||
306 | { | ||
307 | try | ||
308 | { | ||
309 | client.OnLogout += OnClientLoggedOut; | ||
310 | client.OnConnectionClosed += OnClientLoggedOut; | ||
311 | |||
312 | if (client.Name != m_last_new_user) | ||
313 | { | ||
314 | if ((m_irc.Enabled) && (m_irc.Connected) && (m_clientReporting)) | ||
315 | { | ||
316 | m_log.DebugFormat("[IRC] {0} logging on", client.Name); | ||
317 | m_irc.PrivMsg(m_irc.Nick, "Sim", String.Format("notices {0} logging on", client.Name)); | ||
318 | } | ||
319 | m_last_new_user = client.Name; | ||
320 | } | ||
321 | } | ||
322 | catch (Exception ex) | ||
323 | { | ||
324 | m_log.Error("[IRC]: OnNewClient exception trap:" + ex.ToString()); | ||
325 | } | ||
326 | } | ||
327 | |||
328 | public void OnMakeRootAgent(ScenePresence presence) | ||
329 | { | ||
330 | try | ||
331 | { | ||
332 | if ((m_irc.Enabled) && (m_irc.Connected) && (m_clientReporting)) | ||
333 | { | ||
334 | string regionName = presence.Scene.RegionInfo.RegionName; | ||
335 | string clientName = String.Format("{0} {1}", presence.Firstname, presence.Lastname); | ||
336 | m_log.DebugFormat("[IRC] noticing {0} in {1}", clientName, regionName); | ||
337 | m_irc.PrivMsg(m_irc.Nick, "Sim", String.Format("notices {0} in {1}", clientName, regionName)); | ||
338 | } | ||
339 | } | ||
340 | catch (Exception) | ||
341 | { | ||
342 | } | ||
343 | } | ||
344 | |||
345 | public void OnMakeChildAgent(ScenePresence presence) | ||
346 | { | ||
347 | try | ||
348 | { | ||
349 | if ((m_irc.Enabled) && (m_irc.Connected) && (m_clientReporting)) | ||
350 | { | ||
351 | string regionName = presence.Scene.RegionInfo.RegionName; | ||
352 | string clientName = String.Format("{0} {1}", presence.Firstname, presence.Lastname); | ||
353 | m_log.DebugFormat("[IRC] noticing {0} in {1}", clientName, regionName); | ||
354 | m_irc.PrivMsg(m_irc.Nick, "Sim", String.Format("notices {0} left {1}", clientName, regionName)); | ||
355 | } | ||
356 | } | ||
357 | catch (Exception) | ||
358 | { | ||
359 | } | ||
360 | } | ||
361 | |||
362 | |||
363 | public void OnClientLoggedOut(IClientAPI client) | ||
364 | { | ||
365 | lock (m_syncLogout) | ||
366 | { | ||
367 | try | ||
368 | { | ||
369 | if ((m_irc.Enabled) && (m_irc.Connected) && (m_clientReporting)) | ||
370 | { | ||
371 | // handles simple case. May not work for | ||
372 | // hundred connecting in per second. and | ||
373 | // OnNewClients calle getting interleaved but | ||
374 | // filters out multiple reports | ||
375 | if (client.Name != m_last_leaving_user) | ||
376 | { | ||
377 | m_last_leaving_user = client.Name; | ||
378 | m_irc.PrivMsg(m_irc.Nick, "Sim", String.Format("notices {0} logging out", client.Name)); | ||
379 | m_log.InfoFormat("[IRC]: {0} logging out", client.Name); | ||
380 | } | ||
381 | |||
382 | if (m_last_new_user == client.Name) | ||
383 | m_last_new_user = null; | ||
384 | } | ||
385 | } | ||
386 | catch (Exception ex) | ||
387 | { | ||
388 | m_log.Error("[IRC]: ClientLoggedOut exception trap:" + ex.ToString()); | ||
389 | } | ||
390 | } | ||
391 | } | ||
392 | } | ||
393 | } | ||
diff --git a/OpenSim/Region/Environment/Modules/Avatar/Chat/IRCConnector.cs b/OpenSim/Region/Environment/Modules/Avatar/Chat/IRCConnector.cs deleted file mode 100644 index 29ba17d..0000000 --- a/OpenSim/Region/Environment/Modules/Avatar/Chat/IRCConnector.cs +++ /dev/null | |||
@@ -1,702 +0,0 @@ | |||
1 | /* | ||
2 | * Copyright (c) Contributors, http://opensimulator.org/ | ||
3 | * See CONTRIBUTORS.TXT for a full list of copyright holders. | ||
4 | * | ||
5 | * Redistribution and use in source and binary forms, with or without | ||
6 | * modification, are permitted provided that the following conditions are met: | ||
7 | * * Redistributions of source code must retain the above copyright | ||
8 | * notice, this list of conditions and the following disclaimer. | ||
9 | * * Redistributions in binary form must reproduce the above copyright | ||
10 | * notice, this list of conditions and the following disclaimer in the | ||
11 | * documentation and/or other materials provided with the distribution. | ||
12 | * * Neither the name of the OpenSim Project nor the | ||
13 | * names of its contributors may be used to endorse or promote products | ||
14 | * derived from this software without specific prior written permission. | ||
15 | * | ||
16 | * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY | ||
17 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
19 | * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY | ||
20 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | ||
23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
26 | */ | ||
27 | |||
28 | using System; | ||
29 | using System.Collections.Generic; | ||
30 | using System.IO; | ||
31 | using System.Net.Sockets; | ||
32 | using System.Reflection; | ||
33 | using System.Text.RegularExpressions; | ||
34 | using System.Threading; | ||
35 | using OpenMetaverse; | ||
36 | using log4net; | ||
37 | using Nini.Config; | ||
38 | using OpenSim.Framework; | ||
39 | using OpenSim.Region.Environment.Interfaces; | ||
40 | using OpenSim.Region.Environment.Scenes; | ||
41 | |||
42 | namespace OpenSim.Region.Environment.Modules.Avatar.Chat | ||
43 | { | ||
44 | public class IRCConnector | ||
45 | { | ||
46 | #region ErrorReplies enum | ||
47 | |||
48 | public enum ErrorReplies | ||
49 | { | ||
50 | NotRegistered = 451, // ":You have not registered" | ||
51 | NicknameInUse = 433 // "<nick> :Nickname is already in use" | ||
52 | } | ||
53 | |||
54 | #endregion | ||
55 | |||
56 | #region Replies enum | ||
57 | |||
58 | public enum Replies | ||
59 | { | ||
60 | MotdStart = 375, // ":- <server> Message of the day - " | ||
61 | Motd = 372, // ":- <text>" | ||
62 | EndOfMotd = 376 // ":End of /MOTD command" | ||
63 | } | ||
64 | |||
65 | #endregion | ||
66 | |||
67 | private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); | ||
68 | |||
69 | private Thread m_listener = null; | ||
70 | private Thread m_watchdog = null; | ||
71 | private Thread m_pinger = null; | ||
72 | |||
73 | private bool m_randomizeNick = true; | ||
74 | |||
75 | public string m_baseNick = null; | ||
76 | private string m_nick = null; | ||
77 | public string Nick | ||
78 | { | ||
79 | get { return m_baseNick; } | ||
80 | set { m_baseNick = value; } | ||
81 | } | ||
82 | |||
83 | private bool m_enabled = false; | ||
84 | public bool Enabled | ||
85 | { | ||
86 | get { return m_enabled; } | ||
87 | } | ||
88 | |||
89 | private bool m_connected = false; | ||
90 | public bool Connected | ||
91 | { | ||
92 | get { return m_connected; } | ||
93 | } | ||
94 | |||
95 | private string m_ircChannel; | ||
96 | public string IrcChannel | ||
97 | { | ||
98 | get { return m_ircChannel; } | ||
99 | |||
100 | set { m_ircChannel = value; } | ||
101 | } | ||
102 | |||
103 | private bool m_relayPrivateChannels = false; | ||
104 | public bool RelayPrivateChannels | ||
105 | { | ||
106 | get { return m_relayPrivateChannels; } | ||
107 | set { m_relayPrivateChannels = value; } | ||
108 | } | ||
109 | |||
110 | private int m_relayChannel = 0; | ||
111 | public int RelayChannel | ||
112 | { | ||
113 | get { return m_relayChannel; } | ||
114 | set { m_relayChannel = value; } | ||
115 | } | ||
116 | |||
117 | private bool m_clientReporting = true; | ||
118 | public bool ClientReporting | ||
119 | { | ||
120 | get { return m_clientReporting; } | ||
121 | set { m_clientReporting = value; } | ||
122 | } | ||
123 | |||
124 | private uint m_port = 6667; | ||
125 | public uint Port | ||
126 | { | ||
127 | get { return m_port; } | ||
128 | set { m_port = value; } | ||
129 | } | ||
130 | |||
131 | private string m_server = null; | ||
132 | public string Server | ||
133 | { | ||
134 | get { return m_server; } | ||
135 | set { m_server = value; } | ||
136 | } | ||
137 | |||
138 | private string m_privmsgformat = "PRIVMSG {0} :<{1} in {2}>: {3}"; | ||
139 | private StreamReader m_reader; | ||
140 | private List<Scene> m_scenes = new List<Scene>(); | ||
141 | |||
142 | private NetworkStream m_stream = null; | ||
143 | internal object m_syncConnect = new object(); | ||
144 | private TcpClient m_tcp; | ||
145 | private string m_user = "USER OpenSimBot 8 * :I'm an OpenSim to IRC bot"; | ||
146 | private StreamWriter m_writer; | ||
147 | |||
148 | |||
149 | public IRCConnector(IConfigSource config) | ||
150 | { | ||
151 | m_tcp = null; | ||
152 | m_writer = null; | ||
153 | m_reader = null; | ||
154 | |||
155 | // configuration in OpenSim.ini | ||
156 | // [IRC] | ||
157 | // server = chat.freenode.net | ||
158 | // nick = OSimBot_mysim | ||
159 | // nicknum = true | ||
160 | // ;nicknum set to true appends a 2 digit random number to the nick | ||
161 | // ;username = USER OpenSimBot 8 * :I'm a OpenSim to irc bot | ||
162 | // ; username is the IRC command line sent | ||
163 | // ; USER <irc_user> <visible=8,invisible=0> * : <IRC_realname> | ||
164 | // channel = #opensim-regions | ||
165 | // port = 6667 | ||
166 | // ;MSGformat fields : 0=botnick, 1=user, 2=region, 3=message | ||
167 | // ;for <bot>:<user in region> :<message> | ||
168 | // ;msgformat = "PRIVMSG {0} :<{1} in {2}>: {3}" | ||
169 | // ;for <bot>:<message> - <user of region> : | ||
170 | // ;msgformat = "PRIVMSG {0} : {3} - {1} of {2}" | ||
171 | // ;for <bot>:<message> - from <user> : | ||
172 | // ;msgformat = "PRIVMSG {0} : {3} - from {1}" | ||
173 | // Traps I/O disconnects so it does not crash the sim | ||
174 | // Trys to reconnect if disconnected and someone says something | ||
175 | // Tells IRC server "QUIT" when doing a close (just to be nice) | ||
176 | // Default port back to 6667 | ||
177 | |||
178 | try | ||
179 | { | ||
180 | m_server = config.Configs["OIRC"].GetString("server"); | ||
181 | m_baseNick = config.Configs["OIRC"].GetString("nick", "OSimBot"); | ||
182 | |||
183 | m_randomizeNick = config.Configs["OIRC"].GetBoolean("randomize_nick", m_randomizeNick); | ||
184 | m_randomizeNick = config.Configs["OIRC"].GetBoolean("nicknum", m_randomizeNick); // compat | ||
185 | m_ircChannel = config.Configs["OIRC"].GetString("channel"); | ||
186 | m_port = (uint)config.Configs["OIRC"].GetInt("port", (int)m_port); | ||
187 | m_user = config.Configs["OIRC"].GetString("username", m_user); | ||
188 | m_privmsgformat = config.Configs["OIRC"].GetString("msgformat", m_privmsgformat); | ||
189 | |||
190 | m_clientReporting = config.Configs["OIRC"].GetInt("verbosity", 2) > 0; | ||
191 | m_clientReporting = config.Configs["OIRC"].GetBoolean("report_clients", m_clientReporting); | ||
192 | |||
193 | m_relayPrivateChannels = config.Configs["OIRC"].GetBoolean("relay_private_channels", m_relayPrivateChannels); | ||
194 | m_relayPrivateChannels = config.Configs["OIRC"].GetBoolean("useworldcomm", m_relayPrivateChannels); //compat | ||
195 | m_relayChannel = config.Configs["OIRC"].GetInt("relay_private_channel_in", m_relayChannel); | ||
196 | m_relayChannel = config.Configs["OIRC"].GetInt("inchannel", m_relayChannel); | ||
197 | |||
198 | if (m_server != null && m_baseNick != null && m_ircChannel != null) | ||
199 | { | ||
200 | if (m_randomizeNick) | ||
201 | { | ||
202 | m_nick = m_baseNick + Util.RandomClass.Next(1, 99); | ||
203 | } | ||
204 | m_enabled = true; | ||
205 | } | ||
206 | } | ||
207 | catch (Exception ex) | ||
208 | { | ||
209 | m_log.Error("[IRCConnector]: Incomplete IRC configuration, skipping IRC bridge configuration"); | ||
210 | m_log.DebugFormat("[IRCConnector] Incomplete IRC configuration: {0}", ex.ToString()); | ||
211 | } | ||
212 | |||
213 | if (null == m_watchdog) | ||
214 | { | ||
215 | m_watchdog = new Thread(WatchdogRun); | ||
216 | m_watchdog.Name = "IRCWatchdog"; | ||
217 | m_watchdog.IsBackground = true; | ||
218 | } | ||
219 | } | ||
220 | |||
221 | public void Start() | ||
222 | { | ||
223 | if (!m_watchdog.IsAlive) | ||
224 | { | ||
225 | m_watchdog.Start(); | ||
226 | ThreadTracker.Add(m_watchdog); | ||
227 | } | ||
228 | } | ||
229 | |||
230 | public void AddScene(Scene scene) | ||
231 | { | ||
232 | lock (m_syncConnect) m_scenes.Add(scene); | ||
233 | } | ||
234 | |||
235 | public bool Connect() | ||
236 | { | ||
237 | lock (m_syncConnect) | ||
238 | { | ||
239 | try | ||
240 | { | ||
241 | if (m_connected) return true; | ||
242 | |||
243 | m_tcp = new TcpClient(m_server, (int)m_port); | ||
244 | m_stream = m_tcp.GetStream(); | ||
245 | m_reader = new StreamReader(m_stream); | ||
246 | m_writer = new StreamWriter(m_stream); | ||
247 | |||
248 | m_log.DebugFormat("[IRCConnector]: Connected to {0}:{1}", m_server, m_port); | ||
249 | |||
250 | m_pinger = new Thread(new ThreadStart(PingRun)); | ||
251 | m_pinger.Name = "PingSenderThread"; | ||
252 | m_pinger.IsBackground = true; | ||
253 | m_pinger.Start(); | ||
254 | ThreadTracker.Add(m_pinger); | ||
255 | |||
256 | m_listener = new Thread(new ThreadStart(ListenerRun)); | ||
257 | m_listener.Name = "IRCConnectorListenerThread"; | ||
258 | m_listener.IsBackground = true; | ||
259 | m_listener.Start(); | ||
260 | ThreadTracker.Add(m_listener); | ||
261 | |||
262 | m_writer.WriteLine(m_user); | ||
263 | m_writer.Flush(); | ||
264 | m_writer.WriteLine(String.Format("NICK {0}", m_nick)); | ||
265 | m_writer.Flush(); | ||
266 | m_writer.WriteLine(String.Format("JOIN {0}", m_ircChannel)); | ||
267 | m_writer.Flush(); | ||
268 | m_log.Info("[IRCConnector]: Connection fully established"); | ||
269 | m_connected = true; | ||
270 | } | ||
271 | catch (Exception e) | ||
272 | { | ||
273 | m_log.ErrorFormat("[IRCConnector] cannot connect to {0}:{1}: {2}", | ||
274 | m_server, m_port, e.Message); | ||
275 | } | ||
276 | m_log.Debug("[IRCConnector] Connected"); | ||
277 | return m_connected; | ||
278 | } | ||
279 | } | ||
280 | |||
281 | public void Reconnect() | ||
282 | { | ||
283 | m_connected = false; | ||
284 | try | ||
285 | { | ||
286 | m_listener.Abort(); | ||
287 | m_pinger.Abort(); | ||
288 | |||
289 | m_writer.Close(); | ||
290 | m_reader.Close(); | ||
291 | |||
292 | m_tcp.Close(); | ||
293 | } | ||
294 | catch (Exception) | ||
295 | { | ||
296 | } | ||
297 | |||
298 | if (m_enabled) | ||
299 | { | ||
300 | Connect(); | ||
301 | } | ||
302 | } | ||
303 | |||
304 | public void PrivMsg(string from, string region, string msg) | ||
305 | { | ||
306 | m_log.DebugFormat("[IRCConnector] Sending message to IRC from {0}: {1}", from, msg); | ||
307 | |||
308 | // One message to the IRC server | ||
309 | try | ||
310 | { | ||
311 | m_writer.WriteLine(m_privmsgformat, m_ircChannel, from, region, msg); | ||
312 | m_writer.Flush(); | ||
313 | m_log.InfoFormat("[IRCConnector]: PrivMsg {0} in {1}: {2}", from, region, msg); | ||
314 | } | ||
315 | catch (IOException) | ||
316 | { | ||
317 | m_log.Error("[IRCConnector]: Disconnected from IRC server.(PrivMsg)"); | ||
318 | Reconnect(); | ||
319 | } | ||
320 | catch (Exception ex) | ||
321 | { | ||
322 | m_log.ErrorFormat("[IRCConnector]: PrivMsg exception trap: {0}", ex.ToString()); | ||
323 | } | ||
324 | } | ||
325 | |||
326 | public void Send(string msg) | ||
327 | { | ||
328 | try | ||
329 | { | ||
330 | m_writer.WriteLine(msg); | ||
331 | m_writer.Flush(); | ||
332 | m_log.Info("IRC: Sent command string: " + msg); | ||
333 | } | ||
334 | catch (IOException) | ||
335 | { | ||
336 | m_log.Error("[IRCConnector]: Disconnected from IRC server.(PrivMsg)"); | ||
337 | Reconnect(); | ||
338 | } | ||
339 | catch (Exception ex) | ||
340 | { | ||
341 | m_log.ErrorFormat("[IRCConnector]: PrivMsg exception trap: {0}", ex.ToString()); | ||
342 | } | ||
343 | |||
344 | } | ||
345 | |||
346 | |||
347 | private Dictionary<string, string> ExtractMsg(string input) | ||
348 | { | ||
349 | //examines IRC commands and extracts any private messages | ||
350 | // which will then be reboadcast in the Sim | ||
351 | |||
352 | m_log.Info("[IRCConnector]: ExtractMsg: " + input); | ||
353 | Dictionary<string, string> result = null; | ||
354 | string regex = @":(?<nick>[\w-]*)!(?<user>\S*) PRIVMSG (?<channel>\S+) :(?<msg>.*)"; | ||
355 | Regex RE = new Regex(regex, RegexOptions.Multiline); | ||
356 | MatchCollection matches = RE.Matches(input); | ||
357 | |||
358 | // Get some direct matches $1 $4 is a | ||
359 | if ((matches.Count == 0) || (matches.Count != 1) || (matches[0].Groups.Count != 5)) | ||
360 | { | ||
361 | // m_log.Info("[IRCConnector]: Number of matches: " + matches.Count); | ||
362 | // if (matches.Count > 0) | ||
363 | // { | ||
364 | // m_log.Info("[IRCConnector]: Number of groups: " + matches[0].Groups.Count); | ||
365 | // } | ||
366 | return null; | ||
367 | } | ||
368 | |||
369 | result = new Dictionary<string, string>(); | ||
370 | result.Add("nick", matches[0].Groups[1].Value); | ||
371 | result.Add("user", matches[0].Groups[2].Value); | ||
372 | result.Add("channel", matches[0].Groups[3].Value); | ||
373 | result.Add("msg", matches[0].Groups[4].Value); | ||
374 | |||
375 | return result; | ||
376 | } | ||
377 | |||
378 | public void PingRun() | ||
379 | { | ||
380 | // IRC keep alive thread | ||
381 | // send PING ever 15 seconds | ||
382 | while (m_enabled) | ||
383 | { | ||
384 | try | ||
385 | { | ||
386 | if (m_connected == true) | ||
387 | { | ||
388 | m_writer.WriteLine(String.Format("PING :{0}", m_server)); | ||
389 | m_writer.Flush(); | ||
390 | Thread.Sleep(15000); | ||
391 | } | ||
392 | } | ||
393 | catch (IOException) | ||
394 | { | ||
395 | if (m_enabled) | ||
396 | { | ||
397 | m_log.Error("[IRCConnector]: Disconnected from IRC server.(PingRun)"); | ||
398 | Reconnect(); | ||
399 | } | ||
400 | } | ||
401 | catch (Exception ex) | ||
402 | { | ||
403 | m_log.ErrorFormat("[IRCConnector]: PingRun exception trap: {0}\n{1}", ex.ToString(), ex.StackTrace); | ||
404 | } | ||
405 | } | ||
406 | } | ||
407 | |||
408 | static private Vector3 CenterOfRegion = new Vector3(128, 128, 20); | ||
409 | public void ListenerRun() | ||
410 | { | ||
411 | string inputLine; | ||
412 | |||
413 | while (m_enabled) | ||
414 | { | ||
415 | try | ||
416 | { | ||
417 | while ((m_connected) && ((inputLine = m_reader.ReadLine()) != null)) | ||
418 | { | ||
419 | // m_log.Info("[IRCConnector]: " + inputLine); | ||
420 | |||
421 | if (inputLine.Contains(m_ircChannel)) | ||
422 | { | ||
423 | Dictionary<string, string> data = ExtractMsg(inputLine); | ||
424 | // Any chat ??? | ||
425 | if (data != null) | ||
426 | { | ||
427 | OSChatMessage c = new OSChatMessage(); | ||
428 | c.Message = data["msg"]; | ||
429 | c.Type = ChatTypeEnum.Region; | ||
430 | c.Position = CenterOfRegion; | ||
431 | c.Channel = m_relayPrivateChannels ? m_relayChannel : 0; | ||
432 | c.From = data["nick"]; | ||
433 | c.Sender = null; | ||
434 | c.SenderUUID = UUID.Zero; | ||
435 | |||
436 | // is message "\001ACTION foo | ||
437 | // bar\001"? -> "/me foo bar" | ||
438 | if ((1 == c.Message[0]) && c.Message.Substring(1).StartsWith("ACTION")) | ||
439 | c.Message = String.Format("/me {0}", c.Message.Substring(8, c.Message.Length - 9)); | ||
440 | |||
441 | m_log.DebugFormat("[IRCConnector] ListenerRun from: {0}, {1}", c.From, c.Message); | ||
442 | |||
443 | foreach (Scene scene in m_scenes) | ||
444 | { | ||
445 | c.Scene = scene; | ||
446 | scene.EventManager.TriggerOnChatBroadcast(this, c); | ||
447 | } | ||
448 | } | ||
449 | |||
450 | Thread.Sleep(150); | ||
451 | continue; | ||
452 | } | ||
453 | |||
454 | ProcessIRCCommand(inputLine); | ||
455 | Thread.Sleep(150); | ||
456 | } | ||
457 | } | ||
458 | catch (IOException) | ||
459 | { | ||
460 | if (m_enabled) | ||
461 | { | ||
462 | m_log.Error("[IRCConnector]: ListenerRun IOException. Disconnected from IRC server ??? (ListenerRun)"); | ||
463 | Reconnect(); | ||
464 | } | ||
465 | } | ||
466 | catch (Exception ex) | ||
467 | { | ||
468 | m_log.ErrorFormat("[IRCConnector]: ListenerRun exception trap: {0}\n{1}", ex.ToString(), ex.StackTrace); | ||
469 | } | ||
470 | } | ||
471 | } | ||
472 | |||
473 | public void BroadcastSim(string sender, string format, params string[] args) | ||
474 | { | ||
475 | try | ||
476 | { | ||
477 | OSChatMessage c = new OSChatMessage(); | ||
478 | c.From = sender; | ||
479 | c.Message = String.Format(format, args); | ||
480 | c.Type = ChatTypeEnum.Region; // ChatTypeEnum.Say; | ||
481 | c.Channel = m_relayPrivateChannels ? m_relayChannel : 0; | ||
482 | c.Position = CenterOfRegion; | ||
483 | c.Sender = null; | ||
484 | c.SenderUUID = UUID.Zero; | ||
485 | |||
486 | m_log.DebugFormat("[IRCConnector] BroadcastSim from {0}: {1}", c.From, c.Message); | ||
487 | |||
488 | foreach (Scene scene in m_scenes) | ||
489 | { | ||
490 | c.Scene = scene; | ||
491 | scene.EventManager.TriggerOnChatBroadcast(this, c); | ||
492 | // // m_scene.EventManager.TriggerOnChatFromWorld(this, c); | ||
493 | // IWorldComm wComm = m_scene.RequestModuleInterface<IWorldComm>(); | ||
494 | // wComm.DeliverMessage(ChatTypeEnum.Region, m_messageInChannel, sender, UUID.Zero, c.Message); | ||
495 | // //IWorldComm wComm = m_ScriptEngine.World.RequestModuleInterface<IWorldComm>(); | ||
496 | // //wComm.DeliverMessage(ChatTypeEnum.Region, channelID, m_host.Name, m_host.UUID, text); | ||
497 | |||
498 | } | ||
499 | } | ||
500 | catch (Exception ex) // IRC gate should not crash Sim | ||
501 | { | ||
502 | m_log.ErrorFormat("[IRCConnector]: BroadcastSim Exception Trap: {0}\n{1}", ex.ToString(), ex.StackTrace); | ||
503 | } | ||
504 | } | ||
505 | |||
506 | public void ProcessIRCCommand(string command) | ||
507 | { | ||
508 | // m_log.Debug("[IRCConnector]: ProcessIRCCommand:" + command); | ||
509 | |||
510 | string[] commArgs = new string[command.Split(' ').Length]; | ||
511 | string c_server = m_server; | ||
512 | |||
513 | commArgs = command.Split(' '); | ||
514 | if (commArgs[0].Substring(0, 1) == ":") | ||
515 | { | ||
516 | commArgs[0] = commArgs[0].Remove(0, 1); | ||
517 | } | ||
518 | |||
519 | if (commArgs[1] == "002") | ||
520 | { | ||
521 | // fetch the correct servername | ||
522 | // ex: irc.freenode.net -> brown.freenode.net/kornbluth.freenode.net/... | ||
523 | // irc.bluewin.ch -> irc1.bluewin.ch/irc2.bluewin.ch | ||
524 | |||
525 | c_server = (commArgs[6].Split('['))[0]; | ||
526 | m_server = c_server; | ||
527 | } | ||
528 | |||
529 | if (commArgs[0] == "ERROR") | ||
530 | { | ||
531 | m_log.ErrorFormat("[IRCConnector]: IRC SERVER ERROR: {0}", command); | ||
532 | } | ||
533 | |||
534 | if (commArgs[0] == "PING") | ||
535 | { | ||
536 | string p_reply = ""; | ||
537 | |||
538 | for (int i = 1; i < commArgs.Length; i++) | ||
539 | { | ||
540 | p_reply += commArgs[i] + " "; | ||
541 | } | ||
542 | |||
543 | m_writer.WriteLine(String.Format("PONG {0}", p_reply)); | ||
544 | m_writer.Flush(); | ||
545 | } | ||
546 | else if (commArgs[0] == c_server) | ||
547 | { | ||
548 | // server message | ||
549 | try | ||
550 | { | ||
551 | Int32 commandCode = Int32.Parse(commArgs[1]); | ||
552 | switch (commandCode) | ||
553 | { | ||
554 | case (int)ErrorReplies.NicknameInUse: | ||
555 | // Gen a new name | ||
556 | m_nick = m_baseNick + Util.RandomClass.Next(1, 99); | ||
557 | m_log.ErrorFormat("[IRCConnector]: IRC SERVER reports NicknameInUse, trying {0}", m_nick); | ||
558 | // Retry | ||
559 | m_writer.WriteLine(String.Format("NICK {0}", m_nick)); | ||
560 | m_writer.Flush(); | ||
561 | m_writer.WriteLine(String.Format("JOIN {0}", m_ircChannel)); | ||
562 | m_writer.Flush(); | ||
563 | break; | ||
564 | case (int)ErrorReplies.NotRegistered: | ||
565 | break; | ||
566 | case (int)Replies.EndOfMotd: | ||
567 | break; | ||
568 | } | ||
569 | } | ||
570 | catch (Exception) | ||
571 | { | ||
572 | } | ||
573 | } | ||
574 | else | ||
575 | { | ||
576 | // Normal message | ||
577 | string commAct = commArgs[1]; | ||
578 | switch (commAct) | ||
579 | { | ||
580 | case "JOIN": | ||
581 | eventIrcJoin(commArgs); | ||
582 | break; | ||
583 | case "PART": | ||
584 | eventIrcPart(commArgs); | ||
585 | break; | ||
586 | case "MODE": | ||
587 | eventIrcMode(commArgs); | ||
588 | break; | ||
589 | case "NICK": | ||
590 | eventIrcNickChange(commArgs); | ||
591 | break; | ||
592 | case "KICK": | ||
593 | eventIrcKick(commArgs); | ||
594 | break; | ||
595 | case "QUIT": | ||
596 | eventIrcQuit(commArgs); | ||
597 | break; | ||
598 | case "PONG": | ||
599 | break; // that's nice | ||
600 | } | ||
601 | } | ||
602 | } | ||
603 | |||
604 | public void eventIrcJoin(string[] commArgs) | ||
605 | { | ||
606 | string IrcChannel = commArgs[2]; | ||
607 | if (IrcChannel.StartsWith(":")) | ||
608 | IrcChannel = IrcChannel.Substring(1); | ||
609 | string IrcUser = commArgs[0].Split('!')[0]; | ||
610 | if (m_clientReporting) | ||
611 | BroadcastSim(IrcUser, "/me joins {0}", IrcChannel); | ||
612 | } | ||
613 | |||
614 | public void eventIrcPart(string[] commArgs) | ||
615 | { | ||
616 | string IrcChannel = commArgs[2]; | ||
617 | string IrcUser = commArgs[0].Split('!')[0]; | ||
618 | if (m_clientReporting) | ||
619 | BroadcastSim(IrcUser, "/me parts {0}", IrcChannel); | ||
620 | } | ||
621 | |||
622 | public void eventIrcMode(string[] commArgs) | ||
623 | { | ||
624 | string UserMode = ""; | ||
625 | for (int i = 3; i < commArgs.Length; i++) | ||
626 | { | ||
627 | UserMode += commArgs[i] + " "; | ||
628 | } | ||
629 | |||
630 | if (UserMode.Substring(0, 1) == ":") | ||
631 | { | ||
632 | UserMode = UserMode.Remove(0, 1); | ||
633 | } | ||
634 | } | ||
635 | |||
636 | public void eventIrcNickChange(string[] commArgs) | ||
637 | { | ||
638 | string UserOldNick = commArgs[0].Split('!')[0]; | ||
639 | string UserNewNick = commArgs[2].Remove(0, 1); | ||
640 | if (m_clientReporting) | ||
641 | BroadcastSim(UserOldNick, "/me is now known as {0}", UserNewNick); | ||
642 | } | ||
643 | |||
644 | public void eventIrcKick(string[] commArgs) | ||
645 | { | ||
646 | string UserKicker = commArgs[0].Split('!')[0]; | ||
647 | string UserKicked = commArgs[3]; | ||
648 | string IrcChannel = commArgs[2]; | ||
649 | string KickMessage = ""; | ||
650 | for (int i = 4; i < commArgs.Length; i++) | ||
651 | { | ||
652 | KickMessage += commArgs[i] + " "; | ||
653 | } | ||
654 | if (m_clientReporting) | ||
655 | BroadcastSim(UserKicker, "/me kicks kicks {0} off {1} saying \"{2}\"", UserKicked, IrcChannel, KickMessage); | ||
656 | if (UserKicked == m_nick) | ||
657 | { | ||
658 | BroadcastSim(m_nick, "Hey, that was me!!!"); | ||
659 | } | ||
660 | } | ||
661 | |||
662 | public void eventIrcQuit(string[] commArgs) | ||
663 | { | ||
664 | string IrcUser = commArgs[0].Split('!')[0]; | ||
665 | string QuitMessage = ""; | ||
666 | |||
667 | for (int i = 2; i < commArgs.Length; i++) | ||
668 | { | ||
669 | QuitMessage += commArgs[i] + " "; | ||
670 | } | ||
671 | if (m_clientReporting) | ||
672 | BroadcastSim(IrcUser, "/me quits saying \"{0}\"", QuitMessage); | ||
673 | } | ||
674 | |||
675 | public void Close() | ||
676 | { | ||
677 | m_writer.WriteLine(String.Format("QUIT :{0} to {1} wormhole to {2} closing", | ||
678 | m_nick, m_ircChannel, m_server)); | ||
679 | m_writer.Flush(); | ||
680 | |||
681 | m_connected = false; | ||
682 | m_enabled = false; | ||
683 | |||
684 | //listener.Abort(); | ||
685 | //pingSender.Abort(); | ||
686 | |||
687 | m_writer.Close(); | ||
688 | m_reader.Close(); | ||
689 | m_stream.Close(); | ||
690 | m_tcp.Close(); | ||
691 | } | ||
692 | |||
693 | protected void WatchdogRun() | ||
694 | { | ||
695 | while (m_enabled) | ||
696 | { | ||
697 | if (!m_connected) Connect(); | ||
698 | Thread.Sleep(15000); | ||
699 | } | ||
700 | } | ||
701 | } | ||
702 | } | ||