aicq-chat-plugin 3.2.2 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.mjs ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * AICQ Chat Plugin — Channel Plugin Entry Point (ESM)
3
+ *
4
+ * Uses OpenClaw Channel Plugin SDK:
5
+ * - defineChannelPluginEntry from openclaw/plugin-sdk/channel-core
6
+ * - createChatChannelPlugin from openclaw/plugin-sdk/channel-core
7
+ *
8
+ * Architecture: In-process Channel (no sidecar, no independent port)
9
+ */
10
+ import { defineChannelPluginEntry } from 'openclaw/plugin-sdk/channel-core';
11
+ import { aicqChatPlugin } from './src/channel.mjs';
12
+
13
+ export default defineChannelPluginEntry({
14
+ id: 'aicq-chat',
15
+ name: 'AICQ Encrypted Chat',
16
+ description: 'End-to-end encrypted chat channel via AICQ protocol — in-process Channel plugin',
17
+ plugin: aicqChatPlugin,
18
+
19
+ registerCliMetadata(api) {
20
+ api.registerCli(
21
+ ({ program }) => {
22
+ program
23
+ .command('aicq-chat')
24
+ .description('AICQ Encrypted Chat management');
25
+ },
26
+ {
27
+ descriptors: [
28
+ {
29
+ name: 'aicq-chat',
30
+ description: 'AICQ Encrypted Chat management',
31
+ hasSubcommands: false,
32
+ },
33
+ ],
34
+ },
35
+ );
36
+ },
37
+
38
+ registerFull(api) {
39
+ // Register gateway HTTP routes for the SPA UI
40
+ const gatewayMethods = [
41
+ 'aicq.status',
42
+ 'aicq.friends.list',
43
+ 'aicq.friends.add',
44
+ 'aicq.friends.remove',
45
+ 'aicq.friends.requests',
46
+ 'aicq.friends.acceptRequest',
47
+ 'aicq.friends.rejectRequest',
48
+ 'aicq.identity.info',
49
+ 'aicq.agent.create',
50
+ 'aicq.agent.delete',
51
+ 'aicq.chat.send',
52
+ 'aicq.chat.history',
53
+ 'aicq.chat.streamChunk',
54
+ 'aicq.chat.streamEnd',
55
+ 'aicq.groups.list',
56
+ 'aicq.groups.create',
57
+ 'aicq.groups.join',
58
+ 'aicq.groups.messages',
59
+ 'aicq.groups.silent',
60
+ 'aicq.sessions.list',
61
+ ];
62
+
63
+ for (const method of gatewayMethods) {
64
+ api.registerGatewayMethod(method, async (kwargs, ctx) => {
65
+ const { handleGateway } = await import('./src/gateway-handlers.mjs');
66
+ return handleGateway(method, kwargs, ctx);
67
+ });
68
+ }
69
+ },
70
+ });
@@ -2,9 +2,9 @@
2
2
  "kind": "channel",
3
3
  "id": "aicq-chat",
4
4
  "name": "AICQ Encrypted Chat",
5
- "version": "3.2.2",
6
- "description": "End-to-end encrypted chat channel via AICQ protocol — in-process Channel plugin",
7
- "entry": "index.js",
5
+ "version": "3.3.1",
6
+ "description": "End-to-end encrypted chat channel via AICQ protocol — in-process Channel plugin using OpenClaw Channel SDK",
7
+ "entry": "index.mjs",
8
8
  "activation": {
9
9
  "onStartup": true
10
10
  },
@@ -17,7 +17,6 @@
17
17
  "label": "AICQ Encrypted Chat",
18
18
  "description": "End-to-end encrypted chat channel via AICQ protocol",
19
19
  "schema": {
20
- "$schema": "http://json-schema.org/draft-07/schema#",
21
20
  "type": "object",
22
21
  "additionalProperties": false,
23
22
  "properties": {
package/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "aicq-chat-plugin",
3
- "version": "3.2.2",
4
- "description": "AICQ End-to-end Encrypted Chat Channel Plugin for OpenClaw — In-process Channel architecture with friend management, group chat, file transfer, and AI agent communication",
5
- "main": "index.js",
3
+ "version": "3.3.1",
4
+ "description": "AICQ End-to-end Encrypted Chat Channel Plugin for OpenClaw — In-process Channel SDK architecture with friend management, group chat, file transfer, and AI agent communication",
5
+ "type": "module",
6
+ "main": "index.mjs",
6
7
  "bin": {
7
8
  "aicq-plugin": "cli.js"
8
9
  },
9
10
  "files": [
11
+ "index.mjs",
10
12
  "index.js",
13
+ "setup-entry.mjs",
11
14
  "setup-entry.js",
12
15
  "cli.js",
13
16
  "postinstall.js",
@@ -19,7 +22,8 @@
19
22
  "README.md"
20
23
  ],
21
24
  "scripts": {
22
- "start": "node index.js",
25
+ "start": "node index.mjs",
26
+ "start:cjs": "node index.js",
23
27
  "postinstall": "node postinstall.js",
24
28
  "install-deps": "npm install"
25
29
  },
@@ -62,8 +66,9 @@
62
66
  },
63
67
  "openclaw": {
64
68
  "extensions": [
65
- "./index.js"
69
+ "./index.mjs"
66
70
  ],
71
+ "setupEntry": "./setup-entry.mjs",
67
72
  "channel": {
68
73
  "id": "aicq-chat",
69
74
  "label": "AICQ Encrypted Chat",
@@ -0,0 +1,9 @@
1
+ /**
2
+ * AICQ Setup Entry — Setup Wizard for first-time configuration
3
+ *
4
+ * Uses defineSetupPluginEntry from the OpenClaw Channel SDK
5
+ */
6
+ import { defineSetupPluginEntry } from 'openclaw/plugin-sdk/channel-core';
7
+ import { aicqChatPlugin } from './src/channel.mjs';
8
+
9
+ export default defineSetupPluginEntry(aicqChatPlugin);
@@ -0,0 +1,254 @@
1
+ /**
2
+ * AICQ Channel Plugin — Core Channel Logic (ESM)
3
+ *
4
+ * Uses OpenClaw Channel Plugin SDK:
5
+ * - createChatChannelPlugin from openclaw/plugin-sdk/channel-core
6
+ * - createChannelPluginBase from openclaw/plugin-sdk/channel-core
7
+ *
8
+ * Wraps existing lib/ modules (identity, server-client, handshake, chat, database)
9
+ * into the proper OpenClaw Channel plugin interface.
10
+ */
11
+ import { createChatChannelPlugin, createChannelPluginBase } from 'openclaw/plugin-sdk/channel-core';
12
+ import { createRequire } from 'module';
13
+ import path from 'path';
14
+ import fs from 'fs';
15
+ import os from 'os';
16
+
17
+ const require = createRequire(import.meta.url);
18
+ const __dirname = path.dirname(new URL(import.meta.url).pathname);
19
+
20
+ // ── Configuration ──────────────────────────────────────────────────
21
+ const DATA_DIR = process.env.AICQ_DATA_DIR || path.join(os.homedir(), '.aicq-plugin');
22
+ const SERVER_URL = process.env.AICQ_SERVER_URL || 'https://aicq.online';
23
+
24
+ fs.mkdirSync(DATA_DIR, { recursive: true });
25
+
26
+ // ── Lazy-loaded CommonJS modules ───────────────────────────────────
27
+ let _db = null;
28
+ let _identity = null;
29
+ let _serverClient = null;
30
+ let _handshake = null;
31
+ let _chat = null;
32
+ let _initialized = false;
33
+
34
+ /**
35
+ * Initialize all plugin components (async, called once)
36
+ */
37
+ async function ensureInitialized() {
38
+ if (_initialized) return;
39
+
40
+ const PluginDatabase = require('../lib/database');
41
+ const IdentityManager = require('../lib/identity');
42
+ const ServerClient = require('../lib/server-client');
43
+ const HandshakeManager = require('../lib/handshake');
44
+ const ChatManager = require('../lib/chat');
45
+
46
+ // Initialize database
47
+ _db = new PluginDatabase(DATA_DIR);
48
+ await _db.init();
49
+ console.log('[AICQ Channel] Database initialized');
50
+
51
+ // Initialize managers
52
+ _identity = new IdentityManager(_db);
53
+ _serverClient = new ServerClient(_identity, _db, SERVER_URL);
54
+ _handshake = new HandshakeManager(_identity, _serverClient, _db);
55
+ _chat = new ChatManager(_identity, _serverClient, _db, path.join(DATA_DIR, 'uploads'));
56
+
57
+ // Periodic cleanup
58
+ setInterval(() => _db.cleanup(), 3600000);
59
+
60
+ _initialized = true;
61
+ console.log('[AICQ Channel] Plugin components initialized');
62
+ }
63
+
64
+ /**
65
+ * Resolve account from OpenClaw config
66
+ * Reads channels.<channel-id> section and returns a resolved account object
67
+ */
68
+ function resolveAccount(cfg, accountId) {
69
+ const section = (cfg.channels || {})[ 'aicq-chat' ];
70
+ const token = section?.accountId || accountId || null;
71
+ return {
72
+ accountId: token,
73
+ serverUrl: section?.serverUrl || SERVER_URL,
74
+ autoAcceptFriends: section?.autoAcceptFriends ?? true,
75
+ dmPolicy: section?.dmPolicy || 'allowlist',
76
+ enabled: section?.enabled ?? true,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Inspect account without materializing secrets
82
+ */
83
+ function inspectAccount(cfg, accountId) {
84
+ const section = (cfg.channels || {})[ 'aicq-chat' ];
85
+ return {
86
+ enabled: section?.enabled ?? true,
87
+ configured: Boolean(section?.accountId || accountId),
88
+ hasAccountId: Boolean(section?.accountId || accountId),
89
+ };
90
+ }
91
+
92
+ // ── Build the Channel Plugin using SDK ──────────────────────────────
93
+ export const aicqChatPlugin = createChatChannelPlugin({
94
+ base: createChannelPluginBase({
95
+ id: 'aicq-chat',
96
+ setup: {
97
+ resolveAccount,
98
+ inspectAccount,
99
+ },
100
+ }),
101
+
102
+ // DM security: who can message the bot
103
+ security: {
104
+ dm: {
105
+ channelKey: 'aicq-chat',
106
+ resolvePolicy: (account) => account.dmPolicy || 'allowlist',
107
+ resolveAllowFrom: async (account) => {
108
+ // Only friends in the contact list can send DMs
109
+ await ensureInitialized();
110
+ if (!_identity || !account.accountId) return [];
111
+ const friends = _db.listFriends(account.accountId);
112
+ return friends.map(f => f.id || f.friend_id);
113
+ },
114
+ defaultPolicy: 'allowlist',
115
+ },
116
+ },
117
+
118
+ // Pairing: DM approval flow for new contacts
119
+ pairing: {
120
+ text: {
121
+ idLabel: 'AICQ Account ID',
122
+ message: 'Share this pairing code with the other party to verify your identity:',
123
+ generate: async ({ target, code }) => {
124
+ await ensureInitialized();
125
+ if (_handshake && target) {
126
+ try {
127
+ await _serverClient.ensureAuth(target);
128
+ const result = await _handshake.generateFriendCode(target);
129
+ return { code: result.number, instructions: `Share this pairing code: ${result.number}` };
130
+ } catch (e) {
131
+ // Fallback to the generated code
132
+ }
133
+ }
134
+ return { code, instructions: `Share this pairing code: ${code}` };
135
+ },
136
+ verify: async ({ accountId, peerCode }) => {
137
+ await ensureInitialized();
138
+ try {
139
+ const result = await _handshake.addFriendByCode(accountId, peerCode);
140
+ return { success: true, peerId: result.peer_id || result.friend_id || peerCode };
141
+ } catch (e) {
142
+ return { success: false, error: e.message };
143
+ }
144
+ },
145
+ },
146
+ },
147
+
148
+ // Threading: how replies are delivered
149
+ threading: {
150
+ topLevelReplyToMode: 'reply',
151
+ },
152
+
153
+ // Outbound: send messages to the platform
154
+ outbound: {
155
+ attachedResults: {
156
+ sendText: async (params) => {
157
+ await ensureInitialized();
158
+ const result = await _chat.sendMessage(
159
+ params.fromAccountId || params.accountId,
160
+ params.to,
161
+ params.text,
162
+ { isGroup: false }
163
+ );
164
+ return { messageId: result?.message_id || result?.id || `msg_${Date.now()}` };
165
+ },
166
+ },
167
+ base: {
168
+ sendMedia: async (params) => {
169
+ await ensureInitialized();
170
+ const result = await _chat.sendMessage(
171
+ params.fromAccountId || params.accountId,
172
+ params.to,
173
+ params.mediaUrl || params.filePath,
174
+ { type: params.mediaType || 'file', isGroup: false }
175
+ );
176
+ return { messageId: result?.message_id || result?.id || `msg_${Date.now()}` };
177
+ },
178
+ },
179
+ },
180
+
181
+ // Lifecycle: account management
182
+ lifecycle: {
183
+ onAccountCreate: async (accountId) => {
184
+ await ensureInitialized();
185
+ let agentIdentity = _identity.loadAgent(accountId);
186
+ if (!agentIdentity) {
187
+ agentIdentity = _identity.createAgent(accountId, `agent-${accountId.slice(0, 8)}`);
188
+ }
189
+ try {
190
+ await _serverClient.start(accountId);
191
+ } catch (e) {
192
+ console.error('[AICQ Channel] Server connection failed for account:', accountId, e.message);
193
+ }
194
+ },
195
+ onAccountDelete: async (accountId) => {
196
+ try {
197
+ _serverClient.disconnect();
198
+ } catch (e) {}
199
+ _identity.deleteAgent(accountId);
200
+ },
201
+ onShutdown: async () => {
202
+ try {
203
+ _serverClient.stop();
204
+ } catch (e) {}
205
+ console.log('[AICQ Channel] Shutdown complete');
206
+ },
207
+ },
208
+
209
+ // Inbound: process incoming messages (used when AICQ server pushes messages)
210
+ inbound: {
211
+ onText: async (message) => {
212
+ const { toAccountId, fromPeerId, encryptedContent } = message;
213
+ await ensureInitialized();
214
+
215
+ let content = encryptedContent || message.content || message.payload || '';
216
+ const session = _db.loadSession(toAccountId, fromPeerId);
217
+ if (session && session.session_key && typeof content === 'string') {
218
+ try {
219
+ const { decryptMessage } = require('../lib/crypto');
220
+ content = decryptMessage(content, session.session_key);
221
+ } catch (e) {
222
+ // Might be plaintext, keep as is
223
+ }
224
+ }
225
+
226
+ return {
227
+ text: typeof content === 'string' ? content : JSON.stringify(content),
228
+ metadata: {
229
+ fromPeerId,
230
+ timestamp: message.timestamp,
231
+ },
232
+ };
233
+ },
234
+ onMedia: async (message) => {
235
+ await ensureInitialized();
236
+ let content = message.encryptedContent || message.content || '';
237
+ const session = _db.loadSession(message.toAccountId, message.fromPeerId);
238
+ if (session && session.session_key && typeof content === 'string') {
239
+ try {
240
+ const { decryptMessage } = require('../lib/crypto');
241
+ content = decryptMessage(content, session.session_key);
242
+ } catch (e) {}
243
+ }
244
+ return {
245
+ mediaUrl: content,
246
+ mediaType: message.mediaType || 'file',
247
+ metadata: { fromPeerId: message.fromPeerId },
248
+ };
249
+ },
250
+ },
251
+ });
252
+
253
+ // Export for gateway handlers to access managers
254
+ export { ensureInitialized, _db, _identity, _serverClient, _handshake, _chat };
@@ -0,0 +1,178 @@
1
+ /**
2
+ * AICQ Gateway Handlers — Gateway method implementations
3
+ *
4
+ * Handles all aicq.* gateway methods for the SPA UI.
5
+ * Lazy-initializes the plugin components on first call.
6
+ */
7
+
8
+ let _handlersInitialized = false;
9
+ let _db = null;
10
+ let _identity = null;
11
+ let _serverClient = null;
12
+ let _handshake = null;
13
+ let _chat = null;
14
+
15
+ async function ensureHandlersInitialized() {
16
+ if (_handlersInitialized) return;
17
+
18
+ try {
19
+ const channelModule = await import('./channel.mjs');
20
+ await channelModule.ensureInitialized();
21
+ // Access the lazy-loaded managers through the module's exports
22
+ _db = channelModule._db;
23
+ _identity = channelModule._identity;
24
+ _serverClient = channelModule._serverClient;
25
+ _handshake = channelModule._handshake;
26
+ _chat = channelModule._chat;
27
+ _handlersInitialized = true;
28
+ } catch (e) {
29
+ console.error('[AICQ Gateway] Failed to initialize handlers:', e.message);
30
+ throw e;
31
+ }
32
+ }
33
+
34
+ function getCurrentAgentId() {
35
+ if (!_identity) return null;
36
+ const agents = _identity.listAgents();
37
+ return agents.length > 0 ? agents[0].agent_id : null;
38
+ }
39
+
40
+ /**
41
+ * Main gateway method dispatcher
42
+ */
43
+ export async function handleGateway(method, kwargs = {}, ctx = {}) {
44
+ await ensureHandlersInitialized();
45
+ const currentAgentId = getCurrentAgentId();
46
+
47
+ switch (method) {
48
+ case 'aicq.status':
49
+ return {
50
+ state: _serverClient?.connected ? 'connected' : 'disconnected',
51
+ agent_id: currentAgentId,
52
+ version: '3.3.0',
53
+ architecture: 'channel',
54
+ };
55
+
56
+ case 'aicq.friends.list':
57
+ return { friends: _db?.listFriends(currentAgentId) || [] };
58
+
59
+ case 'aicq.friends.add':
60
+ return await _handshake?.addFriendByCode(currentAgentId, kwargs.temp_number);
61
+
62
+ case 'aicq.friends.remove':
63
+ _db?.removeFriend(currentAgentId, kwargs.friend_id);
64
+ return { success: true };
65
+
66
+ case 'aicq.friends.requests':
67
+ return { requests: _db?.getPendingRequests(currentAgentId) || [] };
68
+
69
+ case 'aicq.friends.acceptRequest':
70
+ return await _handshake?.acceptRequest(currentAgentId, kwargs.request_id);
71
+
72
+ case 'aicq.friends.rejectRequest':
73
+ return await _handshake?.rejectRequest(currentAgentId, kwargs.request_id);
74
+
75
+ case 'aicq.identity.info':
76
+ return _identity?.getInfo(currentAgentId) || {};
77
+
78
+ case 'aicq.agent.create':
79
+ _identity?.createAgent(kwargs.agent_id, kwargs.nickname);
80
+ return { success: true };
81
+
82
+ case 'aicq.agent.delete':
83
+ _identity?.deleteAgent(kwargs.agent_id);
84
+ return { success: true };
85
+
86
+ case 'aicq.chat.send':
87
+ return await _chat?.sendMessage(currentAgentId, kwargs.targetId, kwargs.content, { isGroup: kwargs.isGroup });
88
+
89
+ case 'aicq.chat.history':
90
+ return { messages: _db?.getChatHistory(currentAgentId, kwargs.targetId, { limit: kwargs.limit || 50 }) || [] };
91
+
92
+ case 'aicq.chat.streamChunk': {
93
+ if (!kwargs.friend_id && !kwargs.targetId) return { error: 'friend_id or targetId is required' };
94
+ if (!kwargs.data) return { error: 'data is required' };
95
+ const chunkType = kwargs.chunk_type || kwargs.chunkType || 'text';
96
+ const ALLOWED_CHUNK_TYPES = ['text', 'reasoning', 'thinking', 'clear_text', 'tool_call', 'tool_result'];
97
+ if (!ALLOWED_CHUNK_TYPES.includes(chunkType)) return { error: `Invalid chunk_type: ${chunkType}. Allowed: ${ALLOWED_CHUNK_TYPES.join(', ')}` };
98
+ const streamTarget = kwargs.friend_id || kwargs.targetId;
99
+ const sent = _serverClient?.sendWS({
100
+ type: 'stream_chunk',
101
+ to: streamTarget,
102
+ chunkType: chunkType,
103
+ data: kwargs.data,
104
+ });
105
+ if (!sent) return { error: 'Not connected to server', success: false };
106
+ return { success: true };
107
+ }
108
+
109
+ case 'aicq.chat.streamEnd': {
110
+ if (!kwargs.friend_id && !kwargs.targetId) return { error: 'friend_id or targetId is required' };
111
+ const endTarget = kwargs.friend_id || kwargs.targetId;
112
+ const msgId = kwargs.message_id || kwargs.messageId || ('msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6));
113
+ const endSent = _serverClient?.sendWS({
114
+ type: 'stream_end',
115
+ to: endTarget,
116
+ messageId: msgId,
117
+ });
118
+ if (!endSent) return { error: 'Not connected to server', success: false };
119
+ return { success: true, messageId: msgId };
120
+ }
121
+
122
+ case 'aicq.groups.list':
123
+ return { groups: _db?.listGroups(currentAgentId) || [] };
124
+
125
+ case 'aicq.groups.create': {
126
+ await _serverClient?.ensureAuth(currentAgentId);
127
+ const result = await _serverClient?.createGroup(kwargs.name, kwargs.description);
128
+ if (result?.id) {
129
+ _db?.addGroup({
130
+ agent_id: currentAgentId,
131
+ id: result.id,
132
+ name: kwargs.name,
133
+ owner_id: currentAgentId,
134
+ members_json: result.members || '[]',
135
+ description: kwargs.description || '',
136
+ });
137
+ }
138
+ return { success: true, group: result };
139
+ }
140
+
141
+ case 'aicq.groups.join':
142
+ await _serverClient?.ensureAuth(currentAgentId);
143
+ return await _serverClient?.inviteGroupMember(kwargs.group_id, currentAgentId);
144
+
145
+ case 'aicq.groups.messages': {
146
+ await _serverClient?.ensureAuth(currentAgentId);
147
+ return await _serverClient?.getGroupMessages(kwargs.group_id, kwargs.limit || 50);
148
+ }
149
+
150
+ case 'aicq.groups.silent':
151
+ _db?.setGroupSilentMode(currentAgentId, kwargs.group_id, !!kwargs.silent);
152
+ return { success: true, silent: !!kwargs.silent };
153
+
154
+ default:
155
+ return { error: `Unknown method: ${method}` };
156
+ }
157
+ }
158
+
159
+ // Individual method exports (for direct registration)
160
+ export async function getStatus(kwargs, ctx) { return handleGateway('aicq.status', kwargs, ctx); }
161
+ export async function listFriends(kwargs, ctx) { return handleGateway('aicq.friends.list', kwargs, ctx); }
162
+ export async function addFriend(kwargs, ctx) { return handleGateway('aicq.friends.add', kwargs, ctx); }
163
+ export async function removeFriend(kwargs, ctx) { return handleGateway('aicq.friends.remove', kwargs, ctx); }
164
+ export async function listFriendRequests(kwargs, ctx) { return handleGateway('aicq.friends.requests', kwargs, ctx); }
165
+ export async function acceptFriendRequest(kwargs, ctx) { return handleGateway('aicq.friends.acceptRequest', kwargs, ctx); }
166
+ export async function rejectFriendRequest(kwargs, ctx) { return handleGateway('aicq.friends.rejectRequest', kwargs, ctx); }
167
+ export async function getIdentityInfo(kwargs, ctx) { return handleGateway('aicq.identity.info', kwargs, ctx); }
168
+ export async function createAgent(kwargs, ctx) { return handleGateway('aicq.agent.create', kwargs, ctx); }
169
+ export async function deleteAgent(kwargs, ctx) { return handleGateway('aicq.agent.delete', kwargs, ctx); }
170
+ export async function chatSend(kwargs, ctx) { return handleGateway('aicq.chat.send', kwargs, ctx); }
171
+ export async function chatHistory(kwargs, ctx) { return handleGateway('aicq.chat.history', kwargs, ctx); }
172
+ export async function chatStreamChunk(kwargs, ctx) { return handleGateway('aicq.chat.streamChunk', kwargs, ctx); }
173
+ export async function chatStreamEnd(kwargs, ctx) { return handleGateway('aicq.chat.streamEnd', kwargs, ctx); }
174
+ export async function listGroups(kwargs, ctx) { return handleGateway('aicq.groups.list', kwargs, ctx); }
175
+ export async function createGroup(kwargs, ctx) { return handleGateway('aicq.groups.create', kwargs, ctx); }
176
+ export async function joinGroup(kwargs, ctx) { return handleGateway('aicq.groups.join', kwargs, ctx); }
177
+ export async function getGroupMessages(kwargs, ctx) { return handleGateway('aicq.groups.messages', kwargs, ctx); }
178
+ export async function setGroupSilent(kwargs, ctx) { return handleGateway('aicq.groups.silent', kwargs, ctx); }