public async Task <HttpResponseMessage> Get(string code, string state, CancellationToken cancellationToken) { // 從 state 參數轉換為原本的 ConversationReference ConversationReference conversationReference = UrlToken.Decode <ConversationReference>(state); // 請求拿到 Google OAuth 的 Access Token var accessToken = await GoogleOAuthHelper.ExchangeCodeForGoogleAccessToken(code, BotUtility.OAuthCallbackURL); var msg = conversationReference.GetPostToBotMessage(); // 取得目前談話對象的容器,並且把 UserData 加入 Access Token using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, msg)) { IStateClient sc = scope.Resolve <IStateClient>(); BotData userData = sc.BotState.GetUserData(msg.ChannelId, msg.From.Id); userData.SetProperty(BotUtility.AccessToken, accessToken.AccessToken); sc.BotState.SetUserData(msg.ChannelId, msg.From.Id, userData); } // 設定 ResumeAsync 回到 MessagesController 的識別值 (例如: 使用 token 關鍵字, 真實案例不適合這樣用) msg.Text = "token:" + accessToken.AccessToken; // 要記得使用 RsumeAsync 才能夠接回原本的 converstaion await Conversation.ResumeAsync(conversationReference, msg); return(Request.CreateResponse("ok")); }
public static async Task <HttpResponseMessage> Resolve(HttpRequestMessage request, int maxWriteAttempts) { NameValueCollection parameters = null; if (request.Method == HttpMethod.Get) { parameters = request.RequestUri.ParseQueryString(); } else if (request.Method == HttpMethod.Post) { parameters = await request.Content.ReadAsFormDataAsync(); } // Create the message that is send to conversation to resume the login flow string state = parameters["state"]; var resumptionCookie = UrlToken.Decode <ResumptionCookie>(state); var message = resumptionCookie.GetMessage(); string dialogId; using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message)) { IStateClient sc = scope.Resolve <IStateClient>(); BotData userData = sc.BotState.GetUserData(message.ChannelId, message.From.Id); dialogId = userData.GetProperty <string>(AuthenticationConstants.AuthHandlerKey); } AuthCallbackHandler handler; switch (dialogId) { case AuthenticationConstants.AuthDialogId_AzureAD: handler = new mStack.API.Bots.AzureAD.AuthCallbackHandler(maxWriteAttempts); break; case AuthenticationConstants.AuthDialogId_ExactOnline: handler = new mStack.API.Bots.ExactOnline.AuthCallbackHandler(maxWriteAttempts); break; default: throw new ArgumentException("Unknown auth handler type."); } return(await handler.ProcessOAuthCallback(parameters)); }
public static async Task Resume(ResumptionCookie resumptionCookie) { var message = resumptionCookie.GetMessage(); using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message)) { IStateClient sc = scope.Resolve <IStateClient>(); BotData userData = sc.BotState.GetUserData(message.ChannelId, message.From.Id); //Tell Skype to continue the conversation we registered before await Conversation.ResumeAsync(resumptionCookie, message); bool waitingForSkype = true; while (waitingForSkype) { //Keep checking if Skype is done with the questions on that channel userData = sc.BotState.GetUserData(message.ChannelId, message.From.Id); waitingForSkype = userData.GetProperty <bool>("waitingForSkype"); } } }
public ConnectorStore(IStateClient stateClient) { SetField.NotNull(out this.stateClient, nameof(stateClient), stateClient); }
public static async Task <object> HandleOAuthCallback(HttpRequestMessage req, uint maxWriteAttempts) { try { var queryParams = req.RequestUri.ParseQueryString(); if (req.Method != HttpMethod.Post) { throw new ArgumentException("The OAuth postback handler only supports POST requests."); } var formData = await req.Content.ReadAsFormDataAsync(); string stateStr = formData["state"]; string code = formData["code"]; var resumptionCookie = UrlToken.Decode <ResumptionCookie>(stateStr); var message = resumptionCookie.GetMessage(); using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message)) { AuthenticationSettings authSettings = AuthenticationSettings.GetFromAppSettings(); var client = scope.Resolve <IConnectorClient>(); AuthenticationResult authenticationResult = await AzureActiveDirectoryHelper.GetTokenByAuthCodeAsync(code, authSettings); IStateClient sc = scope.Resolve <IStateClient>(); //IMPORTANT: DO NOT REMOVE THE MAGIC NUMBER CHECK THAT WE DO HERE. THIS IS AN ABSOLUTE SECURITY REQUIREMENT //REMOVING THIS WILL REMOVE YOUR BOT AND YOUR USERS TO SECURITY VULNERABILITIES. //MAKE SURE YOU UNDERSTAND THE ATTACK VECTORS AND WHY THIS IS IN PLACE. int magicNumber = GenerateRandomNumber(); bool writeSuccessful = false; uint writeAttempts = 0; while (!writeSuccessful && writeAttempts++ < maxWriteAttempts) { try { BotData userData = sc.BotState.GetUserData(message.ChannelId, message.From.Id); userData.SetProperty(AuthenticationConstants.AuthResultKey, authenticationResult); userData.SetProperty(AuthenticationConstants.MagicNumberKey, magicNumber); userData.SetProperty(AuthenticationConstants.MagicNumberValidated, "false"); sc.BotState.SetUserData(message.ChannelId, message.From.Id, userData); writeSuccessful = true; } catch (HttpOperationException) { writeSuccessful = false; } } var resp = new HttpResponseMessage(HttpStatusCode.OK); if (!writeSuccessful) { message.Text = String.Empty; // fail the login process if we can't write UserData await Conversation.ResumeAsync(resumptionCookie, message); resp.Content = new StringContent("<html><body>Could not log you in at this time, please try again later</body></html>", System.Text.Encoding.UTF8, @"text/html"); } else { await Conversation.ResumeAsync(resumptionCookie, message); resp.Content = new StringContent($"<html><body>Almost done! Please copy this number and paste it back to your chat so your authentication can complete:<br/> <h1>{magicNumber}</h1>.</body></html>", System.Text.Encoding.UTF8, @"text/html"); } return(resp); } } catch (Exception ex) { // Callback is called with no pending message as a result the login flow cannot be resumed. return(req.CreateErrorResponse(HttpStatusCode.BadRequest, ex)); } }
public async Task <HttpResponseMessage> OAuthCallback( [FromUri] string code, [FromUri] string state, CancellationToken cancellationToken) { try { var queryParams = state; object tokenCache = null; if (string.Equals(AuthSettings.Mode, "v1", StringComparison.OrdinalIgnoreCase)) { tokenCache = new Microsoft.IdentityModel.Clients.ActiveDirectory.TokenCache(); } else if (string.Equals(AuthSettings.Mode, "v2", StringComparison.OrdinalIgnoreCase)) { tokenCache = new Microsoft.Identity.Client.TokenCache(); } else if (string.Equals(AuthSettings.Mode, "b2c", StringComparison.OrdinalIgnoreCase)) { } var resumptionCookie = UrlToken.Decode <ResumptionCookie>(queryParams); // Create the message that is send to conversation to resume the login flow var message = resumptionCookie.GetMessage(); using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message)) { var client = scope.Resolve <IConnectorClient>(); AuthResult authResult = null; if (string.Equals(AuthSettings.Mode, "v1", StringComparison.OrdinalIgnoreCase)) { // Exchange the Auth code with Access token var token = await AzureActiveDirectoryHelper.GetTokenByAuthCodeAsync(code, (Microsoft.IdentityModel.Clients.ActiveDirectory.TokenCache) tokenCache); authResult = token; } else if (string.Equals(AuthSettings.Mode, "v2", StringComparison.OrdinalIgnoreCase)) { // Exchange the Auth code with Access token var token = await AzureActiveDirectoryHelper.GetTokenByAuthCodeAsync(code, (Microsoft.Identity.Client.TokenCache) tokenCache, Models.AuthSettings.Scopes); authResult = token; } else if (string.Equals(AuthSettings.Mode, "b2c", StringComparison.OrdinalIgnoreCase)) { } IStateClient sc = scope.Resolve <IStateClient>(); //IMPORTANT: DO NOT REMOVE THE MAGIC NUMBER CHECK THAT WE DO HERE. THIS IS AN ABSOLUTE SECURITY REQUIREMENT //REMOVING THIS WILL REMOVE YOUR BOT AND YOUR USERS TO SECURITY VULNERABILITIES. //MAKE SURE YOU UNDERSTAND THE ATTACK VECTORS AND WHY THIS IS IN PLACE. int magicNumber = GenerateRandomNumber(); bool writeSuccessful = false; uint writeAttempts = 0; while (!writeSuccessful && writeAttempts++ < MaxWriteAttempts) { try { BotData userData = sc.BotState.GetUserData(message.ChannelId, message.From.Id); userData.SetProperty(ContextConstants.AuthResultKey, authResult); userData.SetProperty(ContextConstants.MagicNumberKey, magicNumber); userData.SetProperty(ContextConstants.MagicNumberValidated, "false"); sc.BotState.SetUserData(message.ChannelId, message.From.Id, userData); writeSuccessful = true; } catch (HttpOperationException) { writeSuccessful = false; } } var resp = new HttpResponseMessage(HttpStatusCode.OK); if (!writeSuccessful) { message.Text = String.Empty; // fail the login process if we can't write UserData await Conversation.ResumeAsync(resumptionCookie, message); resp.Content = new StringContent("<html><body>Could not log you in at this time, please try again later</body></html>", System.Text.Encoding.UTF8, @"text/html"); } else { await Conversation.ResumeAsync(resumptionCookie, message); resp.Content = new StringContent($"<html><body>Almost done! Please copy this number and paste it back to your chat so your authentication can complete:<br/> <h1>{magicNumber}</h1>.</body></html>", System.Text.Encoding.UTF8, @"text/html"); } return(resp); } } catch (Exception ex) { // Callback is called with no pending message as a result the login flow cannot be resumed. return(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ex)); } }
public async Task <HttpResponseMessage> ProcessOAuthCallback(NameValueCollection parameters) { try { var queryParams = parameters["state"]; var resumptionCookie = UrlToken.Decode <ResumptionCookie>(queryParams); // Create the message that is send to conversation to resume the login flow var message = resumptionCookie.GetMessage(); using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message)) { var client = scope.Resolve <IConnectorClient>(); AuthenticationResult authResult = null; // Exchange the Auth code with Access token authResult = await GetTokenByAuthCodeAsync(parameters); IStateClient sc = scope.Resolve <IStateClient>(); //IMPORTANT: DO NOT REMOVE THE MAGIC NUMBER CHECK THAT WE DO HERE. THIS IS AN ABSOLUTE SECURITY REQUIREMENT //REMOVING THIS WILL REMOVE YOUR BOT AND YOUR USERS TO SECURITY VULNERABILITIES. //MAKE SURE YOU UNDERSTAND THE ATTACK VECTORS AND WHY THIS IS IN PLACE. int magicNumber = GenerateRandomNumber(); bool writeSuccessful = false; uint writeAttempts = 0; while (!writeSuccessful && writeAttempts++ < _maxWriteAttempts) { try { BotData userData = sc.BotState.GetUserData(message.ChannelId, message.From.Id); userData.SetProperty(dialogId + '_' + AuthenticationConstants.AuthResultKey, authResult); userData.SetProperty(dialogId + '_' + AuthenticationConstants.MagicNumberKey, magicNumber); userData.SetProperty(dialogId + '_' + AuthenticationConstants.MagicNumberValidated, "false"); sc.BotState.SetUserData(message.ChannelId, message.From.Id, userData); writeSuccessful = true; } catch (HttpOperationException) { writeSuccessful = false; } } var resp = new HttpResponseMessage(HttpStatusCode.OK); if (!writeSuccessful) { message.Text = String.Empty; // fail the login process if we can't write UserData await Conversation.ResumeAsync(resumptionCookie, message); resp.Content = new StringContent("<html><body>Could not log you in at this time, please try again later</body></html>", System.Text.Encoding.UTF8, @"text/html"); } else { await Conversation.ResumeAsync(resumptionCookie, message); resp.Content = new StringContent($"<html><body>Almost done! Please copy this number and paste it back to your chat so your authentication can complete:<br/> <h1>{magicNumber}</h1>.</body></html>", System.Text.Encoding.UTF8, @"text/html"); } return(resp); } } catch (Exception ex) { // Callback is called with no pending message as a result the login flow cannot be resumed. var resp = new HttpResponseMessage(HttpStatusCode.InternalServerError); return(resp); } }
public async Task <HttpResponseMessage> Callback([FromUri] string code, [FromUri] string state, CancellationToken cancellationToken) { try { // Use the state parameter to get correct IAuthProvider and ResumptionCookie var decoded = Encoding.UTF8.GetString(HttpServerUtility.UrlTokenDecode(state)); var queryString = HttpUtility.ParseQueryString(decoded); var assembly = Assembly.Load(queryString["providerassembly"]); var type = assembly.GetType(queryString["providertype"]); var providername = queryString["providername"]; IAuthProvider authProvider; if (type.GetConstructor(new Type[] { typeof(string) }) != null) { authProvider = (IAuthProvider)Activator.CreateInstance(type, providername); } else { authProvider = (IAuthProvider)Activator.CreateInstance(type); } // Use the ResumptionCookie to get original conversation var resumptionCookie = UrlToken.Decode <ResumptionCookie>(queryString["resumption"]); var message = resumptionCookie.GetMessage(); using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message)) { // Get the UserData from the original conversation IStateClient sc = scope.Resolve <IStateClient>(); BotData userData = sc.BotState.GetUserData(message.ChannelId, message.From.Id); // Get Access Token using authorization code var authOptions = userData.GetProperty <AuthenticationOptions>($"{authProvider.Name}{ContextConstants.AuthOptions}"); var token = await authProvider.GetTokenByAuthCodeAsync(authOptions, code); // Generate magic number and attempt to write to userdata int magicNumber = GenerateRandomNumber(); bool writeSuccessful = false; uint writeAttempts = 0; while (!writeSuccessful && writeAttempts++ < MaxWriteAttempts) { try { userData.SetProperty($"{authProvider.Name}{ContextConstants.AuthResultKey}", token); userData.SetProperty($"{authProvider.Name}{ContextConstants.MagicNumberKey}", magicNumber); userData.SetProperty($"{authProvider.Name}{ContextConstants.MagicNumberValidated}", "false"); sc.BotState.SetUserData(message.ChannelId, message.From.Id, userData); writeSuccessful = true; } catch (HttpOperationException) { writeSuccessful = false; } } var resp = new HttpResponseMessage(HttpStatusCode.OK); if (!writeSuccessful) { message.Text = String.Empty; // fail the login process if we can't write UserData await Conversation.ResumeAsync(resumptionCookie, message); resp.Content = new StringContent("<html><body>Could not log you in at this time, please try again later</body></html>", System.Text.Encoding.UTF8, @"text/html"); } else { await Conversation.ResumeAsync(resumptionCookie, message); resp.Content = new StringContent($"<html><body>Almost done! Please copy this number and paste it back to your chat so your authentication can complete:<br/> <h1>{magicNumber}</h1>.</body></html>", System.Text.Encoding.UTF8, @"text/html"); } return(resp); } } catch (Exception ex) { // Callback is called with no pending message as a result the login flow cannot be resumed. return(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ex)); } }
/// <summary> /// Initializes a new instance of the <see cref="ReferenceDataRepository"/> class. /// </summary> /// <param name="applicationSetting">The application setting.</param> public ReferenceDataRepository(IApplicationSetting applicationSetting) { if (applicationSetting != null) { this.countryClient = DIContainer.Instance.Resolve<ICountryClient>(new ResolverOverride[] { new ParameterOverride(BaseAddressParameterName, applicationSetting.ReferenceDataServiceBaseAddress) }); this.stateClient = DIContainer.Instance.Resolve<IStateClient>(new ResolverOverride[] { new ParameterOverride(BaseAddressParameterName, applicationSetting.ReferenceDataServiceBaseAddress) }); this.portClient = DIContainer.Instance.Resolve<IPortClient>(new ResolverOverride[] { new ParameterOverride(BaseAddressParameterName, applicationSetting.ReferenceDataServiceBaseAddress) }); this.brandClient = DIContainer.Instance.Resolve<IBrandClient>(new ResolverOverride[] { new ParameterOverride(BaseAddressParameterName, applicationSetting.ReferenceDataServiceBaseAddress) }); this.personTypeClient = DIContainer.Instance.Resolve<IPersonTypeClient>(new ResolverOverride[] { new ParameterOverride(BaseAddressParameterName, applicationSetting.ReferenceDataServiceBaseAddress) }); this.loyaltyLevelTypeClient = DIContainer.Instance.Resolve<ILoyaltyLevelTypeClient>(new ResolverOverride[] { new ParameterOverride(BaseAddressParameterName, applicationSetting.ReferenceDataServiceBaseAddress) }); this.documentTypeClient = DIContainer.Instance.Resolve<IDocumentTypeClient>(new ResolverOverride[] { new ParameterOverride(BaseAddressParameterName, applicationSetting.ReferenceDataServiceBaseAddress) }); } }
public async Task <HttpResponseMessage> OAuthCallback([FromUri] string code, [FromUri] string state, CancellationToken cancellationToken) { try { object tokenCache = null; if (string.Equals(AuthSettings.Mode, "v1", StringComparison.OrdinalIgnoreCase)) { tokenCache = new Microsoft.IdentityModel.Clients.ActiveDirectory.TokenCache(); } else if (string.Equals(AuthSettings.Mode, "v2", StringComparison.OrdinalIgnoreCase)) { tokenCache = new Microsoft.Identity.Client.TokenCache(); } else if (string.Equals(AuthSettings.Mode, "b2c", StringComparison.OrdinalIgnoreCase)) { } // Get the resumption cookie var resumptionCookie = UrlToken.Decode <ResumptionCookie>(state); // Create the message that is send to conversation to resume the login flow var message = resumptionCookie.GetMessage(); using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message)) { var client = scope.Resolve <IConnectorClient>(); AuthResult authResult = null; if (string.Equals(AuthSettings.Mode, "v1", StringComparison.OrdinalIgnoreCase)) { // Exchange the Auth code with Access token var token = await AzureActiveDirectoryHelper.GetTokenByAuthCodeAsync(code, (Microsoft.IdentityModel.Clients.ActiveDirectory.TokenCache) tokenCache); authResult = token; } else if (string.Equals(AuthSettings.Mode, "v2", StringComparison.OrdinalIgnoreCase)) { // Exchange the Auth code with Access token var token = await AzureActiveDirectoryHelper.GetTokenByAuthCodeAsync(code, (Microsoft.Identity.Client.TokenCache) tokenCache, Models.AuthSettings.Scopes); authResult = token; } else if (string.Equals(AuthSettings.Mode, "b2c", StringComparison.OrdinalIgnoreCase)) { } IStateClient sc = scope.Resolve <IStateClient>(); //IMPORTANT: DO NOT REMOVE THE MAGIC NUMBER CHECK THAT WE DO HERE. THIS IS AN ABSOLUTE SECURITY REQUIREMENT //REMOVING THIS WILL REMOVE YOUR BOT AND YOUR USERS TO SECURITY VULNERABILITIES. //MAKE SURE YOU UNDERSTAND THE ATTACK VECTORS AND WHY THIS IS IN PLACE. var dataBag = scope.Resolve <IBotData>(); await dataBag.LoadAsync(cancellationToken); int magicNumber = GenerateRandomNumber(); dataBag.UserData.SetValue(ContextConstants.AuthResultKey, authResult); dataBag.UserData.SetValue(ContextConstants.MagicNumberKey, magicNumber); dataBag.UserData.SetValue(ContextConstants.MagicNumberValidated, "false"); await dataBag.FlushAsync(cancellationToken); await Conversation.ResumeAsync(resumptionCookie, message); var resp = new HttpResponseMessage(HttpStatusCode.OK); resp.Content = new StringContent($"<html><body>Almost done! Please copy this number and paste it back to your chat so your authentication can complete: {magicNumber}.</body></html>", System.Text.Encoding.UTF8, @"text/html"); return(resp); } } catch (Exception ex) { // Callback is called with no pending message as a result the login flow cannot be resumed. return(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ex)); } }
private async Task OnInvoke(IInvokeActivity invoke, IConnectorClient connectorClient, IStateClient stateClient, HttpResponseMessage response, CancellationToken token) { MicrosoftAppCredentials.TrustServiceUrl(invoke.RelatesTo.ServiceUrl); var jobject = invoke.Value as JObject; if (jobject == null) { throw new ArgumentException("Request payload must be a valid json object."); } // This is to workaround the fact that the channelId for webchat is mapped to directline in the RelatesTo object invoke.RelatesTo.ChannelId = (invoke.RelatesTo.ChannelId == "directline") ? "webchat" : invoke.RelatesTo.ChannelId; // workaround the fact that invoke.relatesto.user can be null // we keep the userId in context.ConversationData[cartId] var conversationData = await stateClient.BotState.GetConversationDataAsync(invoke.RelatesTo.ChannelId, invoke.RelatesTo.Conversation.Id, token); var cartId = conversationData.GetProperty <string>(PaymentDialog.CARTKEY); if (invoke.RelatesTo.User == null && !string.IsNullOrEmpty(cartId)) { invoke.RelatesTo.User = new ChannelAccount { Id = conversationData.GetProperty <string>(cartId) }; } var updateResponse = default(object); switch (invoke.Name) { /* * case PaymentOperations.UpdateShippingAddressOperationName: * updateResponse = await this.ProcessShippingUpdate(jobject.ToObject<PaymentRequestUpdate>(), ShippingUpdateKind.Address, token); * break; * * case PaymentOperations.UpdateShippingOptionOperationName: * updateResponse = await this.ProcessShippingUpdate(jobject.ToObject<PaymentRequestUpdate>(), ShippingUpdateKind.Options, token); * break; */ case PaymentOperations.PaymentCompleteOperationName: updateResponse = await this.ProcessPaymentComplete(invoke, jobject.ToObject <PaymentRequestComplete>(), token); break; default: throw new ArgumentException("Invoke activity name is not a supported request type."); } response.Content = new ObjectContent <object>( updateResponse, this.Configuration.Formatters.JsonFormatter, JsonMediaTypeFormatter.DefaultMediaType); response.StatusCode = HttpStatusCode.OK; }
protected async void btnLogin_OnClick(object sender, EventArgs e) { try { phInfo.Visible = true; var state = JsonConvert.DeserializeObject <State>((string)Session["state"]); var token = "fakeToken"; if (!string.IsNullOrWhiteSpace(token)) { int magicNumber = GenerateRandomNumber(); // Create the message that is send to conversation to resume the login flow var message = state.ConversationReference.GetPostToUserMessage(); using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message)) { var authResult = new AuthResult(); authResult.AccessToken = token; authResult.UserName = txtUserId.Text; IStateClient sc = scope.Resolve <IStateClient>(); bool writeSuccessful = false; uint writeAttempts = 0; //將資料存回 botState 之中 while (!writeSuccessful && writeAttempts++ < MaxWriteAttempts) { try { var userData = sc.BotState.GetUserData(message.ChannelId, message.Recipient.Id); userData.SetProperty(ContextConstants.AuthResultKey, authResult); userData.SetProperty(ContextConstants.MagicNumberKey, magicNumber); userData.SetProperty(ContextConstants.MagicNumberValidated, "false"); sc.BotState.SetUserData(message.ChannelId, message.Recipient.Id, userData); writeSuccessful = true; } catch (HttpOperationException) { writeSuccessful = false; } } if (!writeSuccessful) { message.Text = String.Empty; // fail the login process if we can't write UserData await Conversation.ResumeAsync(state.ConversationReference, message); txtAlertMsg.Text = "無法登入,請再試一次,謝謝您!"; } else { await Conversation.ResumeAsync(state.ConversationReference, message); txtAlertMsg.Text = $"請將以下的{ContextConstants.MagicNumberLength}個數字驗證碼輸入到IM之中,以完成登入程序,謝謝您!<br/> <h1>{magicNumber}</h1>"; } } } else { // login fail txtAlertMsg.Text = "登入失敗! 請重新輸入! "; } } catch { txtAlertMsg.Text = "不是從 IM 進來的!"; } }