Files
dashboard/backend/src/services/nextcloud.service.ts
Matthias Hochmeister 7245cd577e update
2026-03-13 21:33:18 +01:00

803 lines
27 KiB
TypeScript

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<LoginFlowResult> {
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<LoginFlowCredentials | null> {
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<string, any>;
reactions: Record<string, any>;
reactionsSelf: string[];
parent?: NextcloudChatMessage;
}
async function getAllConversations(loginName: string, appPassword: string): Promise<NextcloudConversation[]> {
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<NextcloudChatMessage[]> {
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<string, any> = {
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<void> {
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<void> {
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<ConversationsResult> {
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<void> {
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<any> {
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<any> {
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<any[]> {
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<void> {
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<void> {
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<Record<string, any>> {
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<Record<string, any>> {
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 };