Compare commits
1 Commits
0da4f0d6cb
...
82b22fa739
| Author | SHA1 | Date | |
|---|---|---|---|
| 82b22fa739 |
@@ -55,6 +55,13 @@ curl -X POST http://127.0.0.1:33384/message/current \
|
|||||||
| `/channels` | GET | Get list of rooms |
|
| `/channels` | GET | Get list of rooms |
|
||||||
| `/channel` | POST | Switch to a room |
|
| `/channel` | POST | Switch to a room |
|
||||||
| `/message/current` | POST | Send message to current room |
|
| `/message/current` | POST | Send message to current room |
|
||||||
|
| `/room/current` | GET | Get current room info |
|
||||||
|
| `/messages/groups` | GET | Last N messages across groups (`?limit=10`) |
|
||||||
|
| `/messages/dms` | GET | Last N messages across DMs (`?limit=10`) |
|
||||||
|
| `/messages/combined` | GET | Last N messages across groups + DMs (`?limit=10`) |
|
||||||
|
| `/unreads/groups` | GET | Unread group conversations |
|
||||||
|
| `/unreads/dms` | GET | Unread DM conversations |
|
||||||
|
| `/unreads/combined` | GET | Unread groups + DMs |
|
||||||
|
|
||||||
## 🎮 Stream Deck Integration
|
## 🎮 Stream Deck Integration
|
||||||
|
|
||||||
|
|||||||
117
docs/API.md
117
docs/API.md
@@ -280,6 +280,123 @@ Get information about the currently active room.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Get Recent Messages (Groups)
|
||||||
|
|
||||||
|
Get the most recent messages across joined group channels (non-DMs).
|
||||||
|
|
||||||
|
**Endpoint**: `GET /messages/groups`
|
||||||
|
|
||||||
|
**Query Parameters**:
|
||||||
|
- `limit` (optional) — number of messages to return (1–100, default `10`)
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"scope": "groups",
|
||||||
|
"limit": 10,
|
||||||
|
"count": 2,
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"eventId": "$event1",
|
||||||
|
"roomId": "!abc123:matrix.org",
|
||||||
|
"roomName": "General",
|
||||||
|
"isDirect": false,
|
||||||
|
"sender": "@alice:matrix.org",
|
||||||
|
"senderName": "Alice",
|
||||||
|
"timestamp": 1710000000000,
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "Hello!"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Get Recent Messages (DMs)
|
||||||
|
|
||||||
|
Get the most recent messages across joined direct messages.
|
||||||
|
|
||||||
|
**Endpoint**: `GET /messages/dms`
|
||||||
|
|
||||||
|
**Query Parameters**:
|
||||||
|
- `limit` (optional) — number of messages to return (1–100, default `10`)
|
||||||
|
|
||||||
|
**Response**: same shape as groups, with `"scope": "dms"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Get Recent Messages (Combined)
|
||||||
|
|
||||||
|
Get the most recent messages across groups and DMs together.
|
||||||
|
|
||||||
|
**Endpoint**: `GET /messages/combined`
|
||||||
|
|
||||||
|
**Query Parameters**:
|
||||||
|
- `limit` (optional) — number of messages to return (1–100, default `10`)
|
||||||
|
|
||||||
|
**Response**: same shape as groups, with `"scope": "combined"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Get Unread Conversations (Groups)
|
||||||
|
|
||||||
|
List joined group channels that currently have unread activity.
|
||||||
|
|
||||||
|
**Endpoint**: `GET /unreads/groups`
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"scope": "groups",
|
||||||
|
"count": 1,
|
||||||
|
"unreads": [
|
||||||
|
{
|
||||||
|
"roomId": "!abc123:matrix.org",
|
||||||
|
"name": "General",
|
||||||
|
"isDirect": false,
|
||||||
|
"avatar": "mxc://matrix.org/abc123",
|
||||||
|
"total": 3,
|
||||||
|
"highlight": 1,
|
||||||
|
"latest": {
|
||||||
|
"eventId": "$event1",
|
||||||
|
"sender": "@alice:matrix.org",
|
||||||
|
"senderName": "Alice",
|
||||||
|
"timestamp": 1710000000000,
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "Hello!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Muted rooms are excluded. Sorted by latest activity, then highlight/total counts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Get Unread Conversations (DMs)
|
||||||
|
|
||||||
|
**Endpoint**: `GET /unreads/dms`
|
||||||
|
|
||||||
|
**Response**: same shape as groups, with `"scope": "dms"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Get Unread Conversations (Combined)
|
||||||
|
|
||||||
|
**Endpoint**: `GET /unreads/combined`
|
||||||
|
|
||||||
|
**Response**: same shape as groups, with `"scope": "combined"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
All endpoints return error responses in the following format:
|
All endpoints return error responses in the following format:
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ No persistent storage. Actions read/write ephemeral app state (current room, cal
|
|||||||
| `send-message` | `{ roomId, message }` | send result |
|
| `send-message` | `{ roomId, message }` | send result |
|
||||||
| `send-message-current` | `{ message }` | send to active room |
|
| `send-message-current` | `{ message }` | send to active room |
|
||||||
| `get-current-room` | — | current room id |
|
| `get-current-room` | — | current room id |
|
||||||
|
| `get-messages-groups` | `{ limit? }` | recent messages across group channels |
|
||||||
|
| `get-messages-dms` | `{ limit? }` | recent messages across DMs |
|
||||||
|
| `get-messages-combined` | `{ limit? }` | recent messages across groups + DMs |
|
||||||
|
| `get-unreads-groups` | — | unread group conversations |
|
||||||
|
| `get-unreads-dms` | — | unread DM conversations |
|
||||||
|
| `get-unreads-combined` | — | unread groups + DMs |
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,14 @@ import { MatrixClient } from 'matrix-js-sdk';
|
|||||||
import { getHomeRoomPath, getDirectRoomPath, getSpaceRoomPath } from './pages/pathUtils';
|
import { getHomeRoomPath, getDirectRoomPath, getSpaceRoomPath } from './pages/pathUtils';
|
||||||
import { getCanonicalAliasOrRoomId, isRoomId } from './utils/matrix';
|
import { getCanonicalAliasOrRoomId, isRoomId } from './utils/matrix';
|
||||||
import { getCallService } from './features/call/useCall';
|
import { getCallService } from './features/call/useCall';
|
||||||
import { RoomType, StateEvent } from '../types/matrix/room';
|
import {
|
||||||
|
getNotificationType,
|
||||||
|
getUnreadInfo,
|
||||||
|
reactionOrEditEvent,
|
||||||
|
roomHaveNotification,
|
||||||
|
roomHaveUnread,
|
||||||
|
} from './utils/room';
|
||||||
|
import { MessageEvent, NotificationType, RoomType, StateEvent } from '../types/matrix/room';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global reference to the router navigate function
|
* Global reference to the router navigate function
|
||||||
@@ -120,6 +127,30 @@ export function initPaarrotAPI(matrixClient: MatrixClient) {
|
|||||||
result = await getCurrentRoom(matrixClient);
|
result = await getCurrentRoom(matrixClient);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'get-messages-groups':
|
||||||
|
result = await getMessagesByScope(matrixClient, 'groups', action.params.limit);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get-messages-dms':
|
||||||
|
result = await getMessagesByScope(matrixClient, 'dms', action.params.limit);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get-messages-combined':
|
||||||
|
result = await getMessagesByScope(matrixClient, 'combined', action.params.limit);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get-unreads-groups':
|
||||||
|
result = await getUnreadsByScope(matrixClient, 'groups');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get-unreads-dms':
|
||||||
|
result = await getUnreadsByScope(matrixClient, 'dms');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get-unreads-combined':
|
||||||
|
result = await getUnreadsByScope(matrixClient, 'combined');
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown action: ${action.action}`);
|
throw new Error(`Unknown action: ${action.action}`);
|
||||||
}
|
}
|
||||||
@@ -410,6 +441,207 @@ async function getCurrentRoom(matrixClient: any) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_MESSAGE_LIMIT = 10;
|
||||||
|
const MAX_MESSAGE_LIMIT = 100;
|
||||||
|
|
||||||
|
type MessageScope = 'groups' | 'dms' | 'combined';
|
||||||
|
|
||||||
|
type ApiMessage = {
|
||||||
|
eventId: string;
|
||||||
|
roomId: string;
|
||||||
|
roomName: string;
|
||||||
|
isDirect: boolean;
|
||||||
|
sender: string;
|
||||||
|
senderName: string;
|
||||||
|
timestamp: number;
|
||||||
|
msgtype: string;
|
||||||
|
body: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isDirectRoom(room: any): boolean {
|
||||||
|
return room?.guessDMUserId?.() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMessageLimit(limit?: number | string): number {
|
||||||
|
const parsedLimit = Number(limit);
|
||||||
|
return Math.min(
|
||||||
|
Math.max(Number.isFinite(parsedLimit) ? Math.floor(parsedLimit) : DEFAULT_MESSAGE_LIMIT, 1),
|
||||||
|
MAX_MESSAGE_LIMIT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect recent room messages from the local live timeline (newest last).
|
||||||
|
* Skips reactions, edits, and redacted events.
|
||||||
|
*/
|
||||||
|
function collectRecentMessages(room: any, limit: number): ApiMessage[] {
|
||||||
|
const liveEvents = room.getLiveTimeline?.()?.getEvents?.() ?? [];
|
||||||
|
const isDirect = isDirectRoom(room);
|
||||||
|
const roomId = room.roomId;
|
||||||
|
const roomName = room.name || 'Unnamed Room';
|
||||||
|
const messages: ApiMessage[] = [];
|
||||||
|
|
||||||
|
for (let i = liveEvents.length - 1; i >= 0 && messages.length < limit; i -= 1) {
|
||||||
|
const evt = liveEvents[i];
|
||||||
|
if (!evt) continue;
|
||||||
|
if (typeof evt.isRedacted === 'function' && evt.isRedacted()) continue;
|
||||||
|
if (reactionOrEditEvent(evt)) continue;
|
||||||
|
|
||||||
|
const type = evt.getType?.();
|
||||||
|
if (
|
||||||
|
type !== MessageEvent.RoomMessage &&
|
||||||
|
type !== MessageEvent.RoomMessageEncrypted &&
|
||||||
|
type !== MessageEvent.Sticker
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = evt.getContent?.() ?? {};
|
||||||
|
const sender = evt.getSender?.() ?? '';
|
||||||
|
const member = sender ? room.getMember?.(sender) : null;
|
||||||
|
|
||||||
|
if (type === MessageEvent.RoomMessageEncrypted && !content.msgtype) {
|
||||||
|
messages.push({
|
||||||
|
eventId: evt.getId?.() ?? '',
|
||||||
|
roomId,
|
||||||
|
roomName,
|
||||||
|
isDirect,
|
||||||
|
sender,
|
||||||
|
senderName: member?.name ?? sender,
|
||||||
|
timestamp: evt.getTs?.() ?? 0,
|
||||||
|
msgtype: 'm.encrypted',
|
||||||
|
body: '[encrypted]',
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.push({
|
||||||
|
eventId: evt.getId?.() ?? '',
|
||||||
|
roomId,
|
||||||
|
roomName,
|
||||||
|
isDirect,
|
||||||
|
sender,
|
||||||
|
senderName: member?.name ?? sender,
|
||||||
|
timestamp: evt.getTs?.() ?? 0,
|
||||||
|
msgtype: content.msgtype || (type === MessageEvent.Sticker ? 'm.sticker' : 'm.text'),
|
||||||
|
body: content.body || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoomsForScope(matrixClient: any, scope: MessageScope) {
|
||||||
|
const rooms = (matrixClient?.getRooms?.() || []).filter(
|
||||||
|
(room: any) => !isSpaceLikeRoom(room) && room.getMyMembership?.() === 'join'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (scope === 'groups') {
|
||||||
|
return rooms.filter((room: any) => !isDirectRoom(room));
|
||||||
|
}
|
||||||
|
if (scope === 'dms') {
|
||||||
|
return rooms.filter((room: any) => isDirectRoom(room));
|
||||||
|
}
|
||||||
|
return rooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recent messages across groups, DMs, or both (combined).
|
||||||
|
*/
|
||||||
|
async function getMessagesByScope(
|
||||||
|
matrixClient: any,
|
||||||
|
scope: MessageScope,
|
||||||
|
limit?: number | string
|
||||||
|
) {
|
||||||
|
const limitNum = parseMessageLimit(limit);
|
||||||
|
const rooms = getRoomsForScope(matrixClient, scope);
|
||||||
|
|
||||||
|
const candidates: ApiMessage[] = [];
|
||||||
|
for (const room of rooms) {
|
||||||
|
candidates.push(...collectRecentMessages(room, limitNum));
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.sort((a, b) => a.timestamp - b.timestamp);
|
||||||
|
const messages = candidates.slice(-limitNum);
|
||||||
|
|
||||||
|
return {
|
||||||
|
scope,
|
||||||
|
limit: limitNum,
|
||||||
|
count: messages.length,
|
||||||
|
messages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List unread conversations across groups, DMs, or both (combined).
|
||||||
|
* Uses the same unread rules as the app UI (notification counts + read receipts, muted excluded).
|
||||||
|
*/
|
||||||
|
async function getUnreadsByScope(matrixClient: any, scope: MessageScope) {
|
||||||
|
const rooms = getRoomsForScope(matrixClient, scope);
|
||||||
|
const unreads: Array<{
|
||||||
|
roomId: string;
|
||||||
|
name: string;
|
||||||
|
isDirect: boolean;
|
||||||
|
avatar: string | null;
|
||||||
|
total: number;
|
||||||
|
highlight: number;
|
||||||
|
latest?: {
|
||||||
|
eventId: string;
|
||||||
|
sender: string;
|
||||||
|
senderName: string;
|
||||||
|
timestamp: number;
|
||||||
|
msgtype: string;
|
||||||
|
body: string;
|
||||||
|
};
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const room of rooms) {
|
||||||
|
if (getNotificationType(matrixClient, room.roomId) === NotificationType.Mute) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!roomHaveNotification(room) && !roomHaveUnread(matrixClient, room)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unreadInfo = getUnreadInfo(room);
|
||||||
|
const latestMessages = collectRecentMessages(room, 1);
|
||||||
|
const latest = latestMessages[0];
|
||||||
|
|
||||||
|
unreads.push({
|
||||||
|
roomId: room.roomId,
|
||||||
|
name: room.name || 'Unnamed Room',
|
||||||
|
isDirect: isDirectRoom(room),
|
||||||
|
avatar: room.getMxcAvatarUrl?.() || null,
|
||||||
|
total: unreadInfo.total,
|
||||||
|
highlight: unreadInfo.highlight,
|
||||||
|
latest: latest
|
||||||
|
? {
|
||||||
|
eventId: latest.eventId,
|
||||||
|
sender: latest.sender,
|
||||||
|
senderName: latest.senderName,
|
||||||
|
timestamp: latest.timestamp,
|
||||||
|
msgtype: latest.msgtype,
|
||||||
|
body: latest.body,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unreads.sort((a, b) => {
|
||||||
|
const aTs = a.latest?.timestamp ?? 0;
|
||||||
|
const bTs = b.latest?.timestamp ?? 0;
|
||||||
|
if (bTs !== aTs) return bTs - aTs;
|
||||||
|
if (b.highlight !== a.highlight) return b.highlight - a.highlight;
|
||||||
|
return b.total - a.total;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
scope,
|
||||||
|
count: unreads.length,
|
||||||
|
unreads,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the currently active room ID
|
* Get the currently active room ID
|
||||||
* Extracts room ID from the current URL pathname
|
* Extracts room ID from the current URL pathname
|
||||||
|
|||||||
Reference in New Issue
Block a user