diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs index 7774cf50..c475d6a5 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs @@ -5,7 +5,6 @@ public interface IConversationService IConversationStateService States { get; } string ConversationId { get; } Task NewConversation(Conversation conversation); - Task MarkConnectionReady(string conversationId); void SetConversationId(string conversationId, List states); Task GetConversation(string id); Task> GetConversations(); diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs index 874690b5..6a2c495a 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -118,15 +118,4 @@ public partial class ConversationService : IConversationService _state.Load(_conversationId); states.ForEach(x => _state.SetState(x.Split('=')[0], x.Split('=')[1])); } - - public async Task MarkConnectionReady(string conversationId) - { - var hooks = _services.GetServices(); - var conv = await GetConversation(conversationId); - foreach (var hook in hooks) - { - // Need to check if user connected with agent is the first time. - await hook.OnUserAgentConnectedInitially(conv); - } - } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index 97b10cd2..dde37482 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -41,13 +41,6 @@ public class ConversationController : ControllerBase, IApiAdapter return ConversationViewModel.FromSession(conv); } - [HttpPatch("/conversation/{conversationId}/ready")] - public async Task ReadyToInteractive([FromRoute] string conversationId) - { - var service = _services.GetRequiredService(); - await service.MarkConnectionReady(conversationId); - } - [HttpGet("/conversations/{agentId}")] public async Task> GetConversations() { diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs index df2de65e..a65dbc0f 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Messaging; using BotSharp.Abstraction.Messaging.Models.RichContent; using Microsoft.AspNetCore.SignalR; @@ -8,12 +7,15 @@ public class ChatHubConversationHook : ConversationHookBase { private readonly IServiceProvider _services; private readonly IHubContext _chatHub; + private readonly IUserIdentity _user; public ChatHubConversationHook(IServiceProvider services, - IHubContext chatHub) + IHubContext chatHub, + IUserIdentity user) { _services = services; _chatHub = chatHub; + _user = user; } public override async Task OnUserAgentConnectedInitially(Conversation conversation) @@ -33,7 +35,18 @@ public class ChatHubConversationHook : ConversationHookBase foreach (var message in messages) { await Task.Delay(300); - await OnResponseGenerated(new RoleDialogModel(AgentRole.Assistant, message.Text)); + + await _chatHub.Clients.User(_user.Id).SendAsync("OnMessageReceivedFromAssistant", new ChatResponseModel() + { + ConversationId = conversation.Id, + Text = message.Text, + Sender = new UserViewModel() + { + FirstName = "AI", + LastName = "Assistant", + Role = AgentRole.Assistant + } + }); } } @@ -75,7 +88,7 @@ public class ChatHubConversationHook : ConversationHookBase { var conv = _services.GetRequiredService(); - await _chatHub.Clients.All.SendAsync("OnMessageReceivedFromAssistant", new ChatResponseModel() + await _chatHub.Clients.User(_user.Id).SendAsync("OnMessageReceivedFromAssistant", new ChatResponseModel() { ConversationId = conv.ConversationId, MessageId = message.MessageId, diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/SignalRHub.cs b/src/Plugins/BotSharp.Plugin.ChatHub/SignalRHub.cs index d01d2b08..7b5fdd2e 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/SignalRHub.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/SignalRHub.cs @@ -1,25 +1,47 @@ +using BotSharp.Abstraction.Conversations.Models; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; namespace BotSharp.Plugin.ChatHub; +[Authorize] public class SignalRHub : Hub { private readonly IServiceProvider _services; private readonly ILogger _logger; private readonly IUserIdentity _user; + private readonly IHttpContextAccessor _context; public SignalRHub(IServiceProvider services, ILogger logger, - IUserIdentity user) + IUserIdentity user, + IHttpContextAccessor context) { _services = services; _logger = logger; _user = user; + _context = context; } - public override Task OnConnectedAsync() + public override async Task OnConnectedAsync() { - Console.WriteLine($"SignalR Hub: {Context.UserIdentifier} connected in {Context.ConnectionId} [{DateTime.Now}]"); - return base.OnConnectedAsync(); + _logger.LogInformation($"SignalR Hub: {_user.FirstName} {_user.LastName} ({Context.UserIdentifier}) connected in {Context.ConnectionId} [{DateTime.Now}]"); + + var hooks = _services.GetServices(); + var convService = _services.GetRequiredService(); + _context.HttpContext.Request.Query.TryGetValue("conversationId", out var conversationId); + var conv = await convService.GetConversation(conversationId); + + foreach (var hook in hooks) + { + // Check if user connected with agent is the first time. + if (!conv.Dialogs.Any()) + { + await hook.OnUserAgentConnectedInitially(conv); + } + } + + await base.OnConnectedAsync(); } } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs b/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs new file mode 100644 index 00000000..c151a62e --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Http; + +namespace BotSharp.Plugin.ChatHub; + +public class WebSocketsMiddleware +{ + private readonly RequestDelegate _next; + + public WebSocketsMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext httpContext) + { + var request = httpContext.Request; + + // web sockets cannot pass headers so we must take the access token from query param and + // add it to the header before authentication middleware runs + if (request.Path.StartsWithSegments("/chatHub", StringComparison.OrdinalIgnoreCase) && + request.Query.TryGetValue("access_token", out var accessToken)) + { + request.Headers.Add("Authorization", $"Bearer {accessToken}"); + } + + await _next(httpContext); + } +} diff --git a/src/WebStarter/Program.cs b/src/WebStarter/Program.cs index 3e84de6c..b863638a 100644 --- a/src/WebStarter/Program.cs +++ b/src/WebStarter/Program.cs @@ -68,6 +68,9 @@ if (app.Environment.IsDevelopment()) app.UseSwaggerUI(); } +app.MapHub("/chatHub"); +app.UseMiddleware(); + app.UseAuthentication(); app.UseAuthorization(); @@ -80,6 +83,4 @@ app.UseBotSharp(); app.UseCors("MyCorsPolicy"); #endif -app.MapHub("/chatHub"); - app.Run(); diff --git a/src/web-live-chat/src/lib/helpers/http.js b/src/web-live-chat/src/lib/helpers/http.js index 15fdcb91..c915b571 100644 --- a/src/web-live-chat/src/lib/helpers/http.js +++ b/src/web-live-chat/src/lib/helpers/http.js @@ -1,10 +1,11 @@ import axios from 'axios'; +import { getUserStore } from '$lib/helpers/store.js'; /** * Set axios http headers globally - * @param {string} token */ -export function setAuthorization(token) { +export function setAuthorization() { + let user = getUserStore(); let headers = axios.defaults.headers; - headers.common['Authorization'] = `Bearer ${token}`; + headers.common['Authorization'] = `Bearer ${user.token}`; } \ No newline at end of file diff --git a/src/web-live-chat/src/lib/services/api-endpoints.js b/src/web-live-chat/src/lib/services/api-endpoints.js index 421b2fdb..4665b36e 100644 --- a/src/web-live-chat/src/lib/services/api-endpoints.js +++ b/src/web-live-chat/src/lib/services/api-endpoints.js @@ -13,11 +13,10 @@ export const agentListUrl = `${host}/agents`; export const agentDetailUrl = `${host}/agent/{id}`; // conversation -export const conversationInitUrl = `${host}/conversation/{agentId}` -export const conversationReadyUrl = `${host}/conversation/{conversationId}/ready` -export const conversationMessageUrl = `${host}/conversation/{agentId}/{conversationId}` -export const conversationsUrl = `${host}/conversations/{agentId}` -export const dialogsUrl = `${host}/conversation/{conversationId}/dialogs` +export const conversationInitUrl = `${host}/conversation/{agentId}`; +export const conversationMessageUrl = `${host}/conversation/{agentId}/{conversationId}`; +export const conversationsUrl = `${host}/conversations/{agentId}`; +export const dialogsUrl = `${host}/conversation/{conversationId}/dialogs`; // chathub export const chatHubUrl = `${host}/chatHub`; diff --git a/src/web-live-chat/src/lib/services/auth-service.js b/src/web-live-chat/src/lib/services/auth-service.js index 738e5c0e..5e5e4469 100644 --- a/src/web-live-chat/src/lib/services/auth-service.js +++ b/src/web-live-chat/src/lib/services/auth-service.js @@ -1,5 +1,6 @@ import { userStore, getUserStore } from '$lib/helpers/store.js'; import { tokenUrl, myInfoUrl, usrCreationUrl } from './api-endpoints.js'; +import axios from 'axios'; /** * This callback type is called `requestCallback` and is displayed as a global symbol. @@ -37,31 +38,20 @@ export async function getToken(email, password, onSucceed) { } /** + * Set token from exteranl * @param {string} token + */ +export function setToken(token) { + let user = getUserStore(); + userStore.set({ ...user, init: false, loggedIn: true, token: token }); +} + +/** * @returns {Promise} */ -export async function myInfo(token) { - const headers = { - Authorization: `Bearer ${token}`, - }; - - const info = await fetch(myInfoUrl, { - method: 'GET', - headers: headers, - }).then(response => { - if (response.ok) { - return response.json(); - } else { - alert(response.statusText); - } - }).then(result => { - let user = getUserStore(); - userStore.set({ ...user, init: false, id: result.id }); - return result; - }) - .catch(error => alert(error.message)); - - return info; +export async function myInfo() { + const response = await axios.get(myInfoUrl); + return response.data; } /** diff --git a/src/web-live-chat/src/lib/services/conversation-service.js b/src/web-live-chat/src/lib/services/conversation-service.js index d6158c76..fdb53a2b 100644 --- a/src/web-live-chat/src/lib/services/conversation-service.js +++ b/src/web-live-chat/src/lib/services/conversation-service.js @@ -1,10 +1,9 @@ import { conversationInitUrl, conversationMessageUrl, - dialogsUrl, - conversationReadyUrl, + dialogsUrl, } from './api-endpoints.js'; - +import { setAuthorization } from '$lib/helpers/http'; import axios from 'axios'; /** @@ -13,27 +12,19 @@ import axios from 'axios'; * @returns {Promise} */ export async function newConversation(agentId) { + setAuthorization(); let url = conversationInitUrl.replace("{agentId}", agentId); const response = await axios.post(url, {}); return response.data; } -/** - * Coversation is ready - * @param {string} conversationId - */ -export async function conversationReady(conversationId) { - let url = conversationReadyUrl.replace("{conversationId}", conversationId); - const response = await axios.patch(url, {}); - return response.data; -} - /** * Get dialog history * @param {string} conversationId * @returns {Promise} */ export async function GetDialogs(conversationId) { + setAuthorization(); let url = dialogsUrl.replace("{conversationId}", conversationId); const response = await axios.get(url); return response.data; diff --git a/src/web-live-chat/src/lib/services/signalr-service.js b/src/web-live-chat/src/lib/services/signalr-service.js index 4ac401f3..79da13cc 100644 --- a/src/web-live-chat/src/lib/services/signalr-service.js +++ b/src/web-live-chat/src/lib/services/signalr-service.js @@ -1,5 +1,6 @@ import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr'; import { chatHubUrl } from '$lib/services/api-endpoints.js'; +import { getUserStore } from '$lib/helpers/store.js'; // create a writable store to store the connection object /** @type {HubConnection} */ @@ -21,10 +22,12 @@ export const signalr = { onMessageReceivedFromAssistant: () => {}, // start the connection - async start() { + /** @param {string} conversationId */ + async start(conversationId) { // create a new connection object with the hub URL and some options + let user = getUserStore(); connection = new HubConnectionBuilder() - .withUrl(chatHubUrl) // the hub URL, change it according to your server + .withUrl(chatHubUrl + `?conversationId=${conversationId}&access_token=${user.token}`) // the hub URL, change it according to your server .withAutomaticReconnect() // enable automatic reconnection .configureLogging(LogLevel.Information) // configure the logging level .build(); diff --git a/src/web-live-chat/src/routes/chat/[agentId]/+page.svelte b/src/web-live-chat/src/routes/chat/[agentId]/+page.svelte index 9492b4ab..7e66a60f 100644 --- a/src/web-live-chat/src/routes/chat/[agentId]/+page.svelte +++ b/src/web-live-chat/src/routes/chat/[agentId]/+page.svelte @@ -5,7 +5,7 @@ import { page } from '$app/stores'; import { onMount } from 'svelte'; import { newConversation } from '$lib/services/conversation-service.js'; - import { getToken } from '$lib/services/auth-service.js' + import { getToken, setToken } from '$lib/services/auth-service.js' import { setAuthorization } from '$lib/helpers/http'; const params = $page.params; @@ -17,21 +17,19 @@ let agentId = params.agentId; onMount(async () => { - let userToken = ""; if(!$page.url.searchParams.has('token')) { await getToken("guest@gmail.com", "123456", (token) => { - userToken = token; }); } else { - userToken = $page.url.searchParams.get('token') ?? "unauthorized"; + let token = $page.url.searchParams.get('token') ?? "unauthorized"; + setToken(token); } - setAuthorization(userToken); // new conversation conversation = await newConversation(agentId); conversationId = conversation.id; - window.location.href = `/chat/${agentId}/${conversationId}?token=${userToken}`; + window.location.href = `/chat/${agentId}/${conversationId}`; }); diff --git a/src/web-live-chat/src/routes/chat/[agentId]/[conversationId]/+page.svelte b/src/web-live-chat/src/routes/chat/[agentId]/[conversationId]/+page.svelte index 43ccc78e..1a639d16 100644 --- a/src/web-live-chat/src/routes/chat/[agentId]/[conversationId]/+page.svelte +++ b/src/web-live-chat/src/routes/chat/[agentId]/[conversationId]/+page.svelte @@ -2,7 +2,6 @@ import Chat from './chat-box.svelte'; import { page } from '$app/stores'; import { onMount } from 'svelte'; - import { setAuthorization } from '$lib/helpers/http'; import { myInfo } from '$lib/services/auth-service.js'; import { getAgent } from '$lib/services/agent-service.js'; @@ -15,10 +14,7 @@ let currentUser; onMount(async () => { - const token = $page.url.searchParams.get('token') ?? "unauthorized"; - setAuthorization(token); - - currentUser = await myInfo(token); + currentUser = await myInfo(); // get agent profile let agentId = params.agentId; diff --git a/src/web-live-chat/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte b/src/web-live-chat/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte index a3707493..d9af18a9 100644 --- a/src/web-live-chat/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte +++ b/src/web-live-chat/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte @@ -13,9 +13,8 @@ import { page } from '$app/stores'; import { onMount } from 'svelte'; import Link from 'svelte-link'; - import { setAuthorization } from '$lib/helpers/http'; import { signalr } from '$lib/services/signalr-service.js'; - import { sendMessageToHub, GetDialogs, conversationReady } from '$lib/services/conversation-service.js'; + import { sendMessageToHub, GetDialogs } from '$lib/services/conversation-service.js'; const options = { scrollbars: { @@ -45,17 +44,12 @@ let dialogs = []; onMount(async () => { - const token = $page.url.searchParams.get('token') ?? "unauthorized"; - setAuthorization(token); - dialogs = await GetDialogs(params.conversationId); signalr.onMessageReceivedFromClient = onMessageReceivedFromClient; signalr.onMessageReceivedFromCsr = onMessageReceivedFromCsr; signalr.onMessageReceivedFromAssistant = onMessageReceivedFromAssistant; - await signalr.start(); - - await conversationReady(params.conversationId); + await signalr.start(params.conversationId); const scrollElements = document.querySelectorAll('.scrollbar'); scrollElements.forEach((item) => { diff --git a/src/web-live-chat/src/routes/login/+page.svelte b/src/web-live-chat/src/routes/login/+page.svelte index 3f2f7043..c29cfbd3 100644 --- a/src/web-live-chat/src/routes/login/+page.svelte +++ b/src/web-live-chat/src/routes/login/+page.svelte @@ -15,7 +15,6 @@ import Headtitle from '$lib/common/HeadTitle.svelte'; import { getToken } from '$lib/services/auth-service.js' import { goto } from '$app/navigation'; - import { setAuthorization } from '$lib/helpers/http'; let username = 'guest@gmail.com'; let password = ''; @@ -29,8 +28,7 @@ isOpen = true; msg = 'Authentication success'; status = 'success'; - setAuthorization(token); - goto(`/chat/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a?token=${token}`); + goto(`/chat/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a`); }); }