diff --git a/CHANGELOG.md b/CHANGELOG.md index 16774a49d..a33a9e888 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +## v4.8.1 - 2019 - 03 - 18 +## Fixed +- [build] Replaced a missing .icns file that was deleted by mistake in a previous PR. Fixes the app icon on Linux & Mac in PR [2104](https://github.com/microsoft/BotFramework-Emulator/pull/2104) +- [client] Fixed an issue where Restart activity wont appear on selected activity after restarting once in PR [2105](https://github.com/microsoft/BotFramework-Emulator/pull/2105) +- [client] Disable "Restart conversation from here" bubble on DL Speech bots [2107](https://github.com/microsoft/BotFramework-Emulator/pull/2107) +- [client] Fixed an issue where starting a conversation with an unset custom user ID was causing the User member of the conversation to have a blank `id` field in PR [2108](https://github.com/microsoft/BotFramework-Emulator/pull/2108) +- [main] Fixed an issue where the setting `Bypass Ngrok for local addresses` was continuing to use the ngrok tunnel even for local bots in PR [2111](https://github.com/microsoft/BotFramework-Emulator/pull/2111) +- [main] Ngrok Reporting made accurate in PR [2113](https://github.com/microsoft/BotFramework-Emulator/pull/2113) + ## v4.8.0 - 2019 - 03 - 12 ## Added - [client/main] Added Ngrok Status Viewer in PR [2032](https://github.com/microsoft/BotFramework-Emulator/pull/2032) diff --git a/package-lock.json b/package-lock.json index 08721b75b..b26941686 100644 --- a/package-lock.json +++ b/package-lock.json @@ -879,9 +879,9 @@ }, "dependencies": { "regenerator-transform": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.2.tgz", - "integrity": "sha512-V4+lGplCM/ikqi5/mkkpJ06e9Bujq1NFmNLvsCs56zg3ZbzrnUzAtizZ24TXxtRX/W2jcdScwQCnbL0CICTFkQ==", + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.3.tgz", + "integrity": "sha512-zXHNKJspmONxBViAb3ZUmFoFPnTBs3zFhCEZJiwp/gkNzxVbTqNJVjYKx6Qk1tQ1P4XLf4TbH9+KBB7wGoAaUw==", "requires": { "@babel/runtime": "^7.8.4", "private": "^0.1.8" @@ -15977,9 +15977,9 @@ } }, "minimist": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.4.tgz", - "integrity": "sha512-wTiNDqe4D2rbTJGZk1qcdZgFtY0/r+iuE6GDT7V0/+Gu5MLpIDm4+CssDECR79OJs/OxLPXMzdxy153b5Qy3hg==" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "minimist-options": { "version": "3.0.2", @@ -16474,9 +16474,9 @@ "integrity": "sha512-UdS4swXs85fCGWWf6t6DMGgpN/vnlKeSGEQ7hJcrs7PBFoxoKLmibc3QRb7fwiYsjdL7PX8iI/TMSlZ90dgHhQ==" }, "node-releases": { - "version": "1.1.51", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.51.tgz", - "integrity": "sha512-1eQEs6HFYY1kMXQPOLzCf7HdjReErmvn85tZESMczdCNVWP3Y7URYLBAyYynuI7yef1zj4HN5q+oB2x67QU0lw==", + "version": "1.1.52", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.52.tgz", + "integrity": "sha512-snSiT1UypkgGt2wxPqS6ImEUICbNCMb31yaxWrOLXjhlt2z2/IBpaOxzONExqSm4y5oLnAqjjRWu+wsDzK5yNQ==", "requires": { "semver": "^6.3.0" }, @@ -19613,9 +19613,9 @@ } }, "regenerator-runtime": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.4.tgz", - "integrity": "sha512-plpwicqEzfEyTQohIKktWigcLzmNStMGwbOUbykx51/29Z3JOGYldaaNGK7ngNXV+UcoqvIMmloZ48Sr74sd+g==" + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", + "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" }, "regenerator-transform": { "version": "0.10.1", diff --git a/packages/app/client/src/state/sagas/botSagas.spec.ts b/packages/app/client/src/state/sagas/botSagas.spec.ts index 45656e193..ac93ab8af 100644 --- a/packages/app/client/src/state/sagas/botSagas.spec.ts +++ b/packages/app/client/src/state/sagas/botSagas.spec.ts @@ -200,7 +200,7 @@ describe('The botSagas', () => { expect(putNotification.value).toEqual(put(errorNotification)); }); - it('should open a bot via url', () => { + it('should open a bot via url with the custom user ID', () => { const mockAction: any = { payload: { endpoint: 'http://localhost:3978/api/messages', @@ -213,13 +213,86 @@ describe('The botSagas', () => { const gen = BotSagas.openBotViaUrl(mockAction); gen.next(); gen.next('userId'); // select custom user GUID - gen.next('http://localhost:52673'); // select server url + + // select server url + expect(gen.next('http://localhost:52673').value).toEqual( + call([ConversationService, ConversationService.startConversation], 'http://localhost:52673', { + botUrl: mockAction.payload.endpoint, + channelServiceType: mockAction.payload.channelService, + members: [{ id: 'userId', name: 'User', role: 'user' }], + mode: mockAction.payload.mode, + msaAppId: mockAction.payload.appId, + msaPassword: mockAction.payload.appPassword, + }) + ); const mockStartConvoResponse = { json: async () => undefined, ok: true, }; - gen.next(mockStartConvoResponse); // startConversation + + // startConversation + gen.next(mockStartConvoResponse); + gen.next({ + conversationId: 'someConvoId', + endpointId: 'someEndpointId', + members: [], + }); //res.json() + + let next = gen.next(); // bootstrapChat() + + const putOpenValue = next.value; + expect(putOpenValue).toEqual( + put( + openEditorDocument({ + contentType: SharedConstants.ContentTypes.CONTENT_TYPE_LIVE_CHAT, + documentId: 'someConvoId', + isGlobal: false, + }) + ) + ); + gen.next(); // put open() + + gen.next({ ok: true }); // sendInitialLogReport() + next = gen.next({ ok: true }); // sendInitialActivity() + + const callValue = next.value; + expect(callValue).toEqual( + call( + [commandService, commandService.remoteCall], + SharedConstants.Commands.Settings.SaveBotUrl, + 'http://localhost:3978/api/messages' + ) + ); + expect(gen.next().done).toBe(true); + }); + + it('should open a bot via url with a newly generated user GUID', () => { + const mockAction: any = { + payload: { + endpoint: 'http://localhost:3978/api/messages', + channelService: 'public', + mode: 'livechat', + appId: 'someAppId', + appPassword: 'someAppPw', + }, + }; + const gen = BotSagas.openBotViaUrl(mockAction); + gen.next(); + gen.next(''); // select custom user GUID (force generation of new GUID) + + // select server url + const startConversationCall = gen.next('http://localhost:52673').value; + const startConversationPayload = startConversationCall.CALL.args[1]; + expect(startConversationPayload.members[0].id.length).toBeGreaterThan(0); + + const mockStartConvoResponse = { + json: async () => undefined, + ok: true, + }; + + // startConversation + gen.next(mockStartConvoResponse); gen.next({ conversationId: 'someConvoId', endpointId: 'someEndpointId', diff --git a/packages/app/client/src/state/sagas/botSagas.ts b/packages/app/client/src/state/sagas/botSagas.ts index fde538918..802c8b249 100644 --- a/packages/app/client/src/state/sagas/botSagas.ts +++ b/packages/app/client/src/state/sagas/botSagas.ts @@ -96,8 +96,9 @@ export class BotSagas { public static *openBotViaUrl( action: BotAction ): IterableIterator { + const customUserId = yield select(getCustomUserGUID); const user = { - id: yield select(getCustomUserGUID) || uniqueIdv4(), // use custom id or generate new one + id: customUserId || uniqueIdv4(), // use custom id or generate new one name: 'User', role: 'user', }; @@ -110,7 +111,7 @@ export class BotSagas { msaAppId: action.payload.appId, msaPassword: action.payload.appPassword, }; - let res: Response = yield ConversationService.startConversation(serverUrl, payload); + let res: Response = yield call([ConversationService, ConversationService.startConversation], serverUrl, payload); if (!res.ok) { yield* throwErrorFromResponse('Error occurred while starting a new conversation', res); } diff --git a/packages/app/client/src/state/sagas/chatSagas.spec.ts b/packages/app/client/src/state/sagas/chatSagas.spec.ts index 591c2a8ed..0684d9468 100644 --- a/packages/app/client/src/state/sagas/chatSagas.spec.ts +++ b/packages/app/client/src/state/sagas/chatSagas.spec.ts @@ -824,10 +824,13 @@ describe('The ChatSagas,', () => { // put webSpeechFactoryUpdated expect(gen.next().value).toEqual(put(webSpeechFactoryUpdated(payload.documentId, undefined))); + // select custom user GUID + expect(gen.next().value).toEqual(select(getCustomUserGUID)); + // call updateConversation const conversationId = chat.conversationId; const userId = chat.userId; - expect(gen.next().value).toEqual( + expect(gen.next('').value).toEqual( call([ConversationService, ConversationService.updateConversation], serverUrl, chat.conversationId, { conversationId, userId, @@ -957,10 +960,13 @@ describe('The ChatSagas,', () => { // put webSpeechFactoryUpdated expect(gen.next().value).toEqual(put(webSpeechFactoryUpdated(payload.documentId, undefined))); + // select custom user GUID + expect(gen.next().value).toEqual(select(getCustomUserGUID)); + // call updateConversation const conversationId = chat.conversationId; const userId = chat.userId; - expect(gen.next().value).toEqual( + expect(gen.next('').value).toEqual( call([ConversationService, ConversationService.updateConversation], serverUrl, chat.conversationId, { conversationId, userId, diff --git a/packages/app/client/src/state/sagas/chatSagas.ts b/packages/app/client/src/state/sagas/chatSagas.ts index 93fe6afb6..d0a8ce696 100644 --- a/packages/app/client/src/state/sagas/chatSagas.ts +++ b/packages/app/client/src/state/sagas/chatSagas.ts @@ -241,7 +241,8 @@ export class ChatSagas { const { filename } = action.payload; // start a conversation const serverUrl = yield select(getServerUrl); - const user = { id: yield select(getCustomUserGUID) || uniqueIdv4(), name: 'User', role: 'user' }; + const customUserId = yield select(getCustomUserGUID); + const user = { id: customUserId || uniqueIdv4(), name: 'User', role: 'user' }; const payload = { botUrl: '', channelServiceType: '' as any, @@ -535,7 +536,8 @@ export class ChatSagas { userId = uniqueIdv4(); } else { // use the previous id or the custom id from settings - userId = chat.userId || (yield select(getCustomUserGUID)); + const customUserId = yield select(getCustomUserGUID); + userId = chat.userId || customUserId; } // update the main-side conversation object with conversation & user IDs, diff --git a/packages/app/client/src/ui/editor/emulator/parts/chat/chat.tsx b/packages/app/client/src/ui/editor/emulator/parts/chat/chat.tsx index 5f2053db1..85857555a 100644 --- a/packages/app/client/src/ui/editor/emulator/parts/chat/chat.tsx +++ b/packages/app/client/src/ui/editor/emulator/parts/chat/chat.tsx @@ -139,9 +139,6 @@ export class Chat extends PureComponent { } private activityWrapper(next, card, children): ReactNode { - const { mode, restartStatus } = this.props; - const isWebChatDisabled = - mode === 'transcript' || mode === 'debug' || restartStatus === RestartConversationStatus.Started; return ( { onContextMenu={this.onContextMenu} onItemRendererClick={this.onItemRendererClick} onItemRendererKeyDown={this.onItemRendererKeyDown} - restartStatusForActivity={this.props.restartStatus} - isWebChatDisabled={isWebChatDisabled} > {next(card)(children)} diff --git a/packages/app/client/src/ui/editor/emulator/parts/chat/outerActivityWrapper.spec.tsx b/packages/app/client/src/ui/editor/emulator/parts/chat/outerActivityWrapper.spec.tsx index e6de421b2..00165a0dc 100644 --- a/packages/app/client/src/ui/editor/emulator/parts/chat/outerActivityWrapper.spec.tsx +++ b/packages/app/client/src/ui/editor/emulator/parts/chat/outerActivityWrapper.spec.tsx @@ -35,11 +35,15 @@ import * as React from 'react'; import { createStore } from 'redux'; import { Provider } from 'react-redux'; import { mount, shallow } from 'enzyme'; -import { ValueTypes, RestartConversationOptions } from '@bfemulator/app-shared'; +import { ValueTypes, RestartConversationOptions, RestartConversationStatus } from '@bfemulator/app-shared'; import { OuterActivityWrapper } from './outerActivityWrapper'; import { OuterActivityWrapperContainer } from './outerActivityWrapperContainer'; +jest.mock('./chat.scss', () => ({ + hidden: 'hidden-restart', +})); + describe('', () => { it('should render', () => { const storeState = { @@ -49,7 +53,9 @@ describe('', () => { highlightedObjects: [], inspectorObjects: [{ value: {}, valueType: ValueTypes.Activity }], }, - restartStatus: {}, + }, + restartStatus: { + doc1: RestartConversationStatus.Stop, }, }, }; @@ -150,4 +156,201 @@ describe('', () => { expect((instance as any).isUserActivity(userCard.activity)).toBe(true); expect((instance as any).isUserActivity(botCard.activity)).toBe(false); }); + + describe('Restart conversation bubble in OuterActivityWrapper', () => { + it('should show restart bubble if a)not Speech bot; b) Webchat is enabled; c)User activity is selected', () => { + const card = { + activity: { + id: 'card1', + from: { + role: 'user', + }, + channelData: { + test: true, + }, + }, + }; + const storeState = { + chat: { + chats: { + doc1: { + highlightedObjects: [], + inspectorObjects: [{ value: { ...card.activity }, valueType: ValueTypes.Activity }], + mode: 'livechat', + }, + }, + restartStatus: { + doc1: RestartConversationStatus.Stop, + }, + }, + }; + + const wrapper = mount( + state, storeState)}> + + + ); + expect(wrapper.find('hidden-restart').length).toBe(0); + }); + + it('should hide restart bubble if activity not selected', () => { + const card = { + activity: { + id: 'card1', + from: { + role: 'user', + }, + channelData: { + test: true, + }, + }, + }; + const storeState = { + chat: { + chats: { + doc1: { + highlightedObjects: [], + inspectorObjects: [{ value: {}, valueType: ValueTypes.Activity }], + mode: 'livechat', + }, + }, + restartStatus: { + doc1: RestartConversationStatus.Stop, + }, + }, + }; + + const wrapper = mount( + state, storeState)}> + + + ); + expect(wrapper.find('.hidden-restart').length).toBe(1); + }); + + it('should hide restart bubble if it is a speech bot', () => { + const card = { + activity: { + id: 'card1', + from: { + role: 'user', + }, + channelData: { + test: true, + }, + }, + }; + const storeState = { + chat: { + chats: { + doc1: { + highlightedObjects: [], + inspectorObjects: [{ value: { ...card.activity }, valueType: ValueTypes.Activity }], + mode: 'livechat', + speechKey: 'abc', + speechRegion: 'westus', + }, + }, + restartStatus: { + doc1: RestartConversationStatus.Stop, + }, + }, + }; + + const wrapper = mount( + state, storeState)}> + + + ); + expect(wrapper.find('.hidden-restart').length).toBe(1); + }); + + it('should hide restart bubble if restart conversation has a status of "Started" for chat', () => { + const card = { + activity: { + id: 'card1', + from: { + role: 'user', + }, + channelData: { + test: true, + }, + }, + }; + const storeState = { + chat: { + chats: { + doc1: { + highlightedObjects: [], + inspectorObjects: [{ value: { ...card.activity }, valueType: ValueTypes.Activity }], + mode: 'livechat', + }, + }, + restartStatus: { + doc1: RestartConversationStatus.Started, + }, + }, + }; + + const wrapper = mount( + state, storeState)}> + + + ); + expect(wrapper.find('.hidden-restart').length).toBe(1); + }); + + it('should hide restart bubble if chat in transcript mode', () => { + const card = { + activity: { + id: 'card1', + from: { + role: 'user', + }, + channelData: { + test: true, + }, + }, + }; + const storeState = { + chat: { + chats: { + doc1: { + highlightedObjects: [], + inspectorObjects: [{ value: { ...card.activity }, valueType: ValueTypes.Activity }], + mode: 'transcript', + }, + }, + restartStatus: {}, + }, + }; + + const wrapper = mount( + state, storeState)}> + + + ); + expect(wrapper.find('.hidden-restart').length).toBe(1); + }); + }); }); diff --git a/packages/app/client/src/ui/editor/emulator/parts/chat/outerActivityWrapper.tsx b/packages/app/client/src/ui/editor/emulator/parts/chat/outerActivityWrapper.tsx index 188d54556..9c93710c6 100644 --- a/packages/app/client/src/ui/editor/emulator/parts/chat/outerActivityWrapper.tsx +++ b/packages/app/client/src/ui/editor/emulator/parts/chat/outerActivityWrapper.tsx @@ -35,6 +35,7 @@ import * as React from 'react'; import { SharedConstants, RestartConversationOptions } from '@bfemulator/app-shared'; import { Activity } from 'botframework-schema'; import { RestartConversationStatus } from '@bfemulator/app-shared'; +import { EmulatorMode } from '@bfemulator/sdk-shared'; import { areActivitiesEqual } from '../../../../../utils'; @@ -54,16 +55,30 @@ export interface OuterActivityWrapperProps { restartOption: RestartConversationOptions ) => void; currentRestartConversationOption: RestartConversationOptions; - isWebChatDisabled: boolean; + mode: EmulatorMode; + restartStatus: RestartConversationStatus; + isDLSpeechBot: boolean; } export class OuterActivityWrapper extends React.Component { public render() { - const { card, children, onContextMenu, onItemRendererClick, onItemRendererKeyDown, isWebChatDisabled } = this.props; + const { + card, + children, + onContextMenu, + onItemRendererClick, + onItemRendererKeyDown, + mode, + restartStatus, + isDLSpeechBot, + } = this.props; const isSelected = this.shouldBeSelected(card.activity); const isUserActivity = this.isUserActivity(card.activity); - const showRestartBubble = isUserActivity && isSelected && !isWebChatDisabled; + const isWebChatDisabled = + mode === 'transcript' || mode === 'debug' || restartStatus === RestartConversationStatus.Started; + + const showRestartBubble = !isDLSpeechBot && isUserActivity && isSelected && !isWebChatDisabled; return ( ['12', '123'], - }, - endpoints: { - reset: () => null, - push: () => null, +const mockEmulator = jest.fn(() => { + return { + server: { + serverUrl: 'http://localhost:3000', + serverPort: 8080, + state: { + conversations: { + getConversationIds: () => ['12', '123'], + }, + endpoints: { + reset: () => null, + push: () => null, + }, }, }, - }, -}; + }; +}); jest.mock('./emulator', () => ({ Emulator: { - getInstance: () => mockEmulator, + getInstance: () => mockEmulator(), }, })); @@ -209,4 +211,80 @@ describe('The ngrokService', () => { (ngrokService as any).oauthNgrokInstance = undefined; }); + + describe('Conditions where service url returned is an ngrok url', () => { + it('should always return ngrok url if runNgrokAtStartup is selected', async () => { + const settings = { + locale: 'en-us', + bypassNgrokLocalhost: true, + runNgrokAtStartup: true, + ngrokPath: '/usr/bin/ngrok', + }; + store.dispatch(setFrameworkSettings(settings as any)); + const serviceUrl = await ngrokService.getServiceUrl('http://my-azure.com/speech-bot/'); + expect(serviceUrl).toBe('http://fdsfds.ngrok.io'); + }); + + it('should always return ngrok url if remote bot irrespective of options selected', async () => { + const settings = { + locale: 'en-us', + bypassNgrokLocalhost: false, + runNgrokAtStartup: false, + ngrokPath: '/usr/bin/ngrok', + }; + store.dispatch(setFrameworkSettings(settings as any)); + const serviceUrl = await ngrokService.getServiceUrl('http://my-azure/api/messages'); + expect(serviceUrl).toBe('http://fdsfds.ngrok.io'); + }); + + it('should always return ngrok url if bypassNgrokLocalhost is not selected and ngrok is configured', async () => { + const settings = { + locale: 'en-us', + bypassNgrokLocalhost: false, + runNgrokAtStartup: false, + ngrokPath: '/usr/bin/ngrok', + }; + store.dispatch(setFrameworkSettings(settings as any)); + const serviceUrl = await ngrokService.getServiceUrl('http://localhost:3978'); + expect(serviceUrl).toBe('http://fdsfds.ngrok.io'); + }); + }); + + describe('Conditions where service url returned is a localhost url', () => { + it('should always return localhost url if local bot and bypassNgrokLocalhost is selected', async () => { + const settings = { + locale: 'en-us', + bypassNgrokLocalhost: true, + runNgrokAtStartup: false, + ngrokPath: '/usr/bin/ngrok', + }; + store.dispatch(setFrameworkSettings(settings as any)); + const serviceUrl = await ngrokService.getServiceUrl('http://localhost:3978'); + expect(serviceUrl).toBe('http://localhost:8080'); + }); + + it('should always return localhost url if bypassNgrokLocalhost is not selected and ngrok is not configured and its a local bot', async () => { + const settings = { + locale: 'en-us', + bypassNgrokLocalhost: false, + runNgrokAtStartup: false, + ngrokPath: '', + }; + store.dispatch(setFrameworkSettings(settings as any)); + const serviceUrl = await ngrokService.getServiceUrl('http://localhost:3978'); + expect(serviceUrl).toBe('http://localhost:8080'); + }); + + it('should always return localhost url if ngrok is not configured', async () => { + const settings = { + locale: 'en-us', + bypassNgrokLocalhost: false, + runNgrokAtStartup: false, + ngrokPath: '', + }; + store.dispatch(setFrameworkSettings(settings as any)); + const serviceUrl = await ngrokService.getServiceUrl('http://my-azure/api/messages'); + expect(serviceUrl).toBe('http://localhost:8080'); + }); + }); }); diff --git a/packages/app/main/src/ngrokService.ts b/packages/app/main/src/ngrokService.ts index c9b9e1e9d..90af3fb5d 100644 --- a/packages/app/main/src/ngrokService.ts +++ b/packages/app/main/src/ngrokService.ts @@ -74,13 +74,7 @@ export class NgrokService { if (this.pendingRecycle) { await this.pendingRecycle; } - if (this.ngrok.running()) { - return this.serviceUrl; - } - const { bypassNgrokLocalhost, runNgrokAtStartup } = getSettings().framework; - // Use ngrok - const local = !botUrl || isLocalHostUrl(botUrl); - if (runNgrokAtStartup || !local || (local && !bypassNgrokLocalhost)) { + if (this.isUsingNgrok(botUrl)) { if (!this.ngrok.running()) { await this.startup(); } @@ -237,7 +231,7 @@ export class NgrokService { ); } else if (!this.ngrokPath) { this.reportNotConfigured(conversationId); - } else if (this.ngrok.running()) { + } else if (this.isUsingNgrok(botUrl)) { this.reportRunning(conversationId); } else { emulatorApplication.mainWindow.logService.logToChat( @@ -312,4 +306,10 @@ export class NgrokService { } this.localhost = hostname; } + + private isUsingNgrok(botUrl: string) { + const { bypassNgrokLocalhost, runNgrokAtStartup } = getSettings().framework; + const local = !botUrl || isLocalHostUrl(botUrl); + return runNgrokAtStartup || !local || (local && !bypassNgrokLocalhost); + } }