@elizaos/plugin-bluebubbles 2.0.0-alpha.3
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/__tests__/integration.test.ts +260 -0
- package/build.ts +16 -0
- package/dist/index.js +46 -0
- package/package.json +33 -0
- package/src/actions/index.ts +5 -0
- package/src/actions/sendMessage.ts +175 -0
- package/src/actions/sendReaction.ts +186 -0
- package/src/client.ts +389 -0
- package/src/constants.ts +41 -0
- package/src/environment.ts +120 -0
- package/src/index.ts +68 -0
- package/src/providers/chatContext.ts +105 -0
- package/src/providers/chatState.ts +90 -0
- package/src/providers/index.ts +5 -0
- package/src/service.ts +502 -0
- package/src/types.ts +165 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send reaction action for the BlueBubbles plugin.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Action, ActionResult, IAgentRuntime, Memory, State, HandlerCallback } from "@elizaos/core";
|
|
6
|
+
import {
|
|
7
|
+
composePromptFromState,
|
|
8
|
+
logger,
|
|
9
|
+
ModelType,
|
|
10
|
+
parseJSONObjectFromText,
|
|
11
|
+
} from "@elizaos/core";
|
|
12
|
+
import { BlueBubblesService } from "../service.js";
|
|
13
|
+
import { BLUEBUBBLES_SERVICE_NAME } from "../constants.js";
|
|
14
|
+
|
|
15
|
+
const SEND_REACTION_TEMPLATE = `# Task: Extract BlueBubbles reaction parameters
|
|
16
|
+
|
|
17
|
+
Based on the conversation, determine what reaction to add or remove.
|
|
18
|
+
|
|
19
|
+
Recent conversation:
|
|
20
|
+
{{recentMessages}}
|
|
21
|
+
|
|
22
|
+
Extract the following:
|
|
23
|
+
1. emoji: The emoji reaction to add (heart, thumbsup, thumbsdown, haha, exclamation, question, or any emoji)
|
|
24
|
+
2. messageId: The message ID to react to (or "last" for the last message)
|
|
25
|
+
3. remove: true to remove the reaction, false to add it
|
|
26
|
+
|
|
27
|
+
Respond with a JSON object:
|
|
28
|
+
\`\`\`json
|
|
29
|
+
{
|
|
30
|
+
"emoji": "❤️",
|
|
31
|
+
"messageId": "last",
|
|
32
|
+
"remove": false
|
|
33
|
+
}
|
|
34
|
+
\`\`\`
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
interface ReactionParams {
|
|
38
|
+
emoji: string;
|
|
39
|
+
messageId: string;
|
|
40
|
+
remove: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const sendReactionAction: Action = {
|
|
44
|
+
name: "BLUEBUBBLES_SEND_REACTION",
|
|
45
|
+
similes: ["BLUEBUBBLES_REACT", "BB_REACTION", "IMESSAGE_REACT"],
|
|
46
|
+
description: "Add or remove a reaction on a message via BlueBubbles",
|
|
47
|
+
|
|
48
|
+
validate: async (
|
|
49
|
+
runtime: IAgentRuntime,
|
|
50
|
+
message: Memory,
|
|
51
|
+
_state?: State
|
|
52
|
+
): Promise<boolean> => {
|
|
53
|
+
return message.content.source === "bluebubbles";
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
handler: async (
|
|
57
|
+
runtime: IAgentRuntime,
|
|
58
|
+
message: Memory,
|
|
59
|
+
state: State | undefined,
|
|
60
|
+
_options?: Record<string, unknown>,
|
|
61
|
+
callback?: HandlerCallback
|
|
62
|
+
): Promise<ActionResult> => {
|
|
63
|
+
const bbService = runtime.getService<BlueBubblesService>(BLUEBUBBLES_SERVICE_NAME);
|
|
64
|
+
const currentState = state ?? (await runtime.composeState(message));
|
|
65
|
+
|
|
66
|
+
if (!bbService || !bbService.isConnected()) {
|
|
67
|
+
if (callback) {
|
|
68
|
+
await callback({ text: "BlueBubbles service is not available.", source: "bluebubbles" });
|
|
69
|
+
}
|
|
70
|
+
return { success: false, error: "BlueBubbles service not available" };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Extract parameters using LLM
|
|
74
|
+
const prompt = await composePromptFromState({
|
|
75
|
+
template: SEND_REACTION_TEMPLATE,
|
|
76
|
+
state: currentState,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
let reactionInfo: ReactionParams | null = null;
|
|
80
|
+
|
|
81
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
82
|
+
const response = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
83
|
+
prompt,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const parsed = parseJSONObjectFromText(response);
|
|
87
|
+
if (parsed?.emoji) {
|
|
88
|
+
reactionInfo = {
|
|
89
|
+
emoji: String(parsed.emoji),
|
|
90
|
+
messageId: String(parsed.messageId || "last"),
|
|
91
|
+
remove: Boolean(parsed.remove),
|
|
92
|
+
};
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!reactionInfo || !reactionInfo.emoji) {
|
|
98
|
+
if (callback) {
|
|
99
|
+
await callback({
|
|
100
|
+
text: "I couldn't understand the reaction. Please specify an emoji.",
|
|
101
|
+
source: "bluebubbles",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return { success: false, error: "Could not extract reaction parameters" };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Get chat context
|
|
108
|
+
const stateData = (currentState.data || {}) as Record<string, unknown>;
|
|
109
|
+
const chatGuid = stateData.chatGuid as string;
|
|
110
|
+
let messageGuid = reactionInfo.messageId;
|
|
111
|
+
|
|
112
|
+
if (!chatGuid) {
|
|
113
|
+
if (callback) {
|
|
114
|
+
await callback({
|
|
115
|
+
text: "I couldn't determine the chat to react in.",
|
|
116
|
+
source: "bluebubbles",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return { success: false, error: "Could not determine chat" };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// If "last", get the last message GUID from context
|
|
123
|
+
if (messageGuid === "last" || !messageGuid) {
|
|
124
|
+
messageGuid = stateData.lastMessageGuid as string;
|
|
125
|
+
if (!messageGuid) {
|
|
126
|
+
if (callback) {
|
|
127
|
+
await callback({
|
|
128
|
+
text: "I couldn't find the message to react to.",
|
|
129
|
+
source: "bluebubbles",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return { success: false, error: "Could not find message to react to" };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Send reaction - we only support adding reactions, not removing
|
|
137
|
+
// The BlueBubbles API handles remove through a negative reaction type internally
|
|
138
|
+
const reactionValue = reactionInfo.remove ? `-${reactionInfo.emoji}` : reactionInfo.emoji;
|
|
139
|
+
const result = await bbService.sendReaction(
|
|
140
|
+
chatGuid,
|
|
141
|
+
messageGuid,
|
|
142
|
+
reactionValue
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (!result.success) {
|
|
146
|
+
if (callback) {
|
|
147
|
+
await callback({
|
|
148
|
+
text: `Failed to ${reactionInfo.remove ? "remove" : "add"} reaction.`,
|
|
149
|
+
source: "bluebubbles",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return { success: false, error: "Failed to send reaction" };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
logger.debug(
|
|
156
|
+
`${reactionInfo.remove ? "Removed" : "Added"} reaction ${reactionInfo.emoji} on ${messageGuid}`
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (callback) {
|
|
160
|
+
await callback({
|
|
161
|
+
text: reactionInfo.remove
|
|
162
|
+
? "Reaction removed."
|
|
163
|
+
: `Reacted with ${reactionInfo.emoji}.`,
|
|
164
|
+
source: message.content.source as string,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { success: true };
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
examples: [
|
|
172
|
+
[
|
|
173
|
+
{
|
|
174
|
+
name: "{{user1}}",
|
|
175
|
+
content: { text: "React to that message with a heart" },
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "{{agent}}",
|
|
179
|
+
content: {
|
|
180
|
+
text: "I'll add a heart reaction.",
|
|
181
|
+
actions: ["BLUEBUBBLES_SEND_REACTION"],
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
],
|
|
186
|
+
};
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BlueBubbles API client for interacting with the BlueBubbles server
|
|
3
|
+
*/
|
|
4
|
+
import { logger } from "@elizaos/core";
|
|
5
|
+
import { API_ENDPOINTS } from "./constants";
|
|
6
|
+
import type {
|
|
7
|
+
BlueBubblesChat,
|
|
8
|
+
BlueBubblesConfig,
|
|
9
|
+
BlueBubblesMessage,
|
|
10
|
+
BlueBubblesProbeResult,
|
|
11
|
+
BlueBubblesServerInfo,
|
|
12
|
+
SendAttachmentOptions,
|
|
13
|
+
SendMessageOptions,
|
|
14
|
+
SendMessageResult,
|
|
15
|
+
} from "./types";
|
|
16
|
+
|
|
17
|
+
export class BlueBubblesClient {
|
|
18
|
+
private baseUrl: string;
|
|
19
|
+
private password: string;
|
|
20
|
+
|
|
21
|
+
constructor(config: BlueBubblesConfig) {
|
|
22
|
+
this.baseUrl = config.serverUrl.replace(/\/$/, "");
|
|
23
|
+
this.password = config.password;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private async request<T>(
|
|
27
|
+
endpoint: string,
|
|
28
|
+
options: RequestInit = {},
|
|
29
|
+
): Promise<T> {
|
|
30
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
31
|
+
const separator = endpoint.includes("?") ? "&" : "?";
|
|
32
|
+
const urlWithPassword = `${url}${separator}password=${encodeURIComponent(this.password)}`;
|
|
33
|
+
|
|
34
|
+
const response = await fetch(urlWithPassword, {
|
|
35
|
+
...options,
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
...options.headers,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const errorText = await response.text();
|
|
44
|
+
throw new Error(
|
|
45
|
+
`BlueBubbles API error (${response.status}): ${errorText}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return response.json() as Promise<T>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Probes the BlueBubbles server to check connectivity and capabilities
|
|
54
|
+
*/
|
|
55
|
+
async probe(timeoutMs = 5000): Promise<BlueBubblesProbeResult> {
|
|
56
|
+
try {
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
59
|
+
|
|
60
|
+
const info = await this.request<{ data: BlueBubblesServerInfo }>(
|
|
61
|
+
API_ENDPOINTS.SERVER_INFO,
|
|
62
|
+
{ signal: controller.signal },
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
clearTimeout(timeoutId);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
serverVersion: info.data.server_version,
|
|
70
|
+
osVersion: info.data.os_version,
|
|
71
|
+
privateApiEnabled: info.data.private_api,
|
|
72
|
+
helperConnected: info.data.helper_connected,
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const errorMessage =
|
|
76
|
+
error instanceof Error ? error.message : String(error);
|
|
77
|
+
logger.error(`BlueBubbles probe failed: ${errorMessage}`);
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
error: errorMessage,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sends a text message
|
|
87
|
+
*/
|
|
88
|
+
async sendMessage(
|
|
89
|
+
chatGuid: string,
|
|
90
|
+
text: string,
|
|
91
|
+
options: SendMessageOptions = {},
|
|
92
|
+
): Promise<SendMessageResult> {
|
|
93
|
+
const response = await this.request<{ data: BlueBubblesMessage }>(
|
|
94
|
+
API_ENDPOINTS.SEND_MESSAGE,
|
|
95
|
+
{
|
|
96
|
+
method: "POST",
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
chatGuid,
|
|
99
|
+
message: text,
|
|
100
|
+
tempGuid: options.tempGuid,
|
|
101
|
+
method: options.method ?? "apple-script",
|
|
102
|
+
subject: options.subject,
|
|
103
|
+
effectId: options.effectId,
|
|
104
|
+
partIndex: options.partIndex,
|
|
105
|
+
ddScan: options.ddScan,
|
|
106
|
+
}),
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
guid: response.data.guid,
|
|
112
|
+
tempGuid: options.tempGuid,
|
|
113
|
+
status: "sent",
|
|
114
|
+
dateCreated: response.data.dateCreated,
|
|
115
|
+
text: response.data.text ?? text,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Sends an attachment
|
|
121
|
+
*/
|
|
122
|
+
async sendAttachment(
|
|
123
|
+
chatGuid: string,
|
|
124
|
+
attachmentPath: string,
|
|
125
|
+
options: SendAttachmentOptions = {},
|
|
126
|
+
): Promise<SendMessageResult> {
|
|
127
|
+
const formData = new FormData();
|
|
128
|
+
formData.append("chatGuid", chatGuid);
|
|
129
|
+
formData.append("attachment", attachmentPath);
|
|
130
|
+
|
|
131
|
+
if (options.tempGuid) {
|
|
132
|
+
formData.append("tempGuid", options.tempGuid);
|
|
133
|
+
}
|
|
134
|
+
if (options.name) {
|
|
135
|
+
formData.append("name", options.name);
|
|
136
|
+
}
|
|
137
|
+
if (options.isAudioMessage !== undefined) {
|
|
138
|
+
formData.append("isAudioMessage", String(options.isAudioMessage));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const url = `${this.baseUrl}${API_ENDPOINTS.SEND_ATTACHMENT}?password=${encodeURIComponent(this.password)}`;
|
|
142
|
+
const response = await fetch(url, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
body: formData,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
const errorText = await response.text();
|
|
149
|
+
throw new Error(`Failed to send attachment: ${errorText}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const result = (await response.json()) as { data: BlueBubblesMessage };
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
guid: result.data.guid,
|
|
156
|
+
tempGuid: options.tempGuid,
|
|
157
|
+
status: "sent",
|
|
158
|
+
dateCreated: result.data.dateCreated,
|
|
159
|
+
text: result.data.text ?? "",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Sends an attachment from a buffer
|
|
165
|
+
*/
|
|
166
|
+
async sendAttachmentBuffer(
|
|
167
|
+
chatGuid: string,
|
|
168
|
+
buffer: Uint8Array,
|
|
169
|
+
filename: string,
|
|
170
|
+
mimeType: string,
|
|
171
|
+
caption?: string,
|
|
172
|
+
): Promise<SendMessageResult> {
|
|
173
|
+
const blob = new Blob([buffer], { type: mimeType });
|
|
174
|
+
const formData = new FormData();
|
|
175
|
+
formData.append("chatGuid", chatGuid);
|
|
176
|
+
formData.append("attachment", blob, filename);
|
|
177
|
+
if (caption) {
|
|
178
|
+
formData.append("message", caption);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const url = `${this.baseUrl}${API_ENDPOINTS.SEND_ATTACHMENT}?password=${encodeURIComponent(this.password)}`;
|
|
182
|
+
const response = await fetch(url, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
body: formData,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
const errorText = await response.text();
|
|
189
|
+
throw new Error(`Failed to send attachment: ${errorText}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const result = (await response.json()) as { data: BlueBubblesMessage };
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
guid: result.data.guid,
|
|
196
|
+
status: "sent",
|
|
197
|
+
dateCreated: result.data.dateCreated,
|
|
198
|
+
text: caption ?? "",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Gets information about a chat
|
|
204
|
+
*/
|
|
205
|
+
async getChat(chatGuid: string): Promise<BlueBubblesChat> {
|
|
206
|
+
const response = await this.request<{ data: BlueBubblesChat }>(
|
|
207
|
+
`${API_ENDPOINTS.CHAT_INFO}/${encodeURIComponent(chatGuid)}`,
|
|
208
|
+
);
|
|
209
|
+
return response.data;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Lists all chats
|
|
214
|
+
*/
|
|
215
|
+
async listChats(limit = 100, offset = 0): Promise<BlueBubblesChat[]> {
|
|
216
|
+
const response = await this.request<{ data: BlueBubblesChat[] }>(
|
|
217
|
+
`${API_ENDPOINTS.CHATS}?limit=${limit}&offset=${offset}&with=lastMessage,participants`,
|
|
218
|
+
);
|
|
219
|
+
return response.data;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Gets messages for a chat
|
|
224
|
+
*/
|
|
225
|
+
async getMessages(
|
|
226
|
+
chatGuid: string,
|
|
227
|
+
limit = 50,
|
|
228
|
+
offset = 0,
|
|
229
|
+
): Promise<BlueBubblesMessage[]> {
|
|
230
|
+
const response = await this.request<{ data: BlueBubblesMessage[] }>(
|
|
231
|
+
`${API_ENDPOINTS.CHAT_INFO}/${encodeURIComponent(chatGuid)}/message?limit=${limit}&offset=${offset}`,
|
|
232
|
+
);
|
|
233
|
+
return response.data;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Marks a chat as read
|
|
238
|
+
*/
|
|
239
|
+
async markChatRead(chatGuid: string): Promise<void> {
|
|
240
|
+
const endpoint = API_ENDPOINTS.MARK_READ.replace(
|
|
241
|
+
":guid",
|
|
242
|
+
encodeURIComponent(chatGuid),
|
|
243
|
+
);
|
|
244
|
+
await this.request(endpoint, {
|
|
245
|
+
method: "POST",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Sends a reaction to a message
|
|
251
|
+
*/
|
|
252
|
+
async reactToMessage(
|
|
253
|
+
chatGuid: string,
|
|
254
|
+
messageGuid: string,
|
|
255
|
+
reaction: string,
|
|
256
|
+
): Promise<void> {
|
|
257
|
+
await this.request(API_ENDPOINTS.REACT, {
|
|
258
|
+
method: "POST",
|
|
259
|
+
body: JSON.stringify({
|
|
260
|
+
chatGuid,
|
|
261
|
+
messageGuid,
|
|
262
|
+
reaction,
|
|
263
|
+
}),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Edits a message (requires private API)
|
|
269
|
+
*/
|
|
270
|
+
async editMessage(
|
|
271
|
+
messageGuid: string,
|
|
272
|
+
newText: string,
|
|
273
|
+
backwardsCompatMessage?: string,
|
|
274
|
+
): Promise<void> {
|
|
275
|
+
const endpoint = API_ENDPOINTS.EDIT.replace(
|
|
276
|
+
":guid",
|
|
277
|
+
encodeURIComponent(messageGuid),
|
|
278
|
+
);
|
|
279
|
+
await this.request(endpoint, {
|
|
280
|
+
method: "POST",
|
|
281
|
+
body: JSON.stringify({
|
|
282
|
+
editedMessage: newText,
|
|
283
|
+
backwardsCompatibilityMessage: backwardsCompatMessage ?? newText,
|
|
284
|
+
}),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Unsends a message (requires private API)
|
|
290
|
+
*/
|
|
291
|
+
async unsendMessage(messageGuid: string): Promise<void> {
|
|
292
|
+
const endpoint = API_ENDPOINTS.UNSEND.replace(
|
|
293
|
+
":guid",
|
|
294
|
+
encodeURIComponent(messageGuid),
|
|
295
|
+
);
|
|
296
|
+
await this.request(endpoint, {
|
|
297
|
+
method: "POST",
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Resolves a target (handle or chat GUID) to a chat GUID
|
|
303
|
+
*/
|
|
304
|
+
async resolveTarget(target: string): Promise<string> {
|
|
305
|
+
// If it already looks like a chat GUID, return it
|
|
306
|
+
if (target.startsWith("iMessage;") || target.startsWith("SMS;")) {
|
|
307
|
+
return target;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// If it looks like a chat ID or identifier, query for it
|
|
311
|
+
if (target.startsWith("chat_")) {
|
|
312
|
+
const chats = await this.listChats();
|
|
313
|
+
const chat = chats.find(
|
|
314
|
+
(c) =>
|
|
315
|
+
c.chatIdentifier === target ||
|
|
316
|
+
c.guid === target ||
|
|
317
|
+
c.chatIdentifier.includes(target),
|
|
318
|
+
);
|
|
319
|
+
if (chat) {
|
|
320
|
+
return chat.guid;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Otherwise, construct a DM chat GUID
|
|
325
|
+
// First try as iMessage, which is most common
|
|
326
|
+
return `iMessage;-;${target}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Creates a new group chat
|
|
331
|
+
*/
|
|
332
|
+
async createGroupChat(
|
|
333
|
+
participants: string[],
|
|
334
|
+
name?: string,
|
|
335
|
+
message?: string,
|
|
336
|
+
): Promise<BlueBubblesChat> {
|
|
337
|
+
const response = await this.request<{ data: BlueBubblesChat }>(
|
|
338
|
+
API_ENDPOINTS.CHATS,
|
|
339
|
+
{
|
|
340
|
+
method: "POST",
|
|
341
|
+
body: JSON.stringify({
|
|
342
|
+
participants,
|
|
343
|
+
name,
|
|
344
|
+
message,
|
|
345
|
+
}),
|
|
346
|
+
},
|
|
347
|
+
);
|
|
348
|
+
return response.data;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Adds a participant to a group chat
|
|
353
|
+
*/
|
|
354
|
+
async addParticipant(chatGuid: string, handle: string): Promise<void> {
|
|
355
|
+
await this.request(
|
|
356
|
+
`${API_ENDPOINTS.CHAT_INFO}/${encodeURIComponent(chatGuid)}/participant`,
|
|
357
|
+
{
|
|
358
|
+
method: "POST",
|
|
359
|
+
body: JSON.stringify({ address: handle }),
|
|
360
|
+
},
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Removes a participant from a group chat
|
|
366
|
+
*/
|
|
367
|
+
async removeParticipant(chatGuid: string, handle: string): Promise<void> {
|
|
368
|
+
await this.request(
|
|
369
|
+
`${API_ENDPOINTS.CHAT_INFO}/${encodeURIComponent(chatGuid)}/participant`,
|
|
370
|
+
{
|
|
371
|
+
method: "DELETE",
|
|
372
|
+
body: JSON.stringify({ address: handle }),
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Renames a group chat
|
|
379
|
+
*/
|
|
380
|
+
async renameGroupChat(chatGuid: string, newName: string): Promise<void> {
|
|
381
|
+
await this.request(
|
|
382
|
+
`${API_ENDPOINTS.CHAT_INFO}/${encodeURIComponent(chatGuid)}`,
|
|
383
|
+
{
|
|
384
|
+
method: "PATCH",
|
|
385
|
+
body: JSON.stringify({ displayName: newName }),
|
|
386
|
+
},
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants for the BlueBubbles plugin
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const BLUEBUBBLES_SERVICE_NAME = "bluebubbles";
|
|
6
|
+
export const DEFAULT_WEBHOOK_PATH = "/webhooks/bluebubbles";
|
|
7
|
+
export const DEFAULT_TEXT_CHUNK_LIMIT = 4000;
|
|
8
|
+
|
|
9
|
+
// DM Policy options
|
|
10
|
+
export const DM_POLICY_OPEN = "open";
|
|
11
|
+
export const DM_POLICY_PAIRING = "pairing";
|
|
12
|
+
export const DM_POLICY_ALLOWLIST = "allowlist";
|
|
13
|
+
export const DM_POLICY_DISABLED = "disabled";
|
|
14
|
+
|
|
15
|
+
// Group Policy options
|
|
16
|
+
export const GROUP_POLICY_OPEN = "open";
|
|
17
|
+
export const GROUP_POLICY_ALLOWLIST = "allowlist";
|
|
18
|
+
export const GROUP_POLICY_DISABLED = "disabled";
|
|
19
|
+
|
|
20
|
+
// API endpoints
|
|
21
|
+
export const API_ENDPOINTS = {
|
|
22
|
+
SERVER_INFO: "/api/v1/server/info",
|
|
23
|
+
SEND_MESSAGE: "/api/v1/message/text",
|
|
24
|
+
SEND_ATTACHMENT: "/api/v1/message/attachment",
|
|
25
|
+
CHAT_INFO: "/api/v1/chat",
|
|
26
|
+
CHATS: "/api/v1/chat",
|
|
27
|
+
MESSAGES: "/api/v1/message",
|
|
28
|
+
MARK_READ: "/api/v1/chat/:guid/read",
|
|
29
|
+
HANDLE_INFO: "/api/v1/handle",
|
|
30
|
+
REACT: "/api/v1/message/react",
|
|
31
|
+
EDIT: "/api/v1/message/:guid/edit",
|
|
32
|
+
UNSEND: "/api/v1/message/:guid/unsend",
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
// Message types
|
|
36
|
+
export const MESSAGE_TYPES = {
|
|
37
|
+
TEXT: "text",
|
|
38
|
+
ATTACHMENT: "attachment",
|
|
39
|
+
REACTION: "reaction",
|
|
40
|
+
GROUP_ACTION: "group_action",
|
|
41
|
+
} as const;
|