import axios from 'axios'; import httpClient from '../config/httpClient'; import environment from '../config/environment'; import logger from '../utils/logger'; interface NextcloudLastMessage { text: string; author: string; timestamp: number; } interface NextcloudConversation { token: string; displayName: string; unreadMessages: number; lastActivity: number; lastMessage: NextcloudLastMessage | null; type: number; url: string; } interface ConversationsResult { totalUnread: number; conversations: NextcloudConversation[]; } interface LoginFlowResult { loginUrl: string; pollToken: string; pollEndpoint: string; } interface LoginFlowCredentials { loginName: string; appPassword: string; } /** * Validates that a URL is safe to use as an outbound service endpoint. * Rejects non-http(s) protocols and private/loopback IP ranges to prevent SSRF. */ function isValidServiceUrl(raw: string): boolean { let parsed: URL; try { parsed = new URL(raw); } catch { return false; } if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { return false; } const hostname = parsed.hostname.toLowerCase(); // Reject plain loopback / localhost names if (hostname === 'localhost' || hostname === '::1') { return false; } // Reject numeric IPv4 private / loopback / link-local ranges const ipv4Parts = hostname.split('.'); if (ipv4Parts.length === 4) { const [a, b] = ipv4Parts.map(Number); if ( a === 127 || // 127.0.0.0/8 loopback a === 10 || // 10.0.0.0/8 private (a === 172 && b >= 16 && b <= 31) || // 172.16.0.0/12 private (a === 192 && b === 168) || // 192.168.0.0/16 private (a === 169 && b === 254) // 169.254.0.0/16 link-local ) { return false; } } return true; } async function initiateLoginFlow(): Promise { const baseUrl = environment.nextcloudUrl; if (!baseUrl || !isValidServiceUrl(baseUrl)) { throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL'); } try { const response = await httpClient.post(`${baseUrl}/index.php/login/v2`); return { loginUrl: response.data.login, pollToken: response.data.poll.token, pollEndpoint: response.data.poll.endpoint, }; } catch (error) { if (axios.isAxiosError(error)) { logger.error('Nextcloud Login Flow v2 initiation failed', { status: error.response?.status, statusText: error.response?.statusText, }); } logger.error('NextcloudService.initiateLoginFlow failed', { error }); throw new Error('Failed to initiate Nextcloud login flow'); } } async function pollLoginFlow(pollEndpoint: string, pollToken: string): Promise { if (!isValidServiceUrl(pollEndpoint)) { throw new Error('pollEndpoint is not a valid service URL'); } try { const response = await httpClient.post(pollEndpoint, `token=${pollToken}`, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); return { loginName: response.data.loginName, appPassword: response.data.appPassword, }; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 404) { return null; } if (axios.isAxiosError(error)) { logger.error('Nextcloud Login Flow v2 poll failed', { status: error.response?.status, statusText: error.response?.statusText, }); } logger.error('NextcloudService.pollLoginFlow failed', { error }); throw new Error('Failed to poll Nextcloud login flow'); } } interface NextcloudChatMessage { id: number; token: string; actorType: string; actorId: string; actorDisplayName: string; message: string; timestamp: number; messageType: string; systemMessage: string; messageParameters: Record; reactions: Record; reactionsSelf: string[]; parent?: NextcloudChatMessage; } async function getAllConversations(loginName: string, appPassword: string): Promise { const baseUrl = environment.nextcloudUrl; if (!baseUrl || !isValidServiceUrl(baseUrl)) { throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL'); } try { const response = await httpClient.get( `${baseUrl}/ocs/v2.php/apps/spreed/api/v4/room?format=json`, { headers: { 'Authorization': `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`, 'OCS-APIRequest': 'true', 'Accept': 'application/json', }, }, ); const rooms: any[] = response.data?.ocs?.data ?? []; return rooms .filter((r: any) => r.type !== 4) .sort((a: any, b: any) => (b.lastActivity ?? 0) - (a.lastActivity ?? 0)) .map((r: any) => ({ token: r.token, displayName: r.displayName, unreadMessages: r.unreadMessages ?? 0, lastActivity: r.lastActivity ?? 0, lastMessage: r.lastMessage ? { text: r.lastMessage.message ?? '', author: r.lastMessage.actorDisplayName ?? '', timestamp: r.lastMessage.timestamp ?? 0, } : null, type: r.type, url: `${baseUrl}/call/${r.token}`, })); } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { const err = new Error('Nextcloud app password is invalid'); (err as any).code = 'NEXTCLOUD_AUTH_INVALID'; throw err; } if (axios.isAxiosError(error)) { logger.error('Nextcloud getAllConversations failed', { status: error.response?.status, statusText: error.response?.statusText, }); throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`); } logger.error('NextcloudService.getAllConversations failed', { error }); throw new Error('Failed to fetch Nextcloud conversations'); } } interface GetMessagesOptions { lookIntoFuture?: boolean; lastKnownMessageId?: number; timeout?: number; } async function getMessages(token: string, loginName: string, appPassword: string, options?: GetMessagesOptions): Promise { const baseUrl = environment.nextcloudUrl; if (!baseUrl || !isValidServiceUrl(baseUrl)) { throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL'); } const lookIntoFuture = options?.lookIntoFuture ?? false; const ncTimeout = options?.timeout ?? 25; const params: Record = { lookIntoFuture: lookIntoFuture ? 1 : 0, limit: lookIntoFuture ? 100 : 50, setReadMarker: 0, }; if (lookIntoFuture) { params.lastKnownMessageId = options?.lastKnownMessageId ?? 0; params.timeout = ncTimeout; } try { const response = await httpClient.get( `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/chat/${encodeURIComponent(token)}`, { params, headers: { 'Authorization': `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`, 'OCS-APIRequest': 'true', 'Accept': 'application/json', }, ...(lookIntoFuture && { timeout: (ncTimeout + 5) * 1000, validateStatus: (s: number) => (s >= 200 && s < 300) || s === 304, }), }, ); if (response.status === 304) { return []; } const messages: any[] = response.data?.ocs?.data ?? []; return messages.map((m: any) => ({ id: m.id, token: m.token, actorType: m.actorType, actorId: m.actorId, actorDisplayName: m.actorDisplayName, message: m.message, timestamp: m.timestamp, messageType: m.messageType ?? '', systemMessage: m.systemMessage ?? '', messageParameters: m.messageParameters ?? {}, reactions: m.reactions ?? {}, reactionsSelf: m.reactionsSelf ?? [], ...(m.parent ? { parent: { id: m.parent.id, token: m.parent.token, actorType: m.parent.actorType, actorId: m.parent.actorId, actorDisplayName: m.parent.actorDisplayName, message: m.parent.message, timestamp: m.parent.timestamp, messageType: m.parent.messageType ?? '', systemMessage: m.parent.systemMessage ?? '', messageParameters: m.parent.messageParameters ?? {}, reactions: m.parent.reactions ?? {}, reactionsSelf: m.parent.reactionsSelf ?? [], }} : {}), })); } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { const err = new Error('Nextcloud authentication invalid'); (err as any).code = 'NEXTCLOUD_AUTH_INVALID'; throw err; } if (axios.isAxiosError(error)) { logger.error('NextcloudService.getMessages failed', { status: error.response?.status, statusText: error.response?.statusText, }); throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`); } logger.error('NextcloudService.getMessages failed', { error }); throw new Error('Failed to fetch messages'); } } async function sendMessage(token: string, message: string, loginName: string, appPassword: string, replyTo?: number): Promise { const baseUrl = environment.nextcloudUrl; if (!baseUrl || !isValidServiceUrl(baseUrl)) { throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL'); } try { await httpClient.post( `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/chat/${encodeURIComponent(token)}`, { message, ...(replyTo !== undefined ? { replyTo } : {}) }, { headers: { 'Authorization': `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`, 'OCS-APIRequest': 'true', 'Accept': 'application/json', 'Content-Type': 'application/json', }, }, ); } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { const err = new Error('Nextcloud authentication invalid'); (err as any).code = 'NEXTCLOUD_AUTH_INVALID'; throw err; } if (axios.isAxiosError(error)) { logger.error('NextcloudService.sendMessage failed', { status: error.response?.status, statusText: error.response?.statusText, }); throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`); } logger.error('NextcloudService.sendMessage failed', { error }); throw new Error('Failed to send message'); } } async function markAsRead(token: string, loginName: string, appPassword: string): Promise { const baseUrl = environment.nextcloudUrl; if (!baseUrl || !isValidServiceUrl(baseUrl)) { throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL'); } try { await httpClient.post( `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/chat/${encodeURIComponent(token)}/read`, { lastReadMessage: null }, { headers: { 'Authorization': `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`, 'OCS-APIRequest': 'true', 'Accept': 'application/json', 'Content-Type': 'application/json', }, }, ); } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { const err = new Error('Nextcloud authentication invalid'); (err as any).code = 'NEXTCLOUD_AUTH_INVALID'; throw err; } if (axios.isAxiosError(error)) { logger.error('NextcloudService.markAsRead failed', { status: error.response?.status, statusText: error.response?.statusText, }); throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`); } logger.error('NextcloudService.markAsRead failed', { error }); throw new Error('Failed to mark conversation as read'); } } async function getConversations(loginName: string, appPassword: string): Promise { const baseUrl = environment.nextcloudUrl; if (!baseUrl || !isValidServiceUrl(baseUrl)) { throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL'); } try { const response = await httpClient.get( `${baseUrl}/ocs/v2.php/apps/spreed/api/v4/room?format=json`, { headers: { 'Authorization': `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`, 'OCS-APIRequest': 'true', 'Accept': 'application/json', }, }, ); const rooms: any[] = response.data?.ocs?.data ?? []; const filtered = rooms.filter((r: any) => r.type !== 4); const totalUnread = filtered.reduce( (sum: number, r: any) => sum + (r.unreadMessages ?? 0), 0, ); const sorted = [...filtered].sort( (a: any, b: any) => (b.lastActivity ?? 0) - (a.lastActivity ?? 0), ); const conversations: NextcloudConversation[] = sorted.slice(0, 3).map((r: any) => ({ token: r.token, displayName: r.displayName, unreadMessages: r.unreadMessages ?? 0, lastActivity: r.lastActivity ?? 0, lastMessage: r.lastMessage ? { text: r.lastMessage.message ?? '', author: r.lastMessage.actorDisplayName ?? '', timestamp: r.lastMessage.timestamp ?? 0, } : null, type: r.type, url: `${baseUrl}/call/${r.token}`, })); return { totalUnread, conversations }; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { const err = new Error('Nextcloud app password is invalid'); (err as any).code = 'NEXTCLOUD_AUTH_INVALID'; throw err; } if (axios.isAxiosError(error)) { logger.error('Nextcloud API request failed', { status: error.response?.status, statusText: error.response?.statusText, }); throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`); } logger.error('NextcloudService.getConversations failed', { error }); throw new Error('Failed to fetch Nextcloud conversations'); } } async function uploadFileToTalk( token: string, fileBuffer: Buffer, filename: string, mimetype: string, loginName: string, appPassword: string, ): Promise { const baseUrl = environment.nextcloudUrl; if (!baseUrl || !isValidServiceUrl(baseUrl)) { throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL'); } const authHeader = `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`; // Add timestamp to avoid filename conflicts const ext = filename.includes('.') ? filename.slice(filename.lastIndexOf('.')) : ''; const base = filename.includes('.') ? filename.slice(0, filename.lastIndexOf('.')) : filename; const safeName = `${base}_${Date.now()}${ext}`; const remotePath = `/Talk/${safeName}`; try { // Step 1: Upload via WebDAV await httpClient.put( `${baseUrl}/remote.php/dav/files/${encodeURIComponent(loginName)}${remotePath}`, fileBuffer, { headers: { 'Authorization': authHeader, 'Content-Type': mimetype, }, }, ); // Step 2: Share file to room await httpClient.post( `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/chat/${encodeURIComponent(token)}/share`, { path: remotePath, shareType: 10 }, { headers: { 'Authorization': authHeader, 'OCS-APIRequest': 'true', 'Accept': 'application/json', 'Content-Type': 'application/json', }, }, ); } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { const err = new Error('Nextcloud authentication invalid'); (err as any).code = 'NEXTCLOUD_AUTH_INVALID'; throw err; } if (axios.isAxiosError(error)) { logger.error('NextcloudService.uploadFileToTalk failed', { status: error.response?.status, statusText: error.response?.statusText, }); throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`); } logger.error('NextcloudService.uploadFileToTalk failed', { error }); throw new Error('Failed to upload file to Nextcloud Talk'); } } async function downloadFile( filePath: string, loginName: string, appPassword: string, ): Promise { const baseUrl = environment.nextcloudUrl; if (!baseUrl || !isValidServiceUrl(baseUrl)) { throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL'); } const authHeader = `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`; try { const response = await httpClient.get( `${baseUrl}/remote.php/dav/files/${encodeURIComponent(loginName)}/${filePath.replace(/^\//, '')}`, { headers: { 'Authorization': authHeader }, responseType: 'stream', }, ); return response; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { const err = new Error('Nextcloud authentication invalid'); (err as any).code = 'NEXTCLOUD_AUTH_INVALID'; throw err; } if (axios.isAxiosError(error)) { logger.error('NextcloudService.downloadFile failed', { status: error.response?.status, statusText: error.response?.statusText, }); throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`); } logger.error('NextcloudService.downloadFile failed', { error }); throw new Error('Failed to download file from Nextcloud'); } } async function getFilePreview( fileId: number, width: number, height: number, loginName: string, appPassword: string, ): Promise { const baseUrl = environment.nextcloudUrl; if (!baseUrl || !isValidServiceUrl(baseUrl)) { throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL'); } const authHeader = `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`; try { const response = await httpClient.get( `${baseUrl}/index.php/core/preview`, { params: { fileId, x: width, y: height, a: 1 }, headers: { 'Authorization': authHeader }, responseType: 'stream', }, ); return response; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { const err = new Error('Nextcloud authentication invalid'); (err as any).code = 'NEXTCLOUD_AUTH_INVALID'; throw err; } if (axios.isAxiosError(error)) { logger.error('NextcloudService.getFilePreview failed', { status: error.response?.status, statusText: error.response?.statusText, }); throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`); } logger.error('NextcloudService.getFilePreview failed', { error }); throw new Error('Failed to get file preview from Nextcloud'); } } async function searchUsers(query: string, loginName: string, appPassword: string): Promise { const baseUrl = environment.nextcloudUrl; if (!baseUrl || !isValidServiceUrl(baseUrl)) { throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL'); } try { const response = await httpClient.get( `${baseUrl}/ocs/v2.php/core/autocomplete/get?search=${encodeURIComponent(query)}&limit=20&itemType=&itemId=&shareTypes[]=0&shareTypes[]=1&shareTypes[]=7&format=json`, { headers: { 'Authorization': `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`, 'OCS-APIRequest': 'true', 'Accept': 'application/json', }, }, ); return (response.data?.ocs?.data ?? []).map((u: any) => ({ id: u.id, label: u.label, source: u.source })); } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { const err = new Error('Nextcloud authentication invalid'); (err as any).code = 'NEXTCLOUD_AUTH_INVALID'; throw err; } if (axios.isAxiosError(error)) { logger.error('NextcloudService.searchUsers failed', { status: error.response?.status, statusText: error.response?.statusText, }); throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`); } logger.error('NextcloudService.searchUsers failed', { error }); throw new Error('Failed to search users'); } } async function createRoom(roomType: number, invite: string, roomName: string | undefined, loginName: string, appPassword: string): Promise<{ token: string }> { const baseUrl = environment.nextcloudUrl; if (!baseUrl || !isValidServiceUrl(baseUrl)) { throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL'); } try { const response = await httpClient.post( `${baseUrl}/ocs/v2.php/apps/spreed/api/v4/room`, { roomType, invite, ...(roomName ? { roomName } : {}) }, { headers: { 'Authorization': `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`, 'OCS-APIRequest': 'true', 'Accept': 'application/json', 'Content-Type': 'application/json', }, }, ); return { token: response.data?.ocs?.data?.token as string }; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { const err = new Error('Nextcloud authentication invalid'); (err as any).code = 'NEXTCLOUD_AUTH_INVALID'; throw err; } if (axios.isAxiosError(error)) { logger.error('NextcloudService.createRoom failed', { status: error.response?.status, statusText: error.response?.statusText, }); throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`); } logger.error('NextcloudService.createRoom failed', { error }); throw new Error('Failed to create room'); } } async function addReaction(token: string, messageId: number, reaction: string, loginName: string, appPassword: string): Promise { const baseUrl = environment.nextcloudUrl; if (!baseUrl || !isValidServiceUrl(baseUrl)) { throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL'); } try { await httpClient.post( `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/reaction/${encodeURIComponent(token)}/${messageId}`, { reaction }, { headers: { 'Authorization': `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`, 'OCS-APIRequest': 'true', 'Accept': 'application/json', 'Content-Type': 'application/json', }, }, ); } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { const err = new Error('Nextcloud authentication invalid'); (err as any).code = 'NEXTCLOUD_AUTH_INVALID'; throw err; } if (axios.isAxiosError(error)) { logger.error('NextcloudService.addReaction failed', { status: error.response?.status, statusText: error.response?.statusText, }); throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`); } logger.error('NextcloudService.addReaction failed', { error }); throw new Error('Failed to add reaction'); } } async function removeReaction(token: string, messageId: number, reaction: string, loginName: string, appPassword: string): Promise { const baseUrl = environment.nextcloudUrl; if (!baseUrl || !isValidServiceUrl(baseUrl)) { throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL'); } try { await httpClient.delete( `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/reaction/${encodeURIComponent(token)}/${messageId}`, { params: { reaction }, headers: { 'Authorization': `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`, 'OCS-APIRequest': 'true', 'Accept': 'application/json', }, }, ); } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { const err = new Error('Nextcloud authentication invalid'); (err as any).code = 'NEXTCLOUD_AUTH_INVALID'; throw err; } if (axios.isAxiosError(error)) { logger.error('NextcloudService.removeReaction failed', { status: error.response?.status, statusText: error.response?.statusText, }); throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`); } logger.error('NextcloudService.removeReaction failed', { error }); throw new Error('Failed to remove reaction'); } } async function getReactions(token: string, messageId: number, loginName: string, appPassword: string): Promise> { const baseUrl = environment.nextcloudUrl; if (!baseUrl || !isValidServiceUrl(baseUrl)) { throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL'); } try { const response = await httpClient.get( `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/reaction/${encodeURIComponent(token)}/${messageId}`, { headers: { 'Authorization': `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`, 'OCS-APIRequest': 'true', 'Accept': 'application/json', }, }, ); return response.data?.ocs?.data ?? {}; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { const err = new Error('Nextcloud authentication invalid'); (err as any).code = 'NEXTCLOUD_AUTH_INVALID'; throw err; } if (axios.isAxiosError(error)) { logger.error('NextcloudService.getReactions failed', { status: error.response?.status, statusText: error.response?.statusText, }); throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`); } logger.error('NextcloudService.getReactions failed', { error }); throw new Error('Failed to get reactions'); } } async function getPollDetails(token: string, pollId: number, loginName: string, appPassword: string): Promise> { const baseUrl = environment.nextcloudUrl; if (!baseUrl || !isValidServiceUrl(baseUrl)) { throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL'); } try { const response = await httpClient.get( `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/poll/${encodeURIComponent(token)}/${pollId}`, { headers: { 'Authorization': `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`, 'OCS-APIRequest': 'true', 'Accept': 'application/json', }, }, ); return response.data?.ocs?.data ?? {}; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { const err = new Error('Nextcloud authentication invalid'); (err as any).code = 'NEXTCLOUD_AUTH_INVALID'; throw err; } if (axios.isAxiosError(error)) { logger.error('NextcloudService.getPollDetails failed', { status: error.response?.status, statusText: error.response?.statusText, }); throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`); } logger.error('NextcloudService.getPollDetails failed', { error }); throw new Error('Failed to get poll details'); } } export type { NextcloudChatMessage, GetMessagesOptions }; export default { initiateLoginFlow, pollLoginFlow, getConversations, getAllConversations, getMessages, sendMessage, markAsRead, uploadFileToTalk, downloadFile, getFilePreview, searchUsers, createRoom, addReaction, removeReaction, getReactions, getPollDetails };