@relaymessenger/pi 0.1.11-staging.0 → 0.1.11-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +12 -7
- package/dist/native.d.ts +6 -0
- package/dist/native.js +48 -0
- package/dist/pi-channels.d.ts +71 -0
- package/dist/pi-channels.js +143 -0
- package/dist/session.d.ts +2 -0
- package/dist/session.js +15 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,6 +47,52 @@ to the chat the Message came from once Pi reports `agent_settled`. A run that
|
|
|
47
47
|
ends with no words stays silent and sends nothing; a run that fails with no
|
|
48
48
|
words sends `Sorry, something went wrong on my side.`
|
|
49
49
|
|
|
50
|
+
## With pi-channels
|
|
51
|
+
|
|
52
|
+
[pi-channels](https://github.com/espennilsen/pi/tree/main/packages/pi-channels)
|
|
53
|
+
routes Messages between Pi and Telegram, Slack and webhooks. With this
|
|
54
|
+
extension also installed, Relay is one more pi-channels adapter, named
|
|
55
|
+
`relay`: `channel:send` to a Relay chat, routes, cron output and the chat
|
|
56
|
+
bridge all work with it, and Relay Messages arrive on `channel:receive`.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pi install npm:@e9n/pi-channels
|
|
60
|
+
pi install npm:@relaymessenger/pi
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Put a `relay` key in the `pi-channels` settings, beside the routes that name
|
|
64
|
+
your Relay chats (`~/.pi/agent/settings.json`, or `.pi/settings.json` in a
|
|
65
|
+
project):
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"pi-channels": {
|
|
70
|
+
"relay": {
|
|
71
|
+
"agentTokenCommand": ["secret-tool", "lookup", "service", "relay", "username", "agent"],
|
|
72
|
+
"senders": ["alice"]
|
|
73
|
+
},
|
|
74
|
+
"routes": {
|
|
75
|
+
"me": { "adapter": "relay", "recipient": "3f1c9a52-8d4e-4b7a-9c61-2e5f0d8b7a14" }
|
|
76
|
+
},
|
|
77
|
+
"bridge": { "enabled": true }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The token is `RELAY_AGENT_TOKEN`, else whatever `agentTokenCommand` prints; it
|
|
83
|
+
is never logged. `baseURL`, `senders` and `transcribeCpp` work as in session
|
|
84
|
+
mode. Do not add `relay` under `adapters`: pi-channels makes only its own
|
|
85
|
+
adapter types from there, and would report `Unknown adapter type: relay`.
|
|
86
|
+
|
|
87
|
+
The recipient is a Relay chat id. An incoming Message's `sender` is its chat
|
|
88
|
+
id too, so the chat bridge answers in the same chat; the sender's Handle and
|
|
89
|
+
the Message id are in `metadata`. Outgoing text goes through the same answer
|
|
90
|
+
parser as Pi's answers, so `buttons`, `selection`, `place` and the other
|
|
91
|
+
fenced blocks are sent as their parts. Photos and files arrive as
|
|
92
|
+
attachments in a temporary folder; voice notes arrive as their transcript
|
|
93
|
+
when `transcribeCpp` is set. The adapter keeps its own Relay WebSocket, so do
|
|
94
|
+
not also run session mode on the same Agent Token.
|
|
95
|
+
|
|
50
96
|
## Parts, silence and tools
|
|
51
97
|
|
|
52
98
|
Pi's final text carries Relay's parts as fenced blocks, read by the SDK's
|
package/dist/index.d.ts
CHANGED
|
@@ -121,5 +121,10 @@ export declare const repliedContext: (relay: Relay, data: MessageWebhookData) =>
|
|
|
121
121
|
* `answerMessages` makes of it, each on `${key}-${index}`.
|
|
122
122
|
*/
|
|
123
123
|
export declare const sendAnswer: (relay: Relay, data: MessageWebhookData, key: string, answer: string) => Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* Sends an answer to one chat as the Messages `answerMessages` makes of it,
|
|
126
|
+
* each on `${key}-${index}`; the first replies to `replyTo` when given.
|
|
127
|
+
*/
|
|
128
|
+
export declare const sendToChat: (relay: Relay, chatId: string, key: string, answer: string, replyTo?: string) => Promise<void>;
|
|
124
129
|
export declare const runPiChannel: (options: PiChannelOptions, signal?: AbortSignal) => Promise<void>;
|
|
125
130
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -290,9 +290,6 @@ export const repliedContext = async (relay, data) => {
|
|
|
290
290
|
* `answerMessages` makes of it, each on `${key}-${index}`.
|
|
291
291
|
*/
|
|
292
292
|
export const sendAnswer = async (relay, data, key, answer) => {
|
|
293
|
-
const messages = answerMessages(answer);
|
|
294
|
-
if (messages[0]?.error)
|
|
295
|
-
console.error(`Relay: the component block in pi's answer was left as text: ${messages[0].error}.`);
|
|
296
293
|
// An answer to another agent replies to its Message, as a bot's reply
|
|
297
294
|
// names the message it answers (Telegram `reply_parameters.message_id`), so
|
|
298
295
|
// an agent that sent several knows which one it answers. A person's
|
|
@@ -301,9 +298,17 @@ export const sendAnswer = async (relay, data, key, answer) => {
|
|
|
301
298
|
// Turns in one chat already run one after another, so an agent's two
|
|
302
299
|
// messages each get their own answer.
|
|
303
300
|
const opening = data.parts[0]?.type;
|
|
304
|
-
const replyTo = data.sender_handle?.kind === "agent" && opening !== "buttons" && opening !== "selection"
|
|
305
|
-
|
|
306
|
-
|
|
301
|
+
const replyTo = data.sender_handle?.kind === "agent" && opening !== "buttons" && opening !== "selection" ? data.id : undefined;
|
|
302
|
+
await sendToChat(relay, data.chat.id, key, answer, replyTo);
|
|
303
|
+
};
|
|
304
|
+
/**
|
|
305
|
+
* Sends an answer to one chat as the Messages `answerMessages` makes of it,
|
|
306
|
+
* each on `${key}-${index}`; the first replies to `replyTo` when given.
|
|
307
|
+
*/
|
|
308
|
+
export const sendToChat = async (relay, chatId, key, answer, replyTo) => {
|
|
309
|
+
const messages = answerMessages(answer);
|
|
310
|
+
if (messages[0]?.error)
|
|
311
|
+
console.error(`Relay: the component block in pi's answer was left as text: ${messages[0].error}.`);
|
|
307
312
|
for (const [index, message] of messages.entries()) {
|
|
308
313
|
const messageKey = `${key}-${index}`;
|
|
309
314
|
let parts = message.parts;
|
|
@@ -318,7 +323,7 @@ export const sendAnswer = async (relay, data, key, answer) => {
|
|
|
318
323
|
continue;
|
|
319
324
|
}
|
|
320
325
|
}
|
|
321
|
-
await relay.chats.messages.send(
|
|
326
|
+
await relay.chats.messages.send(chatId, { message: { parts, idempotency_key: messageKey, ...(index === 0 && replyTo ? { reply_to: { message_id: replyTo } } : {}) } });
|
|
322
327
|
}
|
|
323
328
|
};
|
|
324
329
|
export const runPiChannel = (options, signal) => new PiChannel(options).run(signal);
|
package/dist/native.d.ts
CHANGED
|
@@ -19,6 +19,12 @@ export interface RelaySettings {
|
|
|
19
19
|
}
|
|
20
20
|
/** The `relay` settings, or none when the file or key is missing or unreadable. */
|
|
21
21
|
export declare const relaySettings: (dir?: string) => Promise<RelaySettings>;
|
|
22
|
+
/**
|
|
23
|
+
* The `relay` key of the `pi-channels` settings, beside pi-channels' own
|
|
24
|
+
* `slack` key: global settings.json, then the project's .pi/settings.json
|
|
25
|
+
* over it, as pi-channels merges its settings. None when absent.
|
|
26
|
+
*/
|
|
27
|
+
export declare const piChannelsRelaySettings: (cwd: string, dir?: string) => Promise<RelaySettings | undefined>;
|
|
22
28
|
/** RELAY_AGENT_TOKEN, else what `agentTokenCommand` prints; never logged. */
|
|
23
29
|
export declare const agentToken: (settings: RelaySettings) => Promise<string | undefined>;
|
|
24
30
|
/**
|
package/dist/native.js
CHANGED
|
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import Relay from "@relaymessenger/sdk";
|
|
6
6
|
import { RELAY_CHAT_ID_ENV, runPiChannel } from "./index.js";
|
|
7
7
|
import { relayTools } from "./tools.js";
|
|
8
|
+
import { registerRelayAdapter, relayChannelAdapter, RELAY_ADAPTER } from "./pi-channels.js";
|
|
8
9
|
import { SessionChannel } from "./session.js";
|
|
9
10
|
import { transcribeCpp } from "./voice.js";
|
|
10
11
|
let controller;
|
|
@@ -21,6 +22,25 @@ export const relaySettings = async (dir = agentDir()) => {
|
|
|
21
22
|
return {};
|
|
22
23
|
}
|
|
23
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* The `relay` key of the `pi-channels` settings, beside pi-channels' own
|
|
27
|
+
* `slack` key: global settings.json, then the project's .pi/settings.json
|
|
28
|
+
* over it, as pi-channels merges its settings. None when absent.
|
|
29
|
+
*/
|
|
30
|
+
export const piChannelsRelaySettings = async (cwd, dir = agentDir()) => {
|
|
31
|
+
const read = async (file) => {
|
|
32
|
+
try {
|
|
33
|
+
const value = JSON.parse(await readFile(file, "utf8"))["pi-channels"]?.relay;
|
|
34
|
+
return value && typeof value === "object" ? value : undefined;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const global = await read(join(dir, "settings.json"));
|
|
41
|
+
const project = await read(join(cwd, ".pi", "settings.json"));
|
|
42
|
+
return global || project ? { ...global, ...project } : undefined;
|
|
43
|
+
};
|
|
24
44
|
/** RELAY_AGENT_TOKEN, else what `agentTokenCommand` prints; never logged. */
|
|
25
45
|
export const agentToken = async (settings) => {
|
|
26
46
|
const env = process.env.RELAY_AGENT_TOKEN?.trim();
|
|
@@ -81,6 +101,33 @@ const attachSession = (pi) => {
|
|
|
81
101
|
channel = undefined;
|
|
82
102
|
});
|
|
83
103
|
};
|
|
104
|
+
/**
|
|
105
|
+
* Relay as a pi-channels adapter, registered with `channel:register` when
|
|
106
|
+
* pi-channels is loaded and its settings have a `relay` key. It registers on
|
|
107
|
+
* `resources_discover`, which Pi fires after every `session_start` handler,
|
|
108
|
+
* because pi-channels' `session_start` drops adapters registered before it.
|
|
109
|
+
*/
|
|
110
|
+
const attachPiChannels = (pi) => {
|
|
111
|
+
pi.on("resources_discover", async (event) => {
|
|
112
|
+
if (process.env.PI_SUBAGENT_CHILD === "1")
|
|
113
|
+
return;
|
|
114
|
+
const settings = await piChannelsRelaySettings(event.cwd);
|
|
115
|
+
if (!settings)
|
|
116
|
+
return;
|
|
117
|
+
const token = await agentToken(settings);
|
|
118
|
+
if (!token) {
|
|
119
|
+
console.error("Relay: no Agent Token (RELAY_AGENT_TOKEN or pi-channels.relay.agentTokenCommand); the pi-channels adapter is off.");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const baseURL = process.env.RELAY_BASE_URL?.trim() || settings.baseURL;
|
|
123
|
+
const voice = settings.transcribeCpp;
|
|
124
|
+
registerRelayAdapter(pi.events, () => relayChannelAdapter({
|
|
125
|
+
relay: new Relay({ apiKey: token, ...(baseURL ? { baseURL } : {}) }),
|
|
126
|
+
...(settings.senders ? { senders: settings.senders } : {}),
|
|
127
|
+
...(voice ? { transcribe: transcribeCpp({ module: home(voice.module), model: home(voice.model) }) } : {}),
|
|
128
|
+
}), RELAY_ADAPTER);
|
|
129
|
+
});
|
|
130
|
+
};
|
|
84
131
|
/** The Relay tools, as Pi registers a tool (docs/extensions.md, registerTool). */
|
|
85
132
|
const registerRelayTools = (pi, relay, chatId) => {
|
|
86
133
|
for (const tool of relayTools(relay, chatId))
|
|
@@ -107,6 +154,7 @@ const attachChatTools = (pi) => {
|
|
|
107
154
|
export default function relayPiExtension(pi) {
|
|
108
155
|
attachChatTools(pi);
|
|
109
156
|
attachSession(pi);
|
|
157
|
+
attachPiChannels(pi);
|
|
110
158
|
pi.registerCommand("relay-connect", {
|
|
111
159
|
description: "Start the Relay channel using the configured Agent Token",
|
|
112
160
|
handler: async (_args, ctx) => {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type Relay from "@relaymessenger/sdk";
|
|
2
|
+
import type { MessageWebhookData, RelayWebhookEvent } from "@relaymessenger/sdk";
|
|
3
|
+
import type { Transcribe } from "./voice.js";
|
|
4
|
+
export interface ChannelMessage {
|
|
5
|
+
readonly adapter: string;
|
|
6
|
+
/** The Relay chat id. */
|
|
7
|
+
readonly recipient: string;
|
|
8
|
+
readonly text?: string;
|
|
9
|
+
readonly source?: string;
|
|
10
|
+
readonly metadata?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
export interface IncomingAttachment {
|
|
13
|
+
readonly type: "image" | "document" | "audio";
|
|
14
|
+
readonly path: string;
|
|
15
|
+
readonly filename?: string;
|
|
16
|
+
readonly mimeType?: string;
|
|
17
|
+
readonly size?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface IncomingMessage {
|
|
20
|
+
readonly adapter: string;
|
|
21
|
+
/** The Relay chat id, so a reply to the sender goes back to the chat. */
|
|
22
|
+
readonly sender: string;
|
|
23
|
+
readonly text: string;
|
|
24
|
+
readonly attachments?: IncomingAttachment[];
|
|
25
|
+
readonly metadata?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
export interface ChannelAdapter {
|
|
28
|
+
readonly direction: "outgoing" | "incoming" | "bidirectional";
|
|
29
|
+
send(message: ChannelMessage): Promise<void>;
|
|
30
|
+
start(onMessage: (message: IncomingMessage) => void): Promise<void>;
|
|
31
|
+
stop(): Promise<void>;
|
|
32
|
+
sendTyping(recipient: string): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
/** The adapter name routes and the chat bridge use. */
|
|
35
|
+
export declare const RELAY_ADAPTER = "relay";
|
|
36
|
+
export interface RelayAdapterOptions {
|
|
37
|
+
readonly relay: Relay;
|
|
38
|
+
/** Handles whose Messages are received; any sender when empty or absent. */
|
|
39
|
+
readonly senders?: readonly string[];
|
|
40
|
+
/** Words for a voice note. Without it a voice note is named, not heard. */
|
|
41
|
+
readonly transcribe?: Transcribe;
|
|
42
|
+
/** Where downloaded photos and files go; pi-channels' Telegram adapter uses the same folder. */
|
|
43
|
+
readonly downloadDir?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* One Relay Message as a pi-channels incoming message: its words, the
|
|
47
|
+
* Message it replies to, voice notes as their transcript, and photos and
|
|
48
|
+
* files downloaded to a temporary path as attachments (as pi-channels'
|
|
49
|
+
* Telegram adapter does). Null for a Message with nothing in it.
|
|
50
|
+
*/
|
|
51
|
+
export declare const incomingMessage: (relay: Relay, event: RelayWebhookEvent & {
|
|
52
|
+
data: MessageWebhookData;
|
|
53
|
+
}, options?: Pick<RelayAdapterOptions, "transcribe" | "downloadDir">) => Promise<IncomingMessage | null>;
|
|
54
|
+
/**
|
|
55
|
+
* Relay as a pi-channels adapter. Incoming Messages arrive on the Agent's
|
|
56
|
+
* Relay WebSocket; outgoing text goes to the chat named as the recipient
|
|
57
|
+
* through `chats.messages.send`, with fenced blocks (buttons, selection,
|
|
58
|
+
* place, ...) sent as their parts by the SDK's answer parser.
|
|
59
|
+
*/
|
|
60
|
+
export declare const relayChannelAdapter: (options: RelayAdapterOptions) => ChannelAdapter;
|
|
61
|
+
/** The part of Pi's event bus the adapter uses (Pi docs/extensions.md, pi.events). */
|
|
62
|
+
export interface EventBus {
|
|
63
|
+
emit(channel: string, data: unknown): void;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Registers the adapter with pi-channels when it is loaded and does not
|
|
67
|
+
* already hold one by that name. pi-channels answers `channel:list` and
|
|
68
|
+
* `channel:register` through their callbacks synchronously, so no answer
|
|
69
|
+
* means it is not installed. Returns whether the adapter is registered now.
|
|
70
|
+
*/
|
|
71
|
+
export declare const registerRelayAdapter: (events: EventBus, adapter: () => ChannelAdapter, name?: string) => boolean;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { extname, join } from "node:path";
|
|
5
|
+
import { repliedContext, sendToChat, wordsOf } from "./index.js";
|
|
6
|
+
import { downloadMedia } from "./session.js";
|
|
7
|
+
/** The adapter name routes and the chat bridge use. */
|
|
8
|
+
export const RELAY_ADAPTER = "relay";
|
|
9
|
+
/** Inbound Messages from a Handle, and only the listed Handles when there is a list. */
|
|
10
|
+
const received = (event, senders = []) => {
|
|
11
|
+
if (event.event_type !== "message.received" || event.data.direction !== "inbound")
|
|
12
|
+
return false;
|
|
13
|
+
const data = event.data;
|
|
14
|
+
if (!data.sender_handle)
|
|
15
|
+
return false;
|
|
16
|
+
if (!senders.length)
|
|
17
|
+
return true;
|
|
18
|
+
const handle = (value) => value.replace(/^@/u, "").toLowerCase();
|
|
19
|
+
return senders.some((sender) => handle(sender) === handle(data.sender_handle.handle));
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* One Relay Message as a pi-channels incoming message: its words, the
|
|
23
|
+
* Message it replies to, voice notes as their transcript, and photos and
|
|
24
|
+
* files downloaded to a temporary path as attachments (as pi-channels'
|
|
25
|
+
* Telegram adapter does). Null for a Message with nothing in it.
|
|
26
|
+
*/
|
|
27
|
+
export const incomingMessage = async (relay, event, options = {}) => {
|
|
28
|
+
const data = event.data;
|
|
29
|
+
const lines = [];
|
|
30
|
+
const attachments = [];
|
|
31
|
+
for (const part of data.parts) {
|
|
32
|
+
if (part.type !== "media")
|
|
33
|
+
continue;
|
|
34
|
+
const media = part;
|
|
35
|
+
const mime = String(media.mime_type ?? "").toLowerCase();
|
|
36
|
+
const name = media.filename || "file";
|
|
37
|
+
if (mime.startsWith("audio/")) {
|
|
38
|
+
const words = options.transcribe
|
|
39
|
+
? await downloadMedia(relay, media).then((bytes) => options.transcribe(bytes, name)).catch(() => undefined)
|
|
40
|
+
: undefined;
|
|
41
|
+
lines.push(words ? `[voice note, transcribed]: ${words}` : `[voice note: ${name}, could not be transcribed]`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const bytes = await downloadMedia(relay, media);
|
|
46
|
+
const dir = options.downloadDir ?? join(tmpdir(), "pi-channels");
|
|
47
|
+
await mkdir(dir, { recursive: true });
|
|
48
|
+
const path = join(dir, `${Date.now()}-${randomUUID()}${extname(name)}`);
|
|
49
|
+
await writeFile(path, bytes);
|
|
50
|
+
attachments.push({ type: mime.startsWith("image/") ? "image" : "document", path, filename: name, ...(mime ? { mimeType: mime } : {}), size: bytes.length });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
lines.push(`[file: ${name}, could not be downloaded]`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const text = [wordsOf(data), lines.join("\n"), await repliedContext(relay, data)].filter(Boolean).join("\n\n");
|
|
57
|
+
if (!text && !attachments.length)
|
|
58
|
+
return null;
|
|
59
|
+
return {
|
|
60
|
+
adapter: RELAY_ADAPTER,
|
|
61
|
+
sender: data.chat.id,
|
|
62
|
+
text,
|
|
63
|
+
...(attachments.length ? { attachments } : {}),
|
|
64
|
+
metadata: {
|
|
65
|
+
eventId: event.event_id,
|
|
66
|
+
messageId: data.id,
|
|
67
|
+
chatId: data.chat.id,
|
|
68
|
+
isGroup: data.chat.is_group,
|
|
69
|
+
handle: data.sender_handle?.handle,
|
|
70
|
+
handleKind: data.sender_handle?.kind,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Relay as a pi-channels adapter. Incoming Messages arrive on the Agent's
|
|
76
|
+
* Relay WebSocket; outgoing text goes to the chat named as the recipient
|
|
77
|
+
* through `chats.messages.send`, with fenced blocks (buttons, selection,
|
|
78
|
+
* place, ...) sent as their parts by the SDK's answer parser.
|
|
79
|
+
*/
|
|
80
|
+
export const relayChannelAdapter = (options) => {
|
|
81
|
+
const { relay } = options;
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
let stop;
|
|
84
|
+
return {
|
|
85
|
+
direction: "bidirectional",
|
|
86
|
+
async send(message) {
|
|
87
|
+
if (!message.recipient)
|
|
88
|
+
throw new Error("Relay needs a chat id as the recipient");
|
|
89
|
+
const text = message.text?.trim();
|
|
90
|
+
if (!text)
|
|
91
|
+
return;
|
|
92
|
+
const key = typeof message.metadata?.idempotencyKey === "string" ? message.metadata.idempotencyKey : `pi-channels-${randomUUID()}`;
|
|
93
|
+
await sendToChat(relay, message.recipient, key, text);
|
|
94
|
+
},
|
|
95
|
+
async start(onMessage) {
|
|
96
|
+
if (stop)
|
|
97
|
+
return;
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
stop = controller;
|
|
100
|
+
void relay.websocket.run({
|
|
101
|
+
signal: controller.signal,
|
|
102
|
+
onEvent: async (event) => {
|
|
103
|
+
if (seen.has(event.event_id) || !received(event, options.senders))
|
|
104
|
+
return;
|
|
105
|
+
const incoming = await incomingMessage(relay, event, options);
|
|
106
|
+
seen.add(event.event_id);
|
|
107
|
+
if (incoming)
|
|
108
|
+
onMessage(incoming);
|
|
109
|
+
},
|
|
110
|
+
// pi-channels keeps no inbox to rebuild from.
|
|
111
|
+
onFullSync: async () => { console.error("Relay: FULL sync acknowledged; Messages sent while pi-channels was away were not delivered."); },
|
|
112
|
+
onError: (error) => { console.error(`Relay: ${error instanceof Error ? error.message : String(error)}`); },
|
|
113
|
+
}).catch((error) => {
|
|
114
|
+
console.error(`Relay: the pi-channels adapter stopped: ${error instanceof Error ? error.message : String(error)}`);
|
|
115
|
+
}).finally(() => { if (stop === controller)
|
|
116
|
+
stop = undefined; });
|
|
117
|
+
},
|
|
118
|
+
async stop() {
|
|
119
|
+
stop?.abort();
|
|
120
|
+
stop = undefined;
|
|
121
|
+
},
|
|
122
|
+
async sendTyping(recipient) {
|
|
123
|
+
await relay.chats.startTyping(recipient);
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Registers the adapter with pi-channels when it is loaded and does not
|
|
129
|
+
* already hold one by that name. pi-channels answers `channel:list` and
|
|
130
|
+
* `channel:register` through their callbacks synchronously, so no answer
|
|
131
|
+
* means it is not installed. Returns whether the adapter is registered now.
|
|
132
|
+
*/
|
|
133
|
+
export const registerRelayAdapter = (events, adapter, name = RELAY_ADAPTER) => {
|
|
134
|
+
let listed;
|
|
135
|
+
events.emit("channel:list", { callback: (items) => { listed = items; } });
|
|
136
|
+
if (!listed)
|
|
137
|
+
return false;
|
|
138
|
+
if (listed.some((item) => item.type === "adapter" && item.name === name))
|
|
139
|
+
return false;
|
|
140
|
+
let ok = false;
|
|
141
|
+
events.emit("channel:register", { name, adapter: adapter(), callback: (result) => { ok = result; } });
|
|
142
|
+
return ok;
|
|
143
|
+
};
|
package/dist/session.d.ts
CHANGED
package/dist/session.js
CHANGED
|
@@ -172,16 +172,20 @@ export class SessionChannel {
|
|
|
172
172
|
this.#pending = undefined;
|
|
173
173
|
this.#ended = [];
|
|
174
174
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// A media url lasts 60 minutes; GET /v1/attachments/{id} makes a fresh one.
|
|
178
|
-
if (!response?.ok && part.id) {
|
|
179
|
-
const fresh = (await this.#relay.attachments.retrieve(part.id)).download_url;
|
|
180
|
-
if (fresh)
|
|
181
|
-
response = await fetch(fresh);
|
|
182
|
-
}
|
|
183
|
-
if (!response?.ok)
|
|
184
|
-
throw new Error(`Relay attachment download failed (${response?.status ?? "no url"})`);
|
|
185
|
-
return Buffer.from(await response.arrayBuffer());
|
|
175
|
+
#download(part) {
|
|
176
|
+
return downloadMedia(this.#relay, part);
|
|
186
177
|
}
|
|
187
178
|
}
|
|
179
|
+
/** The bytes behind a media part; an expired url is renewed once. */
|
|
180
|
+
export const downloadMedia = async (relay, part) => {
|
|
181
|
+
let response = part.url ? await fetch(part.url).catch(() => undefined) : undefined;
|
|
182
|
+
// A media url lasts 60 minutes; GET /v1/attachments/{id} makes a fresh one.
|
|
183
|
+
if (!response?.ok && part.id) {
|
|
184
|
+
const fresh = (await relay.attachments.retrieve(part.id)).download_url;
|
|
185
|
+
if (fresh)
|
|
186
|
+
response = await fetch(fresh);
|
|
187
|
+
}
|
|
188
|
+
if (!response?.ok)
|
|
189
|
+
throw new Error(`Relay attachment download failed (${response?.status ?? "no url"})`);
|
|
190
|
+
return Buffer.from(await response.arrayBuffer());
|
|
191
|
+
};
|