openclaw-groupme 0.0.3 → 0.3.0

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.
@@ -1,43 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- looksLikeGroupMeTargetId,
4
- normalizeGroupMeAllowEntry,
5
- normalizeGroupMeGroupId,
6
- normalizeGroupMeTarget,
7
- normalizeGroupMeUserId,
8
- } from "./normalize.js";
9
-
10
- describe("groupme normalize", () => {
11
- it("normalizes user and group ids", () => {
12
- expect(normalizeGroupMeUserId(" 123 ")).toBe("123");
13
- expect(normalizeGroupMeGroupId(456)).toBe("456");
14
- });
15
-
16
- it("returns undefined for empty IDs", () => {
17
- expect(normalizeGroupMeUserId(" ")).toBeUndefined();
18
- expect(normalizeGroupMeGroupId("")).toBeUndefined();
19
- });
20
-
21
- it("normalizes prefixed targets", () => {
22
- expect(normalizeGroupMeTarget("groupme:group:12345")).toBe("12345");
23
- expect(normalizeGroupMeTarget("groupme:user:54321")).toBe("54321");
24
- expect(normalizeGroupMeTarget("group:abc")).toBe("abc");
25
- expect(normalizeGroupMeTarget("12345")).toBe("12345");
26
- });
27
-
28
- it("returns undefined for empty target", () => {
29
- expect(normalizeGroupMeTarget(" ")).toBeUndefined();
30
- });
31
-
32
- it("normalizes allow entries and keeps wildcard", () => {
33
- expect(normalizeGroupMeAllowEntry("groupme:user:123")).toBe("123");
34
- expect(normalizeGroupMeAllowEntry("*")).toBe("*");
35
- });
36
-
37
- it("validates likely target IDs", () => {
38
- expect(looksLikeGroupMeTargetId("groupme:group:123")).toBe(true);
39
- expect(looksLikeGroupMeTargetId("abc-123")).toBe(true);
40
- expect(looksLikeGroupMeTargetId("has spaces")).toBe(false);
41
- expect(looksLikeGroupMeTargetId(" ")).toBe(false);
42
- });
43
- });
package/src/parse.test.ts DELETED
@@ -1,162 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- detectGroupMeMention,
4
- extractImageUrls,
5
- parseGroupMeCallback,
6
- shouldProcessCallback,
7
- } from "./parse.js";
8
-
9
- const validPayload = {
10
- id: "msg-1",
11
- text: "hello @oddclaw",
12
- name: "Alice",
13
- sender_type: "user",
14
- sender_id: "123",
15
- user_id: "123",
16
- group_id: "999",
17
- source_guid: "src-1",
18
- created_at: 1_700_000_000,
19
- system: false,
20
- avatar_url: "https://i.groupme.com/a.png",
21
- attachments: [{ type: "image", url: "https://i.groupme.com/img" }],
22
- };
23
-
24
- describe("parseGroupMeCallback", () => {
25
- it("parses a valid callback payload", () => {
26
- const parsed = parseGroupMeCallback(validPayload);
27
- expect(parsed).not.toBeNull();
28
- expect(parsed?.id).toBe("msg-1");
29
- expect(parsed?.senderType).toBe("user");
30
- expect(parsed?.attachments).toHaveLength(1);
31
- });
32
-
33
- it("returns null for invalid payload", () => {
34
- expect(parseGroupMeCallback(null)).toBeNull();
35
- expect(parseGroupMeCallback({})).toBeNull();
36
- expect(
37
- parseGroupMeCallback({
38
- ...validPayload,
39
- sender_id: null,
40
- }),
41
- ).toBeNull();
42
- });
43
-
44
- it("handles missing text field by normalizing to empty string", () => {
45
- const parsed = parseGroupMeCallback({
46
- ...validPayload,
47
- text: null,
48
- });
49
- expect(parsed?.text).toBe("");
50
- });
51
-
52
- it("extracts image URLs from attachments", () => {
53
- const parsed = parseGroupMeCallback({
54
- ...validPayload,
55
- attachments: [
56
- { type: "image", url: "https://i.groupme.com/one" },
57
- { type: "emoji", placeholder: "x", charmap: [[1, 2]] },
58
- { type: "image", url: "https://i.groupme.com/two" },
59
- ],
60
- });
61
- expect(extractImageUrls(parsed?.attachments ?? [])).toEqual([
62
- "https://i.groupme.com/one",
63
- "https://i.groupme.com/two",
64
- ]);
65
- });
66
- });
67
-
68
- describe("shouldProcessCallback", () => {
69
- it("accepts user messages", () => {
70
- const parsed = parseGroupMeCallback(validPayload);
71
- expect(parsed).not.toBeNull();
72
- expect(shouldProcessCallback(parsed!)).toBeNull();
73
- });
74
-
75
- it("rejects bot messages", () => {
76
- const parsed = parseGroupMeCallback({
77
- ...validPayload,
78
- sender_type: "bot",
79
- });
80
- expect(parsed).not.toBeNull();
81
- expect(shouldProcessCallback(parsed!)).toBe("non-user message");
82
- });
83
-
84
- it("rejects system messages", () => {
85
- const parsed = parseGroupMeCallback({
86
- ...validPayload,
87
- system: true,
88
- });
89
- expect(parsed).not.toBeNull();
90
- expect(shouldProcessCallback(parsed!)).toBe("system message");
91
- });
92
-
93
- it("rejects empty messages with no attachments", () => {
94
- const parsed = parseGroupMeCallback({
95
- ...validPayload,
96
- text: " ",
97
- attachments: [],
98
- });
99
- expect(parsed).not.toBeNull();
100
- expect(shouldProcessCallback(parsed!)).toBe("empty message");
101
- });
102
-
103
- it("accepts image-only messages", () => {
104
- const parsed = parseGroupMeCallback({
105
- ...validPayload,
106
- text: "",
107
- attachments: [{ type: "image", url: "https://i.groupme.com/only" }],
108
- });
109
- expect(parsed).not.toBeNull();
110
- expect(shouldProcessCallback(parsed!)).toBeNull();
111
- });
112
- });
113
-
114
- describe("detectGroupMeMention", () => {
115
- it("detects exact bot name mention", () => {
116
- expect(detectGroupMeMention({ text: "oddclaw help", botName: "oddclaw" })).toBe(true);
117
- });
118
-
119
- it("detects @botname mention", () => {
120
- expect(detectGroupMeMention({ text: "@oddclaw help", botName: "oddclaw" })).toBe(true);
121
- });
122
-
123
- it("is case-insensitive", () => {
124
- expect(detectGroupMeMention({ text: "ODDCLAW", botName: "oddclaw" })).toBe(true);
125
- });
126
-
127
- it("uses mentionPatterns regex", () => {
128
- expect(
129
- detectGroupMeMention({
130
- text: "hey there",
131
- channelMentionPatterns: ["hey\\s+there"],
132
- }),
133
- ).toBe(true);
134
- });
135
-
136
- it("uses agent mention regexes", () => {
137
- expect(
138
- detectGroupMeMention({
139
- text: "Need oddclaw now",
140
- mentionRegexes: [/\boddclaw\b/i],
141
- }),
142
- ).toBe(true);
143
- });
144
-
145
- it("returns false for unrelated messages", () => {
146
- expect(detectGroupMeMention({ text: "random chat", botName: "oddclaw" })).toBe(false);
147
- });
148
-
149
- it("handles empty text", () => {
150
- expect(detectGroupMeMention({ text: "", botName: "oddclaw" })).toBe(false);
151
- });
152
-
153
- it("ignores invalid regex patterns", () => {
154
- expect(
155
- detectGroupMeMention({
156
- text: "oddclaw",
157
- botName: "oddclaw",
158
- channelMentionPatterns: ["[(invalid"],
159
- }),
160
- ).toBe(true);
161
- });
162
- });
@@ -1,23 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { resolveSenderAccess } from "./policy.js";
3
-
4
- describe("resolveSenderAccess", () => {
5
- it("allows all when allowFrom is empty", () => {
6
- expect(resolveSenderAccess({ senderId: "123", allowFrom: [] })).toBe(true);
7
- expect(resolveSenderAccess({ senderId: "123" })).toBe(true);
8
- });
9
-
10
- it("allows wildcard", () => {
11
- expect(resolveSenderAccess({ senderId: "123", allowFrom: ["*"] })).toBe(true);
12
- });
13
-
14
- it("allows listed sender", () => {
15
- expect(resolveSenderAccess({ senderId: "123", allowFrom: ["123"] })).toBe(true);
16
- expect(resolveSenderAccess({ senderId: "123", allowFrom: [123] })).toBe(true);
17
- expect(resolveSenderAccess({ senderId: "123", allowFrom: ["groupme:user:123"] })).toBe(true);
18
- });
19
-
20
- it("blocks unlisted sender", () => {
21
- expect(resolveSenderAccess({ senderId: "999", allowFrom: ["123", "456"] })).toBe(false);
22
- });
23
- });
package/src/send.test.ts DELETED
@@ -1,153 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import type { CoreConfig } from "./types.js";
3
- import {
4
- sendGroupMeMedia,
5
- sendGroupMeMessage,
6
- sendGroupMeText,
7
- uploadGroupMeImage,
8
- } from "./send.js";
9
-
10
- describe("sendGroupMeMessage", () => {
11
- it("sends text message", async () => {
12
- const fetchMock = vi.fn(async () => new Response("", { status: 201, statusText: "Created" }));
13
-
14
- await sendGroupMeMessage({
15
- botId: "bot-1",
16
- text: "hello",
17
- fetchFn: fetchMock as unknown as typeof fetch,
18
- });
19
-
20
- expect(fetchMock).toHaveBeenCalledTimes(1);
21
- const [url, options] = fetchMock.mock.calls[0] as [string, RequestInit];
22
- expect(url).toBe("https://api.groupme.com/v3/bots/post");
23
- const body = JSON.parse(String(options.body));
24
- expect(body).toEqual({ bot_id: "bot-1", text: "hello" });
25
- });
26
-
27
- it("sends message with picture_url", async () => {
28
- const fetchMock = vi.fn(async () => new Response("", { status: 202, statusText: "Accepted" }));
29
-
30
- await sendGroupMeMessage({
31
- botId: "bot-1",
32
- text: "image",
33
- pictureUrl: "https://i.groupme.com/abc",
34
- fetchFn: fetchMock as unknown as typeof fetch,
35
- });
36
-
37
- const [, options] = fetchMock.mock.calls[0] as [string, RequestInit];
38
- const body = JSON.parse(String(options.body));
39
- expect(body.picture_url).toBe("https://i.groupme.com/abc");
40
- });
41
-
42
- it("throws on API error", async () => {
43
- const fetchMock = vi.fn(
44
- async () => new Response("bad", { status: 400, statusText: "Bad Request" }),
45
- );
46
-
47
- await expect(
48
- sendGroupMeMessage({
49
- botId: "bot-1",
50
- text: "hello",
51
- fetchFn: fetchMock as unknown as typeof fetch,
52
- }),
53
- ).rejects.toThrow("GroupMe API error");
54
- });
55
- });
56
-
57
- describe("uploadGroupMeImage", () => {
58
- it("uploads and returns picture_url", async () => {
59
- const fetchMock = vi.fn(
60
- async () =>
61
- new Response(JSON.stringify({ payload: { picture_url: "https://i.groupme.com/pic" } }), {
62
- status: 200,
63
- }),
64
- );
65
-
66
- const result = await uploadGroupMeImage({
67
- accessToken: "token",
68
- imageData: Buffer.from("abc"),
69
- fetchFn: fetchMock as unknown as typeof fetch,
70
- });
71
-
72
- expect(result).toBe("https://i.groupme.com/pic");
73
- });
74
-
75
- it("throws when picture_url is missing", async () => {
76
- const fetchMock = vi.fn(
77
- async () =>
78
- new Response(JSON.stringify({ payload: {} }), {
79
- status: 200,
80
- }),
81
- );
82
-
83
- await expect(
84
- uploadGroupMeImage({
85
- accessToken: "token",
86
- imageData: Buffer.from("abc"),
87
- fetchFn: fetchMock as unknown as typeof fetch,
88
- }),
89
- ).rejects.toThrow("no picture_url");
90
- });
91
- });
92
-
93
- describe("high-level send helpers", () => {
94
- it("sends text using resolved account", async () => {
95
- const cfg: CoreConfig = {
96
- channels: {
97
- groupme: {
98
- botId: "bot-1",
99
- },
100
- },
101
- };
102
-
103
- const fetchMock = vi.fn(async () => new Response("", { status: 201 }));
104
-
105
- await sendGroupMeText({
106
- cfg,
107
- to: "any",
108
- text: "hello",
109
- fetchFn: fetchMock as unknown as typeof fetch,
110
- });
111
-
112
- expect(fetchMock).toHaveBeenCalledTimes(1);
113
- });
114
-
115
- it("sends media by downloading then uploading", async () => {
116
- const cfg: CoreConfig = {
117
- channels: {
118
- groupme: {
119
- botId: "bot-1",
120
- accessToken: "token-1",
121
- },
122
- },
123
- };
124
-
125
- const fetchMock = vi
126
- .fn()
127
- .mockResolvedValueOnce(
128
- new Response(Buffer.from("img"), {
129
- status: 200,
130
- headers: { "content-type": "image/png" },
131
- }),
132
- )
133
- .mockResolvedValueOnce(
134
- new Response(JSON.stringify({ payload: { picture_url: "https://i.groupme.com/new" } }), {
135
- status: 200,
136
- }),
137
- )
138
- .mockResolvedValueOnce(new Response("", { status: 201 }));
139
-
140
- await sendGroupMeMedia({
141
- cfg,
142
- to: "any",
143
- text: "caption",
144
- mediaUrl: "https://example.com/image.png",
145
- fetchFn: fetchMock as unknown as typeof fetch,
146
- });
147
-
148
- expect(fetchMock).toHaveBeenCalledTimes(3);
149
- expect(fetchMock.mock.calls[0]?.[0]).toBe("https://example.com/image.png");
150
- expect(fetchMock.mock.calls[1]?.[0]).toBe("https://image.groupme.com/pictures");
151
- expect(fetchMock.mock.calls[2]?.[0]).toBe("https://api.groupme.com/v3/bots/post");
152
- });
153
- });