@stacksjs/chat 0.75.54 → 0.75.55
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/dist/inbox/formats/bplist.d.ts +9 -0
- package/dist/inbox/formats/protobuf.d.ts +19 -0
- package/dist/inbox/imessage/chat-db.d.ts +4 -0
- package/dist/inbox/imessage/conversations.d.ts +1 -0
- package/dist/inbox/whatsapp/chat-storage.d.ts +2 -0
- package/dist/index.js +15 -12
- package/package.json +6 -6
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function parseBplist(bytes: Uint8Array): PlistValue;
|
|
2
|
+
/**
|
|
3
|
+
* A reader for Apple's binary property lists (`bplist00`), the format of
|
|
4
|
+
* chat.db's `chat.properties` - where Messages keeps a group's photo,
|
|
5
|
+
* among other things. Read-only and dependency-free; returns plain values:
|
|
6
|
+
* dictionaries as objects, arrays, strings, numbers, booleans, null, Dates
|
|
7
|
+
* and `Uint8Array` for data. NSKeyedArchiver UIDs come back as `{ uid }`.
|
|
8
|
+
*/
|
|
9
|
+
export type PlistValue = null | boolean | number | bigint | string | Date | Uint8Array | { uid: number } | PlistValue[] | { [key: string]: PlistValue }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare function fields(bytes: Uint8Array): ProtoField[];
|
|
2
|
+
/** Fields of a message, or null when the bytes are not one. */
|
|
3
|
+
export declare function tryFields(bytes: Uint8Array | null | undefined): ProtoField[] | null;
|
|
4
|
+
export declare function bytesOf(list: ProtoField[] | null, field: number): Uint8Array | null;
|
|
5
|
+
export declare function allBytesOf(list: ProtoField[] | null, field: number): Uint8Array[];
|
|
6
|
+
export declare function stringOf(list: ProtoField[] | null, field: number): string | null;
|
|
7
|
+
export declare function numberOf(list: ProtoField[] | null, field: number): bigint | null;
|
|
8
|
+
/**
|
|
9
|
+
* A schema-less protocol buffers reader. WhatsApp keeps reactions and quoted
|
|
10
|
+
* replies as protobuf blobs inside its Core Data store, with no published
|
|
11
|
+
* schema, so fields are read by number: `fields(bytes)` splits a message
|
|
12
|
+
* into its wire-level fields, and the helpers pull out what a caller knows
|
|
13
|
+
* by number. Malformed input throws; callers treat that as "not present".
|
|
14
|
+
*/
|
|
15
|
+
export declare interface ProtoField {
|
|
16
|
+
field: number
|
|
17
|
+
wire: number
|
|
18
|
+
value: bigint | Uint8Array
|
|
19
|
+
}
|
|
@@ -9,6 +9,8 @@ export declare function unixMsToAppleDate(ms: number): number;
|
|
|
9
9
|
* "\uFFFC": not empty, and invisible. Shown as text it is an empty bubble.
|
|
10
10
|
*/
|
|
11
11
|
export declare function messageText(plain: string | null, attributedBody: Uint8Array | null): string | null;
|
|
12
|
+
/** A group's photo, which Messages records as an attachment guid in the chat's property list. */
|
|
13
|
+
declare function groupPhotoGuid(properties: Uint8Array | null): string | null;
|
|
12
14
|
/**
|
|
13
15
|
* `associated_message_guid` points at a part of a message, not the message:
|
|
14
16
|
* `p:0/<guid>` for the first part, `bp:<guid>` for a balloon. Reactions are
|
|
@@ -47,6 +49,7 @@ export declare interface ChatRow {
|
|
|
47
49
|
displayName: string | null
|
|
48
50
|
groupId: string | null
|
|
49
51
|
participants: string[]
|
|
52
|
+
groupPhotoGuid: string | null
|
|
50
53
|
messageCount: number
|
|
51
54
|
recoverableCount: number
|
|
52
55
|
unreadCount: number
|
|
@@ -92,6 +95,7 @@ export declare class MessagesAccessError extends Error {
|
|
|
92
95
|
export declare class MessagesDb {
|
|
93
96
|
public readonly path: string;
|
|
94
97
|
constructor(path?: string);
|
|
98
|
+
attachmentFile(guid: string): string | null;
|
|
95
99
|
close(): void;
|
|
96
100
|
latestRowId(): number;
|
|
97
101
|
chats(): ChatRow[];
|
|
@@ -70,6 +70,8 @@ export declare interface MessageRow {
|
|
|
70
70
|
latitude: number | null
|
|
71
71
|
longitude: number | null
|
|
72
72
|
} | null
|
|
73
|
+
metadata: Uint8Array | null
|
|
74
|
+
receipts: Uint8Array | null
|
|
73
75
|
}
|
|
74
76
|
export declare class WhatsAppAccessError extends Error {
|
|
75
77
|
readonly needsFullDiskAccess: boolean;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var Le=Object.defineProperty;var $e=(e)=>e;function Fe(e,t){this[e]=$e.bind(null,t)}var P=(e,t)=>{for(var s in t)Le(e,s,{get:t[s],enumerable:!0,configurable:!0,set:Fe.bind(t,s)})};var Q={};P(Q,{DiscordDriver:()=>x,Driver:()=>x,configure:()=>A,default:()=>We,driver:()=>Ue,send:()=>O,sendEmbed:()=>be,sendWebhook:()=>M});import{log as fe}from"@stacksjs/logging";import{log as je}from"@stacksjs/logging";class h{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));je.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),a=n instanceof Promise?await n:n;r={...r,...a,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 f={};function A(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.`)}f={...f,...e}}class x extends h{name="discord";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)A(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};fe.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 fe.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(f.webhookUrl)return this.sendViaWebhook(e);if(f.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(f.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 ${f.botToken}`,"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){let a=await r.json();throw Error(`Discord API failed: ${r.status} - ${a.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 O(e,t){return new x().send(e,t)}async function M(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 a=await n.text();return{success:!1,provider:"discord",message:`Webhook failed: ${n.status} - ${a}`}}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 be(e,t,s){return M(e,"",{...s,embeds:[t]})}var Ue=new x,We={send:O,sendWebhook:M,sendEmbed:be,configure:A,DiscordDriver:x};var ee={};P(ee,{Driver:()=>C,SlackDriver:()=>C,configure:()=>S,default:()=>Ze,driver:()=>Be,send:()=>N,sendWebhook:()=>L});import{log as ye}from"@stacksjs/logging";var b={};function S(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.`)}b={...b,...e}}class C extends h{name="slack";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)S(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};ye.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 ye.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(b.webhookUrl)return this.sendViaWebhook(e);if(b.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 a=await fetch(b.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!a.ok){let i=await a.text();throw Error(`Slack webhook failed: ${a.status} - ${i}`)}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 ${b.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 N(e,t){return new C().send(e,t)}async function L(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 a=await n.text();return{success:!1,provider:"slack",message:`Webhook failed: ${n.status} - ${a}`}}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 Be=new C,Ze={send:N,sendWebhook:L,configure:S,SlackDriver:C};var se={};P(se,{Driver:()=>w,TeamsDriver:()=>w,configure:()=>k,default:()=>Ge,driver:()=>He,send:()=>F,sendCard:()=>Ae,sendWebhook:()=>j});import{log as ve}from"@stacksjs/logging";var te={};function k(e){te={...te,...e}}class w extends h{name="teams";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)k(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};ve.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 ve.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=te.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),a=await fetch(r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!a.ok){let i=await a.text();throw Error(`Teams webhook failed: ${a.status} - ${i}`)}}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 F(e,t){return new w().send(e,t)}async function j(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 Ae(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 a=await n.text();return{success:!1,provider:"teams",message:`Webhook failed: ${n.status} - ${a}`}}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 He=new w,Ge={send:F,sendWebhook:j,sendCard:Ae,configure:k,TeamsDriver:w};var Ne={};P(Ne,{DiscordInboxDriver:()=>_,IMessageDriver:()=>R,SlackApiError:()=>z,SlackInboxDriver:()=>I,WhatsAppDriver:()=>D,createInboxDriver:()=>Rt,decodeAttributedBody:()=>Z,formatHandle:()=>B,formatJid:()=>ge,normalizeHandle:()=>p,snowflakeTime:()=>ne});function Je(e){if(e[0]===255&&e[1]===216)return"image/jpeg";if(e[0]===137&&e[1]===80&&e[2]===78&&e[3]===71)return"image/png";if(e[0]===71&&e[1]===73&&e[2]===70)return"image/gif";if(e[8]===87&&e[9]===69&&e[10]===66&&e[11]===80)return"image/webp";if(String.fromCharCode(e[4]??0,e[5]??0,e[6]??0,e[7]??0)==="ftyp")return"image/heic";return"application/octet-stream"}function U(e){if(!e||e.length===0)return new Response("Not found",{status:404});return new Response(Uint8Array.from(e),{headers:{"content-type":Je(e)}})}function W(e,t){try{let s=new URL(e);return s.protocol==="https:"&&t.some((r)=>s.hostname===r||s.hostname.endsWith(`.${r}`))}catch{return!1}}var ze=new Set([0,5]),re=new Set([10,11,12]);function ne(e){if(!e)return null;return Number((BigInt(e)>>22n)+1420070400000n)}class _{provider="discord";label="Discord";token;userId;fetch;apiBase;concurrency;guilds=new Map;icons=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(re.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),this.icons.set(r.id,r.icon?`https://cdn.discordapp.com/icons/${r.id}/${r.icon}.png?size=128`:null);let[n,a]=await Promise.all([this.api(`/guilds/${r.id}/channels`).catch(()=>[]),this.api(`/guilds/${r.id}/threads/active`).catch(()=>({threads:[]}))]);t.push(...n.filter((i)=>ze.has(i.type)).map((i)=>({...i,guild_id:r.id}))),t.push(...a.threads.map((i)=>({...i,guild_id:i.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((a)=>this.describe(a))))}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=re.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,avatar:e.guild_id?this.icons.get(e.guild_id)??null:null,lastMessageAt:t?Date.parse(t.timestamp):ne(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=(i,o)=>BigInt(i.id)<BigInt(o.id)?-1:1;if(t.after){let i=t.after;while(r.length<s){let o=await this.api(`/channels/${e}/messages?limit=100&after=${i}`);if(o.length===0)break;if(o.sort(n),r.push(...o),i=o[o.length-1].id,o.length<100)break}}else{let i="";while(r.length<s){let o=await this.api(`/channels/${e}/messages?limit=${Math.min(100,s-r.length)}${i?`&before=${i}`:""}`);if(o.length===0)break;if(o.sort(n),r.unshift(...o),i=o[0].id,o.length<100)break}}let a=[];for(let i of r.slice(0,s)){let o=i.author.id===this.userId;a.push({id:i.id,conversationId:e,kind:[0,19,20,21].includes(i.type)?"message":"event",text:i.content||null,fromMe:o,sentAt:Date.parse(i.timestamp),sender:o?null:{id:i.author.id,name:i.author.global_name||i.author.username,avatar:i.author.avatar?`https://cdn.discordapp.com/avatars/${i.author.id}/${i.author.avatar}.png?size=128`:null},targetId:null,reaction:null,reactionRemoved:!1,replyToId:i.message_reference?.message_id??null,editedAt:i.edited_timestamp?Date.parse(i.edited_timestamp):null,unsent:!1,attachments:(i.attachments??[]).map((l)=>({id:l.id,name:l.filename,mimeType:l.content_type??null,bytes:l.size,path:null,url:l.url})),cursor:i.id});for(let l of i.reactions??[])a.push({id:`${i.id}:${l.emoji.id??l.emoji.name}`,conversationId:e,kind:"reaction",text:l.count>1?String(l.count):null,fromMe:!!l.me,sentAt:Date.parse(i.timestamp),sender:null,targetId:i.id,reaction:l.emoji.name??"\u2022",reactionRemoved:!1,replyToId:null,editedAt:null,unsent:!1,attachments:[],cursor:i.id})}return a}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(!re.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 avatar(e){if(!W(e,["cdn.discordapp.com"]))return new Response("Not found",{status:404});return this.fetch(e)}async attachment(e){return e.url?this.fetch(e.url):new Response("Not found",{status:404})}}import{existsSync as ce,statSync as ct}from"fs";import{dirname as ut}from"path";import dt from"process";import{Database as Ye}from"bun:sqlite";import{homedir as ke}from"os";import{join as we}from"path";function p(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 xe(e){return/^\+\d{7,15}$/.test(e)||/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)}function B(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 Me=new TextEncoder,Ke=new TextDecoder("utf-8",{fatal:!1}),Ve=[Me.encode("NSString"),Me.encode("NSMutableString")];function qe(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 Xe(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 Z(e){if(!e||e.length===0)return null;for(let t of Ve){let s=qe(e,t);if(s===-1)continue;let r=s+t.length,n=Math.min(e.length,r+16);for(let a=r;a<n;a++){if(e[a]!==43)continue;let i=Xe(e,a+1);if(!i||i.start+i.length>e.length)continue;return Ke.decode(e.subarray(i.start,i.start+i.length))}}return null}var ae=we(ke(),"Library","Messages","chat.db"),_e=978307200,Qe=45,H=43;function y(e){if(!e)return null;let t=e>1000000000000?e/1e9:e;return Math.round((t+_e)*1000)}function et(e){return Math.round((e/1000-_e)*1e9)}function Se(e,t){let r=(e?.trim()?e:Z(t))?.replace(/\uFFFC/g,"").trim();return r?r:null}class G 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 tt=(e)=>e>=2000&&e<4000;function Ce(e){if(!e)return null;return e.startsWith("~/")?we(ke(),e.slice(2)):e}function st(e){if(!e)return null;let t=e.indexOf("/");if(t!==-1)return e.slice(t+1);return e.replace(/^bp:/,"")}class ie{path;db;messageColumns;hasRecoverable;constructor(e=ae){this.path=e;try{this.db=new Ye(e,{readonly:!0}),this.db.query("SELECT 1 FROM message LIMIT 1").all()}catch(t){throw new G(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(`
|
|
2
|
+
var Ke=Object.defineProperty;var qe=(e)=>e;function Xe(e,t){this[e]=qe.bind(null,t)}var z=(e,t)=>{for(var s in t)Ke(e,s,{get:t[s],enumerable:!0,configurable:!0,set:Xe.bind(t,s)})};var le={};z(le,{DiscordDriver:()=>T,Driver:()=>T,configure:()=>_,default:()=>et,driver:()=>Qe,send:()=>V,sendEmbed:()=>Te,sendWebhook:()=>R});import{log as _e}from"@stacksjs/logging";import{log as Ye}from"@stacksjs/logging";class S{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 w={};function _(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.`)}w={...w,...e}}class T extends S{name="discord";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)_(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};_e.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 _e.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(w.webhookUrl)return this.sendViaWebhook(e);if(w.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(w.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 ${w.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 V(e,t){return new T().send(e,t)}async function R(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 Te(e,t,s){return R(e,"",{...s,embeds:[t]})}var Qe=new T,et={send:V,sendWebhook:R,sendEmbed:Te,configure:_,DiscordDriver:T};var ce={};z(ce,{Driver:()=>D,SlackDriver:()=>D,configure:()=>I,default:()=>st,driver:()=>tt,send:()=>K,sendWebhook:()=>q});import{log as Re}from"@stacksjs/logging";var E={};function I(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.`)}E={...E,...e}}class D extends S{name="slack";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)I(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};Re.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 Re.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(E.webhookUrl)return this.sendViaWebhook(e);if(E.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(E.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 ${E.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 K(e,t){return new D().send(e,t)}async function q(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 tt=new D,st={send:K,sendWebhook:q,configure:I,SlackDriver:D};var de={};z(de,{Driver:()=>O,TeamsDriver:()=>O,configure:()=>P,default:()=>nt,driver:()=>rt,send:()=>X,sendCard:()=>De,sendWebhook:()=>Y});import{log as Ie}from"@stacksjs/logging";var ue={};function P(e){ue={...ue,...e}}class O extends S{name="teams";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"};Ie.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 Ie.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=ue.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 X(e,t){return new O().send(e,t)}async function Y(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 De(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 rt=new O,nt={send:X,sendWebhook:Y,sendCard:De,configure:P,TeamsDriver:O};var ze={};z(ze,{DiscordInboxDriver:()=>F,IMessageDriver:()=>W,SlackApiError:()=>ie,SlackInboxDriver:()=>B,WhatsAppDriver:()=>J,createInboxDriver:()=>zt,decodeAttributedBody:()=>te,formatHandle:()=>ee,formatJid:()=>ke,normalizeHandle:()=>x,snowflakeTime:()=>pe});function it(e){if(e[0]===255&&e[1]===216)return"image/jpeg";if(e[0]===137&&e[1]===80&&e[2]===78&&e[3]===71)return"image/png";if(e[0]===71&&e[1]===73&&e[2]===70)return"image/gif";if(e[8]===87&&e[9]===69&&e[10]===66&&e[11]===80)return"image/webp";if(String.fromCharCode(e[4]??0,e[5]??0,e[6]??0,e[7]??0)==="ftyp")return"image/heic";return"application/octet-stream"}function N(e){if(!e||e.length===0)return new Response("Not found",{status:404});return new Response(Uint8Array.from(e),{headers:{"content-type":it(e)}})}function Q(e,t){try{let s=new URL(e);return s.protocol==="https:"&&t.some((r)=>s.hostname===r||s.hostname.endsWith(`.${r}`))}catch{return!1}}var at=new Set([0,5]),me=new Set([10,11,12]);function pe(e){if(!e)return null;return Number((BigInt(e)>>22n)+1420070400000n)}class F{provider="discord";label="Discord";token;userId;fetch;apiBase;concurrency;guilds=new Map;icons=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(me.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),this.icons.set(r.id,r.icon?`https://cdn.discordapp.com/icons/${r.id}/${r.icon}.png?size=128`:null);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)=>at.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=me.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,avatar:e.guild_id?this.icons.get(e.guild_id)??null:null,lastMessageAt:t?Date.parse(t.timestamp):pe(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=(a,o)=>BigInt(a.id)<BigInt(o.id)?-1:1;if(t.after){let a=t.after;while(r.length<s){let o=await this.api(`/channels/${e}/messages?limit=100&after=${a}`);if(o.length===0)break;if(o.sort(n),r.push(...o),a=o[o.length-1].id,o.length<100)break}}else{let a="";while(r.length<s){let o=await this.api(`/channels/${e}/messages?limit=${Math.min(100,s-r.length)}${a?`&before=${a}`:""}`);if(o.length===0)break;if(o.sort(n),r.unshift(...o),a=o[0].id,o.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,avatar:a.author.avatar?`https://cdn.discordapp.com/avatars/${a.author.id}/${a.author.avatar}.png?size=128`:null},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((l)=>({id:l.id,name:l.filename,mimeType:l.content_type??null,bytes:l.size,path:null,url:l.url})),cursor:a.id});for(let l of a.reactions??[])i.push({id:`${a.id}:${l.emoji.id??l.emoji.name}`,conversationId:e,kind:"reaction",text:l.count>1?String(l.count):null,fromMe:!!l.me,sentAt:Date.parse(a.timestamp),sender:null,targetId:a.id,reaction:l.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(!me.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 avatar(e){if(!Q(e,["cdn.discordapp.com"]))return new Response("Not found",{status:404});return this.fetch(e)}async attachment(e){return e.url?this.fetch(e.url):new Response("Not found",{status:404})}}import{existsSync as re,readFileSync as St,statSync as Ct}from"fs";import{dirname as wt}from"path";import Et from"process";import{Database as dt}from"bun:sqlite";import{homedir as Le}from"os";import{join as $e}from"path";function Pe(e){if(e.length<40||new TextDecoder().decode(e.subarray(0,8))!=="bplist00")throw Error("Not a binary property list");let t=new DataView(e.buffer,e.byteOffset,e.byteLength),s=e.length-32,r=e[s+6],n=e[s+7],i=Number(t.getBigUint64(s+8)),a=Number(t.getBigUint64(s+16)),o=Number(t.getBigUint64(s+24)),l=(u,b)=>{let M=0;for(let p=0;p<b;p++)M=M*256+e[u+p];return M},g=(u)=>{if(u>=i)throw Error("Object reference out of range");return l(o+u*r,r)},c=new Set,m=(u)=>{if(c.has(u))throw Error("Cyclic property list");c.add(u);try{return v(g(u))}finally{c.delete(u)}},f=(u)=>{let b=e[u]&15;if(b!==15)return{length:b,start:u+1};let M=1<<(e[u+1]&15);return{length:l(u+2,M),start:u+2+M}},v=(u)=>{let b=e[u];switch(b>>4){case 0:return b===8?!1:b===9?!0:null;case 1:{let p=1<<(b&15);if(p===8)return t.getBigInt64(u+1);if(p===16)return t.getBigInt64(u+9);return l(u+1,p)}case 2:return(b&15)===2?t.getFloat32(u+1):t.getFloat64(u+1);case 3:return new Date(978307200000+t.getFloat64(u+1)*1000);case 4:{let{length:p,start:y}=f(u);return e.slice(y,y+p)}case 5:{let{length:p,start:y}=f(u);return new TextDecoder("latin1").decode(e.subarray(y,y+p))}case 6:{let{length:p,start:y}=f(u),C="";for(let A=0;A<p;A++)C+=String.fromCharCode(t.getUint16(y+A*2));return C}case 8:return{uid:l(u+1,(b&15)+1)};case 10:{let{length:p,start:y}=f(u);return Array.from({length:p},(C,A)=>m(l(y+A*n,n)))}case 13:{let{length:p,start:y}=f(u),C={};for(let A=0;A<p;A++){let Ve=m(l(y+A*n,n));C[String(Ve)]=m(l(y+(p+A)*n,n))}return C}default:throw Error(`Unsupported property list object 0x${b.toString(16)}`)}};return m(a)}function x(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 Oe(e){return/^\+\d{7,15}$/.test(e)||/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)}function ee(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 Ne=new TextEncoder,ot=new TextDecoder("utf-8",{fatal:!1}),lt=[Ne.encode("NSString"),Ne.encode("NSMutableString")];function ct(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 ut(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 te(e){if(!e||e.length===0)return null;for(let t of lt){let s=ct(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=ut(e,i+1);if(!a||a.start+a.length>e.length)continue;return ot.decode(e.subarray(a.start,a.start+a.length))}}return null}var ge=$e(Le(),"Library","Messages","chat.db"),Ue=978307200,mt=45,L=43;function k(e){if(!e)return null;let t=e>1000000000000?e/1e9:e;return Math.round((t+Ue)*1000)}function pt(e){return Math.round((e/1000-Ue)*1e9)}function Fe(e,t){let r=(e?.trim()?e:te(t))?.replace(/\uFFFC/g,"").trim();return r?r:null}function ht(e){if(!e||e.length===0)return null;try{let t=Pe(e),s=t&&typeof t==="object"&&!Array.isArray(t)&&!(t instanceof Uint8Array)&&!(t instanceof Date)?t.groupPhotoGuid:null;return typeof s==="string"&&s?s:null}catch{return null}}class se 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 gt=(e)=>e>=2000&&e<4000;function he(e){if(!e)return null;return e.startsWith("~/")?$e(Le(),e.slice(2)):e}function ft(e){if(!e)return null;let t=e.indexOf("/");if(t!==-1)return e.slice(t+1);return e.replace(/^bp:/,"")}class fe{path;db;messageColumns;hasRecoverable;constructor(e=ge){this.path=e;try{this.db=new dt(e,{readonly:!0}),this.db.query("SELECT 1 FROM message LIMIT 1").all()}catch(t){throw new se(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(),this.hasChatProperties=this.db.query("PRAGMA table_info(chat)").all().some((t)=>t.name==="properties")}hasChatProperties;attachmentFile(e){let t=this.db.query("SELECT filename FROM attachment WHERE guid = ?").get(e);return he(t?.filename??null)}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
3
|
SELECT
|
|
4
4
|
c.ROWID AS rowid, c.guid, c.chat_identifier, c.style, c.service_name, c.display_name, c.group_id,
|
|
5
|
+
${this.hasChatProperties?"c.properties":"NULL"} AS properties,
|
|
5
6
|
(SELECT COUNT(*) FROM chat_message_join j WHERE j.chat_id = c.ROWID) AS message_count,
|
|
6
7
|
${e} AS recoverable_count,
|
|
7
8
|
(SELECT COUNT(*) FROM chat_message_join j JOIN message m ON m.ROWID = j.message_id
|
|
@@ -9,9 +10,9 @@ var Le=Object.defineProperty;var $e=(e)=>e;function Fe(e,t){this[e]=$e.bind(null
|
|
|
9
10
|
AND (m.associated_message_type IS NULL OR m.associated_message_type = 0)) AS unread_count,
|
|
10
11
|
(SELECT MAX(j.message_id) FROM chat_message_join j WHERE j.chat_id = c.ROWID) AS last_rowid
|
|
11
12
|
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((
|
|
13
|
+
`).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?x(i.chat_identifier):i.guid,style:i.style??mt,service:i.service_name??"iMessage",displayName:i.display_name?.trim()||null,groupId:i.group_id||null,groupPhotoGuid:ht(i.properties),participants:s.get(i.rowid)??[],messageCount:i.message_count,recoverableCount:i.recoverable_count,unreadCount:i.unread_count,lastMessageAt:a?k(a.date):null,lastMessageRowId:i.last_rowid??0,lastMessageText:a?Fe(a.text,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
14
|
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=
|
|
15
|
+
`).all(),t=new Map;for(let s of e){let r=t.get(s.chat_id)??[],n=x(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 < ${pt(t.beforeMs)}`:"",i=t.includeDeleted&&this.hasRecoverable?"(SELECT chat_id, message_id FROM chat_message_join UNION SELECT chat_id, message_id FROM chat_recoverable_message_join)":"chat_message_join",a=this.db.query(`
|
|
15
16
|
SELECT
|
|
16
17
|
m.ROWID AS rowid, m.guid, m.text, m.attributedBody, m.is_from_me, m.date, m.service,
|
|
17
18
|
m.associated_message_guid, m.associated_message_type, m.item_type, m.group_title,
|
|
@@ -22,20 +23,20 @@ var Le=Object.defineProperty;var $e=(e)=>e;function Fe(e,t){this[e]=$e.bind(null
|
|
|
22
23
|
h.id AS sender,
|
|
23
24
|
c.guid AS chat_guid
|
|
24
25
|
FROM message m
|
|
25
|
-
JOIN ${
|
|
26
|
+
JOIN ${i} cmj ON cmj.message_id = m.ROWID
|
|
26
27
|
JOIN chat c ON c.ROWID = cmj.chat_id
|
|
27
28
|
LEFT JOIN handle h ON h.ROWID = m.handle_id
|
|
28
29
|
WHERE c.guid IN (${s}) AND m.ROWID > ? ${n}
|
|
29
30
|
ORDER BY ${r}
|
|
30
31
|
LIMIT ?
|
|
31
|
-
`).all(...e,t.afterRowId??0,t.limit??1e6),o=this.attachmentsFor(
|
|
32
|
+
`).all(...e,t.afterRowId??0,t.limit??1e6),o=this.attachmentsFor(a.filter((l)=>l.cache_has_attachments===1).map((l)=>l.rowid));return a.map((l)=>{let g=l.associated_message_type??0,c=l.item_type??0,m=gt(g)?"reaction":c!==0?"event":"message";return{rowid:l.rowid,guid:l.guid,chatGuid:l.chat_guid,kind:m,text:Fe(l.text,l.attributedBody),isFromMe:l.is_from_me===1,sentAt:k(l.date)??0,sender:l.sender?x(l.sender):null,service:l.service??"iMessage",targetGuid:m==="reaction"?ft(l.associated_message_guid):null,reactionType:m==="reaction"?g:0,replyToGuid:l.reply_to_guid||null,editedAt:k(l.date_edited),unsent:!!l.date_retracted,eventType:c,groupTitle:l.group_title||null,attachments:o.get(l.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=he(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
33
|
SELECT maj.message_id, a.guid, a.filename, a.transfer_name, a.mime_type, a.uti, a.total_bytes, a.is_sticker
|
|
33
34
|
FROM message_attachment_join maj JOIN attachment a ON a.ROWID = maj.attachment_id
|
|
34
35
|
WHERE maj.message_id IN (${r.map(()=>"?").join(", ")})
|
|
35
|
-
`).all(...r);for(let
|
|
36
|
+
`).all(...r);for(let i of n){let a=t.get(i.message_id)??[],o=he(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 je}from"bun:sqlite";import{existsSync as We,readdirSync as bt,readFileSync as yt}from"fs";import{homedir as vt}from"os";import{dirname as At,join as U}from"path";var be=U(vt(),"Library","Application Support","AddressBook");function Be(e,t){if(!t||t.length<2)return null;if(t[0]===1)return t.subarray(1);if(t[0]===2){let s=new TextDecoder().decode(t.subarray(1)).replace(/\0+$/,"").trim();if(!/^[\w-]+$/.test(s))return null;let r=U(At(e),".AddressBook-v22_SUPPORT","_EXTERNAL_DATA",s);try{return new Uint8Array(yt(r))}catch{return null}}return null}function xt(e){let t=[],s=U(e,"AddressBook-v22.abcddb");if(We(s))t.push(s);let r=U(e,"Sources");try{for(let n of bt(r)){let i=U(r,n,"AddressBook-v22.abcddb");if(We(i))t.push(i)}}catch{}return t}function Ze(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 j{byHandle=new Map;static load(e=be){let t=new j;for(let s of xt(e))try{t.read(s)}catch{}return t}get size(){return this.byHandle.size}read(e){let t=new je(e,{readonly:!0});try{let s=new Set(t.query("PRAGMA table_info(ZABCDRECORD)").all().map((c)=>c.name)),r=["ZTHUMBNAILIMAGEDATA","ZIMAGEDATA"].filter((c)=>s.has(c)),n=r.length>0?`(${r.map((c)=>`${c} IS NOT NULL`).join(" OR ")})`:"0",i=t.query(`
|
|
36
37
|
SELECT Z_PK AS id, ZFIRSTNAME AS first, ZLASTNAME AS last, ZNICKNAME AS nick, ZORGANIZATION AS org, ${n} AS hasPhoto
|
|
37
38
|
FROM ZABCDRECORD
|
|
38
|
-
`).all(),i=new Map,o=new Set;for(let c of a){let v=[c.first,c.last].filter(Boolean).join(" ").trim()||c.nick?.trim()||c.org?.trim();if(v)i.set(c.id,v);if(c.hasPhoto)o.add(c.id)}let l=t.query("SELECT ZOWNER AS owner, ZFULLNUMBER AS value FROM ZABCDPHONENUMBER").all(),g=t.query("SELECT ZOWNER AS owner, ZADDRESS AS value FROM ZABCDEMAILADDRESS").all();for(let c of[...l,...g]){let m=i.get(c.owner);if(!m||!c.value)continue;let v=p(c.value),X=o.has(c.owner)?{file:e,record:c.owner}:null,Y=this.byHandle.get(v);if(!Y)this.byHandle.set(v,{name:m,initials:Ie(m),photo:X});else if(!Y.photo&&X)Y.photo=X}}finally{t.close()}}set(e,t){this.byHandle.set(p(e),{name:t,initials:Ie(t)})}lookup(e){return this.byHandle.get(p(e))??null}nameFor(e){return this.lookup(e)?.name??null}hasPhoto(e){return!!this.lookup(e)?.photo}photo(e){let t=this.lookup(e)?.photo;if(!t)return null;try{let s=new Ee(t.file,{readonly:!0});try{let r=s.query("SELECT ZTHUMBNAILIMAGEDATA AS thumb, ZIMAGEDATA AS full FROM ZABCDRECORD WHERE Z_PK = ?").get(t.record);return Re(t.file,r?.thumb??null)??Re(t.file,r?.full??null)}finally{s.close()}}catch{return null}}}function lt(e){if(e.style===H)return`group:${e.groupId||e.guid}`;return`direct:${e.identifier}`}function De(e){let t=new Map;for(let s of e){let r=lt(s),n=t.get(r);if(!n){t.set(r,{key:r,kind:s.style===H?"group":"direct",chatGuids:[s.guid],displayName:s.displayName,groupId:s.groupId,participants:s.style===H?[...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 a of s.participants)if(n.kind==="group"&&!n.participants.includes(a))n.participants.push(a);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 le(e){if(e.kind==="group")return e.groupId?`sms://open?groupid=${encodeURIComponent(e.groupId)}`:null;let t=e.participants[0];if(!t||!xe(t))return null;return`sms:${t}`}var mt={2000:"\u2764\uFE0F",2001:"\uD83D\uDC4D",2002:"\uD83D\uDC4E",2003:"\uD83D\uDE02",2004:"\u203C\uFE0F",2005:"\u2753",2006:"\uD83D\uDE00",2007:"\uD83D\uDD16"};function pt(e){Bun.spawn(["/usr/bin/open",e],{stdio:["ignore","ignore","ignore"]})}class R{provider="imessage";label="iMessage";databasePath;addressBookDir;openUrl;controller;confirmDeletes;platform;contacts=null;contactsLoadedAt=0;constructor(e={}){this.databasePath=e.databasePath??ae,this.addressBookDir=e.addressBookDir??oe,this.openUrl=e.openUrl??pt,this.controller=e.controller,this.confirmDeletes=e.confirmDeletes??!1,this.platform=e.platform??dt.platform}rowNames(e){let t=[];if(e.kind==="group")t.push(e.displayName);else if(e.participants[0])t.push(this.names().nameFor(e.participants[0]),B(e.participants[0]),e.participants[0]);return[...new Set(t.filter((s)=>!!s))]}open(e){let t=new ie(this.databasePath);try{return e(t)}finally{t.close()}}names(){if(this.addressBookDir===!1)return this.contacts??=new T;if(!this.contacts||Date.now()-this.contactsLoadedAt>600000)this.contacts=T.load(this.addressBookDir),this.contactsLoadedAt=Date.now();return this.contacts}person(e){if(!e)return null;let t=this.names();return{id:e,name:t.nameFor(e),avatar:t.hasPhoto(e)?e:null}}async avatar(e){return U(this.names().photo(e))}grouped=null;chats(e){let t=[this.databasePath,`${this.databasePath}-wal`].map((s)=>{try{let r=ct(s);return`${r.mtimeMs}:${r.size}`}catch{return"-"}}).join("|");if(this.grouped?.stamp!==t)this.grouped={stamp:t,chats:De(e.chats())};return this.grouped.chats}find(e,t){return this.chats(e).find((s)=>s.key===t)}async status(){if(this.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 G&&e.needsFullDiskAccess&&ce(ut(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)=>this.chats(e).map((t)=>this.toConversation(t)))}toConversation(e){let t=le(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,deleted:e.recoverableCount,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[];let n=t.includeDeleted;return(t.after||!t.limit?s.messages(r.chatGuids,{afterRowId:Number(t.after)||0,byRowId:!0,limit:t.limit,includeDeleted:n}):s.messages(r.chatGuids,{newestFirst:!0,limit:t.limit,includeDeleted:n}).reverse()).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"?mt[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&&ce(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?le(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."};let r=this.rowNames(t);if(this.controller&&r.length>0){let n=await this.controller.remove(r,s,this.confirmDeletes);return{mode:"confirm",removed:!1,detail:n.ok?this.confirmDeletes?"Deleted in Messages.":"Messages is asking you to confirm the delete.":n.detail}}return this.openUrl(s),{mode:"confirm",removed:!1,detail:"Messages is open on the conversation: choose Conversation > Delete Conversation to finish."}}async unarchive(e){if(!this.controller)return;let t=this.open((r)=>this.find(r,e));if(!t||t.messageCount>0)return;let s=this.rowNames(t);if(s.length>0)await this.controller.recover(s)}async attachment(e){if(!e.path||!ce(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 ht="im,mpim,public_channel,private_channel",gt={"+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"},J=(e)=>Math.round(Number(e)*1000);class z extends Error{method;code;constructor(e,t){super(`Slack ${e} failed: ${t}`);this.method=e;this.code=t;this.name="SlackApiError"}}class I{provider="slack";label="Slack";token;fetch;apiBase;concurrency;self=null;users=new Map;avatars=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,l])=>[o,String(l)])),a=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(a.status===429&&r<3){await Bun.sleep(Math.min(30,Number(a.headers.get("retry-after"))||1)*1000);continue}let i=await a.json();if(!i.ok)throw new z(e,i.error??`http_${a.status}`);return i}}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),this.avatars.set(e,t.profile?.image_72??null)}catch{this.users.set(e,null)}return this.users.get(e)??null}async person(e){if(!e)return null;let t=await this.userName(e);return{id:e,name:t,avatar:this.avatars.get(e)??null}}async avatar(e){if(!W(e,["slack-edge.com","slack.com","gravatar.com"]))return new Response("Not found",{status:404});return this.fetch(e)}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:ht,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((a)=>this.describe(a,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 a=r.messages[0],i=[];if(n.is_im)i=[await this.person(n.user)];else if(n.is_mpim){let o=await this.api("conversations.members",{channel:n.id,limit:50}).catch(()=>({members:[]}));i=await Promise.all(o.members.filter((l)=>l!==t.userId).map(async(l)=>await this.person(l)))}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:i,workspace:t.team,lastMessageAt:a?J(a.ts):null,preview:a?.text||(a?.files?.length?"Attachment":null),previewFromMe:a?.user===t.userId,unread:n.unread_count_display??0,cursor:a?.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="",a=t.limit??1000;do{let o=await this.api("conversations.history",{channel:e,limit:Math.min(200,a),...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<a);r.sort((o,l)=>Number(o.ts)-Number(l.ts));let i=[];for(let o of r.slice(-a)){let l=await this.person(o.user),g=!!o.subtype&&!["file_share","thread_broadcast","me_message"].includes(o.subtype);i.push({id:o.ts,conversationId:e,kind:g?"event":"message",text:o.text??null,fromMe:o.user===s.userId,sentAt:J(o.ts),sender:o.user===s.userId?null:l??(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?J(o.edited.ts):null,unsent:!1,attachments:(o.files??[]).map((c)=>({id:c.id,name:c.name??c.title??c.id,mimeType:c.mimetype??null,bytes:c.size??0,path:null,url:c.url_private??null})),cursor:o.ts});for(let c of o.reactions??[])for(let m of c.users??[])i.push({id:`${o.ts}:${c.name}:${m}`,conversationId:e,kind:"reaction",text:null,fromMe:m===s.userId,sentAt:J(o.ts),sender:m===s.userId?null:await this.person(m),targetId:o.ts,reaction:gt[c.name]??`:${c.name}:`,reactionRemoved:!1,replyToId:null,editedAt:null,unsent:!1,attachments:[],cursor:o.ts})}return i}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}`}})}}import{existsSync as q,readFileSync as At}from"fs";import{basename as xt,dirname as pe,extname as Mt,join as he}from"path";import St from"process";import{Database as ft}from"bun:sqlite";import{homedir as bt}from"os";import{join as ue}from"path";var Pe=ue(bt(),"Library","Group Containers","group.net.whatsapp.WhatsApp.shared"),de=ue(Pe,"ChatStorage.sqlite"),Oe=ue(Pe,"Message"),K=0,yt=1,vt=4,u={Text:0,Image:1,Video:2,Audio:3,Contact:4,Location:5,GroupEvent:6,Link:7,Document:8,Call:10,Gif:11,Deleted:14,Sticker:15};class V extends Error{path;needsFullDiskAccess;constructor(e,t){let s=t instanceof Error?t.message:String(t);super(`Cannot read ${e}: ${s}`);this.path=e;this.name="WhatsAppAccessError",this.needsFullDiskAccess=/authorization denied|not permitted|unable to open/i.test(s)}}function d(e){let t=e?.replace(/\uFFFC/g,"").trim();return t?t:null}class me{path;db;constructor(e=de){this.path=e;try{this.db=new ft(e,{readonly:!0}),this.db.query("SELECT 1 FROM ZWACHATSESSION LIMIT 1").all()}catch(t){throw new V(e,t)}}close(){this.db.close()}sessions(e){return this.db.query(`
|
|
39
|
+
`).all(),a=new Map,o=new Set;for(let c of i){let f=[c.first,c.last].filter(Boolean).join(" ").trim()||c.nick?.trim()||c.org?.trim();if(f)a.set(c.id,f);if(c.hasPhoto)o.add(c.id)}let l=t.query("SELECT ZOWNER AS owner, ZFULLNUMBER AS value FROM ZABCDPHONENUMBER").all(),g=t.query("SELECT ZOWNER AS owner, ZADDRESS AS value FROM ZABCDEMAILADDRESS").all();for(let c of[...l,...g]){let m=a.get(c.owner);if(!m||!c.value)continue;let f=x(c.value),v=o.has(c.owner)?{file:e,record:c.owner}:null,u=this.byHandle.get(f);if(!u)this.byHandle.set(f,{name:m,initials:Ze(m),photo:v});else if(!u.photo&&v)u.photo=v}}finally{t.close()}}set(e,t){this.byHandle.set(x(e),{name:t,initials:Ze(t)})}lookup(e){return this.byHandle.get(x(e))??null}nameFor(e){return this.lookup(e)?.name??null}hasPhoto(e){return!!this.lookup(e)?.photo}photo(e){let t=this.lookup(e)?.photo;if(!t)return null;try{let s=new je(t.file,{readonly:!0});try{let r=s.query("SELECT ZTHUMBNAILIMAGEDATA AS thumb, ZIMAGEDATA AS full FROM ZABCDRECORD WHERE Z_PK = ?").get(t.record);return Be(t.file,r?.thumb??null)??Be(t.file,r?.full??null)}finally{s.close()}}catch{return null}}}function Mt(e){if(e.style===L)return`group:${e.groupId||e.guid}`;return`direct:${e.identifier}`}function He(e){let t=new Map;for(let s of e){let r=Mt(s),n=t.get(r);if(!n){t.set(r,{key:r,kind:s.style===L?"group":"direct",chatGuids:[s.guid],displayName:s.displayName,groupId:s.groupId,groupPhotoGuid:s.style===L?s.groupPhotoGuid:null,participants:s.style===L?[...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.kind==="group"&&!n.groupPhotoGuid)n.groupPhotoGuid=s.groupPhotoGuid;if(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 ye(e){if(e.kind==="group")return e.groupId?`sms://open?groupid=${encodeURIComponent(e.groupId)}`:null;let t=e.participants[0];if(!t||!Oe(t))return null;return`sms:${t}`}var kt={2000:"\u2764\uFE0F",2001:"\uD83D\uDC4D",2002:"\uD83D\uDC4E",2003:"\uD83D\uDE02",2004:"\u203C\uFE0F",2005:"\u2753",2006:"\uD83D\uDE00",2007:"\uD83D\uDD16"};function _t(e){Bun.spawn(["/usr/bin/open",e],{stdio:["ignore","ignore","ignore"]})}class W{provider="imessage";label="iMessage";databasePath;addressBookDir;openUrl;controller;confirmDeletes;platform;contacts=null;contactsLoadedAt=0;constructor(e={}){this.databasePath=e.databasePath??ge,this.addressBookDir=e.addressBookDir??be,this.openUrl=e.openUrl??_t,this.controller=e.controller,this.confirmDeletes=e.confirmDeletes??!1,this.platform=e.platform??Et.platform}rowNames(e){let t=[];if(e.kind==="group")t.push(e.displayName);else if(e.participants[0])t.push(this.names().nameFor(e.participants[0]),ee(e.participants[0]),e.participants[0]);return[...new Set(t.filter((s)=>!!s))]}open(e){let t=new fe(this.databasePath);try{return e(t)}finally{t.close()}}names(){if(this.addressBookDir===!1)return this.contacts??=new j;if(!this.contacts||Date.now()-this.contactsLoadedAt>600000)this.contacts=j.load(this.addressBookDir),this.contactsLoadedAt=Date.now();return this.contacts}person(e){if(!e)return null;let t=this.names();return{id:e,name:t.nameFor(e),avatar:t.hasPhoto(e)?e:null}}async avatar(e){if(e.startsWith("group:")){let t=this.open((s)=>s.attachmentFile(e.slice(6)));return N(t&&re(t)?new Uint8Array(St(t)):null)}return N(this.names().photo(e))}grouped=null;chats(e){let t=[this.databasePath,`${this.databasePath}-wal`].map((s)=>{try{let r=Ct(s);return`${r.mtimeMs}:${r.size}`}catch{return"-"}}).join("|");if(this.grouped?.stamp!==t)this.grouped={stamp:t,chats:He(e.chats())};return this.grouped.chats}find(e,t){return this.chats(e).find((s)=>s.key===t)}async status(){if(this.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 se&&e.needsFullDiskAccess&&re(wt(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)=>this.chats(e).map((t)=>this.toConversation(t)))}toConversation(e){let t=ye(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,deleted:e.recoverableCount,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,avatar:e.kind==="group"&&e.groupPhotoGuid?`group:${e.groupPhotoGuid}`:null}}async messages(e,t={}){return this.open((s)=>{let r=this.find(s,e);if(!r)return[];let n=t.includeDeleted;return(t.after||!t.limit?s.messages(r.chatGuids,{afterRowId:Number(t.after)||0,byRowId:!0,limit:t.limit,includeDeleted:n}):s.messages(r.chatGuids,{newestFirst:!0,limit:t.limit,includeDeleted:n}).reverse()).map((a)=>this.toMessage(e,a))})}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"?kt[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&&re(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?ye(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."};let r=this.rowNames(t);if(this.controller&&r.length>0){let n=await this.controller.remove(r,s,this.confirmDeletes);return{mode:"confirm",removed:!1,detail:n.ok?this.confirmDeletes?"Deleted in Messages.":"Messages is asking you to confirm the delete.":n.detail}}return this.openUrl(s),{mode:"confirm",removed:!1,detail:"Messages is open on the conversation: choose Conversation > Delete Conversation to finish."}}async unarchive(e){if(!this.controller)return;let t=this.open((r)=>this.find(r,e));if(!t||t.messageCount>0)return;let s=this.rowNames(t);if(s.length>0)await this.controller.recover(s)}async attachment(e){if(!e.path||!re(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 Tt="im,mpim,public_channel,private_channel",Rt={"+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"},ne=(e)=>Math.round(Number(e)*1000);class ie extends Error{method;code;constructor(e,t){super(`Slack ${e} failed: ${t}`);this.method=e;this.code=t;this.name="SlackApiError"}}class B{provider="slack";label="Slack";token;fetch;apiBase;concurrency;self=null;users=new Map;avatars=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,l])=>[o,String(l)])),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 ie(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),this.avatars.set(e,t.profile?.image_72??null)}catch{this.users.set(e,null)}return this.users.get(e)??null}async person(e){if(!e)return null;let t=await this.userName(e);return{id:e,name:t,avatar:this.avatars.get(e)??null}}async avatar(e){if(!Q(e,["slack-edge.com","slack.com","gravatar.com"]))return new Response("Not found",{status:404});return this.fetch(e)}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:Tt,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((l)=>l!==t.userId).map(async(l)=>await this.person(l)))}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?ne(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,l)=>Number(o.ts)-Number(l.ts));let a=[];for(let o of r.slice(-i)){let l=await this.person(o.user),g=!!o.subtype&&!["file_share","thread_broadcast","me_message"].includes(o.subtype);a.push({id:o.ts,conversationId:e,kind:g?"event":"message",text:o.text??null,fromMe:o.user===s.userId,sentAt:ne(o.ts),sender:o.user===s.userId?null:l??(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?ne(o.edited.ts):null,unsent:!1,attachments:(o.files??[]).map((c)=>({id:c.id,name:c.name??c.title??c.id,mimeType:c.mimetype??null,bytes:c.size??0,path:null,url:c.url_private??null})),cursor:o.ts});for(let c of o.reactions??[])for(let m of c.users??[])a.push({id:`${o.ts}:${c.name}:${m}`,conversationId:e,kind:"reaction",text:null,fromMe:m===s.userId,sentAt:ne(o.ts),sender:m===s.userId?null:await this.person(m),targetId:o.ts,reaction:Rt[c.name]??`:${c.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}`}})}}import{existsSync as oe,readFileSync as Lt}from"fs";import{basename as $t,dirname as we,extname as Ut,join as Ee}from"path";import jt from"process";function ve(e,t){let s=0n,r=0n;for(let n=t;n<e.length&&n<t+10;n++){let i=e[n];if(s|=BigInt(i&127)<<r,(i&128)===0)return{value:s,next:n+1};r+=7n}throw Error("Truncated varint")}function It(e){let t=[],s=0;while(s<e.length){let r=ve(e,s);s=r.next;let n=Number(r.value>>3n),i=Number(r.value&7n);if(n===0)throw Error("Field number 0");if(i===0){let a=ve(e,s);t.push({field:n,wire:i,value:a.value}),s=a.next}else if(i===1||i===5){let a=i===1?8:4;if(s+a>e.length)throw Error("Truncated fixed field");let o=0n;for(let l=a-1;l>=0;l--)o=o<<8n|BigInt(e[s+l]);t.push({field:n,wire:i,value:o}),s+=a}else if(i===2){let a=ve(e,s),o=a.next,l=o+Number(a.value);if(l>e.length)throw Error("Truncated length-delimited field");t.push({field:n,wire:i,value:e.subarray(o,l)}),s=l}else throw Error(`Unsupported wire type ${i}`)}return t}function Z(e){if(!e||e.length===0)return null;try{return It(e)}catch{return null}}function Dt(e,t){let s=e?.find((r)=>r.field===t&&r.wire===2);return s?s.value:null}function Ae(e,t){return(e??[]).filter((s)=>s.field===t&&s.wire===2).map((s)=>s.value)}function H(e,t){let s=Dt(e,t);if(!s)return null;let r=new TextDecoder("utf-8",{fatal:!0});try{return r.decode(s)}catch{return null}}function xe(e,t){let s=e?.find((r)=>r.field===t&&r.wire!==2);return s?s.value:null}import{Database as Pt}from"bun:sqlite";import{homedir as Ot}from"os";import{join as Me}from"path";var Ge=Me(Ot(),"Library","Group Containers","group.net.whatsapp.WhatsApp.shared"),Se=Me(Ge,"ChatStorage.sqlite"),Je=Me(Ge,"Message"),G=0,Nt=1,Ft=4,d={Text:0,Image:1,Video:2,Audio:3,Contact:4,Location:5,GroupEvent:6,Link:7,Document:8,Call:10,Gif:11,Deleted:14,Sticker:15};class ae extends Error{path;needsFullDiskAccess;constructor(e,t){let s=t instanceof Error?t.message:String(t);super(`Cannot read ${e}: ${s}`);this.path=e;this.name="WhatsAppAccessError",this.needsFullDiskAccess=/authorization denied|not permitted|unable to open/i.test(s)}}function h(e){let t=e?.replace(/\uFFFC/g,"").trim();return t?t:null}class Ce{path;db;constructor(e=Se){this.path=e;try{this.db=new Pt(e,{readonly:!0}),this.db.query("SELECT 1 FROM ZWACHATSESSION LIMIT 1").all()}catch(t){throw new ae(e,t)}}close(){this.db.close()}columns(e){try{return new Set(this.db.query(`PRAGMA table_info(${e})`).all().map((t)=>t.name))}catch{return new Set}}get hasMetadata(){return this.columns("ZWAMEDIAITEM").has("ZMETADATA")}get hasReceipts(){return this.columns("ZWAMESSAGE").has("ZMESSAGEINFO")&&this.columns("ZWAMESSAGEINFO").has("ZRECEIPTINFO")}sessions(e){return this.db.query(`
|
|
39
40
|
SELECT s.Z_PK AS pk, s.ZCONTACTJID AS jid, s.ZSESSIONTYPE AS type, s.ZPARTNERNAME AS name,
|
|
40
41
|
s.ZARCHIVED AS archived, s.ZUNREADCOUNT AS unread, s.ZLASTMESSAGEDATE AS lastDate,
|
|
41
42
|
s.ZLASTMESSAGETEXT AS lastText, COALESCE(last.ZISFROMME, 0) AS lastFromMe,
|
|
@@ -43,27 +44,29 @@ var Le=Object.defineProperty;var $e=(e)=>e;function Fe(e,t){this[e]=$e.bind(null
|
|
|
43
44
|
(SELECT COUNT(*) FROM ZWAMESSAGE m WHERE m.ZCHATSESSION = s.Z_PK) AS messageCount
|
|
44
45
|
FROM ZWACHATSESSION s
|
|
45
46
|
LEFT JOIN ZWAMESSAGE last ON last.Z_PK = s.ZLASTMESSAGE
|
|
46
|
-
WHERE s.ZSESSIONTYPE IN (${
|
|
47
|
+
WHERE s.ZSESSIONTYPE IN (${G}, ${Nt}, ${Ft})
|
|
47
48
|
AND COALESCE(s.ZHIDDEN, 0) = 0 AND COALESCE(s.ZREMOVED, 0) = 0
|
|
48
49
|
AND s.ZCONTACTJID IS NOT NULL ${e?"AND s.ZCONTACTJID = ?":""}
|
|
49
50
|
ORDER BY s.ZLASTMESSAGEDATE DESC
|
|
50
|
-
`).all(...e?[e]:[]).map((s)=>({pk:s.pk,jid:s.jid,type:s.type,name:
|
|
51
|
+
`).all(...e?[e]:[]).map((s)=>({pk:s.pk,jid:s.jid,type:s.type,name:h(s.name),archived:!!s.archived,unread:s.unread==null?0:s.unread<0?1:s.unread,lastMessageAt:k(s.lastDate),lastMessageText:h(s.lastText),lastMessageFromMe:!!s.lastFromMe,lastMessagePk:s.lastPk,messageCount:s.messageCount}))}session(e){return this.sessions(e)[0]}profilePicturePaths(){try{let e=this.db.query("SELECT ZJID AS jid, ZPATH AS path FROM ZWAPROFILEPICTUREITEM WHERE ZJID IS NOT NULL AND ZPATH IS NOT NULL").all();return new Map(e.map((t)=>[t.jid,t.path]))}catch{return new Map}}members(e){return this.db.query(`
|
|
51
52
|
SELECT g.ZMEMBERJID AS jid, COALESCE(g.ZCONTACTNAME, g.ZFIRSTNAME, p.ZPUSHNAME) AS name
|
|
52
53
|
FROM ZWAGROUPMEMBER g
|
|
53
54
|
LEFT JOIN ZWAPROFILEPUSHNAME p ON p.ZJID = g.ZMEMBERJID
|
|
54
55
|
WHERE g.ZCHATSESSION = ? AND COALESCE(g.ZISACTIVE, 1) = 1 AND g.ZMEMBERJID IS NOT NULL
|
|
55
56
|
ORDER BY name
|
|
56
|
-
`).all(e).map((s)=>({jid:s.jid,name:
|
|
57
|
+
`).all(e).map((s)=>({jid:s.jid,name:h(s.name)}))}messages(e,t={}){let s=t.newestFirst?"DESC":"ASC",r=t.limit?`LIMIT ${Math.max(1,Math.floor(t.limit))}`:"";return this.db.query(`
|
|
57
58
|
SELECT m.Z_PK AS pk, m.ZSTANZAID AS stanzaId, m.ZMESSAGETYPE AS type, m.ZTEXT AS text,
|
|
58
59
|
m.ZISFROMME AS isFromMe, m.ZMESSAGEDATE AS date,
|
|
59
60
|
g.ZMEMBERJID AS senderJid, COALESCE(g.ZCONTACTNAME, g.ZFIRSTNAME, p.ZPUSHNAME, m.ZPUSHNAME) AS senderName,
|
|
60
61
|
i.Z_PK AS mediaPk, i.ZMEDIALOCALPATH AS mediaPath, i.ZVCARDSTRING AS vcard, i.ZTITLE AS title,
|
|
61
|
-
i.ZFILESIZE AS bytes, i.ZLATITUDE AS latitude, i.ZLONGITUDE AS longitude
|
|
62
|
+
i.ZFILESIZE AS bytes, i.ZLATITUDE AS latitude, i.ZLONGITUDE AS longitude,
|
|
63
|
+
${this.hasMetadata?"i.ZMETADATA":"NULL"} AS metadata, ${this.hasReceipts?"info.ZRECEIPTINFO":"NULL"} AS receipts
|
|
62
64
|
FROM ZWAMESSAGE m
|
|
63
65
|
LEFT JOIN ZWAGROUPMEMBER g ON g.Z_PK = m.ZGROUPMEMBER
|
|
64
66
|
LEFT JOIN ZWAPROFILEPUSHNAME p ON p.ZJID = g.ZMEMBERJID
|
|
65
67
|
LEFT JOIN ZWAMEDIAITEM i ON i.Z_PK = m.ZMEDIAITEM
|
|
68
|
+
${this.hasReceipts?"LEFT JOIN ZWAMESSAGEINFO info ON info.Z_PK = m.ZMESSAGEINFO":""}
|
|
66
69
|
WHERE m.ZCHATSESSION = ? AND m.Z_PK > ?
|
|
67
70
|
ORDER BY m.Z_PK ${s}
|
|
68
71
|
${r}
|
|
69
|
-
`).all(e,t.afterPk??0).map((
|
|
72
|
+
`).all(e,t.afterPk??0).map((i)=>({pk:i.pk,stanzaId:i.stanzaId,type:i.type,text:i.text,isFromMe:!!i.isFromMe,sentAt:k(i.date)??0,senderJid:i.senderJid,senderName:h(i.senderName),media:i.mediaPk==null?null:{path:i.mediaPath,vcard:i.vcard,title:i.title,bytes:i.bytes??0,latitude:i.latitude,longitude:i.longitude},metadata:i.metadata,receipts:i.receipts}))}}var Wt="Archives the chat in WhatsApp. WhatsApp brings it back when a new message arrives, unless Keep chats archived is on.",Bt={[d.Image]:"image/jpeg",[d.Video]:"video/mp4",[d.Audio]:"audio/ogg",[d.Gif]:"video/mp4",[d.Sticker]:"image/webp"},Zt={[d.Image]:"Photo",[d.Video]:"Video",[d.Audio]:"Voice message",[d.Document]:"Document",[d.Gif]:"GIF",[d.Sticker]:"Sticker"},Ht={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".webp":"image/webp",".heic":"image/heic",".mp4":"video/mp4",".mov":"video/quicktime",".opus":"audio/ogg",".ogg":"audio/ogg",".m4a":"audio/mp4",".mp3":"audio/mpeg",".pdf":"application/pdf"};function Gt(e){Bun.spawn(["/usr/bin/open",e],{stdio:["ignore","ignore","ignore"]})}function ke(e){let[t=e,s]=e.split("@");return s==="s.whatsapp.net"&&/^\d{7,15}$/.test(t)?`+${t}`:t}function Jt(e){let t=e?.match(/^FN:(.*)$/m);return h(t?.[1])}class J{provider="whatsapp";label="WhatsApp";databasePath;mediaRoot;controller;openUrl;platform;settleMs;constructor(e={}){this.databasePath=e.databasePath??Se,this.mediaRoot=e.mediaRoot??(e.databasePath?Ee(we(e.databasePath),"Message"):Je),this.controller=e.controller,this.openUrl=e.openUrl??Gt,this.platform=e.platform??jt.platform,this.settleMs=e.settleMs??6000}pictureFile(e){if(!e||e.includes(".."))return null;let t=Ee(we(this.databasePath),e);return[`${t}.jpg`,`${t}.thumb`,t].find((s)=>oe(s))??null}pictures(e){let t=e.profilePicturePaths(),s=new Map;return(r)=>{if(!s.has(r))s.set(r,this.pictureFile(t.get(r))?r:null);return s.get(r)}}async avatar(e){let t=this.open((s)=>this.pictureFile(s.profilePicturePaths().get(e)));return N(t?new Uint8Array(Lt(t)):null)}open(e){let t=new Ce(this.databasePath);try{return e(t)}finally{t.close()}}async status(){if(this.platform!=="darwin")return{connected:!1,detail:"WhatsApp is read from WhatsApp for Mac, so it needs a Mac.",needs:"unsupported-platform"};try{return this.open(()=>{return}),{connected:!0,detail:"Reading WhatsApp on this Mac."}}catch(e){if(e instanceof ae&&e.needsFullDiskAccess&&oe(we(this.databasePath)))return{connected:!1,detail:"Grant Full Disk Access in System Settings > Privacy & Security.",needs:"full-disk-access"};return{connected:!1,detail:"Install WhatsApp for Mac and link it to your phone.",needs:"signed-out"}}}archiveSupport(e){if(this.controller)return{mode:"native",detail:Wt};return this.url(e)?{mode:"confirm",detail:"Opens the chat in WhatsApp for you to archive it there."}:{mode:"unsupported",detail:"WhatsApp cannot open a group by link, so archive it there yourself."}}url(e){let[t,s]=e.jid.split("@");return s==="s.whatsapp.net"&&t&&/^\d{7,15}$/.test(t)?`whatsapp://send?phone=${t}`:null}async conversations(){return this.open((e)=>{let t=this.pictures(e);return e.sessions().map((s)=>{let r=s.type===G,n=r?[{id:s.jid,name:s.name,avatar:t(s.jid)}]:e.members(s.pk).map((i)=>({id:i.jid,name:i.name,avatar:t(i.jid)}));return{provider:"whatsapp",id:s.jid,kind:r?"direct":"group",title:r?null:s.name,participants:n,workspace:null,lastMessageAt:s.lastMessageAt,preview:s.lastMessageText,previewFromMe:s.lastMessageFromMe,unread:s.unread,cursor:String(s.lastMessagePk),visible:!s.archived,archive:this.archiveSupport(s),url:this.url(s),avatar:r?null:t(s.jid)}})})}async messages(e,t={}){return this.open((s)=>{let r=s.session(e);if(!r)return[];let n=t.after||!t.limit?s.messages(r.pk,{afterPk:Number(t.after)||0,limit:t.limit}):s.messages(r.pk,{newestFirst:!0,limit:t.limit}).reverse(),i=this.pictures(s),a=new Map(r.type===G?[]:s.members(r.pk).map((o)=>[o.jid,o.name]));return n.flatMap((o)=>[this.toMessage(r,o,i),...this.reactions(r,o,a,i)])})}toMessage(e,t,s){let r=t.isFromMe?null:e.type===G?{id:e.jid,name:e.name,avatar:s(e.jid)}:t.senderJid?{id:t.senderJid,name:t.senderName,avatar:s(t.senderJid)}:null,{kind:n,text:i,unsent:a}=this.describe(t);return{id:t.stanzaId??`wa-${t.pk}`,conversationId:e.jid,kind:n,text:i,fromMe:t.isFromMe,sentAt:t.sentAt,sender:r,targetId:null,reaction:null,reactionRemoved:!1,replyToId:H(Z(t.metadata),5),editedAt:null,unsent:a,attachments:this.attachments(t),cursor:String(t.pk)}}reactions(e,t,s,r){let n=t.stanzaId??`wa-${t.pk}`,i=[];for(let a of Ae(Z(t.receipts),7))Ae(Z(a),1).forEach((o,l)=>{let g=Z(o),c=H(g,3);if(!c)return;let m=H(g,2),f=!m&&xe(g,6)===1n,v=m??(f?null:e.jid),u=Number(xe(g,4)??0n);i.push({id:H(g,1)??`${n}:reaction:${l}`,conversationId:e.jid,kind:"reaction",text:null,fromMe:f,sentAt:u>0?u:t.sentAt,sender:v?{id:v,name:s.get(v)??(v===e.jid?e.name:null),avatar:r(v)}:null,targetId:n,reaction:c,reactionRemoved:!1,replyToId:null,editedAt:null,unsent:!1,attachments:[],cursor:String(t.pk)})});return i}describe(e){let t=e.media;switch(e.type){case d.Text:case d.Link:return{kind:"message",text:h(e.text),unsent:!1};case d.Image:case d.Video:case d.Gif:return{kind:"message",text:h(e.text)??h(t?.title),unsent:!1};case d.Contact:{let s=Jt(t?.vcard??null)??h(t?.title);return{kind:"message",text:s?`Contact card: ${s}`:"Contact card",unsent:!1}}case d.Location:{let s=h(t?.title),r=t?.latitude!=null&&t.longitude!=null&&(t.latitude!==0||t.longitude!==0)?`https://maps.apple.com/?ll=${t.latitude},${t.longitude}`:null;return{kind:"message",text:[s??"Shared a location",r].filter(Boolean).join(" "),unsent:!1}}case d.GroupEvent:case d.Call:return{kind:"event",text:h(e.text),unsent:!1};case d.Deleted:return{kind:"message",text:null,unsent:!0};case d.Audio:case d.Document:case d.Sticker:return{kind:"message",text:null,unsent:!1};default:return h(e.text)||t?.path?{kind:"message",text:h(e.text),unsent:!1}:{kind:"event",text:null,unsent:!1}}}attachments(e){let t=e.media,s=Zt[e.type];if(!t||!t.path&&!s||e.type===d.Text||e.type===d.Link)return[];let r=t.path?Ee(this.mediaRoot,t.path):null,n=t.vcard&&/^[\w.+-]+\/[\w.+-]+/.test(t.vcard)?t.vcard.split(";")[0].trim():null,i=t.path?$t(t.path):null,a=e.type===d.Document?h(t.title)??h(e.text)??i??s:i??s;return[{id:`${e.stanzaId??`wa-${e.pk}`}:media`,name:a,mimeType:n??(t.path?Ht[Ut(t.path).toLowerCase()]:void 0)??Bt[e.type]??null,bytes:t.bytes,path:r&&oe(r)?r:null,url:null}]}rowNames(e){return[...new Set([e.name,ke(e.jid)].filter((t)=>!!t))]}async settled(e,t){let s=Date.now()+this.settleMs;while(!0){if(this.open((r)=>r.session(e))?.archived===t)return!0;if(Date.now()>=s)return!1;await Bun.sleep(250)}}async archive(e){let t=this.open((r)=>r.session(e));if(!t)return{mode:"unsupported",removed:!1,detail:"WhatsApp no longer has this chat."};if(t.archived)return{mode:"native",removed:!0,detail:"Already archived in WhatsApp."};if(this.controller){let r=await this.controller.archive(this.rowNames(t));if(!r.ok)return{mode:"native",removed:!1,detail:r.detail};let n=await this.settled(e,!0);return{mode:"native",removed:n,detail:n?"Archived in WhatsApp.":"WhatsApp did not record the archive."}}let s=this.url(t);if(s)this.openUrl(s);return{mode:"confirm",removed:!1,detail:s?"WhatsApp is open on the chat: choose Archive chat from its menu to finish.":"Archive it in WhatsApp: WhatsApp cannot open a group by link."}}async unarchive(e){if(!this.controller)return;let t=this.open((r)=>r.session(e));if(!t?.archived)return;if((await this.controller.unarchive(this.rowNames(t))).ok)await this.settled(e,!1)}async attachment(e){if(!e.path||!oe(e.path))return new Response("Not found",{status:404});return new Response(Bun.file(e.path),{headers:{"content-type":e.mimeType??"application/octet-stream"}})}}function zt(e,t){switch(e){case"imessage":return new W(t);case"slack":return new B(t);case"discord":return new F(t);case"whatsapp":return new J(t);default:throw Error(`Unknown inbox driver: ${String(e)}`)}}async function ar(e,t={}){let s=t.driver??"slack";switch(s){case"slack":return K(e);case"discord":return V(e);case"teams":return X(e);default:return{success:!1,provider:s,message:`Unknown chat driver: ${s}`}}}async function or(e,t,s){return q(e,t,s)}async function lr(e,t,s){return R(e,t,s)}async function cr(e,t){return Y(e,t)}function ur(e){I(e)}function dr(e){_(e)}function mr(e){P(e)}export{dr as configureDiscord,ur as configureSlack,mr as configureTeams,le as discord,ze as inbox,ar as send,lr as sendToDiscord,or as sendToSlack,cr as sendToTeams,ce as slack,de 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.55",
|
|
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.55"
|
|
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.55",
|
|
65
|
+
"@stacksjs/config": "0.75.55",
|
|
66
|
+
"@stacksjs/error-handling": "0.75.55",
|
|
67
|
+
"@stacksjs/types": "0.75.55",
|
|
68
68
|
"better-dx": "^0.2.24"
|
|
69
69
|
}
|
|
70
70
|
}
|