@plosson/agentio 0.1.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.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/package.json +52 -0
- package/src/auth/oauth.ts +132 -0
- package/src/auth/token-manager.ts +104 -0
- package/src/auth/token-store.ts +114 -0
- package/src/commands/gmail.ts +303 -0
- package/src/commands/telegram.ts +247 -0
- package/src/config/config-manager.ts +127 -0
- package/src/config/credentials.ts +7 -0
- package/src/index.ts +16 -0
- package/src/services/gmail/client.ts +377 -0
- package/src/services/telegram/client.ts +81 -0
- package/src/types/config.ts +16 -0
- package/src/types/gmail.ts +40 -0
- package/src/types/telegram.ts +34 -0
- package/src/types/tokens.ts +13 -0
- package/src/utils/errors.ts +60 -0
- package/src/utils/output.ts +54 -0
- package/src/utils/stdin.ts +18 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { google, gmail_v1 } from 'googleapis';
|
|
2
|
+
import type { OAuth2Client } from 'google-auth-library';
|
|
3
|
+
import { basename } from 'path';
|
|
4
|
+
import type { GmailMessage, GmailListOptions, GmailSendOptions, GmailReplyOptions, GmailAttachment } from '../../types/gmail';
|
|
5
|
+
import { CliError } from '../../utils/errors';
|
|
6
|
+
|
|
7
|
+
// Common MIME types by extension
|
|
8
|
+
const MIME_TYPES: Record<string, string> = {
|
|
9
|
+
'.txt': 'text/plain',
|
|
10
|
+
'.html': 'text/html',
|
|
11
|
+
'.css': 'text/css',
|
|
12
|
+
'.js': 'application/javascript',
|
|
13
|
+
'.json': 'application/json',
|
|
14
|
+
'.xml': 'application/xml',
|
|
15
|
+
'.pdf': 'application/pdf',
|
|
16
|
+
'.zip': 'application/zip',
|
|
17
|
+
'.gz': 'application/gzip',
|
|
18
|
+
'.tar': 'application/x-tar',
|
|
19
|
+
'.png': 'image/png',
|
|
20
|
+
'.jpg': 'image/jpeg',
|
|
21
|
+
'.jpeg': 'image/jpeg',
|
|
22
|
+
'.gif': 'image/gif',
|
|
23
|
+
'.svg': 'image/svg+xml',
|
|
24
|
+
'.webp': 'image/webp',
|
|
25
|
+
'.mp3': 'audio/mpeg',
|
|
26
|
+
'.mp4': 'video/mp4',
|
|
27
|
+
'.wav': 'audio/wav',
|
|
28
|
+
'.doc': 'application/msword',
|
|
29
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
30
|
+
'.xls': 'application/vnd.ms-excel',
|
|
31
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
32
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
33
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function getMimeType(filename: string): string {
|
|
37
|
+
const ext = filename.toLowerCase().match(/\.[^.]+$/)?.[0] || '';
|
|
38
|
+
return MIME_TYPES[ext] || 'application/octet-stream';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class GmailClient {
|
|
42
|
+
private gmail: gmail_v1.Gmail;
|
|
43
|
+
private userEmail: string | null = null;
|
|
44
|
+
|
|
45
|
+
constructor(auth: OAuth2Client) {
|
|
46
|
+
this.gmail = google.gmail({ version: 'v1', auth });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private async getUserEmail(): Promise<string> {
|
|
50
|
+
if (this.userEmail) return this.userEmail;
|
|
51
|
+
|
|
52
|
+
const profile = await this.gmail.users.getProfile({ userId: 'me' });
|
|
53
|
+
this.userEmail = profile.data.emailAddress || 'me';
|
|
54
|
+
return this.userEmail;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private parseHeaders(headers: gmail_v1.Schema$MessagePartHeader[] | undefined): Record<string, string> {
|
|
58
|
+
const result: Record<string, string> = {};
|
|
59
|
+
for (const header of headers || []) {
|
|
60
|
+
if (header.name && header.value) {
|
|
61
|
+
result[header.name.toLowerCase()] = header.value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private parseMessage(message: gmail_v1.Schema$Message): GmailMessage {
|
|
68
|
+
const headers = this.parseHeaders(message.payload?.headers);
|
|
69
|
+
|
|
70
|
+
const parseAddresses = (value?: string): string[] => {
|
|
71
|
+
if (!value) return [];
|
|
72
|
+
return value.split(',').map((addr) => addr.trim());
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
id: message.id!,
|
|
77
|
+
threadId: message.threadId!,
|
|
78
|
+
subject: headers['subject'] || '(no subject)',
|
|
79
|
+
from: headers['from'] || '',
|
|
80
|
+
to: parseAddresses(headers['to']),
|
|
81
|
+
cc: parseAddresses(headers['cc']),
|
|
82
|
+
date: headers['date'] || '',
|
|
83
|
+
snippet: message.snippet || '',
|
|
84
|
+
labels: message.labelIds || [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private getBody(payload: gmail_v1.Schema$MessagePart | undefined, preferHtml: boolean = false): string {
|
|
89
|
+
if (!payload) return '';
|
|
90
|
+
|
|
91
|
+
const findPart = (part: gmail_v1.Schema$MessagePart, mimeType: string): string | null => {
|
|
92
|
+
if (part.mimeType === mimeType && part.body?.data) {
|
|
93
|
+
return Buffer.from(part.body.data, 'base64').toString('utf-8');
|
|
94
|
+
}
|
|
95
|
+
for (const child of part.parts || []) {
|
|
96
|
+
const result = findPart(child, mimeType);
|
|
97
|
+
if (result) return result;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const targetMime = preferHtml ? 'text/html' : 'text/plain';
|
|
103
|
+
const fallbackMime = preferHtml ? 'text/plain' : 'text/html';
|
|
104
|
+
|
|
105
|
+
return findPart(payload, targetMime) || findPart(payload, fallbackMime) || '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async list(options: GmailListOptions = {}): Promise<{ messages: GmailMessage[]; total: number }> {
|
|
109
|
+
const { limit = 10, query, labels } = options;
|
|
110
|
+
|
|
111
|
+
let q = query || '';
|
|
112
|
+
if (labels?.length) {
|
|
113
|
+
q += ' ' + labels.map((l) => `label:${l}`).join(' ');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const response = await this.gmail.users.messages.list({
|
|
118
|
+
userId: 'me',
|
|
119
|
+
maxResults: Math.min(limit, 100),
|
|
120
|
+
q: q.trim() || undefined,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const messageIds = response.data.messages || [];
|
|
124
|
+
const messages: GmailMessage[] = [];
|
|
125
|
+
|
|
126
|
+
for (const { id } of messageIds) {
|
|
127
|
+
if (!id) continue;
|
|
128
|
+
const msg = await this.gmail.users.messages.get({
|
|
129
|
+
userId: 'me',
|
|
130
|
+
id,
|
|
131
|
+
format: 'metadata',
|
|
132
|
+
metadataHeaders: ['From', 'To', 'Cc', 'Subject', 'Date'],
|
|
133
|
+
});
|
|
134
|
+
messages.push(this.parseMessage(msg.data));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
messages,
|
|
139
|
+
total: response.data.resultSizeEstimate || messages.length,
|
|
140
|
+
};
|
|
141
|
+
} catch (error: any) {
|
|
142
|
+
throw new CliError('API_ERROR', `Gmail API error: ${error.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async get(messageId: string, format: 'text' | 'html' | 'raw' = 'text'): Promise<GmailMessage & { body: string }> {
|
|
147
|
+
try {
|
|
148
|
+
const response = await this.gmail.users.messages.get({
|
|
149
|
+
userId: 'me',
|
|
150
|
+
id: messageId,
|
|
151
|
+
format: format === 'raw' ? 'raw' : 'full',
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const message = this.parseMessage(response.data);
|
|
155
|
+
let body: string;
|
|
156
|
+
|
|
157
|
+
if (format === 'raw') {
|
|
158
|
+
body = response.data.raw ? Buffer.from(response.data.raw, 'base64').toString('utf-8') : '';
|
|
159
|
+
} else {
|
|
160
|
+
body = this.getBody(response.data.payload, format === 'html');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { ...message, body };
|
|
164
|
+
} catch (error: any) {
|
|
165
|
+
if (error.code === 404) {
|
|
166
|
+
throw new CliError('NOT_FOUND', `Message not found: ${messageId}`);
|
|
167
|
+
}
|
|
168
|
+
throw new CliError('API_ERROR', `Gmail API error: ${error.message}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async search(query: string, limit: number = 10): Promise<{ messages: GmailMessage[]; total: number }> {
|
|
173
|
+
return this.list({ query, limit });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async send(options: GmailSendOptions): Promise<{ id: string; threadId: string; labelIds: string[] }> {
|
|
177
|
+
const { to, cc, bcc, subject, body, isHtml, attachments } = options;
|
|
178
|
+
const userEmail = await this.getUserEmail();
|
|
179
|
+
|
|
180
|
+
let rawMessage: string;
|
|
181
|
+
|
|
182
|
+
if (attachments && attachments.length > 0) {
|
|
183
|
+
// Build multipart MIME message with attachments
|
|
184
|
+
rawMessage = await this.buildMultipartMessage({
|
|
185
|
+
from: userEmail,
|
|
186
|
+
to,
|
|
187
|
+
cc,
|
|
188
|
+
bcc,
|
|
189
|
+
subject,
|
|
190
|
+
body,
|
|
191
|
+
isHtml,
|
|
192
|
+
attachments,
|
|
193
|
+
});
|
|
194
|
+
} else {
|
|
195
|
+
// Simple message without attachments
|
|
196
|
+
rawMessage = [
|
|
197
|
+
`From: ${userEmail}`,
|
|
198
|
+
`To: ${to.join(', ')}`,
|
|
199
|
+
cc?.length ? `Cc: ${cc.join(', ')}` : null,
|
|
200
|
+
bcc?.length ? `Bcc: ${bcc.join(', ')}` : null,
|
|
201
|
+
`Subject: ${subject}`,
|
|
202
|
+
`Content-Type: ${isHtml ? 'text/html' : 'text/plain'}; charset=utf-8`,
|
|
203
|
+
'',
|
|
204
|
+
body,
|
|
205
|
+
].filter((line): line is string => line !== null).join('\r\n');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const encodedMessage = Buffer.from(rawMessage).toString('base64url');
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const response = await this.gmail.users.messages.send({
|
|
212
|
+
userId: 'me',
|
|
213
|
+
requestBody: { raw: encodedMessage },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
id: response.data.id!,
|
|
218
|
+
threadId: response.data.threadId!,
|
|
219
|
+
labelIds: response.data.labelIds || ['SENT'],
|
|
220
|
+
};
|
|
221
|
+
} catch (error: any) {
|
|
222
|
+
throw new CliError('API_ERROR', `Failed to send email: ${error.message}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private async buildMultipartMessage(options: {
|
|
227
|
+
from: string;
|
|
228
|
+
to: string[];
|
|
229
|
+
cc?: string[];
|
|
230
|
+
bcc?: string[];
|
|
231
|
+
subject: string;
|
|
232
|
+
body: string;
|
|
233
|
+
isHtml?: boolean;
|
|
234
|
+
attachments: GmailAttachment[];
|
|
235
|
+
}): Promise<string> {
|
|
236
|
+
const { from, to, cc, bcc, subject, body, isHtml, attachments } = options;
|
|
237
|
+
const boundary = `----=_Part_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
238
|
+
|
|
239
|
+
const headers = [
|
|
240
|
+
`From: ${from}`,
|
|
241
|
+
`To: ${to.join(', ')}`,
|
|
242
|
+
cc?.length ? `Cc: ${cc.join(', ')}` : null,
|
|
243
|
+
bcc?.length ? `Bcc: ${bcc.join(', ')}` : null,
|
|
244
|
+
`Subject: ${subject}`,
|
|
245
|
+
'MIME-Version: 1.0',
|
|
246
|
+
`Content-Type: multipart/mixed; boundary="${boundary}"`,
|
|
247
|
+
'',
|
|
248
|
+
`--${boundary}`,
|
|
249
|
+
`Content-Type: ${isHtml ? 'text/html' : 'text/plain'}; charset=utf-8`,
|
|
250
|
+
'',
|
|
251
|
+
body,
|
|
252
|
+
].filter((line): line is string => line !== null);
|
|
253
|
+
|
|
254
|
+
// Add attachments
|
|
255
|
+
for (const attachment of attachments) {
|
|
256
|
+
try {
|
|
257
|
+
const file = Bun.file(attachment.path);
|
|
258
|
+
const exists = await file.exists();
|
|
259
|
+
if (!exists) {
|
|
260
|
+
throw new CliError('NOT_FOUND', `Attachment not found: ${attachment.path}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const content = await file.arrayBuffer();
|
|
264
|
+
const base64Content = Buffer.from(content).toString('base64');
|
|
265
|
+
const filename = attachment.filename || basename(attachment.path);
|
|
266
|
+
const mimeType = attachment.mimeType || getMimeType(filename);
|
|
267
|
+
|
|
268
|
+
headers.push(
|
|
269
|
+
`--${boundary}`,
|
|
270
|
+
`Content-Type: ${mimeType}; name="${filename}"`,
|
|
271
|
+
'Content-Transfer-Encoding: base64',
|
|
272
|
+
`Content-Disposition: attachment; filename="${filename}"`,
|
|
273
|
+
'',
|
|
274
|
+
base64Content
|
|
275
|
+
);
|
|
276
|
+
} catch (error: any) {
|
|
277
|
+
if (error instanceof CliError) throw error;
|
|
278
|
+
throw new CliError('API_ERROR', `Failed to read attachment ${attachment.path}: ${error.message}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
headers.push(`--${boundary}--`);
|
|
283
|
+
|
|
284
|
+
return headers.join('\r\n');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async reply(options: GmailReplyOptions): Promise<{ id: string; threadId: string; labelIds: string[] }> {
|
|
288
|
+
const { threadId, body, isHtml } = options;
|
|
289
|
+
|
|
290
|
+
// Get the thread to find the last message
|
|
291
|
+
try {
|
|
292
|
+
const thread = await this.gmail.users.threads.get({
|
|
293
|
+
userId: 'me',
|
|
294
|
+
id: threadId,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const messages = thread.data.messages || [];
|
|
298
|
+
if (messages.length === 0) {
|
|
299
|
+
throw new CliError('NOT_FOUND', `Thread not found: ${threadId}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const lastMessage = messages[messages.length - 1];
|
|
303
|
+
const headers = this.parseHeaders(lastMessage.payload?.headers);
|
|
304
|
+
|
|
305
|
+
const userEmail = await this.getUserEmail();
|
|
306
|
+
const replyTo = headers['reply-to'] || headers['from'] || '';
|
|
307
|
+
const subject = headers['subject']?.startsWith('Re:')
|
|
308
|
+
? headers['subject']
|
|
309
|
+
: `Re: ${headers['subject'] || '(no subject)'}`;
|
|
310
|
+
const messageId = headers['message-id'] || '';
|
|
311
|
+
|
|
312
|
+
const rawHeaders = [
|
|
313
|
+
`From: ${userEmail}`,
|
|
314
|
+
`To: ${replyTo}`,
|
|
315
|
+
`Subject: ${subject}`,
|
|
316
|
+
messageId ? `In-Reply-To: ${messageId}` : '',
|
|
317
|
+
messageId ? `References: ${messageId}` : '',
|
|
318
|
+
`Content-Type: ${isHtml ? 'text/html' : 'text/plain'}; charset=utf-8`,
|
|
319
|
+
'',
|
|
320
|
+
body,
|
|
321
|
+
].filter(Boolean).join('\r\n');
|
|
322
|
+
|
|
323
|
+
const encodedMessage = Buffer.from(rawHeaders).toString('base64url');
|
|
324
|
+
|
|
325
|
+
const response = await this.gmail.users.messages.send({
|
|
326
|
+
userId: 'me',
|
|
327
|
+
requestBody: {
|
|
328
|
+
raw: encodedMessage,
|
|
329
|
+
threadId,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
id: response.data.id!,
|
|
335
|
+
threadId: response.data.threadId!,
|
|
336
|
+
labelIds: response.data.labelIds || ['SENT'],
|
|
337
|
+
};
|
|
338
|
+
} catch (error: any) {
|
|
339
|
+
if (error instanceof CliError) throw error;
|
|
340
|
+
throw new CliError('API_ERROR', `Failed to send reply: ${error.message}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async archive(messageId: string): Promise<void> {
|
|
345
|
+
try {
|
|
346
|
+
await this.gmail.users.messages.modify({
|
|
347
|
+
userId: 'me',
|
|
348
|
+
id: messageId,
|
|
349
|
+
requestBody: {
|
|
350
|
+
removeLabelIds: ['INBOX'],
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
} catch (error: any) {
|
|
354
|
+
if (error.code === 404) {
|
|
355
|
+
throw new CliError('NOT_FOUND', `Message not found: ${messageId}`);
|
|
356
|
+
}
|
|
357
|
+
throw new CliError('API_ERROR', `Failed to archive: ${error.message}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async mark(messageId: string, read: boolean): Promise<void> {
|
|
362
|
+
try {
|
|
363
|
+
await this.gmail.users.messages.modify({
|
|
364
|
+
userId: 'me',
|
|
365
|
+
id: messageId,
|
|
366
|
+
requestBody: read
|
|
367
|
+
? { removeLabelIds: ['UNREAD'] }
|
|
368
|
+
: { addLabelIds: ['UNREAD'] },
|
|
369
|
+
});
|
|
370
|
+
} catch (error: any) {
|
|
371
|
+
if (error.code === 404) {
|
|
372
|
+
throw new CliError('NOT_FOUND', `Message not found: ${messageId}`);
|
|
373
|
+
}
|
|
374
|
+
throw new CliError('API_ERROR', `Failed to update message: ${error.message}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { TelegramBotInfo, TelegramChat, TelegramMessage, TelegramSendOptions } from '../../types/telegram';
|
|
2
|
+
import { CliError } from '../../utils/errors';
|
|
3
|
+
|
|
4
|
+
const TELEGRAM_API_BASE = 'https://api.telegram.org/bot';
|
|
5
|
+
|
|
6
|
+
interface TelegramApiResponse<T> {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
result?: T;
|
|
9
|
+
description?: string;
|
|
10
|
+
error_code?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class TelegramClient {
|
|
14
|
+
private baseUrl: string;
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private botToken: string,
|
|
18
|
+
private channelId: string
|
|
19
|
+
) {
|
|
20
|
+
this.baseUrl = `${TELEGRAM_API_BASE}${botToken}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private async request<T>(method: string, params?: Record<string, unknown>): Promise<T> {
|
|
24
|
+
const url = `${this.baseUrl}/${method}`;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
body: params ? JSON.stringify(params) : undefined,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const data = (await response.json()) as TelegramApiResponse<T>;
|
|
34
|
+
|
|
35
|
+
if (!data.ok) {
|
|
36
|
+
const errorMessage = data.description || 'Unknown Telegram API error';
|
|
37
|
+
|
|
38
|
+
if (data.error_code === 401) {
|
|
39
|
+
throw new CliError('AUTH_FAILED', `Invalid bot token: ${errorMessage}`);
|
|
40
|
+
}
|
|
41
|
+
if (data.error_code === 403) {
|
|
42
|
+
throw new CliError('PERMISSION_DENIED', `Bot cannot access channel: ${errorMessage}`);
|
|
43
|
+
}
|
|
44
|
+
if (data.error_code === 404) {
|
|
45
|
+
throw new CliError('NOT_FOUND', `Channel not found: ${errorMessage}`);
|
|
46
|
+
}
|
|
47
|
+
if (data.error_code === 429) {
|
|
48
|
+
throw new CliError('RATE_LIMITED', `Rate limited: ${errorMessage}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw new CliError('API_ERROR', `Telegram API error: ${errorMessage}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return data.result!;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error instanceof CliError) throw error;
|
|
57
|
+
|
|
58
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
59
|
+
throw new CliError('NETWORK_ERROR', `Failed to connect to Telegram: ${message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async getMe(): Promise<TelegramBotInfo> {
|
|
64
|
+
return this.request<TelegramBotInfo>('getMe');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getChat(chatId?: string): Promise<TelegramChat> {
|
|
68
|
+
return this.request<TelegramChat>('getChat', {
|
|
69
|
+
chat_id: chatId || this.channelId,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async sendMessage(text: string, options?: TelegramSendOptions): Promise<TelegramMessage> {
|
|
74
|
+
return this.request<TelegramMessage>('sendMessage', {
|
|
75
|
+
chat_id: this.channelId,
|
|
76
|
+
text,
|
|
77
|
+
parse_mode: options?.parse_mode,
|
|
78
|
+
disable_notification: options?.disable_notification,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
profiles: {
|
|
3
|
+
gmail?: string[];
|
|
4
|
+
gchat?: string[];
|
|
5
|
+
jira?: string[];
|
|
6
|
+
telegram?: string[];
|
|
7
|
+
};
|
|
8
|
+
defaults: {
|
|
9
|
+
gmail?: string;
|
|
10
|
+
gchat?: string;
|
|
11
|
+
jira?: string;
|
|
12
|
+
telegram?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ServiceName = 'gmail' | 'gchat' | 'jira' | 'telegram';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface GmailMessage {
|
|
2
|
+
id: string;
|
|
3
|
+
threadId: string;
|
|
4
|
+
subject: string;
|
|
5
|
+
from: string;
|
|
6
|
+
to: string[];
|
|
7
|
+
cc: string[];
|
|
8
|
+
date: string;
|
|
9
|
+
snippet: string;
|
|
10
|
+
labels: string[];
|
|
11
|
+
body?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GmailListOptions {
|
|
15
|
+
limit?: number;
|
|
16
|
+
query?: string;
|
|
17
|
+
labels?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface GmailAttachment {
|
|
21
|
+
filename: string;
|
|
22
|
+
path: string;
|
|
23
|
+
mimeType?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface GmailSendOptions {
|
|
27
|
+
to: string[];
|
|
28
|
+
cc?: string[];
|
|
29
|
+
bcc?: string[];
|
|
30
|
+
subject: string;
|
|
31
|
+
body: string;
|
|
32
|
+
isHtml?: boolean;
|
|
33
|
+
attachments?: GmailAttachment[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface GmailReplyOptions {
|
|
37
|
+
threadId: string;
|
|
38
|
+
body: string;
|
|
39
|
+
isHtml?: boolean;
|
|
40
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface TelegramCredentials {
|
|
2
|
+
bot_token: string;
|
|
3
|
+
channel_id: string;
|
|
4
|
+
bot_username?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface TelegramBotInfo {
|
|
8
|
+
id: number;
|
|
9
|
+
is_bot: boolean;
|
|
10
|
+
first_name: string;
|
|
11
|
+
username: string;
|
|
12
|
+
can_join_groups: boolean;
|
|
13
|
+
can_read_all_group_messages: boolean;
|
|
14
|
+
supports_inline_queries: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TelegramChat {
|
|
18
|
+
id: number;
|
|
19
|
+
type: 'private' | 'group' | 'supergroup' | 'channel';
|
|
20
|
+
title?: string;
|
|
21
|
+
username?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TelegramMessage {
|
|
25
|
+
message_id: number;
|
|
26
|
+
chat: TelegramChat;
|
|
27
|
+
date: number;
|
|
28
|
+
text?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TelegramSendOptions {
|
|
32
|
+
parse_mode?: 'HTML' | 'Markdown' | 'MarkdownV2';
|
|
33
|
+
disable_notification?: boolean;
|
|
34
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface OAuthTokens {
|
|
2
|
+
access_token: string;
|
|
3
|
+
refresh_token?: string;
|
|
4
|
+
expiry_date?: number;
|
|
5
|
+
token_type: string;
|
|
6
|
+
scope?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface StoredCredentials {
|
|
10
|
+
[service: string]: {
|
|
11
|
+
[profile: string]: Record<string, unknown>;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type ErrorCode =
|
|
2
|
+
| 'AUTH_FAILED'
|
|
3
|
+
| 'TOKEN_EXPIRED'
|
|
4
|
+
| 'PROFILE_NOT_FOUND'
|
|
5
|
+
| 'INVALID_PARAMS'
|
|
6
|
+
| 'API_ERROR'
|
|
7
|
+
| 'NETWORK_ERROR'
|
|
8
|
+
| 'PERMISSION_DENIED'
|
|
9
|
+
| 'RATE_LIMITED'
|
|
10
|
+
| 'NOT_FOUND'
|
|
11
|
+
| 'CONFIG_ERROR';
|
|
12
|
+
|
|
13
|
+
export class CliError extends Error {
|
|
14
|
+
constructor(
|
|
15
|
+
public code: ErrorCode,
|
|
16
|
+
message: string,
|
|
17
|
+
public suggestion?: string
|
|
18
|
+
) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'CliError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function exitCodeForError(code: ErrorCode): number {
|
|
25
|
+
switch (code) {
|
|
26
|
+
case 'AUTH_FAILED':
|
|
27
|
+
case 'TOKEN_EXPIRED':
|
|
28
|
+
case 'PERMISSION_DENIED':
|
|
29
|
+
return 2;
|
|
30
|
+
case 'CONFIG_ERROR':
|
|
31
|
+
case 'PROFILE_NOT_FOUND':
|
|
32
|
+
return 3;
|
|
33
|
+
case 'NETWORK_ERROR':
|
|
34
|
+
return 4;
|
|
35
|
+
case 'API_ERROR':
|
|
36
|
+
case 'RATE_LIMITED':
|
|
37
|
+
case 'NOT_FOUND':
|
|
38
|
+
return 5;
|
|
39
|
+
default:
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function handleError(error: unknown): never {
|
|
45
|
+
if (error instanceof CliError) {
|
|
46
|
+
console.error(`Error [${error.code}]: ${error.message}`);
|
|
47
|
+
if (error.suggestion) {
|
|
48
|
+
console.error(`Suggestion: ${error.suggestion}`);
|
|
49
|
+
}
|
|
50
|
+
process.exit(exitCodeForError(error.code));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (error instanceof Error) {
|
|
54
|
+
console.error(`Error: ${error.message}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.error('An unexpected error occurred');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { GmailMessage } from '../types/gmail';
|
|
2
|
+
|
|
3
|
+
// Format a list of Gmail messages
|
|
4
|
+
export function printMessageList(messages: GmailMessage[], total: number): void {
|
|
5
|
+
console.log(`Messages (${messages.length} of ~${total})\n`);
|
|
6
|
+
|
|
7
|
+
for (let i = 0; i < messages.length; i++) {
|
|
8
|
+
const msg = messages[i];
|
|
9
|
+
console.log(`[${i + 1}] ${msg.id} | thread:${msg.threadId}`);
|
|
10
|
+
console.log(` From: ${msg.from}`);
|
|
11
|
+
if (msg.to.length) console.log(` To: ${msg.to.join(', ')}`);
|
|
12
|
+
console.log(` Date: ${msg.date}`);
|
|
13
|
+
console.log(` Subject: ${msg.subject}`);
|
|
14
|
+
if (msg.labels.length) console.log(` Labels: ${msg.labels.join(', ')}`);
|
|
15
|
+
if (msg.snippet) console.log(` > ${msg.snippet}`);
|
|
16
|
+
console.log('');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Format a single Gmail message with body
|
|
21
|
+
export function printMessage(msg: GmailMessage & { body: string }): void {
|
|
22
|
+
console.log(`ID: ${msg.id}`);
|
|
23
|
+
console.log(`Thread: ${msg.threadId}`);
|
|
24
|
+
console.log(`From: ${msg.from}`);
|
|
25
|
+
if (msg.to.length) console.log(`To: ${msg.to.join(', ')}`);
|
|
26
|
+
if (msg.cc.length) console.log(`CC: ${msg.cc.join(', ')}`);
|
|
27
|
+
console.log(`Date: ${msg.date}`);
|
|
28
|
+
console.log(`Subject: ${msg.subject}`);
|
|
29
|
+
if (msg.labels.length) console.log(`Labels: ${msg.labels.join(', ')}`);
|
|
30
|
+
console.log('---');
|
|
31
|
+
console.log(msg.body);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Format send/reply result
|
|
35
|
+
export function printSendResult(result: { id: string; threadId: string }): void {
|
|
36
|
+
console.log('Message sent');
|
|
37
|
+
console.log(`ID: ${result.id}`);
|
|
38
|
+
console.log(`Thread: ${result.threadId}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Format archive confirmation
|
|
42
|
+
export function printArchived(messageId: string): void {
|
|
43
|
+
console.log(`Archived: ${messageId}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Format mark read/unread confirmation
|
|
47
|
+
export function printMarked(messageId: string, read: boolean): void {
|
|
48
|
+
console.log(`Marked ${messageId} as ${read ? 'read' : 'unread'}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Output raw text (for body-only mode)
|
|
52
|
+
export function raw(text: string): void {
|
|
53
|
+
console.log(text);
|
|
54
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export async function readStdin(): Promise<string | null> {
|
|
2
|
+
// Check if stdin is a TTY (interactive terminal)
|
|
3
|
+
if (process.stdin.isTTY) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const chunks: Buffer[] = [];
|
|
8
|
+
|
|
9
|
+
for await (const chunk of process.stdin) {
|
|
10
|
+
chunks.push(chunk);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (chunks.length === 0) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return Buffer.concat(chunks).toString('utf-8').trim();
|
|
18
|
+
}
|