@stacksjs/chat 0.75.40 → 0.75.42
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/README.md +13 -0
- package/dist/inbox/discord.d.ts +33 -0
- package/dist/inbox/imessage/chat-db.d.ts +91 -0
- package/dist/inbox/imessage/contacts.d.ts +24 -0
- package/dist/inbox/imessage/conversations.d.ts +34 -0
- package/dist/inbox/imessage/driver.d.ts +29 -0
- package/dist/inbox/imessage/handles.d.ts +15 -0
- package/dist/inbox/imessage/typedstream.d.ts +6 -0
- package/dist/inbox/index.d.ts +38 -0
- package/dist/inbox/slack.d.ts +32 -0
- package/dist/inbox/types.d.ts +98 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +43 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -44,6 +44,19 @@ SLACK*CLIENT*ID=SCID123
|
|
|
44
44
|
SLACK*SECRET*KEY=SSK123
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
### Inbox: read and archive conversations
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { inbox } from '@stacksjs/chat'
|
|
51
|
+
|
|
52
|
+
const slack = inbox.createInboxDriver('slack', { token: process.env.SLACK_USER_TOKEN! })
|
|
53
|
+
for (const conversation of await slack.conversations())
|
|
54
|
+
console.log(conversation.title, conversation.archive.mode) // 'native' | 'confirm' | 'unsupported'
|
|
55
|
+
await slack.archive('D0123') // closes the DM in Slack
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Drivers for iMessage (macOS chat.db), Slack (user token) and Discord (bot token).
|
|
59
|
+
|
|
47
60
|
## 🧪 Testing
|
|
48
61
|
|
|
49
62
|
```bash
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ArchiveOutcome, Fetch, InboxAttachment, InboxConversation, InboxDriver, InboxMessage, InboxStatus, MessageQuery } from './types';
|
|
2
|
+
/** Discord ids are snowflakes: milliseconds since 2015 in the high bits. */
|
|
3
|
+
export declare function snowflakeTime(id: string | null | undefined): number | null;
|
|
4
|
+
export declare interface DiscordInboxConfig {
|
|
5
|
+
token: string
|
|
6
|
+
userId?: string
|
|
7
|
+
fetch?: Fetch
|
|
8
|
+
apiBase?: string
|
|
9
|
+
concurrency?: number
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Discord, through the official API with a bot token.
|
|
13
|
+
*
|
|
14
|
+
* What a bot can see is what Discord allows: the channels and threads of the
|
|
15
|
+
* servers it has been added to. Personal DMs are out of reach - only a user
|
|
16
|
+
* token reads them, and automating a user account breaks Discord's terms and
|
|
17
|
+
* gets accounts banned - so this driver does not offer them.
|
|
18
|
+
*
|
|
19
|
+
* Archiving a thread archives it in Discord, which is Discord's own way of
|
|
20
|
+
* putting one away (it reopens when someone posts). A channel cannot be hidden
|
|
21
|
+
* for one person through the API, so it is archived in the inbox only.
|
|
22
|
+
*/
|
|
23
|
+
export declare class DiscordInboxDriver implements InboxDriver {
|
|
24
|
+
readonly provider: 'discord';
|
|
25
|
+
readonly label: string;
|
|
26
|
+
constructor(config: DiscordInboxConfig);
|
|
27
|
+
status(): Promise<InboxStatus>;
|
|
28
|
+
conversations(): Promise<InboxConversation[]>;
|
|
29
|
+
messages(conversationId: string, query?: MessageQuery): Promise<InboxMessage[]>;
|
|
30
|
+
archive(conversationId: string): Promise<ArchiveOutcome>;
|
|
31
|
+
unarchive(conversationId: string): Promise<void>;
|
|
32
|
+
attachment(attachment: InboxAttachment): Promise<Response>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export declare function appleDateToUnixMs(date: number | null | undefined): number | null;
|
|
2
|
+
/** Apple-epoch nanoseconds, the unit chat.db has used since High Sierra. */
|
|
3
|
+
export declare function unixMsToAppleDate(ms: number): number;
|
|
4
|
+
/**
|
|
5
|
+
* `associated_message_guid` points at a part of a message, not the message:
|
|
6
|
+
* `p:0/<guid>` for the first part, `bp:<guid>` for a balloon. Reactions are
|
|
7
|
+
* shown on the message, so strip the part.
|
|
8
|
+
*/
|
|
9
|
+
export declare function reactionTarget(raw: string | null): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Read-only access to the Messages app's database.
|
|
12
|
+
*
|
|
13
|
+
* `~/Library/Messages/chat.db` is protected by macOS privacy controls (TCC):
|
|
14
|
+
* the process reading it needs Full Disk Access, or every open fails with
|
|
15
|
+
* "authorization denied". Attic never writes to it. Messages' own daemon owns
|
|
16
|
+
* that file, iCloud syncs it, and an edit behind its back is at best ignored
|
|
17
|
+
* until the next launch and at worst synced to every device - which is why
|
|
18
|
+
* archiving is a copy plus Messages' own Delete, never a flag flipped here.
|
|
19
|
+
*/
|
|
20
|
+
export declare const DEFAULT_MESSAGES_DB: unknown;
|
|
21
|
+
/** `chat.style` for a one-to-one conversation; 43 is a group. */
|
|
22
|
+
export declare const CHAT_STYLE_DIRECT: 45;
|
|
23
|
+
export declare const CHAT_STYLE_GROUP: 43;
|
|
24
|
+
export declare interface MessageQuery {
|
|
25
|
+
afterRowId?: number
|
|
26
|
+
beforeMs?: number
|
|
27
|
+
limit?: number
|
|
28
|
+
newestFirst?: boolean
|
|
29
|
+
byRowId?: boolean
|
|
30
|
+
}
|
|
31
|
+
/** One row of `chat`: Messages' unit of conversation. */
|
|
32
|
+
export declare interface ChatRow {
|
|
33
|
+
rowid: number
|
|
34
|
+
guid: string
|
|
35
|
+
identifier: string
|
|
36
|
+
style: number
|
|
37
|
+
service: string
|
|
38
|
+
displayName: string | null
|
|
39
|
+
groupId: string | null
|
|
40
|
+
participants: string[]
|
|
41
|
+
messageCount: number
|
|
42
|
+
recoverableCount: number
|
|
43
|
+
unreadCount: number
|
|
44
|
+
lastMessageAt: number | null
|
|
45
|
+
lastMessageRowId: number
|
|
46
|
+
lastMessageText: string | null
|
|
47
|
+
lastMessageFromMe: boolean
|
|
48
|
+
}
|
|
49
|
+
export declare interface AttachmentRow {
|
|
50
|
+
guid: string
|
|
51
|
+
path: string | null
|
|
52
|
+
name: string
|
|
53
|
+
mimeType: string | null
|
|
54
|
+
uti: string | null
|
|
55
|
+
bytes: number
|
|
56
|
+
isSticker: boolean
|
|
57
|
+
}
|
|
58
|
+
export declare interface MessageRow {
|
|
59
|
+
rowid: number
|
|
60
|
+
guid: string
|
|
61
|
+
chatGuid: string
|
|
62
|
+
kind: MessageKind
|
|
63
|
+
text: string | null
|
|
64
|
+
isFromMe: boolean
|
|
65
|
+
sentAt: number
|
|
66
|
+
sender: string | null
|
|
67
|
+
service: string
|
|
68
|
+
targetGuid: string | null
|
|
69
|
+
reactionType: number
|
|
70
|
+
replyToGuid: string | null
|
|
71
|
+
editedAt: number | null
|
|
72
|
+
unsent: boolean
|
|
73
|
+
eventType: number
|
|
74
|
+
groupTitle: string | null
|
|
75
|
+
attachments: AttachmentRow[]
|
|
76
|
+
}
|
|
77
|
+
export type MessageKind = 'message' | 'reaction' | 'event';
|
|
78
|
+
export declare class MessagesAccessError extends Error {
|
|
79
|
+
readonly needsFullDiskAccess: boolean;
|
|
80
|
+
public readonly path: string;
|
|
81
|
+
constructor(path: string, cause: unknown);
|
|
82
|
+
}
|
|
83
|
+
export declare class MessagesDb {
|
|
84
|
+
public readonly path: string;
|
|
85
|
+
constructor(path?: string);
|
|
86
|
+
close(): void;
|
|
87
|
+
latestRowId(): number;
|
|
88
|
+
chats(): ChatRow[];
|
|
89
|
+
messages(chatGuids: string[], options?: MessageQuery): MessageRow[];
|
|
90
|
+
attachmentPath(guid: string): { path: string | null, mimeType: string | null, name: string } | null;
|
|
91
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare function initialsOf(name: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* Names for handles, read from the Contacts database the way Messages shows
|
|
4
|
+
* them. chat.db only knows `+15551234567`; a list of phone numbers is not a
|
|
5
|
+
* conversation list anyone recognizes.
|
|
6
|
+
*
|
|
7
|
+
* Contacts keeps one SQLite store per account (iCloud, Google, On My Mac)
|
|
8
|
+
* under `AddressBook/Sources/<uuid>/`, plus a legacy one at the top level.
|
|
9
|
+
* They sit behind the same privacy wall as chat.db, so the Full Disk Access
|
|
10
|
+
* Attic already needs covers them. Every failure here is soft: no names just
|
|
11
|
+
* means handles are shown as numbers.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DEFAULT_ADDRESS_BOOK_DIR: unknown;
|
|
14
|
+
export declare interface ContactCard {
|
|
15
|
+
name: string
|
|
16
|
+
initials: string
|
|
17
|
+
}
|
|
18
|
+
export declare class Contacts {
|
|
19
|
+
static load(root?: string): Contacts;
|
|
20
|
+
get size(): number;
|
|
21
|
+
set(handle: string, name: string): void;
|
|
22
|
+
lookup(handle: string): ContactCard | null;
|
|
23
|
+
nameFor(handle: string): string | null;
|
|
24
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ChatRow } from './chat-db';
|
|
2
|
+
/**
|
|
3
|
+
* What a person calls "a conversation" is not one row of `chat`. Messages
|
|
4
|
+
* keeps a separate chat per service (`iMessage;-;+1555...`, `SMS;-;+1555...`,
|
|
5
|
+
* and `any;-;+1555...` on newer systems) and shows them merged, and a deleted
|
|
6
|
+
* conversation that someone texts again comes back as a NEW chat row. So
|
|
7
|
+
* Attic keys a conversation on what survives both: the other person's handle
|
|
8
|
+
* for a direct chat, the group id for a group.
|
|
9
|
+
*/
|
|
10
|
+
export declare function conversationKey(chat: Pick<ChatRow, 'style' | 'identifier' | 'groupId' | 'guid'>): string;
|
|
11
|
+
/** Merge chat rows into conversations, newest activity first. */
|
|
12
|
+
export declare function groupChats(chats: ChatRow[]): LiveConversation[];
|
|
13
|
+
/**
|
|
14
|
+
* A URL that opens this conversation in Messages. Messages builds the group
|
|
15
|
+
* form itself (`sms://open?groupid=`); a direct chat opens by address. Null
|
|
16
|
+
* for conversations Messages cannot open by URL, like short codes.
|
|
17
|
+
*/
|
|
18
|
+
export declare function messagesUrl(conversation: Pick<LiveConversation, 'kind' | 'groupId' | 'participants'>): string | null;
|
|
19
|
+
export declare interface LiveConversation {
|
|
20
|
+
key: string
|
|
21
|
+
kind: 'direct' | 'group'
|
|
22
|
+
chatGuids: string[]
|
|
23
|
+
displayName: string | null
|
|
24
|
+
groupId: string | null
|
|
25
|
+
participants: string[]
|
|
26
|
+
messageCount: number
|
|
27
|
+
recoverableCount: number
|
|
28
|
+
unreadCount: number
|
|
29
|
+
lastMessageAt: number | null
|
|
30
|
+
lastMessageRowId: number
|
|
31
|
+
lastMessageText: string | null
|
|
32
|
+
lastMessageFromMe: boolean
|
|
33
|
+
services: string[]
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ArchiveOutcome, InboxAttachment, InboxConversation, InboxDriver, InboxMessage, InboxStatus, MessageQuery } from '../types';
|
|
2
|
+
export declare interface IMessageConfig {
|
|
3
|
+
databasePath?: string
|
|
4
|
+
addressBookDir?: string | false
|
|
5
|
+
openUrl?: (url: string) => void
|
|
6
|
+
runAppleScript?: (script: string) => Promise<boolean>
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* iMessage and SMS, read from the Messages database on this Mac.
|
|
10
|
+
*
|
|
11
|
+
* Reading needs Full Disk Access for the process (chat.db and Contacts sit
|
|
12
|
+
* behind macOS privacy controls); nothing here ever writes to either. Apple
|
|
13
|
+
* offers no API for archiving or hiding a conversation, and an edit to
|
|
14
|
+
* chat.db behind Messages' back is ignored or synced to every device, so
|
|
15
|
+
* archiving opens the conversation in Messages and invokes its own Delete
|
|
16
|
+
* Conversation, which the person confirms. Messages keeps it in Recently
|
|
17
|
+
* Deleted for 30 days, and a new message brings the thread back on its own.
|
|
18
|
+
*/
|
|
19
|
+
export declare class IMessageDriver implements InboxDriver {
|
|
20
|
+
readonly provider: 'imessage';
|
|
21
|
+
readonly label: string;
|
|
22
|
+
constructor(config?: IMessageConfig);
|
|
23
|
+
status(): Promise<InboxStatus>;
|
|
24
|
+
conversations(): Promise<InboxConversation[]>;
|
|
25
|
+
messages(conversationId: string, query?: MessageQuery): Promise<InboxMessage[]>;
|
|
26
|
+
archive(conversationId: string): Promise<ArchiveOutcome>;
|
|
27
|
+
unarchive(): Promise<void>;
|
|
28
|
+
attachment(attachment: InboxAttachment): Promise<Response>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messages identifies people by handle: an E.164 phone number (`+15551234567`)
|
|
3
|
+
* or an email. The same person shows up spelled differently across `handle`,
|
|
4
|
+
* `chat.chat_identifier` and the Contacts database (`(555) 123-4567`,
|
|
5
|
+
* `Me@iCloud.com`), so everything is compared in one normal form.
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalizeHandle(raw: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* True for something Messages can address: an E.164 phone number or an
|
|
10
|
+
* email. Short codes (`22395`) and business ids (`urn:biz:...`) are real
|
|
11
|
+
* conversations but cannot be opened by address.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isAddressable(handle: string): boolean;
|
|
14
|
+
/** `+15551234567` as `+1 (555) 123-4567`; anything else unchanged. */
|
|
15
|
+
export declare function formatHandle(handle: string): string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function decodeAttributedBody(blob: Uint8Array | null | undefined): string | null;
|
|
2
|
+
/**
|
|
3
|
+
* The inverse, used only by tests to build fixture rows that look like what
|
|
4
|
+
* Messages writes. Produces the same prefix macOS emits for a plain message.
|
|
5
|
+
*/
|
|
6
|
+
export declare function encodeAttributedBody(text: string): Uint8Array;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { DiscordInboxDriver } from './discord';
|
|
2
|
+
import { IMessageDriver } from './imessage/driver';
|
|
3
|
+
import { SlackInboxDriver } from './slack';
|
|
4
|
+
import type { DiscordInboxConfig } from './discord';
|
|
5
|
+
import type { IMessageConfig } from './imessage/driver';
|
|
6
|
+
import type { InboxDriver } from './types';
|
|
7
|
+
import type { SlackInboxConfig } from './slack';
|
|
8
|
+
export type { DiscordInboxConfig } from './discord';
|
|
9
|
+
export type { IMessageConfig } from './imessage/driver';
|
|
10
|
+
export type { SlackInboxConfig } from './slack';
|
|
11
|
+
/**
|
|
12
|
+
* An inbox driver by name:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { inbox } from '@stacksjs/chat'
|
|
16
|
+
*
|
|
17
|
+
* const drivers = [
|
|
18
|
+
* inbox.createInboxDriver('imessage', {}),
|
|
19
|
+
* inbox.createInboxDriver('slack', { token: env.SLACK_USER_TOKEN }),
|
|
20
|
+
* ]
|
|
21
|
+
* for (const driver of drivers) {
|
|
22
|
+
* for (const conversation of await driver.conversations())
|
|
23
|
+
* console.log(driver.label, conversation.title, conversation.archive.mode)
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function createInboxDriver<P extends keyof InboxDriverConfigs>(provider: P, config: InboxDriverConfigs[P]): InboxDriver;
|
|
28
|
+
export declare interface InboxDriverConfigs {
|
|
29
|
+
imessage: IMessageConfig
|
|
30
|
+
slack: SlackInboxConfig
|
|
31
|
+
discord: DiscordInboxConfig
|
|
32
|
+
}
|
|
33
|
+
export * from './types';
|
|
34
|
+
export { DiscordInboxDriver, snowflakeTime } from './discord';
|
|
35
|
+
export { IMessageDriver } from './imessage/driver';
|
|
36
|
+
export { SlackApiError, SlackInboxDriver } from './slack';
|
|
37
|
+
export { decodeAttributedBody } from './imessage/typedstream';
|
|
38
|
+
export { formatHandle, normalizeHandle } from './imessage/handles';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ArchiveOutcome, Fetch, InboxAttachment, InboxConversation, InboxDriver, InboxMessage, InboxStatus, MessageQuery } from './types';
|
|
2
|
+
export declare interface SlackInboxConfig {
|
|
3
|
+
token: string
|
|
4
|
+
fetch?: Fetch
|
|
5
|
+
apiBase?: string
|
|
6
|
+
concurrency?: number
|
|
7
|
+
}
|
|
8
|
+
export declare class SlackApiError extends Error {
|
|
9
|
+
public readonly method: string;
|
|
10
|
+
public readonly code: string;
|
|
11
|
+
constructor(method: string, code: string);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Slack, through the Web API with the person's own user token.
|
|
15
|
+
*
|
|
16
|
+
* Archiving maps onto what Slack can do for one person. A DM or group DM is
|
|
17
|
+
* closed (`conversations.close`): it leaves the sidebar, and Slack reopens it
|
|
18
|
+
* by itself when someone writes - an email archive's behaviour exactly. A
|
|
19
|
+
* public channel is left (and rejoined on unarchive). A private channel is
|
|
20
|
+
* not touched: leaving it would need someone to invite the person back.
|
|
21
|
+
*/
|
|
22
|
+
export declare class SlackInboxDriver implements InboxDriver {
|
|
23
|
+
readonly provider: 'slack';
|
|
24
|
+
readonly label: string;
|
|
25
|
+
constructor(config: SlackInboxConfig);
|
|
26
|
+
status(): Promise<InboxStatus>;
|
|
27
|
+
conversations(): Promise<InboxConversation[]>;
|
|
28
|
+
messages(conversationId: string, query?: MessageQuery): Promise<InboxMessage[]>;
|
|
29
|
+
archive(conversationId: string): Promise<ArchiveOutcome>;
|
|
30
|
+
unarchive(conversationId: string): Promise<void>;
|
|
31
|
+
attachment(attachment: InboxAttachment): Promise<Response>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export declare interface ArchiveSupport {
|
|
2
|
+
mode: ArchiveMode
|
|
3
|
+
detail: string
|
|
4
|
+
}
|
|
5
|
+
export declare interface InboxPerson {
|
|
6
|
+
id: string
|
|
7
|
+
name: string | null
|
|
8
|
+
}
|
|
9
|
+
export declare interface InboxConversation {
|
|
10
|
+
provider: InboxProvider
|
|
11
|
+
id: string
|
|
12
|
+
kind: 'direct' | 'group' | 'channel' | 'thread'
|
|
13
|
+
title: string | null
|
|
14
|
+
participants: InboxPerson[]
|
|
15
|
+
workspace: string | null
|
|
16
|
+
lastMessageAt: number | null
|
|
17
|
+
preview: string | null
|
|
18
|
+
previewFromMe: boolean
|
|
19
|
+
unread: number
|
|
20
|
+
cursor: string
|
|
21
|
+
visible: boolean
|
|
22
|
+
archive: ArchiveSupport
|
|
23
|
+
url: string | null
|
|
24
|
+
}
|
|
25
|
+
export declare interface InboxAttachment {
|
|
26
|
+
id: string
|
|
27
|
+
name: string
|
|
28
|
+
mimeType: string | null
|
|
29
|
+
bytes: number
|
|
30
|
+
path: string | null
|
|
31
|
+
url: string | null
|
|
32
|
+
}
|
|
33
|
+
export declare interface InboxMessage {
|
|
34
|
+
id: string
|
|
35
|
+
conversationId: string
|
|
36
|
+
kind: 'message' | 'reaction' | 'event'
|
|
37
|
+
text: string | null
|
|
38
|
+
fromMe: boolean
|
|
39
|
+
sentAt: number
|
|
40
|
+
sender: InboxPerson | null
|
|
41
|
+
targetId: string | null
|
|
42
|
+
reaction: string | null
|
|
43
|
+
reactionRemoved: boolean
|
|
44
|
+
replyToId: string | null
|
|
45
|
+
editedAt: number | null
|
|
46
|
+
unsent: boolean
|
|
47
|
+
attachments: InboxAttachment[]
|
|
48
|
+
cursor: string
|
|
49
|
+
}
|
|
50
|
+
export declare interface InboxStatus {
|
|
51
|
+
connected: boolean
|
|
52
|
+
detail: string
|
|
53
|
+
needs?: 'full-disk-access' | 'token' | 'messages-signed-out' | 'unsupported-platform'
|
|
54
|
+
}
|
|
55
|
+
export declare interface ArchiveOutcome {
|
|
56
|
+
mode: ArchiveMode
|
|
57
|
+
removed: boolean
|
|
58
|
+
detail: string
|
|
59
|
+
}
|
|
60
|
+
export declare interface MessageQuery {
|
|
61
|
+
after?: string
|
|
62
|
+
limit?: number
|
|
63
|
+
}
|
|
64
|
+
export declare interface InboxDriver {
|
|
65
|
+
readonly provider: InboxProvider
|
|
66
|
+
readonly label: string
|
|
67
|
+
status: () => Promise<InboxStatus>
|
|
68
|
+
conversations: () => Promise<InboxConversation[]>
|
|
69
|
+
messages: (conversationId: string, query?: MessageQuery) => Promise<InboxMessage[]>
|
|
70
|
+
archive: (conversationId: string) => Promise<ArchiveOutcome>
|
|
71
|
+
unarchive: (conversationId: string) => Promise<void>
|
|
72
|
+
attachment: (attachment: InboxAttachment) => Promise<Response>
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The inbox side of `@stacksjs/chat`: reading conversations out of the apps
|
|
76
|
+
* people already use and archiving them there, behind one interface.
|
|
77
|
+
*
|
|
78
|
+
* `send()` and the webhook helpers push messages out. An inbox driver works
|
|
79
|
+
* the other way round - it lists a person's conversations, reads their
|
|
80
|
+
* messages, notices new ones, and takes a conversation out of the real app's
|
|
81
|
+
* list when it is archived. Each provider can do that to a different extent,
|
|
82
|
+
* and a driver says so through {@link ArchiveSupport} rather than pretending.
|
|
83
|
+
*/
|
|
84
|
+
export type InboxProvider = 'imessage' | 'slack' | 'discord';
|
|
85
|
+
/**
|
|
86
|
+
* How archiving a conversation takes it out of the real app.
|
|
87
|
+
*
|
|
88
|
+
* - `native`: the provider's API does it (Slack closes a DM; Discord archives
|
|
89
|
+
* a thread). Reversible through {@link InboxDriver.unarchive}.
|
|
90
|
+
* - `confirm`: the app is driven to its own delete, which the person confirms
|
|
91
|
+
* there (iMessage: Apple offers no API, and Messages' own Delete is the only
|
|
92
|
+
* way that stays consistent with iCloud).
|
|
93
|
+
* - `unsupported`: the provider has no per-person way to hide it. The
|
|
94
|
+
* conversation can still be archived in the inbox itself.
|
|
95
|
+
*/
|
|
96
|
+
export type ArchiveMode = 'native' | 'confirm' | 'unsupported';
|
|
97
|
+
/** A `fetch` the HTTP drivers call, so tests and proxies can stand in. */
|
|
98
|
+
export type Fetch = (input: string, init?: RequestInit) => Promise<Response>;
|
package/dist/index.d.ts
CHANGED
|
@@ -41,3 +41,5 @@ export type ChatDriver = 'slack' | 'discord' | 'teams';
|
|
|
41
41
|
export * as discord from './drivers/discord';
|
|
42
42
|
export * as slack from './drivers/slack';
|
|
43
43
|
export * as teams from './drivers/teams';
|
|
44
|
+
/** Reading and archiving conversations in iMessage, Slack and Discord. */
|
|
45
|
+
export * as inbox from './inbox/index';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,44 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var A=Object.defineProperty;var D=(e)=>e;function U(e,t){this[e]=D.bind(null,t)}var x=(e,t)=>{for(var r in t)A(e,r,{get:t[r],enumerable:!0,configurable:!0,set:U.bind(t,r)})};var T={};x(T,{DiscordDriver:()=>l,Driver:()=>l,configure:()=>d,default:()=>_,driver:()=>B,send:()=>g,sendEmbed:()=>E,sendWebhook:()=>u});import{log as S}from"@stacksjs/logging";import{log as W}from"@stacksjs/logging";class i{config;constructor(e){this.config={maxRetries:e?.maxRetries||3,retryTimeout:e?.retryTimeout||1000,...e}}configure(e){this.config={...this.config,...e}}validateMessage(e){if(!e.to)throw Error("Message recipient is required");if(!e.content&&!e.template)throw Error("Either message content or template is required");return!0}async handleError(e,t){let r=e instanceof Error?e:Error(String(e));W.error(`[${this.name}] Message sending failed`,{error:r.message,stack:r.stack?.replace(/https?:\/\/[^\s)\]"']+/g,"[REDACTED-URL]"),to:t.to,subject:t.subject});let s={message:`Message sending failed: ${r.message}`,success:!1,provider:this.name};if(t.onError){let o=t.onError(r),n=o instanceof Promise?await o:o;s={...s,...n,success:!1,provider:this.name}}return s}async handleSuccess(e,t){let r={message:"Message sent successfully",success:!0,provider:this.name,messageId:t};try{if(e.handle){let s=e.handle(),o=s instanceof Promise?await s:s;r={...r,...o,success:!0,provider:this.name,messageId:t}}if(e.onSuccess){let s=e.onSuccess(),o=s instanceof Promise?await s:s;r={...r,...o,success:!0,provider:this.name,messageId:t}}}catch(s){return this.handleError(s,e)}return r}}var a={};function d(e){if(e.webhookUrl){let t;try{t=new URL(e.webhookUrl)}catch{throw Error(`[chat/discord] webhookUrl is not a valid URL: ${e.webhookUrl}`)}if(t.protocol!=="https:")throw Error("[chat/discord] webhookUrl must use https://");if(!/(?:^|\.)discord(?:app)?\.com$/i.test(t.hostname))throw Error(`[chat/discord] webhookUrl host "${t.hostname}" is not a Discord-owned domain.`)}a={...a,...e}}class l extends i{name="discord";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)d(e)}async send(e,t){let r={provider:this.name,to:e.to,subject:e.subject||"No subject"};S.info("Sending message via Discord...",r);try{return this.validateMessage(e),await this.sendWithRetry(e,t)}catch(s){return this.handleError(s,e)}}async sendWithRetry(e,t,r=1){try{let s=await this.sendMessage(e,t);return this.handleSuccess(e,s.id)}catch(s){if(r<this.config.maxRetries)return S.warn(`[${this.name}] Message send failed, retrying (${r}/${this.config.maxRetries})`),await new Promise((o)=>setTimeout(o,this.config.retryTimeout)),this.sendWithRetry(e,t,r+1);throw s}}async sendMessage(e,t){if(a.webhookUrl)return this.sendViaWebhook(e);if(a.botToken)return this.sendViaBotToken(e);throw Error("Discord not configured: provide webhookUrl or botToken")}async sendViaWebhook(e){let t={content:e.content,username:e.from?.name};if(e.subject||e.template)t.embeds=[this.buildEmbed(e)];let r=await fetch(a.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok){let o=await r.text();throw Error(`Discord webhook failed: ${r.status} - ${o}`)}if(r.status===204)return{};return{id:(await r.json()).id}}async sendViaBotToken(e){let t=e.to,r={content:e.content};if(e.subject||e.template)r.embeds=[this.buildEmbed(e)];let s=await fetch(`https://discord.com/api/v10/channels/${t}/messages`,{method:"POST",headers:{Authorization:`Bot ${a.botToken}`,"Content-Type":"application/json"},body:JSON.stringify(r)});if(!s.ok){let n=await s.json();throw Error(`Discord API failed: ${s.status} - ${n.message}`)}return{id:(await s.json()).id}}buildEmbed(e){let t={};if(e.subject)t.title=e.subject;if(e.content)t.description=e.content;return t.color=5793266,t.timestamp=new Date().toISOString(),t}}async function g(e,t){return new l().send(e,t)}async function u(e,t,r){try{let s={content:t,...r},o=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!o.ok){let n=await o.text();return{success:!1,provider:"discord",message:`Webhook failed: ${o.status} - ${n}`}}return{success:!0,provider:"discord",message:"Message sent successfully"}}catch(s){return{success:!1,provider:"discord",message:(s instanceof Error?s:Error(String(s))).message}}}async function E(e,t,r){return u(e,"",{...r,embeds:[t]})}var B=new l,_={send:g,sendWebhook:u,sendEmbed:E,configure:d,DiscordDriver:l};var v={};x(v,{Driver:()=>p,SlackDriver:()=>p,configure:()=>h,default:()=>N,driver:()=>L,send:()=>y,sendWebhook:()=>b});import{log as P}from"@stacksjs/logging";var c={};function h(e){if(e.webhookUrl){let t;try{t=new URL(e.webhookUrl)}catch{throw Error(`[chat/slack] webhookUrl is not a valid URL: ${e.webhookUrl}`)}if(t.protocol!=="https:")throw Error("[chat/slack] webhookUrl must use https:// - Slack does not accept plain HTTP webhooks.");if(!/(?:^|\.)slack\.com$/i.test(t.hostname))throw Error(`[chat/slack] webhookUrl host "${t.hostname}" is not a Slack-owned domain.`)}c={...c,...e}}class p extends i{name="slack";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)h(e)}async send(e,t){let r={provider:this.name,to:e.to,subject:e.subject||"No subject"};P.info("Sending message via Slack...",r);try{return this.validateMessage(e),await this.sendWithRetry(e,t)}catch(s){return this.handleError(s,e)}}async sendWithRetry(e,t,r=1){try{let s=await this.sendMessage(e,t);return this.handleSuccess(e,s.ts)}catch(s){if(r<this.config.maxRetries)return P.warn(`[${this.name}] Message send failed, retrying (${r}/${this.config.maxRetries})`),await new Promise((o)=>setTimeout(o,this.config.retryTimeout)),this.sendWithRetry(e,t,r+1);throw s}}async sendMessage(e,t){if(c.webhookUrl)return this.sendViaWebhook(e);if(c.botToken)return this.sendViaBotToken(e);throw Error("Slack not configured: provide webhookUrl or botToken")}async sendViaWebhook(e){let r=e.content||e.subject||"",o={text:r.length>40000?`${r.slice(0,39986)}\u2026 [truncated]`:r,username:e.from?.name,mrkdwn:!0};if(e.template)o.blocks=this.buildBlocks(e);let n=await fetch(c.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)});if(!n.ok){let C=await n.text();throw Error(`Slack webhook failed: ${n.status} - ${C}`)}return{}}async sendViaBotToken(e){let t={channel:e.to,text:e.content||e.subject,mrkdwn:!0};if(e.template)t.blocks=this.buildBlocks(e);let r=await fetch("https://slack.com/api/chat.postMessage",{method:"POST",headers:{Authorization:`Bearer ${c.botToken}`,"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok)throw Error(`Slack API failed: ${r.status}`);let s=await r.json();if(!s.ok)throw Error(`Slack API error: ${s.error}`);return{ts:s.ts}}buildBlocks(e){let t=[];if(e.subject)t.push({type:"header",text:{type:"plain_text",text:e.subject,emoji:!0}});if(e.content)t.push({type:"section",text:{type:"mrkdwn",text:e.content}});return t}}async function y(e,t){return new p().send(e,t)}async function b(e,t,r){try{let s={text:t,...r},o=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!o.ok){let n=await o.text();return{success:!1,provider:"slack",message:`Webhook failed: ${o.status} - ${n}`}}return{success:!0,provider:"slack",message:"Message sent successfully"}}catch(s){return{success:!1,provider:"slack",message:(s instanceof Error?s:Error(String(s))).message}}}var L=new p,N={send:y,sendWebhook:b,configure:h,SlackDriver:p};var M={};x(M,{Driver:()=>f,TeamsDriver:()=>f,configure:()=>m,default:()=>J,driver:()=>I,send:()=>k,sendCard:()=>O,sendWebhook:()=>w});import{log as j}from"@stacksjs/logging";var R={};function m(e){R={...R,...e}}class f extends i{name="teams";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)m(e)}async send(e,t){let r={provider:this.name,to:e.to,subject:e.subject||"No subject"};j.info("Sending message via Microsoft Teams...",r);try{return this.validateMessage(e),await this.sendWithRetry(e,t)}catch(s){return this.handleError(s,e)}}async sendWithRetry(e,t,r=1){try{return await this.sendMessage(e,t),this.handleSuccess(e)}catch(s){if(r<this.config.maxRetries)return j.warn(`[${this.name}] Message send failed, retrying (${r}/${this.config.maxRetries})`),await new Promise((o)=>setTimeout(o,this.config.retryTimeout)),this.sendWithRetry(e,t,r+1);throw s}}async sendMessage(e,t){let s=R.webhookUrl||(Array.isArray(e.to)?e.to[0]:e.to);if(!s||!s.includes("webhook.office.com"))throw Error("Teams webhook URL not configured or invalid");let o=this.buildPayload(e),n=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)});if(!n.ok){let C=await n.text();throw Error(`Teams webhook failed: ${n.status} - ${C}`)}}buildPayload(e){if(!e.subject&&!e.template)return{type:"message",text:e.content||""};let t=this.buildAdaptiveCard(e);return{type:"message",summary:e.subject||e.content?.substring(0,50)||"Notification",attachments:[{contentType:"application/vnd.microsoft.card.adaptive",content:t}]}}buildAdaptiveCard(e){let t=[];if(e.subject)t.push({type:"TextBlock",text:e.subject,size:"Large",weight:"Bolder",wrap:!0});if(e.content)t.push({type:"TextBlock",text:e.content,wrap:!0});return t.push({type:"TextBlock",text:new Date().toLocaleString(),size:"Small",color:"Dark",wrap:!0}),{type:"AdaptiveCard",version:"1.4",$schema:"http://adaptivecards.io/schemas/adaptive-card.json",body:t}}}async function k(e,t){return new f().send(e,t)}async function w(e,t){try{let s=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"message",text:t})});if(!s.ok){let o=await s.text();return{success:!1,provider:"teams",message:`Webhook failed: ${s.status} - ${o}`}}return{success:!0,provider:"teams",message:"Message sent successfully"}}catch(r){return{success:!1,provider:"teams",message:(r instanceof Error?r:Error(String(r))).message}}}async function O(e,t,r){try{let o=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"message",summary:r||"Notification",attachments:[{contentType:"application/vnd.microsoft.card.adaptive",content:t}]})});if(!o.ok){let n=await o.text();return{success:!1,provider:"teams",message:`Webhook failed: ${o.status} - ${n}`}}return{success:!0,provider:"teams",message:"Card sent successfully"}}catch(s){return{success:!1,provider:"teams",message:(s instanceof Error?s:Error(String(s))).message}}}var I=new f,J={send:k,sendWebhook:w,sendCard:O,configure:m,TeamsDriver:f};async function Y(e,t={}){let r=t.driver??"slack";switch(r){case"slack":return y(e);case"discord":return g(e);case"teams":return k(e);default:return{success:!1,provider:r,message:`Unknown chat driver: ${r}`}}}async function Z(e,t,r){return b(e,t,r)}async function ee(e,t,r){return u(e,t,r)}async function te(e,t){return w(e,t)}function re(e){h(e)}function se(e){d(e)}function oe(e){m(e)}export{se as configureDiscord,re as configureSlack,oe as configureTeams,T as discord,Y as send,ee as sendToDiscord,Z as sendToSlack,te as sendToTeams,v as slack,M as teams};
|
|
2
|
+
var pe=Object.defineProperty;var ge=(e)=>e;function fe(e,t){this[e]=ge.bind(null,t)}var R=(e,t)=>{for(var s in t)pe(e,s,{get:t[s],enumerable:!0,configurable:!0,set:fe.bind(t,s)})};var B={};R(B,{DiscordDriver:()=>g,Driver:()=>g,configure:()=>p,default:()=>ve,driver:()=>be,send:()=>T,sendEmbed:()=>ee,sendWebhook:()=>f});import{log as Y}from"@stacksjs/logging";import{log as ye}from"@stacksjs/logging";class u{config;constructor(e){this.config={maxRetries:e?.maxRetries||3,retryTimeout:e?.retryTimeout||1000,...e}}configure(e){this.config={...this.config,...e}}validateMessage(e){if(!e.to)throw Error("Message recipient is required");if(!e.content&&!e.template)throw Error("Either message content or template is required");return!0}async handleError(e,t){let s=e instanceof Error?e:Error(String(e));ye.error(`[${this.name}] Message sending failed`,{error:s.message,stack:s.stack?.replace(/https?:\/\/[^\s)\]"']+/g,"[REDACTED-URL]"),to:t.to,subject:t.subject});let r={message:`Message sending failed: ${s.message}`,success:!1,provider:this.name};if(t.onError){let n=t.onError(s),i=n instanceof Promise?await n:n;r={...r,...i,success:!1,provider:this.name}}return r}async handleSuccess(e,t){let s={message:"Message sent successfully",success:!0,provider:this.name,messageId:t};try{if(e.handle){let r=e.handle(),n=r instanceof Promise?await r:r;s={...s,...n,success:!0,provider:this.name,messageId:t}}if(e.onSuccess){let r=e.onSuccess(),n=r instanceof Promise?await r:r;s={...s,...n,success:!0,provider:this.name,messageId:t}}}catch(r){return this.handleError(r,e)}return s}}var m={};function p(e){if(e.webhookUrl){let t;try{t=new URL(e.webhookUrl)}catch{throw Error(`[chat/discord] webhookUrl is not a valid URL: ${e.webhookUrl}`)}if(t.protocol!=="https:")throw Error("[chat/discord] webhookUrl must use https://");if(!/(?:^|\.)discord(?:app)?\.com$/i.test(t.hostname))throw Error(`[chat/discord] webhookUrl host "${t.hostname}" is not a Discord-owned domain.`)}m={...m,...e}}class g extends u{name="discord";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)p(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};Y.info("Sending message via Discord...",s);try{return this.validateMessage(e),await this.sendWithRetry(e,t)}catch(r){return this.handleError(r,e)}}async sendWithRetry(e,t,s=1){try{let r=await this.sendMessage(e,t);return this.handleSuccess(e,r.id)}catch(r){if(s<this.config.maxRetries)return Y.warn(`[${this.name}] Message send failed, retrying (${s}/${this.config.maxRetries})`),await new Promise((n)=>setTimeout(n,this.config.retryTimeout)),this.sendWithRetry(e,t,s+1);throw r}}async sendMessage(e,t){if(m.webhookUrl)return this.sendViaWebhook(e);if(m.botToken)return this.sendViaBotToken(e);throw Error("Discord not configured: provide webhookUrl or botToken")}async sendViaWebhook(e){let t={content:e.content,username:e.from?.name};if(e.subject||e.template)t.embeds=[this.buildEmbed(e)];let s=await fetch(m.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!s.ok){let n=await s.text();throw Error(`Discord webhook failed: ${s.status} - ${n}`)}if(s.status===204)return{};return{id:(await s.json()).id}}async sendViaBotToken(e){let t=e.to,s={content:e.content};if(e.subject||e.template)s.embeds=[this.buildEmbed(e)];let r=await fetch(`https://discord.com/api/v10/channels/${t}/messages`,{method:"POST",headers:{Authorization:`Bot ${m.botToken}`,"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){let i=await r.json();throw Error(`Discord API failed: ${r.status} - ${i.message}`)}return{id:(await r.json()).id}}buildEmbed(e){let t={};if(e.subject)t.title=e.subject;if(e.content)t.description=e.content;return t.color=5793266,t.timestamp=new Date().toISOString(),t}}async function T(e,t){return new g().send(e,t)}async function f(e,t,s){try{let r={content:t,...s},n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});if(!n.ok){let i=await n.text();return{success:!1,provider:"discord",message:`Webhook failed: ${n.status} - ${i}`}}return{success:!0,provider:"discord",message:"Message sent successfully"}}catch(r){return{success:!1,provider:"discord",message:(r instanceof Error?r:Error(String(r))).message}}}async function ee(e,t,s){return f(e,"",{...s,embeds:[t]})}var be=new g,ve={send:T,sendWebhook:f,sendEmbed:ee,configure:p,DiscordDriver:g};var U={};R(U,{Driver:()=>b,SlackDriver:()=>b,configure:()=>y,default:()=>_e,driver:()=>xe,send:()=>I,sendWebhook:()=>D});import{log as te}from"@stacksjs/logging";var h={};function y(e){if(e.webhookUrl){let t;try{t=new URL(e.webhookUrl)}catch{throw Error(`[chat/slack] webhookUrl is not a valid URL: ${e.webhookUrl}`)}if(t.protocol!=="https:")throw Error("[chat/slack] webhookUrl must use https:// - Slack does not accept plain HTTP webhooks.");if(!/(?:^|\.)slack\.com$/i.test(t.hostname))throw Error(`[chat/slack] webhookUrl host "${t.hostname}" is not a Slack-owned domain.`)}h={...h,...e}}class b extends u{name="slack";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)y(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};te.info("Sending message via Slack...",s);try{return this.validateMessage(e),await this.sendWithRetry(e,t)}catch(r){return this.handleError(r,e)}}async sendWithRetry(e,t,s=1){try{let r=await this.sendMessage(e,t);return this.handleSuccess(e,r.ts)}catch(r){if(s<this.config.maxRetries)return te.warn(`[${this.name}] Message send failed, retrying (${s}/${this.config.maxRetries})`),await new Promise((n)=>setTimeout(n,this.config.retryTimeout)),this.sendWithRetry(e,t,s+1);throw r}}async sendMessage(e,t){if(h.webhookUrl)return this.sendViaWebhook(e);if(h.botToken)return this.sendViaBotToken(e);throw Error("Slack not configured: provide webhookUrl or botToken")}async sendViaWebhook(e){let s=e.content||e.subject||"",n={text:s.length>40000?`${s.slice(0,39986)}\u2026 [truncated]`:s,username:e.from?.name,mrkdwn:!0};if(e.template)n.blocks=this.buildBlocks(e);let i=await fetch(h.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!i.ok){let a=await i.text();throw Error(`Slack webhook failed: ${i.status} - ${a}`)}return{}}async sendViaBotToken(e){let t={channel:e.to,text:e.content||e.subject,mrkdwn:!0};if(e.template)t.blocks=this.buildBlocks(e);let s=await fetch("https://slack.com/api/chat.postMessage",{method:"POST",headers:{Authorization:`Bearer ${h.botToken}`,"Content-Type":"application/json"},body:JSON.stringify(t)});if(!s.ok)throw Error(`Slack API failed: ${s.status}`);let r=await s.json();if(!r.ok)throw Error(`Slack API error: ${r.error}`);return{ts:r.ts}}buildBlocks(e){let t=[];if(e.subject)t.push({type:"header",text:{type:"plain_text",text:e.subject,emoji:!0}});if(e.content)t.push({type:"section",text:{type:"mrkdwn",text:e.content}});return t}}async function I(e,t){return new b().send(e,t)}async function D(e,t,s){try{let r={text:t,...s},n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});if(!n.ok){let i=await n.text();return{success:!1,provider:"slack",message:`Webhook failed: ${n.status} - ${i}`}}return{success:!0,provider:"slack",message:"Message sent successfully"}}catch(r){return{success:!1,provider:"slack",message:(r instanceof Error?r:Error(String(r))).message}}}var xe=new b,_e={send:I,sendWebhook:D,configure:y,SlackDriver:b};var H={};R(H,{Driver:()=>x,TeamsDriver:()=>x,configure:()=>v,default:()=>ke,driver:()=>Ce,send:()=>E,sendCard:()=>re,sendWebhook:()=>O});import{log as se}from"@stacksjs/logging";var W={};function v(e){W={...W,...e}}class x extends u{name="teams";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)v(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};se.info("Sending message via Microsoft Teams...",s);try{return this.validateMessage(e),await this.sendWithRetry(e,t)}catch(r){return this.handleError(r,e)}}async sendWithRetry(e,t,s=1){try{return await this.sendMessage(e,t),this.handleSuccess(e)}catch(r){if(s<this.config.maxRetries)return se.warn(`[${this.name}] Message send failed, retrying (${s}/${this.config.maxRetries})`),await new Promise((n)=>setTimeout(n,this.config.retryTimeout)),this.sendWithRetry(e,t,s+1);throw r}}async sendMessage(e,t){let r=W.webhookUrl||(Array.isArray(e.to)?e.to[0]:e.to);if(!r||!r.includes("webhook.office.com"))throw Error("Teams webhook URL not configured or invalid");let n=this.buildPayload(e),i=await fetch(r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!i.ok){let a=await i.text();throw Error(`Teams webhook failed: ${i.status} - ${a}`)}}buildPayload(e){if(!e.subject&&!e.template)return{type:"message",text:e.content||""};let t=this.buildAdaptiveCard(e);return{type:"message",summary:e.subject||e.content?.substring(0,50)||"Notification",attachments:[{contentType:"application/vnd.microsoft.card.adaptive",content:t}]}}buildAdaptiveCard(e){let t=[];if(e.subject)t.push({type:"TextBlock",text:e.subject,size:"Large",weight:"Bolder",wrap:!0});if(e.content)t.push({type:"TextBlock",text:e.content,wrap:!0});return t.push({type:"TextBlock",text:new Date().toLocaleString(),size:"Small",color:"Dark",wrap:!0}),{type:"AdaptiveCard",version:"1.4",$schema:"http://adaptivecards.io/schemas/adaptive-card.json",body:t}}}async function E(e,t){return new x().send(e,t)}async function O(e,t){try{let r=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"message",text:t})});if(!r.ok){let n=await r.text();return{success:!1,provider:"teams",message:`Webhook failed: ${r.status} - ${n}`}}return{success:!0,provider:"teams",message:"Message sent successfully"}}catch(s){return{success:!1,provider:"teams",message:(s instanceof Error?s:Error(String(s))).message}}}async function re(e,t,s){try{let n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"message",summary:s||"Notification",attachments:[{contentType:"application/vnd.microsoft.card.adaptive",content:t}]})});if(!n.ok){let i=await n.text();return{success:!1,provider:"teams",message:`Webhook failed: ${n.status} - ${i}`}}return{success:!0,provider:"teams",message:"Card sent successfully"}}catch(r){return{success:!1,provider:"teams",message:(r instanceof Error?r:Error(String(r))).message}}}var Ce=new x,ke={send:E,sendWebhook:O,sendCard:re,configure:v,TeamsDriver:x};var he={};R(he,{DiscordInboxDriver:()=>_,IMessageDriver:()=>w,SlackApiError:()=>F,SlackInboxDriver:()=>M,createInboxDriver:()=>Je,decodeAttributedBody:()=>C,formatHandle:()=>ie,normalizeHandle:()=>d,snowflakeTime:()=>G});var we=new Set([0,5]),z=new Set([10,11,12]);function G(e){if(!e)return null;return Number((BigInt(e)>>22n)+1420070400000n)}class _{provider="discord";label="Discord";token;userId;fetch;apiBase;concurrency;guilds=new Map;channels=new Map;constructor(e){this.token=e.token,this.userId=e.userId,this.fetch=e.fetch??((t,s)=>fetch(t,s)),this.apiBase=(e.apiBase??"https://discord.com/api/v10").replace(/\/$/,""),this.concurrency=e.concurrency??3}async api(e,t={}){for(let s=0;;s++){let r=await this.fetch(`${this.apiBase}${e}`,{...t,headers:{authorization:`Bot ${this.token}`,"content-type":"application/json",...t.headers??{}}});if(r.status===429&&s<3){let n=await r.json().catch(()=>({}));await Bun.sleep(Math.min(30,n.retry_after??1)*1000);continue}if(!r.ok){let n=await r.json().catch(()=>({}));throw Error(`Discord ${t.method??"GET"} ${e} failed: ${n.message??r.status}`)}return r.status===204?void 0:await r.json()}}async status(){if(!this.token)return{connected:!1,detail:"Add a Discord bot token.",needs:"token"};try{return{connected:!0,detail:`Reading the servers ${(await this.api("/users/@me")).username} is in.`}}catch(e){return{connected:!1,detail:e instanceof Error?e.message:String(e),needs:"token"}}}archiveSupport(e){if(z.has(e.type))return{mode:"native",detail:"Archives the thread in Discord. It reopens when someone posts."};return{mode:"unsupported",detail:"Discord cannot hide a channel for one person, so it stays in Discord."}}async conversations(){let e=await this.api("/users/@me/guilds"),t=[];for(let r of e){this.guilds.set(r.id,r.name);let[n,i]=await Promise.all([this.api(`/guilds/${r.id}/channels`).catch(()=>[]),this.api(`/guilds/${r.id}/threads/active`).catch(()=>({threads:[]}))]);t.push(...n.filter((a)=>we.has(a.type)).map((a)=>({...a,guild_id:r.id}))),t.push(...i.threads.map((a)=>({...a,guild_id:a.guild_id??r.id})))}let s=[];for(let r=0;r<t.length;r+=this.concurrency){let n=t.slice(r,r+this.concurrency);s.push(...await Promise.all(n.map((i)=>this.describe(i))))}return s.sort((r,n)=>(n.lastMessageAt??0)-(r.lastMessageAt??0))}async describe(e){this.channels.set(e.id,e);let t=e.last_message_id?(await this.api(`/channels/${e.id}/messages?limit=1`).catch(()=>[]))[0]:void 0,s=z.has(e.type);return{provider:"discord",id:e.id,kind:s?"thread":"channel",title:s?e.name??"Thread":`#${e.name}`,participants:[],workspace:e.guild_id?this.guilds.get(e.guild_id)??null:null,lastMessageAt:t?Date.parse(t.timestamp):G(e.last_message_id),preview:t?t.content||(t.attachments?.length?"Attachment":null):null,previewFromMe:!!t&&t.author.id===this.userId,unread:0,cursor:e.last_message_id??"0",visible:!e.thread_metadata?.archived,archive:this.archiveSupport(e),url:`https://discord.com/channels/${e.guild_id??"@me"}/${e.id}`}}async messages(e,t={}){let s=t.limit??1000,r=[],n=t.after??"0";while(r.length<s){let a=await this.api(`/channels/${e}/messages?limit=100&after=${n}`);if(a.length===0)break;if(a.sort((o,c)=>BigInt(o.id)<BigInt(c.id)?-1:1),r.push(...a),n=a[a.length-1].id,a.length<100)break}let i=[];for(let a of r.slice(0,s)){let o=a.author.id===this.userId;i.push({id:a.id,conversationId:e,kind:[0,19,20,21].includes(a.type)?"message":"event",text:a.content||null,fromMe:o,sentAt:Date.parse(a.timestamp),sender:o?null:{id:a.author.id,name:a.author.global_name||a.author.username},targetId:null,reaction:null,reactionRemoved:!1,replyToId:a.message_reference?.message_id??null,editedAt:a.edited_timestamp?Date.parse(a.edited_timestamp):null,unsent:!1,attachments:(a.attachments??[]).map((c)=>({id:c.id,name:c.filename,mimeType:c.content_type??null,bytes:c.size,path:null,url:c.url})),cursor:a.id});for(let c of a.reactions??[])i.push({id:`${a.id}:${c.emoji.id??c.emoji.name}`,conversationId:e,kind:"reaction",text:c.count>1?String(c.count):null,fromMe:!!c.me,sentAt:Date.parse(a.timestamp),sender:null,targetId:a.id,reaction:c.emoji.name??"\u2022",reactionRemoved:!1,replyToId:null,editedAt:null,unsent:!1,attachments:[],cursor:a.id})}return i}async channel(e){let t=this.channels.get(e);if(t)return t;let s=await this.api(`/channels/${e}`);return this.channels.set(e,s),s}async archive(e){let t=await this.channel(e),s=this.archiveSupport(t);if(s.mode!=="native")return{mode:s.mode,removed:!1,detail:s.detail};return await this.api(`/channels/${e}`,{method:"PATCH",body:JSON.stringify({archived:!0})}),this.channels.set(e,{...t,thread_metadata:{archived:!0}}),{mode:"native",removed:!0,detail:s.detail}}async unarchive(e){let t=await this.channel(e);if(!z.has(t.type))return;await this.api(`/channels/${e}`,{method:"PATCH",body:JSON.stringify({archived:!1})}),this.channels.set(e,{...t,thread_metadata:{archived:!1}})}async attachment(e){return e.url?this.fetch(e.url):new Response("Not found",{status:404})}}import{existsSync as X}from"fs";import{dirname as Fe}from"path";import Be from"process";import{Database as Te}from"bun:sqlite";import{homedir as ce}from"os";import{join as le}from"path";function d(e){let t=e.trim();if(t.includes("@"))return t.toLowerCase();let s=t.replace(/[^\d+]/g,"");if(!/^\+?\d{7,15}$/.test(s))return t;if(s.startsWith("+"))return s;if(s.length===10)return`+1${s}`;return`+${s}`}function ne(e){return/^\+\d{7,15}$/.test(e)||/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)}function ie(e){let t=e.match(/^\+1(\d{3})(\d{3})(\d{4})$/);if(t)return`+1 (${t[1]}) ${t[2]}-${t[3]}`;return e}var ae=new TextEncoder,Me=new TextDecoder("utf-8",{fatal:!1}),Se=[ae.encode("NSString"),ae.encode("NSMutableString")];function Ae(e,t,s=0){e:for(let r=s;r<=e.length-t.length;r++){for(let n=0;n<t.length;n++)if(e[r+n]!==t[n])continue e;return r}return-1}function Re(e,t){let s=e[t];if(s===void 0)return null;let r=new DataView(e.buffer,e.byteOffset,e.byteLength);if(s===129){if(t+3>e.length)return null;return{length:r.getUint16(t+1,!0),start:t+3}}if(s===130){if(t+5>e.length)return null;return{length:r.getUint32(t+1,!0),start:t+5}}return{length:s,start:t+1}}function C(e){if(!e||e.length===0)return null;for(let t of Se){let s=Ae(e,t);if(s===-1)continue;let r=s+t.length,n=Math.min(e.length,r+16);for(let i=r;i<n;i++){if(e[i]!==43)continue;let a=Re(e,i+1);if(!a||a.start+a.length>e.length)continue;return Me.decode(e.subarray(a.start,a.start+a.length))}}return null}var q=le(ce(),"Library","Messages","chat.db"),de=978307200,Ie=45,P=43;function J(e){if(!e)return null;let t=e>1000000000000?e/1e9:e;return Math.round((t+de)*1000)}function De(e){return Math.round((e/1000-de)*1e9)}class j extends Error{path;needsFullDiskAccess;constructor(e,t){let s=t instanceof Error?t.message:String(t),r=/authorization denied|not permitted|unable to open/i.test(s);super(`Cannot read ${e}: ${s}`);this.path=e;this.name="MessagesAccessError",this.needsFullDiskAccess=r}}var Ee=(e)=>e>=2000&&e<4000;function oe(e){if(!e)return null;return e.startsWith("~/")?le(ce(),e.slice(2)):e}function Oe(e){if(!e)return null;let t=e.indexOf("/");if(t!==-1)return e.slice(t+1);return e.replace(/^bp:/,"")}class K{path;db;messageColumns;hasRecoverable;constructor(e=q){this.path=e;try{this.db=new Te(e,{readonly:!0}),this.db.query("SELECT 1 FROM message LIMIT 1").all()}catch(t){throw new j(e,t)}this.messageColumns=new Set(this.db.query("PRAGMA table_info(message)").all().map((t)=>t.name)),this.hasRecoverable=!!this.db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'chat_recoverable_message_join'").get()}close(){this.db.close()}latestRowId(){return this.db.query("SELECT MAX(ROWID) AS id FROM message").get().id??0}column(e,t="NULL"){return this.messageColumns.has(e)?`m.${e}`:t}chats(){let e=this.hasRecoverable?"(SELECT COUNT(*) FROM chat_recoverable_message_join r WHERE r.chat_id = c.ROWID)":"0",t=this.db.query(`
|
|
3
|
+
SELECT
|
|
4
|
+
c.ROWID AS rowid, c.guid, c.chat_identifier, c.style, c.service_name, c.display_name, c.group_id,
|
|
5
|
+
(SELECT COUNT(*) FROM chat_message_join j WHERE j.chat_id = c.ROWID) AS message_count,
|
|
6
|
+
${e} AS recoverable_count,
|
|
7
|
+
(SELECT COUNT(*) FROM chat_message_join j JOIN message m ON m.ROWID = j.message_id
|
|
8
|
+
WHERE j.chat_id = c.ROWID AND m.is_from_me = 0 AND m.is_read = 0 AND m.item_type = 0
|
|
9
|
+
AND (m.associated_message_type IS NULL OR m.associated_message_type = 0)) AS unread_count,
|
|
10
|
+
(SELECT MAX(j.message_id) FROM chat_message_join j WHERE j.chat_id = c.ROWID) AS last_rowid
|
|
11
|
+
FROM chat c
|
|
12
|
+
`).all(),s=this.participantsByChat(),r=this.db.query("SELECT m.text, m.attributedBody, m.date, m.is_from_me FROM message m WHERE m.ROWID = ?");return t.map((i)=>{let a=i.last_rowid?r.get(i.last_rowid):null;return{rowid:i.rowid,guid:i.guid,identifier:i.chat_identifier?d(i.chat_identifier):i.guid,style:i.style??Ie,service:i.service_name??"iMessage",displayName:i.display_name?.trim()||null,groupId:i.group_id||null,participants:s.get(i.rowid)??[],messageCount:i.message_count,recoverableCount:i.recoverable_count,unreadCount:i.unread_count,lastMessageAt:a?J(a.date):null,lastMessageRowId:i.last_rowid??0,lastMessageText:a?a.text?.trim()?a.text:C(a.attributedBody):null,lastMessageFromMe:a?.is_from_me===1}}).sort((i,a)=>(a.lastMessageAt??0)-(i.lastMessageAt??0))}participantsByChat(){let e=this.db.query(`
|
|
13
|
+
SELECT chj.chat_id, h.id FROM chat_handle_join chj JOIN handle h ON h.ROWID = chj.handle_id
|
|
14
|
+
`).all(),t=new Map;for(let s of e){let r=t.get(s.chat_id)??[],n=d(s.id);if(!r.includes(n))r.push(n);t.set(s.chat_id,r)}return t}messages(e,t={}){if(e.length===0)return[];let s=e.map(()=>"?").join(", "),r=t.newestFirst?"m.date DESC, m.ROWID DESC":t.byRowId?"m.ROWID ASC":"m.date ASC, m.ROWID ASC",n=t.beforeMs?`AND m.date < ${De(t.beforeMs)}`:"",i=this.db.query(`
|
|
15
|
+
SELECT
|
|
16
|
+
m.ROWID AS rowid, m.guid, m.text, m.attributedBody, m.is_from_me, m.date, m.service,
|
|
17
|
+
m.associated_message_guid, m.associated_message_type, m.item_type, m.group_title,
|
|
18
|
+
m.cache_has_attachments,
|
|
19
|
+
${this.column("reply_to_guid")} AS reply_to_guid,
|
|
20
|
+
${this.column("date_edited","0")} AS date_edited,
|
|
21
|
+
${this.column("date_retracted","0")} AS date_retracted,
|
|
22
|
+
h.id AS sender,
|
|
23
|
+
c.guid AS chat_guid
|
|
24
|
+
FROM message m
|
|
25
|
+
JOIN chat_message_join cmj ON cmj.message_id = m.ROWID
|
|
26
|
+
JOIN chat c ON c.ROWID = cmj.chat_id
|
|
27
|
+
LEFT JOIN handle h ON h.ROWID = m.handle_id
|
|
28
|
+
WHERE c.guid IN (${s}) AND m.ROWID > ? ${n}
|
|
29
|
+
ORDER BY ${r}
|
|
30
|
+
LIMIT ?
|
|
31
|
+
`).all(...e,t.afterRowId??0,t.limit??1e6),a=this.attachmentsFor(i.filter((o)=>o.cache_has_attachments===1).map((o)=>o.rowid));return i.map((o)=>{let c=o.associated_message_type??0,S=o.item_type??0,l=Ee(c)?"reaction":S!==0?"event":"message";return{rowid:o.rowid,guid:o.guid,chatGuid:o.chat_guid,kind:l,text:o.text?.trim()?o.text:C(o.attributedBody),isFromMe:o.is_from_me===1,sentAt:J(o.date)??0,sender:o.sender?d(o.sender):null,service:o.service??"iMessage",targetGuid:l==="reaction"?Oe(o.associated_message_guid):null,reactionType:l==="reaction"?c:0,replyToGuid:o.reply_to_guid||null,editedAt:J(o.date_edited),unsent:!!o.date_retracted,eventType:S,groupTitle:o.group_title||null,attachments:a.get(o.rowid)??[]}})}attachmentPath(e){let t=this.db.query("SELECT filename, mime_type, transfer_name FROM attachment WHERE guid = ?").get(e);if(!t)return null;let s=oe(t.filename);return{path:s,mimeType:t.mime_type,name:t.transfer_name||(s?s.split("/").pop():e)}}attachmentsFor(e){let t=new Map;for(let s=0;s<e.length;s+=500){let r=e.slice(s,s+500),n=this.db.query(`
|
|
32
|
+
SELECT maj.message_id, a.guid, a.filename, a.transfer_name, a.mime_type, a.uti, a.total_bytes, a.is_sticker
|
|
33
|
+
FROM message_attachment_join maj JOIN attachment a ON a.ROWID = maj.attachment_id
|
|
34
|
+
WHERE maj.message_id IN (${r.map(()=>"?").join(", ")})
|
|
35
|
+
`).all(...r);for(let i of n){let a=t.get(i.message_id)??[],o=oe(i.filename);a.push({guid:i.guid,path:o,name:i.transfer_name||(o?o.split("/").pop():i.guid),mimeType:i.mime_type,uti:i.uti,bytes:i.total_bytes??0,isSticker:i.is_sticker===1}),t.set(i.message_id,a)}}return t}}import{Database as Pe}from"bun:sqlite";import{existsSync as ue,readdirSync as $e}from"fs";import{homedir as je}from"os";import{join as N}from"path";var Z=N(je(),"Library","Application Support","AddressBook");function Ne(e){let t=[],s=N(e,"AddressBook-v22.abcddb");if(ue(s))t.push(s);let r=N(e,"Sources");try{for(let n of $e(r)){let i=N(r,n,"AddressBook-v22.abcddb");if(ue(i))t.push(i)}}catch{}return t}function me(e){let t=e.replace(/[^\p{L}\p{N}\s]/gu," ").trim().split(/\s+/).filter(Boolean);if(t.length===0)return"?";let s=t[0][0],r=t.length>1?t[t.length-1][0]:"";return(s+r).toUpperCase()}class k{byHandle=new Map;static load(e=Z){let t=new k;for(let s of Ne(e))try{t.read(s)}catch{}return t}get size(){return this.byHandle.size}read(e){let t=new Pe(e,{readonly:!0});try{let s=t.query(`
|
|
36
|
+
SELECT Z_PK AS id, ZFIRSTNAME AS first, ZLASTNAME AS last, ZNICKNAME AS nick, ZORGANIZATION AS org
|
|
37
|
+
FROM ZABCDRECORD
|
|
38
|
+
`).all(),r=new Map;for(let a of s){let c=[a.first,a.last].filter(Boolean).join(" ").trim()||a.nick?.trim()||a.org?.trim();if(c)r.set(a.id,c)}let n=t.query("SELECT ZOWNER AS owner, ZFULLNUMBER AS value FROM ZABCDPHONENUMBER").all(),i=t.query("SELECT ZOWNER AS owner, ZADDRESS AS value FROM ZABCDEMAILADDRESS").all();for(let a of[...n,...i]){let o=r.get(a.owner);if(!o||!a.value)continue;let c=d(a.value);if(!this.byHandle.has(c))this.byHandle.set(c,{name:o,initials:me(o)})}}finally{t.close()}}set(e,t){this.byHandle.set(d(e),{name:t,initials:me(t)})}lookup(e){return this.byHandle.get(d(e))??null}nameFor(e){return this.lookup(e)?.name??null}}function Le(e){if(e.style===P)return`group:${e.groupId||e.guid}`;return`direct:${e.identifier}`}function Q(e){let t=new Map;for(let s of e){let r=Le(s),n=t.get(r);if(!n){t.set(r,{key:r,kind:s.style===P?"group":"direct",chatGuids:[s.guid],displayName:s.displayName,groupId:s.groupId,participants:s.style===P?[...s.participants]:[s.identifier],messageCount:s.messageCount,recoverableCount:s.recoverableCount,unreadCount:s.unreadCount,lastMessageAt:s.lastMessageAt,lastMessageRowId:s.lastMessageRowId,lastMessageText:s.lastMessageText,lastMessageFromMe:s.lastMessageFromMe,services:[s.service]});continue}if(n.chatGuids.push(s.guid),n.messageCount+=s.messageCount,n.recoverableCount+=s.recoverableCount,n.unreadCount+=s.unreadCount,!n.services.includes(s.service))n.services.push(s.service);for(let i of s.participants)if(n.kind==="group"&&!n.participants.includes(i))n.participants.push(i);if((s.lastMessageAt??0)>(n.lastMessageAt??0))n.lastMessageAt=s.lastMessageAt,n.lastMessageRowId=s.lastMessageRowId,n.lastMessageText=s.lastMessageText,n.lastMessageFromMe=s.lastMessageFromMe,n.displayName=s.displayName??n.displayName}return[...t.values()].sort((s,r)=>(r.lastMessageAt??0)-(s.lastMessageAt??0))}function V(e){if(e.kind==="group")return e.groupId?`sms://open?groupid=${encodeURIComponent(e.groupId)}`:null;let t=e.participants[0];if(!t||!ne(t))return null;return`sms:${t}`}var Ue={2000:"\u2764\uFE0F",2001:"\uD83D\uDC4D",2002:"\uD83D\uDC4E",2003:"\uD83D\uDE02",2004:"\u203C\uFE0F",2005:"\u2753",2006:"\uD83D\uDE00",2007:"\uD83D\uDD16"},We=`
|
|
39
|
+
tell application "Messages" to activate
|
|
40
|
+
delay 0.8
|
|
41
|
+
tell application "System Events" to tell process "Messages"
|
|
42
|
+
click menu item "Delete Conversation\u2026" of menu "Conversation" of menu bar 1
|
|
43
|
+
end tell
|
|
44
|
+
`;function He(e){Bun.spawn(["/usr/bin/open",e],{stdio:["ignore","ignore","ignore"]})}async function ze(e){return await Bun.spawn(["/usr/bin/osascript","-e",e],{stdio:["ignore","ignore","ignore"]}).exited===0}class w{provider="imessage";label="iMessage";databasePath;addressBookDir;openUrl;runAppleScript;contacts=null;contactsLoadedAt=0;constructor(e={}){this.databasePath=e.databasePath??q,this.addressBookDir=e.addressBookDir??Z,this.openUrl=e.openUrl??He,this.runAppleScript=e.runAppleScript??ze}open(e){let t=new K(this.databasePath);try{return e(t)}finally{t.close()}}names(){if(this.addressBookDir===!1)return this.contacts??=new k;if(!this.contacts||Date.now()-this.contactsLoadedAt>600000)this.contacts=k.load(this.addressBookDir),this.contactsLoadedAt=Date.now();return this.contacts}person(e){return e?{id:e,name:this.names().nameFor(e)}:null}find(e,t){return Q(e.chats()).find((s)=>s.key===t)}async status(){if(Be.platform!=="darwin")return{connected:!1,detail:"iMessage is only available on a Mac.",needs:"unsupported-platform"};try{return this.open(()=>{return}),{connected:!0,detail:"Reading Messages on this Mac."}}catch(e){if(e instanceof j&&e.needsFullDiskAccess&&X(Fe(this.databasePath)))return{connected:!1,detail:"Grant Full Disk Access in System Settings > Privacy & Security.",needs:"full-disk-access"};return{connected:!1,detail:"Sign in to Messages on this Mac.",needs:"messages-signed-out"}}}async conversations(){return this.open((e)=>Q(e.chats()).map((t)=>this.toConversation(t)))}toConversation(e){let t=V(e);return{provider:"imessage",id:e.key,kind:e.kind,title:e.kind==="group"?e.displayName:null,participants:e.participants.map((s)=>this.person(s)),workspace:null,lastMessageAt:e.lastMessageAt,preview:e.messageCount>0?e.lastMessageText:null,previewFromMe:e.lastMessageFromMe,unread:e.unreadCount,cursor:String(e.lastMessageRowId),visible:e.messageCount>0,archive:t?{mode:"confirm",detail:"Opens it in Messages at Delete Conversation for you to confirm. Messages keeps it in Recently Deleted for 30 days."}:{mode:"unsupported",detail:"Messages cannot open this conversation by address, so delete it there yourself."},url:t}}async messages(e,t={}){return this.open((s)=>{let r=this.find(s,e);if(!r)return[];return s.messages(r.chatGuids,{afterRowId:Number(t.after)||0,byRowId:!0,limit:t.limit}).map((i)=>this.toMessage(e,i))})}toMessage(e,t){let s=t.reactionType>=3000;return{id:t.guid,conversationId:e,kind:t.kind,text:t.kind==="event"&&t.groupTitle?`Named the conversation \u201C${t.groupTitle}\u201D`:t.text,fromMe:t.isFromMe,sentAt:t.sentAt,sender:t.isFromMe?null:this.person(t.sender),targetId:t.targetGuid,reaction:t.kind==="reaction"?Ue[s?t.reactionType-1000:t.reactionType]??"\u2022":null,reactionRemoved:s,replyToId:t.replyToGuid,editedAt:t.editedAt,unsent:t.unsent,attachments:t.attachments.map((r)=>({id:r.guid,name:r.name,mimeType:r.mimeType,bytes:r.bytes,path:r.path&&X(r.path)?r.path:null,url:null})),cursor:String(t.rowid)}}async archive(e){let t=this.open((n)=>this.find(n,e)),s=t?V(t):null;if(!t||!s)return{mode:"unsupported",removed:!1,detail:"Messages cannot open this conversation by address, so delete it there yourself."};if(t.messageCount===0)return{mode:"confirm",removed:!0,detail:"Already gone from Messages."};return this.openUrl(s),{mode:"confirm",removed:!1,detail:await this.runAppleScript(We)?"Messages is asking you to confirm the delete.":"Messages is open on the conversation: choose Conversation > Delete Conversation to finish. (Allow Accessibility access to have this done for you.)"}}async unarchive(){}async attachment(e){if(!e.path||!X(e.path))return new Response("Not found",{status:404});return new Response(Bun.file(e.path),{headers:{"content-type":e.mimeType??"application/octet-stream"}})}}var Ge={"+1":"\uD83D\uDC4D",thumbsup:"\uD83D\uDC4D","-1":"\uD83D\uDC4E",heart:"\u2764\uFE0F",joy:"\uD83D\uDE02",laughing:"\uD83D\uDE06",tada:"\uD83C\uDF89",eyes:"\uD83D\uDC40",fire:"\uD83D\uDD25",white_check_mark:"\u2705",pray:"\uD83D\uDE4F",raised_hands:"\uD83D\uDE4C",rocket:"\uD83D\uDE80"},L=(e)=>Math.round(Number(e)*1000);class F extends Error{method;code;constructor(e,t){super(`Slack ${e} failed: ${t}`);this.method=e;this.code=t;this.name="SlackApiError"}}class M{provider="slack";label="Slack";token;fetch;apiBase;concurrency;self=null;users=new Map;channels=new Map;constructor(e){this.token=e.token,this.fetch=e.fetch??((t,s)=>fetch(t,s)),this.apiBase=(e.apiBase??"https://slack.com/api").replace(/\/$/,""),this.concurrency=e.concurrency??4}async api(e,t={},s=!1){for(let r=0;;r++){let n=new URLSearchParams(Object.entries(t).map(([o,c])=>[o,String(c)])),i=s?await this.fetch(`${this.apiBase}/${e}`,{method:"POST",headers:{authorization:`Bearer ${this.token}`,"content-type":"application/x-www-form-urlencoded"},body:n.toString()}):await this.fetch(`${this.apiBase}/${e}?${n}`,{headers:{authorization:`Bearer ${this.token}`}});if(i.status===429&&r<3){await Bun.sleep(Math.min(30,Number(i.headers.get("retry-after"))||1)*1000);continue}let a=await i.json();if(!a.ok)throw new F(e,a.error??`http_${i.status}`);return a}}async whoami(){if(!this.self){let e=await this.api("auth.test");this.self={userId:e.user_id,team:e.team,teamId:e.team_id}}return this.self}async userName(e){if(!e)return null;if(!this.users.has(e))try{let{user:t}=await this.api("users.info",{user:e});this.users.set(e,t.profile?.display_name||t.profile?.real_name||t.real_name||t.name||null)}catch{this.users.set(e,null)}return this.users.get(e)??null}async person(e){return e?{id:e,name:await this.userName(e)}:null}async status(){if(!this.token)return{connected:!1,detail:"Add a Slack user token.",needs:"token"};try{return{connected:!0,detail:`Signed in to ${(await this.whoami()).team}.`}}catch(e){return{connected:!1,detail:e instanceof Error?e.message:String(e),needs:"token"}}}archiveSupport(e){if(e.is_im||e.is_mpim)return{mode:"native",detail:"Closes the conversation in Slack. Slack reopens it when someone writes."};if(!e.is_private)return{mode:"native",detail:"Leaves the channel in Slack, and rejoins it if you unarchive."};return{mode:"unsupported",detail:"Leaving a private channel would need someone to invite you back, so it stays in Slack."}}async list(){let e=[],t="";do{let s=await this.api("users.conversations",{types:"im,mpim,public_channel,private_channel",exclude_archived:!0,limit:200,...t?{cursor:t}:{}});e.push(...s.channels),t=s.response_metadata?.next_cursor??""}while(t);return e}async conversations(){let e=await this.whoami(),t=await this.list(),s=[];for(let r=0;r<t.length;r+=this.concurrency){let n=t.slice(r,r+this.concurrency);s.push(...await Promise.all(n.map((i)=>this.describe(i,e))))}return s.sort((r,n)=>(n.lastMessageAt??0)-(r.lastMessageAt??0))}async describe(e,t){let[{channel:s},r]=await Promise.all([this.api("conversations.info",{channel:e.id}).catch(()=>({channel:e})),this.api("conversations.history",{channel:e.id,limit:1}).catch(()=>({messages:[]}))]),n={...e,...s};this.channels.set(n.id,n);let i=r.messages[0],a=[];if(n.is_im)a=[await this.person(n.user)];else if(n.is_mpim){let o=await this.api("conversations.members",{channel:n.id,limit:50}).catch(()=>({members:[]}));a=await Promise.all(o.members.filter((c)=>c!==t.userId).map(async(c)=>await this.person(c)))}return{provider:"slack",id:n.id,kind:n.is_im?"direct":n.is_mpim?"group":"channel",title:n.is_im||n.is_mpim?null:`#${n.name}`,participants:a,workspace:t.team,lastMessageAt:i?L(i.ts):null,preview:i?.text||(i?.files?.length?"Attachment":null),previewFromMe:i?.user===t.userId,unread:n.unread_count_display??0,cursor:i?.ts??"0",visible:n.is_im||n.is_mpim?n.is_open!==!1:!0,archive:this.archiveSupport(n),url:`slack://channel?team=${t.teamId}&id=${n.id}`}}async messages(e,t={}){let s=await this.whoami(),r=[],n="",i=t.limit??1000;do{let o=await this.api("conversations.history",{channel:e,limit:Math.min(200,i),...t.after?{oldest:t.after,inclusive:!1}:{},...n?{cursor:n}:{}});r.push(...o.messages),n=o.has_more?o.response_metadata?.next_cursor??"":""}while(n&&r.length<i);r.sort((o,c)=>Number(o.ts)-Number(c.ts));let a=[];for(let o of r.slice(-i)){let c=await this.person(o.user),S=!!o.subtype&&!["file_share","thread_broadcast","me_message"].includes(o.subtype);a.push({id:o.ts,conversationId:e,kind:S?"event":"message",text:o.text??null,fromMe:o.user===s.userId,sentAt:L(o.ts),sender:o.user===s.userId?null:c??(o.username?{id:o.bot_id??o.username,name:o.username}:null),targetId:null,reaction:null,reactionRemoved:!1,replyToId:o.thread_ts&&o.thread_ts!==o.ts?o.thread_ts:null,editedAt:o.edited?L(o.edited.ts):null,unsent:!1,attachments:(o.files??[]).map((l)=>({id:l.id,name:l.name??l.title??l.id,mimeType:l.mimetype??null,bytes:l.size??0,path:null,url:l.url_private??null})),cursor:o.ts});for(let l of o.reactions??[])for(let A of l.users??[])a.push({id:`${o.ts}:${l.name}:${A}`,conversationId:e,kind:"reaction",text:null,fromMe:A===s.userId,sentAt:L(o.ts),sender:A===s.userId?null:await this.person(A),targetId:o.ts,reaction:Ge[l.name]??`:${l.name}:`,reactionRemoved:!1,replyToId:null,editedAt:null,unsent:!1,attachments:[],cursor:o.ts})}return a}async channel(e){let t=this.channels.get(e);if(t)return t;let{channel:s}=await this.api("conversations.info",{channel:e});return this.channels.set(e,s),s}async archive(e){let t=await this.channel(e),s=this.archiveSupport(t);if(s.mode!=="native")return{mode:s.mode,removed:!1,detail:s.detail};if(t.is_im||t.is_mpim)await this.api("conversations.close",{channel:e},!0);else await this.api("conversations.leave",{channel:e},!0);return this.channels.set(e,{...t,is_open:!1}),{mode:"native",removed:!0,detail:s.detail}}async unarchive(e){let t=await this.channel(e);if(t.is_im||t.is_mpim)await this.api("conversations.open",{channel:e},!0);else if(!t.is_private)await this.api("conversations.join",{channel:e},!0);this.channels.set(e,{...t,is_open:!0})}async attachment(e){if(!e.url)return new Response("Not found",{status:404});return this.fetch(e.url,{headers:{authorization:`Bearer ${this.token}`}})}}function Je(e,t){switch(e){case"imessage":return new w(t);case"slack":return new M(t);case"discord":return new _(t);default:throw Error(`Unknown inbox driver: ${String(e)}`)}}async function Nt(e,t={}){let s=t.driver??"slack";switch(s){case"slack":return I(e);case"discord":return T(e);case"teams":return E(e);default:return{success:!1,provider:s,message:`Unknown chat driver: ${s}`}}}async function Lt(e,t,s){return D(e,t,s)}async function Ft(e,t,s){return f(e,t,s)}async function Bt(e,t){return O(e,t)}function Ut(e){y(e)}function Wt(e){p(e)}function Ht(e){v(e)}export{Wt as configureDiscord,Ut as configureSlack,Ht as configureTeams,B as discord,he as inbox,Nt as send,Ft as sendToDiscord,Lt as sendToSlack,Bt as sendToTeams,U as slack,H as teams};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/chat",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.42",
|
|
6
6
|
"description": "Easily interact with chat APIs.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -58,13 +58,13 @@
|
|
|
58
58
|
"prepublishOnly": "bun run build"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@stacksjs/logging": "0.75.
|
|
61
|
+
"@stacksjs/logging": "0.75.42"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@stacksjs/cli": "0.75.
|
|
65
|
-
"@stacksjs/config": "0.75.
|
|
66
|
-
"@stacksjs/error-handling": "0.75.
|
|
67
|
-
"@stacksjs/types": "0.75.
|
|
64
|
+
"@stacksjs/cli": "0.75.42",
|
|
65
|
+
"@stacksjs/config": "0.75.42",
|
|
66
|
+
"@stacksjs/error-handling": "0.75.42",
|
|
67
|
+
"@stacksjs/types": "0.75.42",
|
|
68
68
|
"better-dx": "^0.2.24"
|
|
69
69
|
}
|
|
70
70
|
}
|