@elizaos/plugin-signal 2.0.0-alpha
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/dist/accounts.d.ts +124 -0
- package/dist/accounts.d.ts.map +1 -0
- package/dist/actions/listContacts.d.ts +4 -0
- package/dist/actions/listContacts.d.ts.map +1 -0
- package/dist/actions/listGroups.d.ts +4 -0
- package/dist/actions/listGroups.d.ts.map +1 -0
- package/dist/actions/sendMessage.d.ts +4 -0
- package/dist/actions/sendMessage.d.ts.map +1 -0
- package/dist/actions/sendReaction.d.ts +4 -0
- package/dist/actions/sendReaction.d.ts.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1488 -0
- package/dist/index.js.map +19 -0
- package/dist/providers/conversationState.d.ts +7 -0
- package/dist/providers/conversationState.d.ts.map +1 -0
- package/dist/rpc.d.ts +163 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/service.d.ts +53 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/types.d.ts +172 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +100 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1488 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { logger } from "@elizaos/core";
|
|
3
|
+
|
|
4
|
+
// src/types.ts
|
|
5
|
+
var SignalEventTypes;
|
|
6
|
+
((SignalEventTypes2) => {
|
|
7
|
+
SignalEventTypes2["MESSAGE_RECEIVED"] = "SIGNAL_MESSAGE_RECEIVED";
|
|
8
|
+
SignalEventTypes2["MESSAGE_SENT"] = "SIGNAL_MESSAGE_SENT";
|
|
9
|
+
SignalEventTypes2["REACTION_RECEIVED"] = "SIGNAL_REACTION_RECEIVED";
|
|
10
|
+
SignalEventTypes2["GROUP_JOINED"] = "SIGNAL_GROUP_JOINED";
|
|
11
|
+
SignalEventTypes2["GROUP_LEFT"] = "SIGNAL_GROUP_LEFT";
|
|
12
|
+
SignalEventTypes2["TYPING_STARTED"] = "SIGNAL_TYPING_STARTED";
|
|
13
|
+
SignalEventTypes2["TYPING_STOPPED"] = "SIGNAL_TYPING_STOPPED";
|
|
14
|
+
SignalEventTypes2["READ_RECEIPT"] = "SIGNAL_READ_RECEIPT";
|
|
15
|
+
})(SignalEventTypes ||= {});
|
|
16
|
+
var SIGNAL_SERVICE_NAME = "signal";
|
|
17
|
+
var ServiceType = {
|
|
18
|
+
SIGNAL: "signal"
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
class SignalPluginError extends Error {
|
|
22
|
+
code;
|
|
23
|
+
constructor(message, code) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.name = "SignalPluginError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class SignalServiceNotInitializedError extends SignalPluginError {
|
|
31
|
+
constructor() {
|
|
32
|
+
super("Signal service is not initialized", "SERVICE_NOT_INITIALIZED");
|
|
33
|
+
this.name = "SignalServiceNotInitializedError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class SignalClientNotAvailableError extends SignalPluginError {
|
|
38
|
+
constructor() {
|
|
39
|
+
super("Signal client is not available", "CLIENT_NOT_AVAILABLE");
|
|
40
|
+
this.name = "SignalClientNotAvailableError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class SignalConfigurationError extends SignalPluginError {
|
|
45
|
+
constructor(missingConfig) {
|
|
46
|
+
super(`Missing required configuration: ${missingConfig}`, "MISSING_CONFIG");
|
|
47
|
+
this.name = "SignalConfigurationError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class SignalApiError extends SignalPluginError {
|
|
52
|
+
apiErrorCode;
|
|
53
|
+
constructor(message, apiErrorCode) {
|
|
54
|
+
super(message, "API_ERROR");
|
|
55
|
+
this.apiErrorCode = apiErrorCode;
|
|
56
|
+
this.name = "SignalApiError";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function normalizeE164(number) {
|
|
60
|
+
let cleaned = number.replace(/[^\d+]/g, "");
|
|
61
|
+
if (!cleaned.startsWith("+")) {
|
|
62
|
+
if (cleaned.length === 10) {
|
|
63
|
+
cleaned = `+1${cleaned}`;
|
|
64
|
+
} else if (cleaned.length === 11 && cleaned.startsWith("1")) {
|
|
65
|
+
cleaned = `+${cleaned}`;
|
|
66
|
+
} else {
|
|
67
|
+
cleaned = `+${cleaned}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!/^\+\d{7,15}$/.test(cleaned)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return cleaned;
|
|
74
|
+
}
|
|
75
|
+
function isValidE164(number) {
|
|
76
|
+
return /^\+\d{7,15}$/.test(number);
|
|
77
|
+
}
|
|
78
|
+
function isValidGroupId(id) {
|
|
79
|
+
return /^[A-Za-z0-9+/]+=*$/.test(id) && id.length >= 32;
|
|
80
|
+
}
|
|
81
|
+
function isValidUuid(uuid) {
|
|
82
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(uuid);
|
|
83
|
+
}
|
|
84
|
+
function getSignalContactDisplayName(contact) {
|
|
85
|
+
return contact.profileName || contact.name || contact.number;
|
|
86
|
+
}
|
|
87
|
+
var MAX_SIGNAL_MESSAGE_LENGTH = 4000;
|
|
88
|
+
var MAX_SIGNAL_ATTACHMENT_SIZE = 100 * 1024 * 1024;
|
|
89
|
+
|
|
90
|
+
// src/actions/listContacts.ts
|
|
91
|
+
var listContacts = {
|
|
92
|
+
name: "SIGNAL_LIST_CONTACTS",
|
|
93
|
+
similes: [
|
|
94
|
+
"LIST_SIGNAL_CONTACTS",
|
|
95
|
+
"SHOW_CONTACTS",
|
|
96
|
+
"GET_CONTACTS",
|
|
97
|
+
"SIGNAL_CONTACTS"
|
|
98
|
+
],
|
|
99
|
+
description: "List Signal contacts",
|
|
100
|
+
validate: async (_runtime, message, _state) => {
|
|
101
|
+
return message.content.source === "signal";
|
|
102
|
+
},
|
|
103
|
+
handler: async (runtime, message, _state, _options, callback) => {
|
|
104
|
+
const signalService = runtime.getService(SIGNAL_SERVICE_NAME);
|
|
105
|
+
if (!signalService || !signalService.isServiceConnected()) {
|
|
106
|
+
await callback?.({
|
|
107
|
+
text: "Signal service is not available.",
|
|
108
|
+
source: "signal"
|
|
109
|
+
});
|
|
110
|
+
return { success: false, error: "Signal service not available" };
|
|
111
|
+
}
|
|
112
|
+
const contacts = await signalService.getContacts();
|
|
113
|
+
const activeContacts = contacts.filter((c) => !c.blocked).sort((a, b) => {
|
|
114
|
+
const nameA = getSignalContactDisplayName(a);
|
|
115
|
+
const nameB = getSignalContactDisplayName(b);
|
|
116
|
+
return nameA.localeCompare(nameB);
|
|
117
|
+
});
|
|
118
|
+
const contactList = activeContacts.map((c) => {
|
|
119
|
+
const name = getSignalContactDisplayName(c);
|
|
120
|
+
const number = c.number;
|
|
121
|
+
return `• ${name} (${number})`;
|
|
122
|
+
});
|
|
123
|
+
const response = {
|
|
124
|
+
text: `Found ${activeContacts.length} contacts:
|
|
125
|
+
|
|
126
|
+
${contactList.join(`
|
|
127
|
+
`)}`,
|
|
128
|
+
source: message.content.source
|
|
129
|
+
};
|
|
130
|
+
runtime.logger.debug({
|
|
131
|
+
src: "plugin:signal:action:list-contacts",
|
|
132
|
+
contactCount: activeContacts.length
|
|
133
|
+
}, "[SIGNAL_LIST_CONTACTS] Contacts listed");
|
|
134
|
+
await callback?.(response);
|
|
135
|
+
return {
|
|
136
|
+
success: true,
|
|
137
|
+
data: {
|
|
138
|
+
contactCount: activeContacts.length,
|
|
139
|
+
contacts: activeContacts.map((c) => ({
|
|
140
|
+
number: c.number,
|
|
141
|
+
name: getSignalContactDisplayName(c),
|
|
142
|
+
uuid: c.uuid
|
|
143
|
+
}))
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
examples: [
|
|
148
|
+
[
|
|
149
|
+
{
|
|
150
|
+
name: "{{user1}}",
|
|
151
|
+
content: {
|
|
152
|
+
text: "Show me my Signal contacts"
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "{{agent}}",
|
|
157
|
+
content: {
|
|
158
|
+
text: "I'll list your Signal contacts.",
|
|
159
|
+
actions: ["SIGNAL_LIST_CONTACTS"]
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
]
|
|
164
|
+
};
|
|
165
|
+
var listContacts_default = listContacts;
|
|
166
|
+
|
|
167
|
+
// src/actions/listGroups.ts
|
|
168
|
+
var listGroups = {
|
|
169
|
+
name: "SIGNAL_LIST_GROUPS",
|
|
170
|
+
similes: ["LIST_SIGNAL_GROUPS", "SHOW_GROUPS", "GET_GROUPS", "SIGNAL_GROUPS"],
|
|
171
|
+
description: "List Signal groups",
|
|
172
|
+
validate: async (_runtime, message, _state) => {
|
|
173
|
+
return message.content.source === "signal";
|
|
174
|
+
},
|
|
175
|
+
handler: async (runtime, message, _state, _options, callback) => {
|
|
176
|
+
const signalService = runtime.getService(SIGNAL_SERVICE_NAME);
|
|
177
|
+
if (!signalService || !signalService.isServiceConnected()) {
|
|
178
|
+
await callback?.({
|
|
179
|
+
text: "Signal service is not available.",
|
|
180
|
+
source: "signal"
|
|
181
|
+
});
|
|
182
|
+
return { success: false, error: "Signal service not available" };
|
|
183
|
+
}
|
|
184
|
+
const groups = await signalService.getGroups();
|
|
185
|
+
const activeGroups = groups.filter((g) => g.isMember && !g.isBlocked).sort((a, b) => a.name.localeCompare(b.name));
|
|
186
|
+
const groupList = activeGroups.map((g) => {
|
|
187
|
+
const memberCount = g.members.length;
|
|
188
|
+
const description = g.description ? ` - ${g.description.slice(0, 50)}${g.description.length > 50 ? "..." : ""}` : "";
|
|
189
|
+
return `• ${g.name} (${memberCount} members)${description}`;
|
|
190
|
+
});
|
|
191
|
+
const response = {
|
|
192
|
+
text: `Found ${activeGroups.length} groups:
|
|
193
|
+
|
|
194
|
+
${groupList.join(`
|
|
195
|
+
`)}`,
|
|
196
|
+
source: message.content.source
|
|
197
|
+
};
|
|
198
|
+
runtime.logger.debug({
|
|
199
|
+
src: "plugin:signal:action:list-groups",
|
|
200
|
+
groupCount: activeGroups.length
|
|
201
|
+
}, "[SIGNAL_LIST_GROUPS] Groups listed");
|
|
202
|
+
await callback?.(response);
|
|
203
|
+
return {
|
|
204
|
+
success: true,
|
|
205
|
+
data: {
|
|
206
|
+
groupCount: activeGroups.length,
|
|
207
|
+
groups: activeGroups.map((g) => ({
|
|
208
|
+
id: g.id,
|
|
209
|
+
name: g.name,
|
|
210
|
+
description: g.description,
|
|
211
|
+
memberCount: g.members.length
|
|
212
|
+
}))
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
examples: [
|
|
217
|
+
[
|
|
218
|
+
{
|
|
219
|
+
name: "{{user1}}",
|
|
220
|
+
content: {
|
|
221
|
+
text: "Show me my Signal groups"
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: "{{agent}}",
|
|
226
|
+
content: {
|
|
227
|
+
text: "I'll list your Signal groups.",
|
|
228
|
+
actions: ["SIGNAL_LIST_GROUPS"]
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
]
|
|
233
|
+
};
|
|
234
|
+
var listGroups_default = listGroups;
|
|
235
|
+
|
|
236
|
+
// src/actions/sendMessage.ts
|
|
237
|
+
import {
|
|
238
|
+
composePromptFromState,
|
|
239
|
+
ModelType,
|
|
240
|
+
parseJSONObjectFromText
|
|
241
|
+
} from "@elizaos/core";
|
|
242
|
+
var sendMessageTemplate = `You are helping to extract send message parameters for Signal.
|
|
243
|
+
|
|
244
|
+
The user wants to send a message to a Signal contact or group.
|
|
245
|
+
|
|
246
|
+
Recent conversation:
|
|
247
|
+
{{recentMessages}}
|
|
248
|
+
|
|
249
|
+
Extract the following:
|
|
250
|
+
1. text: The message text to send
|
|
251
|
+
2. recipient: The phone number (E.164 format like +1234567890) or group ID to send to (default: "current" for current conversation)
|
|
252
|
+
|
|
253
|
+
Respond with a JSON object like:
|
|
254
|
+
{
|
|
255
|
+
"text": "The message to send",
|
|
256
|
+
"recipient": "current"
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
Only respond with the JSON object, no other text.`;
|
|
260
|
+
var sendMessage = {
|
|
261
|
+
name: "SIGNAL_SEND_MESSAGE",
|
|
262
|
+
similes: [
|
|
263
|
+
"SEND_SIGNAL_MESSAGE",
|
|
264
|
+
"TEXT_SIGNAL",
|
|
265
|
+
"MESSAGE_SIGNAL",
|
|
266
|
+
"SIGNAL_TEXT"
|
|
267
|
+
],
|
|
268
|
+
description: "Send a message to a Signal contact or group",
|
|
269
|
+
validate: async (_runtime, message, _state) => {
|
|
270
|
+
return message.content.source === "signal";
|
|
271
|
+
},
|
|
272
|
+
handler: async (runtime, message, state, _options, callback) => {
|
|
273
|
+
const signalService = runtime.getService(SIGNAL_SERVICE_NAME);
|
|
274
|
+
if (!signalService || !signalService.isServiceConnected()) {
|
|
275
|
+
await callback?.({
|
|
276
|
+
text: "Signal service is not available.",
|
|
277
|
+
source: "signal"
|
|
278
|
+
});
|
|
279
|
+
return { success: false, error: "Signal service not available" };
|
|
280
|
+
}
|
|
281
|
+
const composedState = state ?? {
|
|
282
|
+
values: {},
|
|
283
|
+
data: {},
|
|
284
|
+
text: ""
|
|
285
|
+
};
|
|
286
|
+
const prompt = composePromptFromState({
|
|
287
|
+
state: composedState,
|
|
288
|
+
template: sendMessageTemplate
|
|
289
|
+
});
|
|
290
|
+
let messageInfo = null;
|
|
291
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
292
|
+
const response2 = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
293
|
+
prompt
|
|
294
|
+
});
|
|
295
|
+
const parsedResponse = parseJSONObjectFromText(response2);
|
|
296
|
+
if (parsedResponse?.text) {
|
|
297
|
+
messageInfo = {
|
|
298
|
+
text: String(parsedResponse.text),
|
|
299
|
+
recipient: parsedResponse.recipient ? String(parsedResponse.recipient) : "current"
|
|
300
|
+
};
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (!messageInfo || !messageInfo.text) {
|
|
305
|
+
await callback?.({
|
|
306
|
+
text: "I couldn't understand what message you want me to send. Please try again with a clearer request.",
|
|
307
|
+
source: "signal"
|
|
308
|
+
});
|
|
309
|
+
return { success: false, error: "Could not extract message parameters" };
|
|
310
|
+
}
|
|
311
|
+
const stateData = state?.data;
|
|
312
|
+
const room = stateData?.room || await runtime.getRoom(message.roomId);
|
|
313
|
+
if (!room) {
|
|
314
|
+
await callback?.({
|
|
315
|
+
text: "I couldn't determine the current conversation.",
|
|
316
|
+
source: "signal"
|
|
317
|
+
});
|
|
318
|
+
return { success: false, error: "Could not determine conversation" };
|
|
319
|
+
}
|
|
320
|
+
let targetRecipient = room.channelId || "";
|
|
321
|
+
const isGroup = room.metadata?.isGroup || false;
|
|
322
|
+
if (messageInfo.recipient && messageInfo.recipient !== "current") {
|
|
323
|
+
const normalized = normalizeE164(messageInfo.recipient);
|
|
324
|
+
if (normalized) {
|
|
325
|
+
targetRecipient = normalized;
|
|
326
|
+
} else if (isValidGroupId(messageInfo.recipient)) {
|
|
327
|
+
targetRecipient = messageInfo.recipient;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
let result;
|
|
331
|
+
if (isGroup || isValidGroupId(targetRecipient)) {
|
|
332
|
+
result = await signalService.sendGroupMessage(targetRecipient, messageInfo.text);
|
|
333
|
+
} else {
|
|
334
|
+
result = await signalService.sendMessage(targetRecipient, messageInfo.text);
|
|
335
|
+
}
|
|
336
|
+
const response = {
|
|
337
|
+
text: "Message sent successfully.",
|
|
338
|
+
source: message.content.source
|
|
339
|
+
};
|
|
340
|
+
runtime.logger.debug({
|
|
341
|
+
src: "plugin:signal:action:send-message",
|
|
342
|
+
timestamp: result.timestamp,
|
|
343
|
+
recipient: targetRecipient
|
|
344
|
+
}, "[SIGNAL_SEND_MESSAGE] Message sent successfully");
|
|
345
|
+
await callback?.(response);
|
|
346
|
+
return {
|
|
347
|
+
success: true,
|
|
348
|
+
data: {
|
|
349
|
+
timestamp: result.timestamp,
|
|
350
|
+
recipient: targetRecipient
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
examples: [
|
|
355
|
+
[
|
|
356
|
+
{
|
|
357
|
+
name: "{{user1}}",
|
|
358
|
+
content: {
|
|
359
|
+
text: "Send a message to +1234567890 saying 'Hello!'"
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
name: "{{agent}}",
|
|
364
|
+
content: {
|
|
365
|
+
text: "I'll send that message for you.",
|
|
366
|
+
actions: ["SIGNAL_SEND_MESSAGE"]
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
]
|
|
370
|
+
]
|
|
371
|
+
};
|
|
372
|
+
var sendMessage_default = sendMessage;
|
|
373
|
+
|
|
374
|
+
// src/actions/sendReaction.ts
|
|
375
|
+
import {
|
|
376
|
+
composePromptFromState as composePromptFromState2,
|
|
377
|
+
ModelType as ModelType2,
|
|
378
|
+
parseJSONObjectFromText as parseJSONObjectFromText2
|
|
379
|
+
} from "@elizaos/core";
|
|
380
|
+
var sendReactionTemplate = `You are helping to extract reaction parameters for Signal.
|
|
381
|
+
|
|
382
|
+
The user wants to react to a Signal message with an emoji.
|
|
383
|
+
|
|
384
|
+
Recent conversation:
|
|
385
|
+
{{recentMessages}}
|
|
386
|
+
|
|
387
|
+
Extract the following:
|
|
388
|
+
1. emoji: The emoji to react with (single emoji character)
|
|
389
|
+
2. targetTimestamp: The timestamp of the message to react to (number)
|
|
390
|
+
3. targetAuthor: The phone number of the message author
|
|
391
|
+
4. remove: Whether to remove the reaction instead of adding it (default: false)
|
|
392
|
+
|
|
393
|
+
Respond with a JSON object like:
|
|
394
|
+
{
|
|
395
|
+
"emoji": "\uD83D\uDC4D",
|
|
396
|
+
"targetTimestamp": 1234567890000,
|
|
397
|
+
"targetAuthor": "+1234567890",
|
|
398
|
+
"remove": false
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
Only respond with the JSON object, no other text.`;
|
|
402
|
+
var sendReaction = {
|
|
403
|
+
name: "SIGNAL_SEND_REACTION",
|
|
404
|
+
similes: [
|
|
405
|
+
"REACT_SIGNAL",
|
|
406
|
+
"SIGNAL_REACT",
|
|
407
|
+
"ADD_SIGNAL_REACTION",
|
|
408
|
+
"SIGNAL_EMOJI"
|
|
409
|
+
],
|
|
410
|
+
description: "React to a Signal message with an emoji",
|
|
411
|
+
validate: async (_runtime, message, _state) => {
|
|
412
|
+
return message.content.source === "signal";
|
|
413
|
+
},
|
|
414
|
+
handler: async (runtime, message, state, _options, callback) => {
|
|
415
|
+
const signalService = runtime.getService(SIGNAL_SERVICE_NAME);
|
|
416
|
+
if (!signalService || !signalService.isServiceConnected()) {
|
|
417
|
+
await callback?.({
|
|
418
|
+
text: "Signal service is not available.",
|
|
419
|
+
source: "signal"
|
|
420
|
+
});
|
|
421
|
+
return { success: false, error: "Signal service not available" };
|
|
422
|
+
}
|
|
423
|
+
const composedState = state ?? {
|
|
424
|
+
values: {},
|
|
425
|
+
data: {},
|
|
426
|
+
text: ""
|
|
427
|
+
};
|
|
428
|
+
const prompt = composePromptFromState2({
|
|
429
|
+
state: composedState,
|
|
430
|
+
template: sendReactionTemplate
|
|
431
|
+
});
|
|
432
|
+
let reactionInfo = null;
|
|
433
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
434
|
+
const response2 = await runtime.useModel(ModelType2.TEXT_SMALL, {
|
|
435
|
+
prompt
|
|
436
|
+
});
|
|
437
|
+
const parsedResponse = parseJSONObjectFromText2(response2);
|
|
438
|
+
if (parsedResponse?.emoji && parsedResponse?.targetTimestamp && parsedResponse?.targetAuthor) {
|
|
439
|
+
reactionInfo = {
|
|
440
|
+
emoji: String(parsedResponse.emoji),
|
|
441
|
+
targetTimestamp: Number(parsedResponse.targetTimestamp),
|
|
442
|
+
targetAuthor: String(parsedResponse.targetAuthor),
|
|
443
|
+
remove: Boolean(parsedResponse.remove)
|
|
444
|
+
};
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (!reactionInfo) {
|
|
449
|
+
await callback?.({
|
|
450
|
+
text: "I couldn't understand the reaction request. Please specify the emoji and message to react to.",
|
|
451
|
+
source: "signal"
|
|
452
|
+
});
|
|
453
|
+
return { success: false, error: "Could not extract reaction parameters" };
|
|
454
|
+
}
|
|
455
|
+
const stateData = state?.data;
|
|
456
|
+
const room = stateData?.room || await runtime.getRoom(message.roomId);
|
|
457
|
+
const recipient = room?.channelId || reactionInfo.targetAuthor;
|
|
458
|
+
if (reactionInfo.remove) {
|
|
459
|
+
await signalService.removeReaction(recipient, reactionInfo.emoji, reactionInfo.targetTimestamp, reactionInfo.targetAuthor);
|
|
460
|
+
} else {
|
|
461
|
+
await signalService.sendReaction(recipient, reactionInfo.emoji, reactionInfo.targetTimestamp, reactionInfo.targetAuthor);
|
|
462
|
+
}
|
|
463
|
+
const actionWord = reactionInfo.remove ? "removed" : "added";
|
|
464
|
+
const response = {
|
|
465
|
+
text: `Reaction ${reactionInfo.emoji} ${actionWord} successfully.`,
|
|
466
|
+
source: message.content.source
|
|
467
|
+
};
|
|
468
|
+
await callback?.(response);
|
|
469
|
+
return {
|
|
470
|
+
success: true,
|
|
471
|
+
data: {
|
|
472
|
+
emoji: reactionInfo.emoji,
|
|
473
|
+
targetTimestamp: reactionInfo.targetTimestamp,
|
|
474
|
+
targetAuthor: reactionInfo.targetAuthor,
|
|
475
|
+
action: actionWord
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
},
|
|
479
|
+
examples: [
|
|
480
|
+
[
|
|
481
|
+
{
|
|
482
|
+
name: "{{user1}}",
|
|
483
|
+
content: {
|
|
484
|
+
text: "React to the last message with a thumbs up"
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
name: "{{agent}}",
|
|
489
|
+
content: {
|
|
490
|
+
text: "I'll add a thumbs up reaction.",
|
|
491
|
+
actions: ["SIGNAL_SEND_REACTION"]
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
]
|
|
495
|
+
]
|
|
496
|
+
};
|
|
497
|
+
var sendReaction_default = sendReaction;
|
|
498
|
+
|
|
499
|
+
// src/providers/conversationState.ts
|
|
500
|
+
var conversationStateProvider = {
|
|
501
|
+
name: "signalConversationState",
|
|
502
|
+
description: "Provides information about the current Signal conversation context",
|
|
503
|
+
get: async (runtime, message, state) => {
|
|
504
|
+
const room = state.data?.room ?? await runtime.getRoom(message.roomId);
|
|
505
|
+
if (!room) {
|
|
506
|
+
return {
|
|
507
|
+
data: {},
|
|
508
|
+
values: {},
|
|
509
|
+
text: ""
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (message.content.source !== "signal") {
|
|
513
|
+
return {
|
|
514
|
+
data: {},
|
|
515
|
+
values: {},
|
|
516
|
+
text: ""
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
const agentName = String(state?.agentName || "The agent");
|
|
520
|
+
const senderName = String(state?.senderName || "someone");
|
|
521
|
+
let responseText = "";
|
|
522
|
+
let conversationType = "";
|
|
523
|
+
let contactName = "";
|
|
524
|
+
let groupName = "";
|
|
525
|
+
const channelId = room.channelId ?? "";
|
|
526
|
+
const signalService = runtime.getService(ServiceType.SIGNAL);
|
|
527
|
+
if (!signalService || !signalService.isServiceConnected()) {
|
|
528
|
+
return {
|
|
529
|
+
data: {
|
|
530
|
+
room,
|
|
531
|
+
conversationType: "unknown",
|
|
532
|
+
channelId
|
|
533
|
+
},
|
|
534
|
+
values: {
|
|
535
|
+
conversationType: "unknown",
|
|
536
|
+
channelId
|
|
537
|
+
},
|
|
538
|
+
text: ""
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
const isGroup = room.metadata?.isGroup || false;
|
|
542
|
+
if (isGroup) {
|
|
543
|
+
conversationType = "GROUP";
|
|
544
|
+
const groupId = room.metadata?.groupId;
|
|
545
|
+
const group = signalService.getCachedGroup(groupId);
|
|
546
|
+
groupName = group?.name || room.name || "Unknown Group";
|
|
547
|
+
responseText = `${agentName} is currently in a Signal group chat: "${groupName}".`;
|
|
548
|
+
responseText += `
|
|
549
|
+
${agentName} should be aware that multiple people can see this conversation and should participate when relevant.`;
|
|
550
|
+
if (group?.description) {
|
|
551
|
+
responseText += `
|
|
552
|
+
Group description: ${group.description}`;
|
|
553
|
+
}
|
|
554
|
+
} else {
|
|
555
|
+
conversationType = "DM";
|
|
556
|
+
const contact = signalService.getContact(channelId);
|
|
557
|
+
contactName = contact ? String(getSignalContactDisplayName(contact)) : senderName;
|
|
558
|
+
responseText = `${agentName} is currently in a direct message conversation with ${contactName} on Signal.`;
|
|
559
|
+
responseText += `
|
|
560
|
+
${agentName} should engage naturally in conversation, responding to messages addressed to them.`;
|
|
561
|
+
}
|
|
562
|
+
responseText += `
|
|
563
|
+
|
|
564
|
+
Signal is an encrypted messaging platform, so all messages are secure and private.`;
|
|
565
|
+
return {
|
|
566
|
+
data: {
|
|
567
|
+
room,
|
|
568
|
+
conversationType,
|
|
569
|
+
contactName,
|
|
570
|
+
groupName,
|
|
571
|
+
channelId,
|
|
572
|
+
isGroup,
|
|
573
|
+
accountNumber: signalService.getAccountNumber()
|
|
574
|
+
},
|
|
575
|
+
values: {
|
|
576
|
+
conversationType,
|
|
577
|
+
contactName,
|
|
578
|
+
groupName,
|
|
579
|
+
channelId,
|
|
580
|
+
isGroup
|
|
581
|
+
},
|
|
582
|
+
text: responseText
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// src/service.ts
|
|
588
|
+
import {
|
|
589
|
+
ChannelType,
|
|
590
|
+
createUniqueUuid,
|
|
591
|
+
Service,
|
|
592
|
+
stringToUuid
|
|
593
|
+
} from "@elizaos/core";
|
|
594
|
+
var getMessageService = (runtime) => {
|
|
595
|
+
if ("messageService" in runtime) {
|
|
596
|
+
const withMessageService = runtime;
|
|
597
|
+
return withMessageService.messageService ?? null;
|
|
598
|
+
}
|
|
599
|
+
return null;
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
class SignalApiClient {
|
|
603
|
+
baseUrl;
|
|
604
|
+
accountNumber;
|
|
605
|
+
constructor(baseUrl, accountNumber) {
|
|
606
|
+
this.baseUrl = baseUrl;
|
|
607
|
+
this.accountNumber = accountNumber;
|
|
608
|
+
}
|
|
609
|
+
async request(method, endpoint, body) {
|
|
610
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
611
|
+
const options = {
|
|
612
|
+
method,
|
|
613
|
+
headers: {
|
|
614
|
+
"Content-Type": "application/json"
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
if (body) {
|
|
618
|
+
options.body = JSON.stringify(body);
|
|
619
|
+
}
|
|
620
|
+
const response = await fetch(url, options);
|
|
621
|
+
if (!response.ok) {
|
|
622
|
+
const errorText = await response.text();
|
|
623
|
+
throw new Error(`Signal API error: ${response.status} - ${errorText}`);
|
|
624
|
+
}
|
|
625
|
+
const text = await response.text();
|
|
626
|
+
return text ? JSON.parse(text) : {};
|
|
627
|
+
}
|
|
628
|
+
async sendMessage(recipient, message, options) {
|
|
629
|
+
const body = {
|
|
630
|
+
message,
|
|
631
|
+
number: this.accountNumber,
|
|
632
|
+
recipients: [recipient]
|
|
633
|
+
};
|
|
634
|
+
if (options?.attachments) {
|
|
635
|
+
body.base64_attachments = options.attachments;
|
|
636
|
+
}
|
|
637
|
+
if (options?.quote) {
|
|
638
|
+
body.quote_timestamp = options.quote.timestamp;
|
|
639
|
+
body.quote_author = options.quote.author;
|
|
640
|
+
}
|
|
641
|
+
return this.request("POST", "/v2/send", body);
|
|
642
|
+
}
|
|
643
|
+
async sendGroupMessage(groupId, message, options) {
|
|
644
|
+
const body = {
|
|
645
|
+
message,
|
|
646
|
+
number: this.accountNumber,
|
|
647
|
+
recipients: [`group.${groupId}`]
|
|
648
|
+
};
|
|
649
|
+
if (options?.attachments) {
|
|
650
|
+
body.base64_attachments = options.attachments;
|
|
651
|
+
}
|
|
652
|
+
return this.request("POST", "/v2/send", body);
|
|
653
|
+
}
|
|
654
|
+
async sendReaction(recipient, emoji, targetTimestamp, targetAuthor, remove = false) {
|
|
655
|
+
await this.request("POST", `/v1/reactions/${this.accountNumber}`, {
|
|
656
|
+
recipient,
|
|
657
|
+
reaction: emoji,
|
|
658
|
+
target_author: targetAuthor,
|
|
659
|
+
timestamp: targetTimestamp,
|
|
660
|
+
remove
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
async getContacts() {
|
|
664
|
+
const result = await this.request("GET", `/v1/contacts/${this.accountNumber}`);
|
|
665
|
+
return result.contacts || [];
|
|
666
|
+
}
|
|
667
|
+
async getGroups() {
|
|
668
|
+
const result = await this.request("GET", `/v1/groups/${this.accountNumber}`);
|
|
669
|
+
return result || [];
|
|
670
|
+
}
|
|
671
|
+
async getGroup(groupId) {
|
|
672
|
+
const groups = await this.getGroups();
|
|
673
|
+
return groups.find((g) => g.id === groupId) || null;
|
|
674
|
+
}
|
|
675
|
+
async receive() {
|
|
676
|
+
const result = await this.request("GET", `/v1/receive/${this.accountNumber}`);
|
|
677
|
+
return result || [];
|
|
678
|
+
}
|
|
679
|
+
async sendTyping(recipient, stop = false) {
|
|
680
|
+
await this.request("PUT", `/v1/typing-indicator/${this.accountNumber}`, {
|
|
681
|
+
recipient,
|
|
682
|
+
stop
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
async setProfile(name, about) {
|
|
686
|
+
await this.request("PUT", `/v1/profiles/${this.accountNumber}`, {
|
|
687
|
+
name,
|
|
688
|
+
about: about || ""
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
async getIdentities() {
|
|
692
|
+
const result = await this.request("GET", `/v1/identities/${this.accountNumber}`);
|
|
693
|
+
return result || [];
|
|
694
|
+
}
|
|
695
|
+
async trustIdentity(number, trustLevel) {
|
|
696
|
+
await this.request("PUT", `/v1/identities/${this.accountNumber}/trust/${number}`, {
|
|
697
|
+
trust_level: trustLevel
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
class SignalService extends Service {
|
|
703
|
+
static serviceType = SIGNAL_SERVICE_NAME;
|
|
704
|
+
capabilityDescription = "The agent is able to send and receive messages on Signal";
|
|
705
|
+
async stop() {
|
|
706
|
+
await this.shutdown();
|
|
707
|
+
}
|
|
708
|
+
character;
|
|
709
|
+
accountNumber = null;
|
|
710
|
+
isConnected = false;
|
|
711
|
+
client = null;
|
|
712
|
+
settings;
|
|
713
|
+
contactCache = new Map;
|
|
714
|
+
groupCache = new Map;
|
|
715
|
+
pollInterval = null;
|
|
716
|
+
isPolling = false;
|
|
717
|
+
constructor(runtime) {
|
|
718
|
+
super(runtime);
|
|
719
|
+
if (runtime) {
|
|
720
|
+
this.character = runtime.character;
|
|
721
|
+
this.settings = this.loadSettings();
|
|
722
|
+
} else {
|
|
723
|
+
this.character = {};
|
|
724
|
+
this.settings = {
|
|
725
|
+
shouldIgnoreGroupMessages: false,
|
|
726
|
+
allowedGroups: undefined,
|
|
727
|
+
blockedNumbers: undefined
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
loadSettings() {
|
|
732
|
+
const ignoreGroups = this.runtime.getSetting("SIGNAL_SHOULD_IGNORE_GROUP_MESSAGES");
|
|
733
|
+
return {
|
|
734
|
+
shouldIgnoreGroupMessages: ignoreGroups === "true" || ignoreGroups === true,
|
|
735
|
+
allowedGroups: undefined,
|
|
736
|
+
blockedNumbers: undefined
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
static async start(runtime) {
|
|
740
|
+
const service = new SignalService(runtime);
|
|
741
|
+
const accountNumber = runtime.getSetting("SIGNAL_ACCOUNT_NUMBER");
|
|
742
|
+
const httpUrl = runtime.getSetting("SIGNAL_HTTP_URL");
|
|
743
|
+
if (!accountNumber) {
|
|
744
|
+
runtime.logger.warn({ src: "plugin:signal", agentId: runtime.agentId }, "SIGNAL_ACCOUNT_NUMBER not provided, Signal service will not start");
|
|
745
|
+
return service;
|
|
746
|
+
}
|
|
747
|
+
const normalizedNumber = normalizeE164(accountNumber);
|
|
748
|
+
if (!normalizedNumber) {
|
|
749
|
+
runtime.logger.error({ src: "plugin:signal", agentId: runtime.agentId, accountNumber }, "Invalid SIGNAL_ACCOUNT_NUMBER format");
|
|
750
|
+
return service;
|
|
751
|
+
}
|
|
752
|
+
service.accountNumber = normalizedNumber;
|
|
753
|
+
if (httpUrl) {
|
|
754
|
+
service.client = new SignalApiClient(httpUrl, normalizedNumber);
|
|
755
|
+
await service.initialize();
|
|
756
|
+
} else {
|
|
757
|
+
runtime.logger.warn({ src: "plugin:signal", agentId: runtime.agentId }, "SIGNAL_HTTP_URL not provided, Signal service will not be able to communicate");
|
|
758
|
+
}
|
|
759
|
+
return service;
|
|
760
|
+
}
|
|
761
|
+
static async stop(runtime) {
|
|
762
|
+
const service = runtime.getService(SIGNAL_SERVICE_NAME);
|
|
763
|
+
if (service) {
|
|
764
|
+
await service.shutdown();
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
async initialize() {
|
|
768
|
+
if (!this.client)
|
|
769
|
+
return;
|
|
770
|
+
this.runtime.logger.info({
|
|
771
|
+
src: "plugin:signal",
|
|
772
|
+
agentId: this.runtime.agentId,
|
|
773
|
+
accountNumber: this.accountNumber
|
|
774
|
+
}, "Initializing Signal service");
|
|
775
|
+
const contacts = await this.client.getContacts();
|
|
776
|
+
this.runtime.logger.info({
|
|
777
|
+
src: "plugin:signal",
|
|
778
|
+
agentId: this.runtime.agentId,
|
|
779
|
+
contactCount: contacts.length
|
|
780
|
+
}, "Signal service connected");
|
|
781
|
+
for (const contact of contacts) {
|
|
782
|
+
this.contactCache.set(contact.number, contact);
|
|
783
|
+
}
|
|
784
|
+
const groups = await this.client.getGroups();
|
|
785
|
+
for (const group of groups) {
|
|
786
|
+
this.groupCache.set(group.id, group);
|
|
787
|
+
}
|
|
788
|
+
this.isConnected = true;
|
|
789
|
+
this.startPolling();
|
|
790
|
+
}
|
|
791
|
+
async shutdown() {
|
|
792
|
+
this.stopPolling();
|
|
793
|
+
this.client = null;
|
|
794
|
+
this.isConnected = false;
|
|
795
|
+
this.runtime.logger.info({ src: "plugin:signal", agentId: this.runtime.agentId }, "Signal service stopped");
|
|
796
|
+
}
|
|
797
|
+
startPolling() {
|
|
798
|
+
if (this.pollInterval)
|
|
799
|
+
return;
|
|
800
|
+
this.pollInterval = setInterval(async () => {
|
|
801
|
+
await this.pollMessages();
|
|
802
|
+
}, 2000);
|
|
803
|
+
}
|
|
804
|
+
stopPolling() {
|
|
805
|
+
if (this.pollInterval) {
|
|
806
|
+
clearInterval(this.pollInterval);
|
|
807
|
+
this.pollInterval = null;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
async pollMessages() {
|
|
811
|
+
if (!this.client || this.isPolling)
|
|
812
|
+
return;
|
|
813
|
+
this.isPolling = true;
|
|
814
|
+
const messages = await this.client.receive();
|
|
815
|
+
for (const msg of messages) {
|
|
816
|
+
await this.handleIncomingMessage(msg);
|
|
817
|
+
}
|
|
818
|
+
this.isPolling = false;
|
|
819
|
+
}
|
|
820
|
+
async handleIncomingMessage(msg) {
|
|
821
|
+
if (msg.reaction) {
|
|
822
|
+
await this.handleReaction(msg);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (!msg.message && msg.attachments.length === 0) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const isGroupMessage = Boolean(msg.groupId);
|
|
829
|
+
if (isGroupMessage && this.settings.shouldIgnoreGroupMessages) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const memory = await this.buildMemoryFromMessage(msg);
|
|
833
|
+
if (!memory)
|
|
834
|
+
return;
|
|
835
|
+
const room = await this.ensureRoomExists(msg.sender, msg.groupId);
|
|
836
|
+
await this.runtime.createMemory(memory, "messages");
|
|
837
|
+
await this.runtime.emitEvent("SIGNAL_MESSAGE_RECEIVED" /* MESSAGE_RECEIVED */, {
|
|
838
|
+
runtime: this.runtime,
|
|
839
|
+
source: "signal"
|
|
840
|
+
});
|
|
841
|
+
await this.processMessage(memory, room, msg.sender, msg.groupId);
|
|
842
|
+
}
|
|
843
|
+
async handleReaction(msg) {
|
|
844
|
+
if (!msg.reaction)
|
|
845
|
+
return;
|
|
846
|
+
await this.runtime.emitEvent("SIGNAL_REACTION_RECEIVED" /* REACTION_RECEIVED */, {
|
|
847
|
+
runtime: this.runtime,
|
|
848
|
+
source: "signal"
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
async processMessage(memory, room, sender, groupId) {
|
|
852
|
+
const callback = async (response) => {
|
|
853
|
+
if (groupId) {
|
|
854
|
+
await this.sendGroupMessage(groupId, response.text || "");
|
|
855
|
+
} else {
|
|
856
|
+
await this.sendMessage(sender, response.text || "");
|
|
857
|
+
}
|
|
858
|
+
const responseMemory = {
|
|
859
|
+
id: createUniqueUuid(this.runtime, `signal-response-${Date.now()}`),
|
|
860
|
+
agentId: this.runtime.agentId,
|
|
861
|
+
roomId: room.id,
|
|
862
|
+
entityId: this.runtime.agentId,
|
|
863
|
+
content: {
|
|
864
|
+
text: response.text || "",
|
|
865
|
+
source: "signal",
|
|
866
|
+
inReplyTo: memory.id
|
|
867
|
+
},
|
|
868
|
+
createdAt: Date.now()
|
|
869
|
+
};
|
|
870
|
+
await this.runtime.createMemory(responseMemory, "messages");
|
|
871
|
+
await this.runtime.emitEvent("SIGNAL_MESSAGE_SENT" /* MESSAGE_SENT */, {
|
|
872
|
+
runtime: this.runtime,
|
|
873
|
+
source: "signal"
|
|
874
|
+
});
|
|
875
|
+
return [responseMemory];
|
|
876
|
+
};
|
|
877
|
+
const messageService = getMessageService(this.runtime);
|
|
878
|
+
if (messageService) {
|
|
879
|
+
await messageService.handleMessage(this.runtime, memory, callback);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
async buildMemoryFromMessage(msg) {
|
|
883
|
+
const roomId = await this.getRoomId(msg.sender, msg.groupId);
|
|
884
|
+
const entityId = this.getEntityId(msg.sender);
|
|
885
|
+
const contact = this.contactCache.get(msg.sender);
|
|
886
|
+
const displayName = contact ? getSignalContactDisplayName(contact) : msg.sender;
|
|
887
|
+
const media = msg.attachments.map((att) => ({
|
|
888
|
+
id: att.id,
|
|
889
|
+
url: `signal://attachment/${att.id}`,
|
|
890
|
+
title: att.filename || att.id,
|
|
891
|
+
source: "signal",
|
|
892
|
+
description: att.caption || att.filename,
|
|
893
|
+
contentType: att.contentType
|
|
894
|
+
}));
|
|
895
|
+
const memory = {
|
|
896
|
+
id: createUniqueUuid(this.runtime, `signal-${msg.timestamp}`),
|
|
897
|
+
agentId: this.runtime.agentId,
|
|
898
|
+
roomId,
|
|
899
|
+
entityId,
|
|
900
|
+
content: {
|
|
901
|
+
text: msg.message || "",
|
|
902
|
+
source: "signal",
|
|
903
|
+
name: displayName,
|
|
904
|
+
...media.length > 0 ? { attachments: media } : {}
|
|
905
|
+
},
|
|
906
|
+
createdAt: msg.timestamp
|
|
907
|
+
};
|
|
908
|
+
return memory;
|
|
909
|
+
}
|
|
910
|
+
async getRoomId(sender, groupId) {
|
|
911
|
+
const roomKey = groupId || sender;
|
|
912
|
+
return createUniqueUuid(this.runtime, `signal-room-${roomKey}`);
|
|
913
|
+
}
|
|
914
|
+
getEntityId(number) {
|
|
915
|
+
return stringToUuid(`signal-user-${number}`);
|
|
916
|
+
}
|
|
917
|
+
async ensureRoomExists(sender, groupId) {
|
|
918
|
+
const roomId = await this.getRoomId(sender, groupId);
|
|
919
|
+
const existingRoom = await this.runtime.getRoom(roomId);
|
|
920
|
+
if (existingRoom)
|
|
921
|
+
return existingRoom;
|
|
922
|
+
const isGroup = Boolean(groupId);
|
|
923
|
+
const group = groupId ? this.groupCache.get(groupId) : null;
|
|
924
|
+
const contact = this.contactCache.get(sender);
|
|
925
|
+
const room = {
|
|
926
|
+
id: roomId,
|
|
927
|
+
name: isGroup ? group?.name || `Signal Group ${groupId}` : contact ? getSignalContactDisplayName(contact) : sender,
|
|
928
|
+
agentId: this.runtime.agentId,
|
|
929
|
+
source: "signal",
|
|
930
|
+
type: isGroup ? ChannelType.GROUP : ChannelType.DM,
|
|
931
|
+
channelId: groupId || sender,
|
|
932
|
+
metadata: {
|
|
933
|
+
isGroup,
|
|
934
|
+
groupId,
|
|
935
|
+
sender,
|
|
936
|
+
groupName: group?.name,
|
|
937
|
+
groupDescription: group?.description
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
await this.runtime.createRoom(room);
|
|
941
|
+
return room;
|
|
942
|
+
}
|
|
943
|
+
async sendMessage(recipient, text, options) {
|
|
944
|
+
if (!this.client) {
|
|
945
|
+
throw new Error("Signal client not initialized");
|
|
946
|
+
}
|
|
947
|
+
const normalizedRecipient = normalizeE164(recipient);
|
|
948
|
+
if (!normalizedRecipient) {
|
|
949
|
+
throw new Error(`Invalid recipient number: ${recipient}`);
|
|
950
|
+
}
|
|
951
|
+
const messages = this.splitMessage(text);
|
|
952
|
+
let lastTimestamp = 0;
|
|
953
|
+
for (const msg of messages) {
|
|
954
|
+
const result = await this.client.sendMessage(normalizedRecipient, msg, options);
|
|
955
|
+
lastTimestamp = result.timestamp;
|
|
956
|
+
}
|
|
957
|
+
return { timestamp: lastTimestamp };
|
|
958
|
+
}
|
|
959
|
+
async sendGroupMessage(groupId, text, options) {
|
|
960
|
+
if (!this.client) {
|
|
961
|
+
throw new Error("Signal client not initialized");
|
|
962
|
+
}
|
|
963
|
+
const messages = this.splitMessage(text);
|
|
964
|
+
let lastTimestamp = 0;
|
|
965
|
+
for (const msg of messages) {
|
|
966
|
+
const result = await this.client.sendGroupMessage(groupId, msg, options);
|
|
967
|
+
lastTimestamp = result.timestamp;
|
|
968
|
+
}
|
|
969
|
+
return { timestamp: lastTimestamp };
|
|
970
|
+
}
|
|
971
|
+
async sendReaction(recipient, emoji, targetTimestamp, targetAuthor) {
|
|
972
|
+
if (!this.client) {
|
|
973
|
+
throw new Error("Signal client not initialized");
|
|
974
|
+
}
|
|
975
|
+
await this.client.sendReaction(recipient, emoji, targetTimestamp, targetAuthor);
|
|
976
|
+
}
|
|
977
|
+
async removeReaction(recipient, emoji, targetTimestamp, targetAuthor) {
|
|
978
|
+
if (!this.client) {
|
|
979
|
+
throw new Error("Signal client not initialized");
|
|
980
|
+
}
|
|
981
|
+
await this.client.sendReaction(recipient, emoji, targetTimestamp, targetAuthor, true);
|
|
982
|
+
}
|
|
983
|
+
async getContacts() {
|
|
984
|
+
if (!this.client) {
|
|
985
|
+
throw new Error("Signal client not initialized");
|
|
986
|
+
}
|
|
987
|
+
const contacts = await this.client.getContacts();
|
|
988
|
+
for (const contact of contacts) {
|
|
989
|
+
this.contactCache.set(contact.number, contact);
|
|
990
|
+
}
|
|
991
|
+
return contacts;
|
|
992
|
+
}
|
|
993
|
+
async getGroups() {
|
|
994
|
+
if (!this.client) {
|
|
995
|
+
throw new Error("Signal client not initialized");
|
|
996
|
+
}
|
|
997
|
+
const groups = await this.client.getGroups();
|
|
998
|
+
for (const group of groups) {
|
|
999
|
+
this.groupCache.set(group.id, group);
|
|
1000
|
+
}
|
|
1001
|
+
return groups;
|
|
1002
|
+
}
|
|
1003
|
+
async getGroup(groupId) {
|
|
1004
|
+
if (!this.client) {
|
|
1005
|
+
throw new Error("Signal client not initialized");
|
|
1006
|
+
}
|
|
1007
|
+
const group = await this.client.getGroup(groupId);
|
|
1008
|
+
if (group) {
|
|
1009
|
+
this.groupCache.set(group.id, group);
|
|
1010
|
+
}
|
|
1011
|
+
return group;
|
|
1012
|
+
}
|
|
1013
|
+
async sendTypingIndicator(recipient) {
|
|
1014
|
+
if (!this.client)
|
|
1015
|
+
return;
|
|
1016
|
+
await this.client.sendTyping(recipient);
|
|
1017
|
+
}
|
|
1018
|
+
async stopTypingIndicator(recipient) {
|
|
1019
|
+
if (!this.client)
|
|
1020
|
+
return;
|
|
1021
|
+
await this.client.sendTyping(recipient, true);
|
|
1022
|
+
}
|
|
1023
|
+
splitMessage(text) {
|
|
1024
|
+
if (text.length <= MAX_SIGNAL_MESSAGE_LENGTH) {
|
|
1025
|
+
return [text];
|
|
1026
|
+
}
|
|
1027
|
+
const messages = [];
|
|
1028
|
+
let remaining = text;
|
|
1029
|
+
while (remaining.length > 0) {
|
|
1030
|
+
if (remaining.length <= MAX_SIGNAL_MESSAGE_LENGTH) {
|
|
1031
|
+
messages.push(remaining);
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
let splitIndex = MAX_SIGNAL_MESSAGE_LENGTH;
|
|
1035
|
+
const lastNewline = remaining.lastIndexOf(`
|
|
1036
|
+
`, MAX_SIGNAL_MESSAGE_LENGTH);
|
|
1037
|
+
if (lastNewline > MAX_SIGNAL_MESSAGE_LENGTH / 2) {
|
|
1038
|
+
splitIndex = lastNewline + 1;
|
|
1039
|
+
} else {
|
|
1040
|
+
const lastSpace = remaining.lastIndexOf(" ", MAX_SIGNAL_MESSAGE_LENGTH);
|
|
1041
|
+
if (lastSpace > MAX_SIGNAL_MESSAGE_LENGTH / 2) {
|
|
1042
|
+
splitIndex = lastSpace + 1;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
messages.push(remaining.slice(0, splitIndex));
|
|
1046
|
+
remaining = remaining.slice(splitIndex);
|
|
1047
|
+
}
|
|
1048
|
+
return messages;
|
|
1049
|
+
}
|
|
1050
|
+
getContact(number) {
|
|
1051
|
+
return this.contactCache.get(number) || null;
|
|
1052
|
+
}
|
|
1053
|
+
getCachedGroup(groupId) {
|
|
1054
|
+
return this.groupCache.get(groupId) || null;
|
|
1055
|
+
}
|
|
1056
|
+
getAccountNumber() {
|
|
1057
|
+
return this.accountNumber;
|
|
1058
|
+
}
|
|
1059
|
+
isServiceConnected() {
|
|
1060
|
+
return this.isConnected;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// src/accounts.ts
|
|
1065
|
+
var DEFAULT_ACCOUNT_ID = "default";
|
|
1066
|
+
function normalizeAccountId(accountId) {
|
|
1067
|
+
if (!accountId || typeof accountId !== "string") {
|
|
1068
|
+
return DEFAULT_ACCOUNT_ID;
|
|
1069
|
+
}
|
|
1070
|
+
const trimmed = accountId.trim().toLowerCase();
|
|
1071
|
+
return trimmed || DEFAULT_ACCOUNT_ID;
|
|
1072
|
+
}
|
|
1073
|
+
function getMultiAccountConfig(runtime) {
|
|
1074
|
+
const characterSignal = runtime.character?.settings?.signal;
|
|
1075
|
+
return {
|
|
1076
|
+
enabled: characterSignal?.enabled,
|
|
1077
|
+
account: characterSignal?.account,
|
|
1078
|
+
httpUrl: characterSignal?.httpUrl,
|
|
1079
|
+
accounts: characterSignal?.accounts
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
function listSignalAccountIds(runtime) {
|
|
1083
|
+
const config = getMultiAccountConfig(runtime);
|
|
1084
|
+
const accounts = config.accounts;
|
|
1085
|
+
if (!accounts || typeof accounts !== "object") {
|
|
1086
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
1087
|
+
}
|
|
1088
|
+
const ids = Object.keys(accounts).filter(Boolean);
|
|
1089
|
+
if (ids.length === 0) {
|
|
1090
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
1091
|
+
}
|
|
1092
|
+
return ids.toSorted((a, b) => a.localeCompare(b));
|
|
1093
|
+
}
|
|
1094
|
+
function resolveDefaultSignalAccountId(runtime) {
|
|
1095
|
+
const ids = listSignalAccountIds(runtime);
|
|
1096
|
+
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
1097
|
+
return DEFAULT_ACCOUNT_ID;
|
|
1098
|
+
}
|
|
1099
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
1100
|
+
}
|
|
1101
|
+
function getAccountConfig(runtime, accountId) {
|
|
1102
|
+
const config = getMultiAccountConfig(runtime);
|
|
1103
|
+
const accounts = config.accounts;
|
|
1104
|
+
if (!accounts || typeof accounts !== "object") {
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
return accounts[accountId];
|
|
1108
|
+
}
|
|
1109
|
+
function filterDefined(obj) {
|
|
1110
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
|
|
1111
|
+
}
|
|
1112
|
+
function mergeSignalAccountConfig(runtime, accountId) {
|
|
1113
|
+
const multiConfig = getMultiAccountConfig(runtime);
|
|
1114
|
+
const { accounts: _ignored, ...baseConfig } = multiConfig;
|
|
1115
|
+
const accountConfig = getAccountConfig(runtime, accountId) ?? {};
|
|
1116
|
+
const envAccount = runtime.getSetting("SIGNAL_ACCOUNT_NUMBER");
|
|
1117
|
+
const envHttpUrl = runtime.getSetting("SIGNAL_HTTP_URL");
|
|
1118
|
+
const envCliPath = runtime.getSetting("SIGNAL_CLI_PATH");
|
|
1119
|
+
const envIgnoreGroups = runtime.getSetting("SIGNAL_SHOULD_IGNORE_GROUP_MESSAGES");
|
|
1120
|
+
const envConfig = {
|
|
1121
|
+
account: envAccount || undefined,
|
|
1122
|
+
httpUrl: envHttpUrl || undefined,
|
|
1123
|
+
cliPath: envCliPath || undefined,
|
|
1124
|
+
shouldIgnoreGroupMessages: envIgnoreGroups?.toLowerCase() === "true"
|
|
1125
|
+
};
|
|
1126
|
+
return {
|
|
1127
|
+
...filterDefined(envConfig),
|
|
1128
|
+
...filterDefined(baseConfig),
|
|
1129
|
+
...filterDefined(accountConfig)
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
function resolveBaseUrl(config) {
|
|
1133
|
+
if (config.httpUrl?.trim()) {
|
|
1134
|
+
return config.httpUrl.trim().replace(/\/+$/, "");
|
|
1135
|
+
}
|
|
1136
|
+
const host = config.httpHost?.trim() || "127.0.0.1";
|
|
1137
|
+
const port = config.httpPort ?? 8080;
|
|
1138
|
+
return `http://${host}:${port}`;
|
|
1139
|
+
}
|
|
1140
|
+
function resolveSignalAccount(runtime, accountId) {
|
|
1141
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
1142
|
+
const multiConfig = getMultiAccountConfig(runtime);
|
|
1143
|
+
const baseEnabled = multiConfig.enabled !== false;
|
|
1144
|
+
const merged = mergeSignalAccountConfig(runtime, normalizedAccountId);
|
|
1145
|
+
const accountEnabled = merged.enabled !== false;
|
|
1146
|
+
const enabled = baseEnabled && accountEnabled;
|
|
1147
|
+
const baseUrl = resolveBaseUrl(merged);
|
|
1148
|
+
const configured = Boolean(merged.account?.trim() || merged.httpUrl?.trim() || merged.cliPath?.trim() || merged.httpHost?.trim() || typeof merged.httpPort === "number" || typeof merged.autoStart === "boolean");
|
|
1149
|
+
return {
|
|
1150
|
+
accountId: normalizedAccountId,
|
|
1151
|
+
enabled,
|
|
1152
|
+
name: merged.name?.trim() || undefined,
|
|
1153
|
+
account: merged.account?.trim(),
|
|
1154
|
+
baseUrl,
|
|
1155
|
+
configured,
|
|
1156
|
+
config: merged
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
function listEnabledSignalAccounts(runtime) {
|
|
1160
|
+
return listSignalAccountIds(runtime).map((accountId) => resolveSignalAccount(runtime, accountId)).filter((account) => account.enabled && account.configured);
|
|
1161
|
+
}
|
|
1162
|
+
function isMultiAccountEnabled(runtime) {
|
|
1163
|
+
const accounts = listEnabledSignalAccounts(runtime);
|
|
1164
|
+
return accounts.length > 1;
|
|
1165
|
+
}
|
|
1166
|
+
// src/rpc.ts
|
|
1167
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
1168
|
+
function normalizeBaseUrl(url) {
|
|
1169
|
+
const trimmed = url.trim();
|
|
1170
|
+
if (!trimmed) {
|
|
1171
|
+
throw new Error("Signal base URL is required");
|
|
1172
|
+
}
|
|
1173
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
1174
|
+
return trimmed.replace(/\/+$/, "");
|
|
1175
|
+
}
|
|
1176
|
+
return `http://${trimmed}`.replace(/\/+$/, "");
|
|
1177
|
+
}
|
|
1178
|
+
function generateId() {
|
|
1179
|
+
return crypto.randomUUID();
|
|
1180
|
+
}
|
|
1181
|
+
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
1182
|
+
const controller = new AbortController;
|
|
1183
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1184
|
+
try {
|
|
1185
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
1186
|
+
} finally {
|
|
1187
|
+
clearTimeout(timer);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
async function signalRpcRequest(method, params, opts) {
|
|
1191
|
+
const baseUrl = normalizeBaseUrl(opts.baseUrl);
|
|
1192
|
+
const id = generateId();
|
|
1193
|
+
const body = JSON.stringify({
|
|
1194
|
+
jsonrpc: "2.0",
|
|
1195
|
+
method,
|
|
1196
|
+
params,
|
|
1197
|
+
id
|
|
1198
|
+
});
|
|
1199
|
+
const res = await fetchWithTimeout(`${baseUrl}/api/v1/rpc`, {
|
|
1200
|
+
method: "POST",
|
|
1201
|
+
headers: { "Content-Type": "application/json" },
|
|
1202
|
+
body
|
|
1203
|
+
}, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
1204
|
+
if (res.status === 201) {
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
const text = await res.text();
|
|
1208
|
+
if (!text) {
|
|
1209
|
+
throw new Error(`Signal RPC empty response (status ${res.status})`);
|
|
1210
|
+
}
|
|
1211
|
+
const parsed = JSON.parse(text);
|
|
1212
|
+
if (parsed.error) {
|
|
1213
|
+
const code = parsed.error.code ?? "unknown";
|
|
1214
|
+
const msg = parsed.error.message ?? "Signal RPC error";
|
|
1215
|
+
throw new Error(`Signal RPC ${code}: ${msg}`);
|
|
1216
|
+
}
|
|
1217
|
+
return parsed.result;
|
|
1218
|
+
}
|
|
1219
|
+
async function signalCheck(baseUrl, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
1220
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
1221
|
+
try {
|
|
1222
|
+
const res = await fetchWithTimeout(`${normalized}/api/v1/check`, { method: "GET" }, timeoutMs);
|
|
1223
|
+
if (!res.ok) {
|
|
1224
|
+
return { ok: false, status: res.status, error: `HTTP ${res.status}` };
|
|
1225
|
+
}
|
|
1226
|
+
return { ok: true, status: res.status, error: null };
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
return {
|
|
1229
|
+
ok: false,
|
|
1230
|
+
status: null,
|
|
1231
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
async function signalGetVersion(opts) {
|
|
1236
|
+
return signalRpcRequest("version", undefined, opts);
|
|
1237
|
+
}
|
|
1238
|
+
async function signalListAccounts(opts) {
|
|
1239
|
+
return signalRpcRequest("listAccounts", undefined, opts);
|
|
1240
|
+
}
|
|
1241
|
+
async function signalListContacts(account, opts) {
|
|
1242
|
+
return signalRpcRequest("listContacts", { account }, opts);
|
|
1243
|
+
}
|
|
1244
|
+
async function signalListGroups(account, opts) {
|
|
1245
|
+
return signalRpcRequest("listGroups", { account }, opts);
|
|
1246
|
+
}
|
|
1247
|
+
async function signalSend(params, opts) {
|
|
1248
|
+
return signalRpcRequest("send", params, opts);
|
|
1249
|
+
}
|
|
1250
|
+
async function signalSendReaction(params, opts) {
|
|
1251
|
+
return signalRpcRequest("sendReaction", params, opts);
|
|
1252
|
+
}
|
|
1253
|
+
async function signalSendTyping(params, opts) {
|
|
1254
|
+
return signalRpcRequest("sendTyping", params, opts);
|
|
1255
|
+
}
|
|
1256
|
+
async function signalSendReadReceipt(params, opts) {
|
|
1257
|
+
return signalRpcRequest("sendReadReceipt", params, opts);
|
|
1258
|
+
}
|
|
1259
|
+
async function streamSignalEvents(params) {
|
|
1260
|
+
const baseUrl = normalizeBaseUrl(params.baseUrl);
|
|
1261
|
+
const url = new URL(`${baseUrl}/api/v1/events`);
|
|
1262
|
+
if (params.account) {
|
|
1263
|
+
url.searchParams.set("account", params.account);
|
|
1264
|
+
}
|
|
1265
|
+
const res = await fetch(url, {
|
|
1266
|
+
method: "GET",
|
|
1267
|
+
headers: { Accept: "text/event-stream" },
|
|
1268
|
+
signal: params.abortSignal
|
|
1269
|
+
});
|
|
1270
|
+
if (!res.ok || !res.body) {
|
|
1271
|
+
throw new Error(`Signal SSE failed (${res.status} ${res.statusText || "error"})`);
|
|
1272
|
+
}
|
|
1273
|
+
const reader = res.body.getReader();
|
|
1274
|
+
const decoder = new TextDecoder;
|
|
1275
|
+
let buffer = "";
|
|
1276
|
+
let currentEvent = {};
|
|
1277
|
+
const flushEvent = () => {
|
|
1278
|
+
if (!currentEvent.data && !currentEvent.event && !currentEvent.id) {
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
params.onEvent({
|
|
1282
|
+
event: currentEvent.event,
|
|
1283
|
+
data: currentEvent.data,
|
|
1284
|
+
id: currentEvent.id
|
|
1285
|
+
});
|
|
1286
|
+
currentEvent = {};
|
|
1287
|
+
};
|
|
1288
|
+
while (true) {
|
|
1289
|
+
const { value, done } = await reader.read();
|
|
1290
|
+
if (done) {
|
|
1291
|
+
break;
|
|
1292
|
+
}
|
|
1293
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1294
|
+
let lineEnd = buffer.indexOf(`
|
|
1295
|
+
`);
|
|
1296
|
+
while (lineEnd !== -1) {
|
|
1297
|
+
let line = buffer.slice(0, lineEnd);
|
|
1298
|
+
buffer = buffer.slice(lineEnd + 1);
|
|
1299
|
+
if (line.endsWith("\r")) {
|
|
1300
|
+
line = line.slice(0, -1);
|
|
1301
|
+
}
|
|
1302
|
+
if (line === "") {
|
|
1303
|
+
flushEvent();
|
|
1304
|
+
lineEnd = buffer.indexOf(`
|
|
1305
|
+
`);
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
if (line.startsWith(":")) {
|
|
1309
|
+
lineEnd = buffer.indexOf(`
|
|
1310
|
+
`);
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
const colonIndex = line.indexOf(":");
|
|
1314
|
+
if (colonIndex === -1) {
|
|
1315
|
+
lineEnd = buffer.indexOf(`
|
|
1316
|
+
`);
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
const field = line.slice(0, colonIndex).trim();
|
|
1320
|
+
let value2 = line.slice(colonIndex + 1);
|
|
1321
|
+
if (value2.startsWith(" ")) {
|
|
1322
|
+
value2 = value2.slice(1);
|
|
1323
|
+
}
|
|
1324
|
+
if (field === "event") {
|
|
1325
|
+
currentEvent.event = value2;
|
|
1326
|
+
} else if (field === "data") {
|
|
1327
|
+
currentEvent.data = currentEvent.data ? `${currentEvent.data}
|
|
1328
|
+
${value2}` : value2;
|
|
1329
|
+
} else if (field === "id") {
|
|
1330
|
+
currentEvent.id = value2;
|
|
1331
|
+
}
|
|
1332
|
+
lineEnd = buffer.indexOf(`
|
|
1333
|
+
`);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
flushEvent();
|
|
1337
|
+
}
|
|
1338
|
+
function parseSignalEventData(data) {
|
|
1339
|
+
if (!data) {
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
try {
|
|
1343
|
+
return JSON.parse(data);
|
|
1344
|
+
} catch {
|
|
1345
|
+
return null;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
function createSignalEventStream(params) {
|
|
1349
|
+
let abortController = null;
|
|
1350
|
+
let running = false;
|
|
1351
|
+
let reconnectDelay = params.reconnectDelayMs ?? 1000;
|
|
1352
|
+
const maxDelay = params.maxReconnectDelayMs ?? 30000;
|
|
1353
|
+
const connect = async () => {
|
|
1354
|
+
if (!running) {
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
abortController = new AbortController;
|
|
1358
|
+
try {
|
|
1359
|
+
params.onConnect?.();
|
|
1360
|
+
reconnectDelay = params.reconnectDelayMs ?? 1000;
|
|
1361
|
+
await streamSignalEvents({
|
|
1362
|
+
baseUrl: params.baseUrl,
|
|
1363
|
+
account: params.account,
|
|
1364
|
+
abortSignal: abortController.signal,
|
|
1365
|
+
onEvent: params.onEvent
|
|
1366
|
+
});
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
params.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
1372
|
+
} finally {
|
|
1373
|
+
params.onDisconnect?.();
|
|
1374
|
+
}
|
|
1375
|
+
if (running) {
|
|
1376
|
+
setTimeout(connect, reconnectDelay);
|
|
1377
|
+
reconnectDelay = Math.min(reconnectDelay * 2, maxDelay);
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
return {
|
|
1381
|
+
start: () => {
|
|
1382
|
+
if (running) {
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
running = true;
|
|
1386
|
+
connect();
|
|
1387
|
+
},
|
|
1388
|
+
stop: () => {
|
|
1389
|
+
running = false;
|
|
1390
|
+
abortController?.abort();
|
|
1391
|
+
abortController = null;
|
|
1392
|
+
},
|
|
1393
|
+
isRunning: () => running
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// src/index.ts
|
|
1398
|
+
var signalPlugin = {
|
|
1399
|
+
name: "signal",
|
|
1400
|
+
description: "Signal messaging integration plugin for ElizaOS with end-to-end encryption",
|
|
1401
|
+
services: [SignalService],
|
|
1402
|
+
actions: [sendMessage_default, sendReaction_default, listContacts_default, listGroups_default],
|
|
1403
|
+
providers: [conversationStateProvider],
|
|
1404
|
+
init: async (_config, runtime) => {
|
|
1405
|
+
const accountNumber = runtime.getSetting("SIGNAL_ACCOUNT_NUMBER");
|
|
1406
|
+
const httpUrl = runtime.getSetting("SIGNAL_HTTP_URL");
|
|
1407
|
+
const cliPath = runtime.getSetting("SIGNAL_CLI_PATH");
|
|
1408
|
+
const ignoreGroups = runtime.getSetting("SIGNAL_SHOULD_IGNORE_GROUP_MESSAGES");
|
|
1409
|
+
const maskNumber = (number) => {
|
|
1410
|
+
if (!number || number.trim() === "")
|
|
1411
|
+
return "[not set]";
|
|
1412
|
+
if (number.length <= 6)
|
|
1413
|
+
return "***";
|
|
1414
|
+
return `${number.slice(0, 3)}...${number.slice(-2)}`;
|
|
1415
|
+
};
|
|
1416
|
+
logger.info({
|
|
1417
|
+
src: "plugin:signal",
|
|
1418
|
+
agentId: runtime.agentId,
|
|
1419
|
+
settings: {
|
|
1420
|
+
accountNumber: maskNumber(accountNumber),
|
|
1421
|
+
httpUrl: httpUrl || "[not set]",
|
|
1422
|
+
cliPath: cliPath || "[not set]",
|
|
1423
|
+
ignoreGroups: ignoreGroups || "false"
|
|
1424
|
+
}
|
|
1425
|
+
}, "Signal plugin initializing");
|
|
1426
|
+
if (!accountNumber || accountNumber.trim() === "") {
|
|
1427
|
+
logger.warn({ src: "plugin:signal", agentId: runtime.agentId }, "SIGNAL_ACCOUNT_NUMBER not provided - Signal plugin is loaded but will not be functional");
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
const normalizedNumber = normalizeE164(accountNumber);
|
|
1431
|
+
if (!normalizedNumber) {
|
|
1432
|
+
logger.error({ src: "plugin:signal", agentId: runtime.agentId, accountNumber }, "SIGNAL_ACCOUNT_NUMBER is not a valid E.164 phone number");
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
if (!httpUrl && !cliPath) {
|
|
1436
|
+
logger.warn({ src: "plugin:signal", agentId: runtime.agentId }, "Neither SIGNAL_HTTP_URL nor SIGNAL_CLI_PATH provided - Signal plugin will not be able to communicate");
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
logger.info({ src: "plugin:signal", agentId: runtime.agentId }, "Signal plugin configuration validated successfully");
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
var src_default = signalPlugin;
|
|
1443
|
+
export {
|
|
1444
|
+
streamSignalEvents,
|
|
1445
|
+
signalSendTyping,
|
|
1446
|
+
signalSendReadReceipt,
|
|
1447
|
+
signalSendReaction,
|
|
1448
|
+
signalSend,
|
|
1449
|
+
signalRpcRequest,
|
|
1450
|
+
signalListGroups,
|
|
1451
|
+
signalListContacts,
|
|
1452
|
+
signalListAccounts,
|
|
1453
|
+
signalGetVersion,
|
|
1454
|
+
signalCheck,
|
|
1455
|
+
sendReaction,
|
|
1456
|
+
sendMessage,
|
|
1457
|
+
resolveSignalAccount,
|
|
1458
|
+
resolveDefaultSignalAccountId,
|
|
1459
|
+
parseSignalEventData,
|
|
1460
|
+
normalizeE164,
|
|
1461
|
+
normalizeBaseUrl,
|
|
1462
|
+
normalizeAccountId,
|
|
1463
|
+
listSignalAccountIds,
|
|
1464
|
+
listGroups,
|
|
1465
|
+
listEnabledSignalAccounts,
|
|
1466
|
+
listContacts,
|
|
1467
|
+
isValidUuid,
|
|
1468
|
+
isValidGroupId,
|
|
1469
|
+
isValidE164,
|
|
1470
|
+
isMultiAccountEnabled,
|
|
1471
|
+
getSignalContactDisplayName,
|
|
1472
|
+
src_default as default,
|
|
1473
|
+
createSignalEventStream,
|
|
1474
|
+
conversationStateProvider,
|
|
1475
|
+
SignalServiceNotInitializedError,
|
|
1476
|
+
SignalService,
|
|
1477
|
+
SignalPluginError,
|
|
1478
|
+
SignalEventTypes,
|
|
1479
|
+
SignalConfigurationError,
|
|
1480
|
+
SignalClientNotAvailableError,
|
|
1481
|
+
SignalApiError,
|
|
1482
|
+
SIGNAL_SERVICE_NAME,
|
|
1483
|
+
MAX_SIGNAL_MESSAGE_LENGTH,
|
|
1484
|
+
MAX_SIGNAL_ATTACHMENT_SIZE,
|
|
1485
|
+
DEFAULT_ACCOUNT_ID
|
|
1486
|
+
};
|
|
1487
|
+
|
|
1488
|
+
//# debugId=35D8D7AB25F728BD64756E2164756E21
|