npm package: @noematicsllc/talk-sdk
Description: UNOFFICIAL TypeScript SDK for Nextcloud Talk API - Not affiliated with or endorsed by Nextcloud
Latest version: 0.2.0

# @noematicsllc/talk-sdk

> **⚠️ UNOFFICIAL SDK**: This is an **unofficial, community-maintained** TypeScript SDK for the Nextcloud Talk API. This package is **NOT** affiliated with, endorsed by, or connected to Nextcloud GmbH or the Nextcloud project. Use at your own risk.

TypeScript SDK for the Nextcloud Talk API.

## Installation

```bash
npm install @noematicsllc/talk-sdk
# or
pnpm add @noematicsllc/talk-sdk
```

## Quick Start

```typescript
import { TalkClient } from '@noematicsllc/talk-sdk';

const client = new TalkClient({
  host: 'https://nextcloud.local',
  username: 'admin',
  password: 'app-password'
});

// List all rooms
const rooms = await client.rooms.list();

// Find a room by token
const targetRoom = rooms.find(r => r.token === 'abcdefgh');

// Send a message
await targetRoom.sendMessage("Automated system check initialized.");
```

## Architecture

The SDK uses a 4-layer architecture:

### 1. Transport Layer (`HttpClient`)

Base HTTP client using the native Fetch API. Automatically injects mandatory OCS headers:
- `OCS-APIRequest: true`
- `Accept: application/json`
- `Authorization: Basic ...`

### 2. Resource Layer

Logical modules mapped to specific API versions:

| Resource | API Version | Base Path |
|----------|-------------|-----------|
| `RoomResource` | v4 | `/api/v4/room` |
| `ParticipantResource` | v4 | `/api/v4/room/{token}/participants` |
| `CallResource` | v4 | `/api/v4/call` |
| `SignalingResource` | v3 | `/api/v3/signaling` |
| `ChatResource` | v1 | `/api/v1/chat` |
| `PollResource` | v1 | `/api/v1/poll` |
| `ReactionResource` | v1 | `/api/v1/reaction` |

### 3. Normalization Layer

DTO mappers that strip the `ocs.meta` and `ocs.data` envelopes, returning clean, flat objects.

### 4. Domain Layer

High-level entities like `Room` that encapsulate both the `token` (string) and `id` (integer) to handle the API's identifier inconsistencies.

## Room Entity

The `Room` class provides a high-level interface for room operations:

```typescript
const room = await client.rooms.get('abcdefgh');

// Chat
await room.sendMessage("Hello!");
const messages = await room.getHistory({ limit: 50 });
const result = await room.pollMessages({ lastKnownMessageId: 123 });

// Participants
const participants = await room.getParticipants();
await room.addParticipant('user-id');

// Calls
await room.joinCall({ flags: 7 });
const peers = await room.getCallPeers();
await room.leaveCall();

// Polls
await room.createPoll({
  question: "What's for lunch?",
  options: ["Pizza", "Sushi", "Tacos"]
});

// Reactions
await room.addReaction(messageId, "👍");
```

## Video Calls

> **⚠️ Experimental/Beta Feature**: The video call functionality (WebRTC, signaling, and `VideoCall` API) is currently in **beta/experimental** status. While the core implementation is complete, it has not been extensively tested in production environments. We recommend thorough testing before using in production applications. The API may change in future versions based on feedback and real-world usage.

### Basic Video Call Setup

```typescript
import { TalkClient, isWebRTCSupported } from '@noematicsllc/talk-sdk';

// Check browser compatibility first
if (!isWebRTCSupported()) {
  console.error('WebRTC is not supported in this browser');
  return;
}

const client = new TalkClient({
  host: 'https://nextcloud.local',
  username: 'admin',
  password: 'app-password'
});

// Start a video call
const videoCall = client.startVideoCall('abc123');

// Join with media
await videoCall.join({
  video: true,
  audio: true,
  video: { width: 1280, height: 720 } // Optional video constraints
});

// Control media
await videoCall.setVideoEnabled(false);
await videoCall.setAudioEnabled(true);

// Leave call
await videoCall.leave();
```

### Custom UI Composability

The VideoCall API is designed for full UI composability - you control how streams are rendered:

#### Event-Based Rendering

```typescript
const videoCall = client.startVideoCall('abc123');

// Render each participant's stream in a separate window
videoCall.on('stream', (stream, peer) => {
  const window = window.open('', `peer-${peer.sessionId}`, 'width=640,height=480');
  if (window) {
    const video = window.document.createElement('video');
    video.srcObject = stream;
    video.autoplay = true;
    video.playsInline = true;
    window.document.body.appendChild(video);
  }
});

// Handle peer join/leave
videoCall.on('peer-joined', (peer) => {
  console.log(`${peer.displayName} joined the call`);
});

videoCall.on('peer-left', (peer) => {
  console.log(`${peer.displayName} left the call`);
});

await videoCall.join({ video: true, audio: true });
```

#### Direct Stream Access

```typescript
// Get all peer streams and render them however you want
const streams = videoCall.getPeerStreams();

streams.forEach((stream, sessionId) => {
  // Render in custom layout, separate windows, canvas, etc.
  renderInCustomLayout(stream, sessionId);
});

// Get local stream for preview
const localStream = videoCall.getLocalStream();
if (localStream) {
  previewElement.srcObject = localStream;
}
```

#### Complete Custom UI Example

```typescript
const videoCall = client.startVideoCall('abc123');

// Custom UI with separate windows for each participant
const participantWindows = new Map<string, Window>();

videoCall.on('stream', (stream, peer) => {
  // Open new window for this participant
  const win = window.open('', `peer-${peer.sessionId}`, 'width=640,height=480');
  if (!win) return;

  win.document.write(`
    <!DOCTYPE html>
    <html>
      <head><title>${peer.displayName}</title></head>
      <body style="margin:0;background:#000;">
        <video autoplay playsinline style="width:100%;height:100%;object-fit:contain;"></video>
      </body>
    </html>
  `);

  win.document.querySelector('video')!.srcObject = stream;
  participantWindows.set(peer.sessionId, win);
});

videoCall.on('peer-left', (peer) => {
  const win = participantWindows.get(peer.sessionId);
  if (win) {
    win.close();
    participantWindows.delete(peer.sessionId);
  }
});

await videoCall.join({ video: true, audio: true });
```

### Low-Level API (Advanced Users)

> **Note**: The low-level WebRTC APIs (`WebRTCManager`, `SignalingConnection`) are also experimental and may change.

For advanced use cases, you can use the low-level WebRTC APIs directly:

```typescript
import { WebRTCManager, SignalingConnection, getUserMedia } from '@noematicsllc/talk-sdk';

// Get signaling settings
const settings = await client.call.getSignalingSettings(token);

// Create signaling connection
const signaling = new SignalingConnection({
  server: settings.server!,
  token,
  helloAuthParams: settings.helloAuthParams,
}, {
  onMessage: (message) => {
    // Handle signaling messages yourself
  }
});

// Get local media
const localStream = await getUserMedia({ video: true, audio: true });

// Create WebRTC manager
const manager = new WebRTCManager({
  signalingSettings: settings,
  roomToken: token,
  sessionId: 'your-session-id',
  handlers: {
    onStream: (stream, peer) => {
      // Handle peer streams
    }
  }
});

manager.setLocalStream(localStream);
await manager.connect();

// Add peers
const peers = await client.call.getPeers(token);
for (const peer of peers) {
  await manager.addPeer(peer);
}
```

### Browser Compatibility

Before using video calls, check browser support:

```typescript
import { checkBrowserCompatibility, requireWebRTC } from '@noematicsllc/talk-sdk';

// Check compatibility
const compat = checkBrowserCompatibility();
if (!compat.webrtcSupported) {
  console.error('WebRTC not supported:', compat.errors);
}

// Or throw error if not supported
requireWebRTC(); // Throws if not supported
```

## Message Polling

Implement efficient message polling using the spec's pattern:

```typescript
let lastKnownMessageId = 0;

while (polling) {
  const result = await room.pollMessages({
    lastKnownMessageId,
    timeout: 30, // Long-poll timeout
  });

  for (const message of result.messages) {
    console.log(`${message.actorDisplayName}: ${message.message}`);
  }

  if (result.lastKnownMessageId) {
    lastKnownMessageId = result.lastKnownMessageId;
  }
}
```

## Direct Resource Access

For advanced use cases, access resources directly:

```typescript
// Direct resource access
const roomData = await client.roomResource.get('token');
const messages = await client.chat.receiveMessages('token', {
  lookIntoFuture: 0,
  limit: 100,
});

// Raw HTTP access
const response = await client.http.get('/custom/endpoint');
```

## Types

All API types are exported for TypeScript users:

```typescript
import {
  TalkRoom,
  TalkChatMessage,
  TalkParticipant,
  ConversationType,
  ParticipantType,
  Permission,
  InCallFlag,
} from '@noematicsllc/talk-sdk';
```

## Error Handling

The SDK throws `HttpClientError` for API errors:

```typescript
import { HttpClientError } from '@noematicsllc/talk-sdk';

try {
  await client.rooms.get('invalid-token');
} catch (error) {
  if (error instanceof HttpClientError) {
    console.error(`HTTP ${error.status}: ${error.message}`);
    console.error(`OCS Status: ${error.ocsStatus}`);
  }
}
```

## Disclaimer

This is an **unofficial, community-maintained** TypeScript SDK for the Nextcloud Talk API. 

- **NOT** affiliated with Nextcloud GmbH
- **NOT** endorsed by the Nextcloud project
- **NOT** officially supported by Nextcloud
- Maintained independently by the community
- Use at your own risk

For official Nextcloud resources and support, please visit [nextcloud.com](https://nextcloud.com) or the [Nextcloud GitHub organization](https://github.com/nextcloud).

## License

MIT