diff options
author | Sean McNamara | 2011-04-23 18:33:08 -0400 |
---|---|---|
committer | Sean McNamara | 2011-04-23 18:33:08 -0400 |
commit | d287e0ac683acb0cab51bf0dbbc9d1fd08bd6bc9 (patch) | |
tree | 30a24bf66050c66ebe74b68745c4f1764fe09181 /OpenSim/Region/OptionalModules/Avatar/Voice/FreeSwitchVoice/FreeSwitchVoiceModule.cs | |
parent | AutoBackup: Removed unneeded imports. (diff) | |
parent | Added MaxAgents configuration option to RegionConfig.ini allowing region host... (diff) | |
download | opensim-SC_OLD-d287e0ac683acb0cab51bf0dbbc9d1fd08bd6bc9.zip opensim-SC_OLD-d287e0ac683acb0cab51bf0dbbc9d1fd08bd6bc9.tar.gz opensim-SC_OLD-d287e0ac683acb0cab51bf0dbbc9d1fd08bd6bc9.tar.bz2 opensim-SC_OLD-d287e0ac683acb0cab51bf0dbbc9d1fd08bd6bc9.tar.xz |
Merge git://opensimulator.org/git/opensim
Diffstat (limited to 'OpenSim/Region/OptionalModules/Avatar/Voice/FreeSwitchVoice/FreeSwitchVoiceModule.cs')
-rw-r--r-- | OpenSim/Region/OptionalModules/Avatar/Voice/FreeSwitchVoice/FreeSwitchVoiceModule.cs | 126 |
1 files changed, 95 insertions, 31 deletions
diff --git a/OpenSim/Region/OptionalModules/Avatar/Voice/FreeSwitchVoice/FreeSwitchVoiceModule.cs b/OpenSim/Region/OptionalModules/Avatar/Voice/FreeSwitchVoice/FreeSwitchVoiceModule.cs index 7909d8a..42efd67 100644 --- a/OpenSim/Region/OptionalModules/Avatar/Voice/FreeSwitchVoice/FreeSwitchVoiceModule.cs +++ b/OpenSim/Region/OptionalModules/Avatar/Voice/FreeSwitchVoice/FreeSwitchVoiceModule.cs | |||
@@ -118,7 +118,7 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
118 | 118 | ||
119 | if (serviceDll == String.Empty) | 119 | if (serviceDll == String.Empty) |
120 | { | 120 | { |
121 | m_log.Error("[FreeSwitchVoice]: No LocalServiceModule named in section FreeSwitchVoice"); | 121 | m_log.Error("[FreeSwitchVoice]: No LocalServiceModule named in section FreeSwitchVoice. Not starting."); |
122 | return; | 122 | return; |
123 | } | 123 | } |
124 | 124 | ||
@@ -143,8 +143,7 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
143 | if (String.IsNullOrEmpty(m_freeSwitchRealm) || | 143 | if (String.IsNullOrEmpty(m_freeSwitchRealm) || |
144 | String.IsNullOrEmpty(m_freeSwitchAPIPrefix)) | 144 | String.IsNullOrEmpty(m_freeSwitchAPIPrefix)) |
145 | { | 145 | { |
146 | m_log.Error("[FreeSwitchVoice] plugin mis-configured"); | 146 | m_log.Error("[FreeSwitchVoice]: Freeswitch service mis-configured. Not starting."); |
147 | m_log.Info("[FreeSwitchVoice] plugin disabled: incomplete configuration"); | ||
148 | return; | 147 | return; |
149 | } | 148 | } |
150 | 149 | ||
@@ -164,24 +163,24 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
164 | // String.Format("{0}/viv_get_prelogin.php", m_freeSwitchAPIPrefix), FreeSwitchSLVoiceGetPreloginHTTPHandler); | 163 | // String.Format("{0}/viv_get_prelogin.php", m_freeSwitchAPIPrefix), FreeSwitchSLVoiceGetPreloginHTTPHandler); |
165 | // MainServer.Instance.AddStreamHandler(h); | 164 | // MainServer.Instance.AddStreamHandler(h); |
166 | 165 | ||
167 | |||
168 | |||
169 | MainServer.Instance.AddHTTPHandler(String.Format("{0}/viv_signin.php", m_freeSwitchAPIPrefix), | 166 | MainServer.Instance.AddHTTPHandler(String.Format("{0}/viv_signin.php", m_freeSwitchAPIPrefix), |
170 | FreeSwitchSLVoiceSigninHTTPHandler); | 167 | FreeSwitchSLVoiceSigninHTTPHandler); |
171 | 168 | ||
172 | MainServer.Instance.AddHTTPHandler(String.Format("{0}/viv_buddy.php", m_freeSwitchAPIPrefix), | 169 | MainServer.Instance.AddHTTPHandler(String.Format("{0}/viv_buddy.php", m_freeSwitchAPIPrefix), |
173 | FreeSwitchSLVoiceBuddyHTTPHandler); | 170 | FreeSwitchSLVoiceBuddyHTTPHandler); |
171 | |||
172 | MainServer.Instance.AddHTTPHandler(String.Format("{0}/viv_watcher.php", m_freeSwitchAPIPrefix), | ||
173 | FreeSwitchSLVoiceWatcherHTTPHandler); | ||
174 | 174 | ||
175 | m_log.InfoFormat("[FreeSwitchVoice] using FreeSwitch server {0}", m_freeSwitchRealm); | 175 | m_log.InfoFormat("[FreeSwitchVoice]: using FreeSwitch server {0}", m_freeSwitchRealm); |
176 | 176 | ||
177 | m_Enabled = true; | 177 | m_Enabled = true; |
178 | 178 | ||
179 | m_log.Info("[FreeSwitchVoice] plugin enabled"); | 179 | m_log.Info("[FreeSwitchVoice]: plugin enabled"); |
180 | } | 180 | } |
181 | catch (Exception e) | 181 | catch (Exception e) |
182 | { | 182 | { |
183 | m_log.ErrorFormat("[FreeSwitchVoice] plugin initialization failed: {0}", e.Message); | 183 | m_log.ErrorFormat("[FreeSwitchVoice]: plugin initialization failed: {0} {1}", e.Message, e.StackTrace); |
184 | m_log.DebugFormat("[FreeSwitchVoice] plugin initialization failed: {0}", e.ToString()); | ||
185 | return; | 184 | return; |
186 | } | 185 | } |
187 | 186 | ||
@@ -240,7 +239,7 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
240 | { | 239 | { |
241 | if (m_Enabled) | 240 | if (m_Enabled) |
242 | { | 241 | { |
243 | m_log.Info("[FreeSwitchVoice] registering IVoiceModule with the scene"); | 242 | m_log.Info("[FreeSwitchVoice]: registering IVoiceModule with the scene"); |
244 | 243 | ||
245 | // register the voice interface for this module, so the script engine can call us | 244 | // register the voice interface for this module, so the script engine can call us |
246 | scene.RegisterModuleInterface<IVoiceModule>(this); | 245 | scene.RegisterModuleInterface<IVoiceModule>(this); |
@@ -302,7 +301,9 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
302 | // </summary> | 301 | // </summary> |
303 | public void OnRegisterCaps(Scene scene, UUID agentID, Caps caps) | 302 | public void OnRegisterCaps(Scene scene, UUID agentID, Caps caps) |
304 | { | 303 | { |
305 | m_log.DebugFormat("[FreeSwitchVoice] OnRegisterCaps: agentID {0} caps {1}", agentID, caps); | 304 | m_log.DebugFormat( |
305 | "[FreeSwitchVoice]: OnRegisterCaps() called with agentID {0} caps {1} in scene {2}", | ||
306 | agentID, caps, scene.RegionInfo.RegionName); | ||
306 | 307 | ||
307 | string capsBase = "/CAPS/" + caps.CapsObjectPath; | 308 | string capsBase = "/CAPS/" + caps.CapsObjectPath; |
308 | caps.RegisterHandler("ProvisionVoiceAccountRequest", | 309 | caps.RegisterHandler("ProvisionVoiceAccountRequest", |
@@ -344,6 +345,9 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
344 | public string ProvisionVoiceAccountRequest(Scene scene, string request, string path, string param, | 345 | public string ProvisionVoiceAccountRequest(Scene scene, string request, string path, string param, |
345 | UUID agentID, Caps caps) | 346 | UUID agentID, Caps caps) |
346 | { | 347 | { |
348 | m_log.DebugFormat( | ||
349 | "[FreeSwitchVoice][PROVISIONVOICE]: ProvisionVoiceAccountRequest() request: {0}, path: {1}, param: {2}", request, path, param); | ||
350 | |||
347 | ScenePresence avatar = scene.GetScenePresence(agentID); | 351 | ScenePresence avatar = scene.GetScenePresence(agentID); |
348 | if (avatar == null) | 352 | if (avatar == null) |
349 | { | 353 | { |
@@ -357,9 +361,6 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
357 | 361 | ||
358 | try | 362 | try |
359 | { | 363 | { |
360 | //m_log.DebugFormat("[FreeSwitchVoice][PROVISIONVOICE]: request: {0}, path: {1}, param: {2}", | ||
361 | // request, path, param); | ||
362 | |||
363 | //XmlElement resp; | 364 | //XmlElement resp; |
364 | string agentname = "x" + Convert.ToBase64String(agentID.GetBytes()); | 365 | string agentname = "x" + Convert.ToBase64String(agentID.GetBytes()); |
365 | string password = "1234";//temp hack//new UUID(Guid.NewGuid()).ToString().Replace('-','Z').Substring(0,16); | 366 | string password = "1234";//temp hack//new UUID(Guid.NewGuid()).ToString().Replace('-','Z').Substring(0,16); |
@@ -390,7 +391,7 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
390 | 391 | ||
391 | string r = LLSDHelpers.SerialiseLLSDReply(voiceAccountResponse); | 392 | string r = LLSDHelpers.SerialiseLLSDReply(voiceAccountResponse); |
392 | 393 | ||
393 | m_log.DebugFormat("[FreeSwitchVoice][PROVISIONVOICE]: avatar \"{0}\": {1}", avatarName, r); | 394 | // m_log.DebugFormat("[FreeSwitchVoice][PROVISIONVOICE]: avatar \"{0}\": {1}", avatarName, r); |
394 | 395 | ||
395 | return r; | 396 | return r; |
396 | } | 397 | } |
@@ -416,6 +417,10 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
416 | public string ParcelVoiceInfoRequest(Scene scene, string request, string path, string param, | 417 | public string ParcelVoiceInfoRequest(Scene scene, string request, string path, string param, |
417 | UUID agentID, Caps caps) | 418 | UUID agentID, Caps caps) |
418 | { | 419 | { |
420 | // m_log.DebugFormat( | ||
421 | // "[FreeSwitchVoice][PARCELVOICE]: ParcelVoiceInfoRequest() on {0} for {1}", | ||
422 | // scene.RegionInfo.RegionName, agentID); | ||
423 | |||
419 | ScenePresence avatar = scene.GetScenePresence(agentID); | 424 | ScenePresence avatar = scene.GetScenePresence(agentID); |
420 | string avatarName = avatar.Name; | 425 | string avatarName = avatar.Name; |
421 | 426 | ||
@@ -453,8 +458,8 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
453 | 458 | ||
454 | if ((land.Flags & (uint)ParcelFlags.AllowVoiceChat) == 0) | 459 | if ((land.Flags & (uint)ParcelFlags.AllowVoiceChat) == 0) |
455 | { | 460 | { |
456 | m_log.DebugFormat("[FreeSwitchVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": voice not enabled for parcel", | 461 | // m_log.DebugFormat("[FreeSwitchVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": voice not enabled for parcel", |
457 | scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName); | 462 | // scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName); |
458 | channelUri = String.Empty; | 463 | channelUri = String.Empty; |
459 | } | 464 | } |
460 | else | 465 | else |
@@ -469,8 +474,8 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
469 | parcelVoiceInfo = new LLSDParcelVoiceInfoResponse(scene.RegionInfo.RegionName, land.LocalID, creds); | 474 | parcelVoiceInfo = new LLSDParcelVoiceInfoResponse(scene.RegionInfo.RegionName, land.LocalID, creds); |
470 | string r = LLSDHelpers.SerialiseLLSDReply(parcelVoiceInfo); | 475 | string r = LLSDHelpers.SerialiseLLSDReply(parcelVoiceInfo); |
471 | 476 | ||
472 | m_log.DebugFormat("[FreeSwitchVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": {4}", | 477 | // m_log.DebugFormat("[FreeSwitchVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": {4}", |
473 | scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName, r); | 478 | // scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName, r); |
474 | return r; | 479 | return r; |
475 | } | 480 | } |
476 | catch (Exception e) | 481 | catch (Exception e) |
@@ -502,6 +507,7 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
502 | 507 | ||
503 | m_log.DebugFormat("[FreeSwitchVoice][CHATSESSION]: avatar \"{0}\": request: {1}, path: {2}, param: {3}", | 508 | m_log.DebugFormat("[FreeSwitchVoice][CHATSESSION]: avatar \"{0}\": request: {1}, path: {2}, param: {3}", |
504 | avatarName, request, path, param); | 509 | avatarName, request, path, param); |
510 | |||
505 | return "<llsd>true</llsd>"; | 511 | return "<llsd>true</llsd>"; |
506 | } | 512 | } |
507 | 513 | ||
@@ -555,10 +561,9 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
555 | return response; | 561 | return response; |
556 | } | 562 | } |
557 | 563 | ||
558 | |||
559 | public Hashtable FreeSwitchSLVoiceGetPreloginHTTPHandler(Hashtable request) | 564 | public Hashtable FreeSwitchSLVoiceGetPreloginHTTPHandler(Hashtable request) |
560 | { | 565 | { |
561 | m_log.Debug("[FreeSwitchVoice] FreeSwitchSLVoiceGetPreloginHTTPHandler called"); | 566 | m_log.Debug("[FreeSwitchVoice]: FreeSwitchSLVoiceGetPreloginHTTPHandler called"); |
562 | 567 | ||
563 | Hashtable response = new Hashtable(); | 568 | Hashtable response = new Hashtable(); |
564 | response["content_type"] = "text/xml"; | 569 | response["content_type"] = "text/xml"; |
@@ -592,6 +597,8 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
592 | 597 | ||
593 | public Hashtable FreeSwitchSLVoiceBuddyHTTPHandler(Hashtable request) | 598 | public Hashtable FreeSwitchSLVoiceBuddyHTTPHandler(Hashtable request) |
594 | { | 599 | { |
600 | m_log.Debug("[FreeSwitchVoice]: FreeSwitchSLVoiceBuddyHTTPHandler called"); | ||
601 | |||
595 | Hashtable response = new Hashtable(); | 602 | Hashtable response = new Hashtable(); |
596 | response["int_response_code"] = 200; | 603 | response["int_response_code"] = 200; |
597 | response["str_response_string"] = string.Empty; | 604 | response["str_response_string"] = string.Empty; |
@@ -650,21 +657,64 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
650 | <bdy_status>A</bdy_status> | 657 | <bdy_status>A</bdy_status> |
651 | <modified_ts>{3}</modified_ts> | 658 | <modified_ts>{3}</modified_ts> |
652 | <b2g_group_id></b2g_group_id> | 659 | <b2g_group_id></b2g_group_id> |
653 | </level3>", ids[i],i,m_freeSwitchRealm,dt)); | 660 | </level3>", ids[i], i ,m_freeSwitchRealm, dt)); |
654 | } | 661 | } |
655 | 662 | ||
656 | resp.Append("</buddies><groups></groups></body></level0></response>"); | 663 | resp.Append("</buddies><groups></groups></body></level0></response>"); |
657 | 664 | ||
658 | response["str_response_string"] = resp.ToString(); | 665 | response["str_response_string"] = resp.ToString(); |
659 | // Regex normalizeEndLines = new Regex(@"\r\n", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.Multiline); | 666 | // Regex normalizeEndLines = new Regex(@"(\r\n|\n)", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.Multiline); |
660 | 667 | // | |
661 | //m_log.DebugFormat("[FREESWITCH]: {0}", normalizeEndLines.Replace((string)response["str_response_string"],"")); | 668 | // m_log.DebugFormat( |
669 | // "[FREESWITCH]: FreeSwitchSLVoiceBuddyHTTPHandler() response {0}", | ||
670 | // normalizeEndLines.Replace((string)response["str_response_string"],"")); | ||
671 | |||
662 | return response; | 672 | return response; |
663 | } | 673 | } |
664 | 674 | ||
675 | public Hashtable FreeSwitchSLVoiceWatcherHTTPHandler(Hashtable request) | ||
676 | { | ||
677 | m_log.Debug("[FreeSwitchVoice]: FreeSwitchSLVoiceWatcherHTTPHandler called"); | ||
678 | |||
679 | Hashtable response = new Hashtable(); | ||
680 | response["int_response_code"] = 200; | ||
681 | response["content-type"] = "text/xml"; | ||
682 | |||
683 | Hashtable requestBody = ParseRequestBody((string)request["body"]); | ||
684 | |||
685 | string auth_token = (string)requestBody["auth_token"]; | ||
686 | //string[] auth_tokenvals = auth_token.Split(':'); | ||
687 | //string username = auth_tokenvals[0]; | ||
688 | |||
689 | StringBuilder resp = new StringBuilder(); | ||
690 | resp.Append("<?xml version=\"1.0\" encoding=\"iso-8859-1\" ?><response xmlns=\"http://www.vivox.com\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation= \"/xsd/buddy_list.xsd\">"); | ||
691 | |||
692 | // FIXME: This is enough of a response to stop viewer 2 complaining about a login failure and get voice to work. If we don't | ||
693 | // give an OK response, then viewer 2 engages in an continuous viv_signin.php, viv_buddy.php, viv_watcher.php loop | ||
694 | // Viewer 1 appeared happy to ignore the lack of reply and still works with this reply. | ||
695 | // | ||
696 | // However, really we need to fill in whatever watcher data should be here (whatever that is). | ||
697 | resp.Append(string.Format(@"<level0> | ||
698 | <status>OK</status> | ||
699 | <cookie_name>lib_session</cookie_name> | ||
700 | <cookie>{0}</cookie> | ||
701 | <auth_token>{0}</auth_token> | ||
702 | <body/></level0></response>", auth_token)); | ||
703 | |||
704 | response["str_response_string"] = resp.ToString(); | ||
705 | |||
706 | // Regex normalizeEndLines = new Regex(@"(\r\n|\n)", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.Multiline); | ||
707 | // | ||
708 | // m_log.DebugFormat( | ||
709 | // "[FREESWITCH]: FreeSwitchSLVoiceWatcherHTTPHandler() response {0}", | ||
710 | // normalizeEndLines.Replace((string)response["str_response_string"],"")); | ||
711 | |||
712 | return response; | ||
713 | } | ||
714 | |||
665 | public Hashtable FreeSwitchSLVoiceSigninHTTPHandler(Hashtable request) | 715 | public Hashtable FreeSwitchSLVoiceSigninHTTPHandler(Hashtable request) |
666 | { | 716 | { |
667 | m_log.Debug("[FreeSwitchVoice] FreeSwitchSLVoiceSigninHTTPHandler called"); | 717 | m_log.Debug("[FreeSwitchVoice]: FreeSwitchSLVoiceSigninHTTPHandler called"); |
668 | // string requestbody = (string)request["body"]; | 718 | // string requestbody = (string)request["body"]; |
669 | // string uri = (string)request["uri"]; | 719 | // string uri = (string)request["uri"]; |
670 | // string contenttype = (string)request["content-type"]; | 720 | // string contenttype = (string)request["content-type"]; |
@@ -709,7 +759,10 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
709 | </level0> | 759 | </level0> |
710 | </response>", userid, pos, avatarName); | 760 | </response>", userid, pos, avatarName); |
711 | 761 | ||
712 | response["int_response_code"] = 200; | 762 | response["int_response_code"] = 200; |
763 | |||
764 | // m_log.DebugFormat("[FreeSwitchVoice]: Sending FreeSwitchSLVoiceSigninHTTPHandler response"); | ||
765 | |||
713 | return response; | 766 | return response; |
714 | } | 767 | } |
715 | 768 | ||
@@ -795,16 +848,27 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
795 | response["keepalive"] = false; | 848 | response["keepalive"] = false; |
796 | response["int_response_code"] = 500; | 849 | response["int_response_code"] = 500; |
797 | 850 | ||
798 | Hashtable requestBody = ParseRequestBody((string) request["body"]); | 851 | Hashtable requestBody = ParseRequestBody((string)request["body"]); |
799 | 852 | ||
800 | string section = (string) requestBody["section"]; | 853 | string section = (string) requestBody["section"]; |
801 | 854 | ||
802 | if (section == "directory") | 855 | if (section == "directory") |
856 | { | ||
857 | string eventCallingFunction = (string)requestBody["Event-Calling-Function"]; | ||
858 | m_log.DebugFormat( | ||
859 | "[FreeSwitchVoice]: Received request for config section directory, event calling function '{0}'", | ||
860 | eventCallingFunction); | ||
861 | |||
803 | response = m_FreeswitchService.HandleDirectoryRequest(requestBody); | 862 | response = m_FreeswitchService.HandleDirectoryRequest(requestBody); |
863 | } | ||
804 | else if (section == "dialplan") | 864 | else if (section == "dialplan") |
865 | { | ||
866 | m_log.DebugFormat("[FreeSwitchVoice]: Received request for config section dialplan"); | ||
867 | |||
805 | response = m_FreeswitchService.HandleDialplanRequest(requestBody); | 868 | response = m_FreeswitchService.HandleDialplanRequest(requestBody); |
869 | } | ||
806 | else | 870 | else |
807 | m_log.WarnFormat("[FreeSwitchVoice]: section was {0}", section); | 871 | m_log.WarnFormat("[FreeSwitchVoice]: Unknown section {0} was requested from config.", section); |
808 | 872 | ||
809 | return response; | 873 | return response; |
810 | } | 874 | } |
@@ -821,4 +885,4 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice | |||
821 | 885 | ||
822 | #endregion | 886 | #endregion |
823 | } | 887 | } |
824 | } | 888 | } \ No newline at end of file |