@stacksjs/chat 0.75.48 → 0.75.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,14 @@
1
1
  export declare function appleDateToUnixMs(date: number | null | undefined): number | null;
2
2
  /** Apple-epoch nanoseconds, the unit chat.db has used since High Sierra. */
3
3
  export declare function unixMsToAppleDate(ms: number): number;
4
+ /**
5
+ * A message's readable text, or null when it has none.
6
+ *
7
+ * Messages writes U+FFFC (the object replacement character) into the text
8
+ * wherever an attachment sits, so a photo sent on its own has the text
9
+ * "\uFFFC": not empty, and invisible. Shown as text it is an empty bubble.
10
+ */
11
+ export declare function messageText(plain: string | null, attributedBody: Uint8Array | null): string | null;
4
12
  /**
5
13
  * `associated_message_guid` points at a part of a message, not the message:
6
14
  * `p:0/<guid>` for the first part, `bp:<guid>` for a balloon. Reactions are
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var ge=Object.defineProperty;var pe=(e)=>e;function fe(e,t){this[e]=pe.bind(null,t)}var R=(e,t)=>{for(var s in t)ge(e,s,{get:t[s],enumerable:!0,configurable:!0,set:fe.bind(t,s)})};var U={};R(U,{DiscordDriver:()=>f,Driver:()=>f,configure:()=>p,default:()=>ve,driver:()=>ye,send:()=>T,sendEmbed:()=>te,sendWebhook:()=>b});import{log as ee}from"@stacksjs/logging";import{log as be}from"@stacksjs/logging";class m{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));be.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 h={};function p(e){if(e.webhookUrl){let t;try{t=new URL(e.webhookUrl)}catch{throw Error(`[chat/discord] webhookUrl is not a valid URL: ${e.webhookUrl}`)}if(t.protocol!=="https:")throw Error("[chat/discord] webhookUrl must use https://");if(!/(?:^|\.)discord(?:app)?\.com$/i.test(t.hostname))throw Error(`[chat/discord] webhookUrl host "${t.hostname}" is not a Discord-owned domain.`)}h={...h,...e}}class f extends m{name="discord";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)p(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};ee.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 ee.warn(`[${this.name}] Message send failed, retrying (${s}/${this.config.maxRetries})`),await new Promise((n)=>setTimeout(n,this.config.retryTimeout)),this.sendWithRetry(e,t,s+1);throw r}}async sendMessage(e,t){if(h.webhookUrl)return this.sendViaWebhook(e);if(h.botToken)return this.sendViaBotToken(e);throw Error("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(h.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 ${h.botToken}`,"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){let i=await r.json();throw Error(`Discord API failed: ${r.status} - ${i.message}`)}return{id:(await r.json()).id}}buildEmbed(e){let t={};if(e.subject)t.title=e.subject;if(e.content)t.description=e.content;return t.color=5793266,t.timestamp=new Date().toISOString(),t}}async function T(e,t){return new f().send(e,t)}async function b(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 b(e,"",{...s,embeds:[t]})}var ye=new f,ve={send:T,sendWebhook:b,sendEmbed:te,configure:p,DiscordDriver:f};var W={};R(W,{Driver:()=>v,SlackDriver:()=>v,configure:()=>y,default:()=>_e,driver:()=>xe,send:()=>D,sendWebhook:()=>I});import{log as se}from"@stacksjs/logging";var g={};function y(e){if(e.webhookUrl){let t;try{t=new URL(e.webhookUrl)}catch{throw Error(`[chat/slack] webhookUrl is not a valid URL: ${e.webhookUrl}`)}if(t.protocol!=="https:")throw Error("[chat/slack] webhookUrl must use https:// - Slack does not accept plain HTTP webhooks.");if(!/(?:^|\.)slack\.com$/i.test(t.hostname))throw Error(`[chat/slack] webhookUrl host "${t.hostname}" is not a Slack-owned domain.`)}g={...g,...e}}class v extends m{name="slack";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)y(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};se.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 se.warn(`[${this.name}] Message send failed, retrying (${s}/${this.config.maxRetries})`),await new Promise((n)=>setTimeout(n,this.config.retryTimeout)),this.sendWithRetry(e,t,s+1);throw r}}async sendMessage(e,t){if(g.webhookUrl)return this.sendViaWebhook(e);if(g.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(g.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 ${g.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 D(e,t){return new v().send(e,t)}async function I(e,t,s){try{let r={text:t,...s},n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});if(!n.ok){let i=await n.text();return{success:!1,provider:"slack",message:`Webhook failed: ${n.status} - ${i}`}}return{success:!0,provider:"slack",message:"Message sent successfully"}}catch(r){return{success:!1,provider:"slack",message:(r instanceof Error?r:Error(String(r))).message}}}var xe=new v,_e={send:D,sendWebhook:I,configure:y,SlackDriver:v};var z={};R(z,{Driver:()=>_,TeamsDriver:()=>_,configure:()=>x,default:()=>ke,driver:()=>Ce,send:()=>E,sendCard:()=>ne,sendWebhook:()=>O});import{log as re}from"@stacksjs/logging";var H={};function x(e){H={...H,...e}}class _ extends m{name="teams";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)x(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};re.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 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){let r=H.webhookUrl||(Array.isArray(e.to)?e.to[0]:e.to);if(!r||!r.includes("webhook.office.com"))throw Error("Teams webhook URL not configured or invalid");let n=this.buildPayload(e),i=await fetch(r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!i.ok){let a=await i.text();throw Error(`Teams webhook failed: ${i.status} - ${a}`)}}buildPayload(e){if(!e.subject&&!e.template)return{type:"message",text:e.content||""};let t=this.buildAdaptiveCard(e);return{type:"message",summary:e.subject||e.content?.substring(0,50)||"Notification",attachments:[{contentType:"application/vnd.microsoft.card.adaptive",content:t}]}}buildAdaptiveCard(e){let t=[];if(e.subject)t.push({type:"TextBlock",text:e.subject,size:"Large",weight:"Bolder",wrap:!0});if(e.content)t.push({type:"TextBlock",text:e.content,wrap:!0});return t.push({type:"TextBlock",text:new Date().toLocaleString(),size:"Small",color:"Dark",wrap:!0}),{type:"AdaptiveCard",version:"1.4",$schema:"http://adaptivecards.io/schemas/adaptive-card.json",body:t}}}async function E(e,t){return new _().send(e,t)}async function O(e,t){try{let r=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"message",text:t})});if(!r.ok){let n=await r.text();return{success:!1,provider:"teams",message:`Webhook failed: ${r.status} - ${n}`}}return{success:!0,provider:"teams",message:"Message sent successfully"}}catch(s){return{success:!1,provider:"teams",message:(s instanceof Error?s:Error(String(s))).message}}}async function ne(e,t,s){try{let n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"message",summary:s||"Notification",attachments:[{contentType:"application/vnd.microsoft.card.adaptive",content:t}]})});if(!n.ok){let i=await n.text();return{success:!1,provider:"teams",message:`Webhook failed: ${n.status} - ${i}`}}return{success:!0,provider:"teams",message:"Card sent successfully"}}catch(r){return{success:!1,provider:"teams",message:(r instanceof Error?r:Error(String(r))).message}}}var Ce=new _,ke={send:E,sendWebhook:O,sendCard:ne,configure:x,TeamsDriver:_};var he={};R(he,{DiscordInboxDriver:()=>C,IMessageDriver:()=>M,SlackApiError:()=>B,SlackInboxDriver:()=>S,createInboxDriver:()=>ze,decodeAttributedBody:()=>k,formatHandle:()=>P,normalizeHandle:()=>d,snowflakeTime:()=>J});var we=new Set([0,5]),G=new Set([10,11,12]);function J(e){if(!e)return null;return Number((BigInt(e)>>22n)+1420070400000n)}class C{provider="discord";label="Discord";token;userId;fetch;apiBase;concurrency;guilds=new Map;channels=new Map;constructor(e){this.token=e.token,this.userId=e.userId,this.fetch=e.fetch??((t,s)=>fetch(t,s)),this.apiBase=(e.apiBase??"https://discord.com/api/v10").replace(/\/$/,""),this.concurrency=e.concurrency??3}async api(e,t={}){for(let s=0;;s++){let r=await this.fetch(`${this.apiBase}${e}`,{...t,headers:{authorization:`Bot ${this.token}`,"content-type":"application/json",...t.headers??{}}});if(r.status===429&&s<3){let n=await r.json().catch(()=>({}));await Bun.sleep(Math.min(30,n.retry_after??1)*1000);continue}if(!r.ok){let n=await r.json().catch(()=>({}));throw Error(`Discord ${t.method??"GET"} ${e} failed: ${n.message??r.status}`)}return r.status===204?void 0:await r.json()}}async status(){if(!this.token)return{connected:!1,detail:"Add a Discord bot token.",needs:"token"};try{return{connected:!0,detail:`Reading the servers ${(await this.api("/users/@me")).username} is in.`}}catch(e){return{connected:!1,detail:e instanceof Error?e.message:String(e),needs:"token"}}}archiveSupport(e){if(G.has(e.type))return{mode:"native",detail:"Archives the thread in Discord. It reopens when someone posts."};return{mode:"unsupported",detail:"Discord cannot hide a channel for one person, so it stays in Discord."}}async conversations(){let e=await this.api("/users/@me/guilds"),t=[];for(let r of e){this.guilds.set(r.id,r.name);let[n,i]=await Promise.all([this.api(`/guilds/${r.id}/channels`).catch(()=>[]),this.api(`/guilds/${r.id}/threads/active`).catch(()=>({threads:[]}))]);t.push(...n.filter((a)=>we.has(a.type)).map((a)=>({...a,guild_id:r.id}))),t.push(...i.threads.map((a)=>({...a,guild_id:a.guild_id??r.id})))}let s=[];for(let r=0;r<t.length;r+=this.concurrency){let n=t.slice(r,r+this.concurrency);s.push(...await Promise.all(n.map((i)=>this.describe(i))))}return s.sort((r,n)=>(n.lastMessageAt??0)-(r.lastMessageAt??0))}async describe(e){this.channels.set(e.id,e);let t=e.last_message_id?(await this.api(`/channels/${e.id}/messages?limit=1`).catch(()=>[]))[0]:void 0,s=G.has(e.type);return{provider:"discord",id:e.id,kind:s?"thread":"channel",title:s?e.name??"Thread":`#${e.name}`,participants:[],workspace:e.guild_id?this.guilds.get(e.guild_id)??null:null,lastMessageAt:t?Date.parse(t.timestamp):J(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},targetId:null,reaction:null,reactionRemoved:!1,replyToId:a.message_reference?.message_id??null,editedAt:a.edited_timestamp?Date.parse(a.edited_timestamp):null,unsent:!1,attachments:(a.attachments??[]).map((c)=>({id:c.id,name:c.filename,mimeType:c.content_type??null,bytes:c.size,path:null,url:c.url})),cursor:a.id});for(let c of a.reactions??[])i.push({id:`${a.id}:${c.emoji.id??c.emoji.name}`,conversationId:e,kind:"reaction",text:c.count>1?String(c.count):null,fromMe:!!c.me,sentAt:Date.parse(a.timestamp),sender:null,targetId:a.id,reaction:c.emoji.name??"\u2022",reactionRemoved:!1,replyToId:null,editedAt:null,unsent:!1,attachments:[],cursor:a.id})}return i}async channel(e){let t=this.channels.get(e);if(t)return t;let s=await this.api(`/channels/${e}`);return this.channels.set(e,s),s}async archive(e){let t=await this.channel(e),s=this.archiveSupport(t);if(s.mode!=="native")return{mode:s.mode,removed:!1,detail:s.detail};return await this.api(`/channels/${e}`,{method:"PATCH",body:JSON.stringify({archived:!0})}),this.channels.set(e,{...t,thread_metadata:{archived:!0}}),{mode:"native",removed:!0,detail:s.detail}}async unarchive(e){let t=await this.channel(e);if(!G.has(t.type))return;await this.api(`/channels/${e}`,{method:"PATCH",body:JSON.stringify({archived:!1})}),this.channels.set(e,{...t,thread_metadata:{archived:!1}})}async attachment(e){return e.url?this.fetch(e.url):new Response("Not found",{status:404})}}import{existsSync as Y}from"fs";import{dirname as Le}from"path";import Be from"process";import{Database as Te}from"bun:sqlite";import{homedir as ce}from"os";import{join as le}from"path";function d(e){let t=e.trim();if(t.includes("@"))return t.toLowerCase();let s=t.replace(/[^\d+]/g,"");if(!/^\+?\d{7,15}$/.test(s))return t;if(s.startsWith("+"))return s;if(s.length===10)return`+1${s}`;return`+${s}`}function ie(e){return/^\+\d{7,15}$/.test(e)||/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)}function P(e){let t=e.match(/^\+1(\d{3})(\d{3})(\d{4})$/);if(t)return`+1 (${t[1]}) ${t[2]}-${t[3]}`;return e}var ae=new TextEncoder,Me=new TextDecoder("utf-8",{fatal:!1}),Se=[ae.encode("NSString"),ae.encode("NSMutableString")];function Ae(e,t,s=0){e:for(let r=s;r<=e.length-t.length;r++){for(let n=0;n<t.length;n++)if(e[r+n]!==t[n])continue e;return r}return-1}function Re(e,t){let s=e[t];if(s===void 0)return null;let r=new DataView(e.buffer,e.byteOffset,e.byteLength);if(s===129){if(t+3>e.length)return null;return{length:r.getUint16(t+1,!0),start:t+3}}if(s===130){if(t+5>e.length)return null;return{length:r.getUint32(t+1,!0),start:t+5}}return{length:s,start:t+1}}function k(e){if(!e||e.length===0)return null;for(let t of Se){let s=Ae(e,t);if(s===-1)continue;let r=s+t.length,n=Math.min(e.length,r+16);for(let i=r;i<n;i++){if(e[i]!==43)continue;let a=Re(e,i+1);if(!a||a.start+a.length>e.length)continue;return Me.decode(e.subarray(a.start,a.start+a.length))}}return null}var Z=le(ce(),"Library","Messages","chat.db"),de=978307200,De=45,N=43;function K(e){if(!e)return null;let t=e>1000000000000?e/1e9:e;return Math.round((t+de)*1000)}function Ie(e){return Math.round((e/1000-de)*1e9)}class j extends Error{path;needsFullDiskAccess;constructor(e,t){let s=t instanceof Error?t.message:String(t),r=/authorization denied|not permitted|unable to open/i.test(s);super(`Cannot read ${e}: ${s}`);this.path=e;this.name="MessagesAccessError",this.needsFullDiskAccess=r}}var Ee=(e)=>e>=2000&&e<4000;function oe(e){if(!e)return null;return e.startsWith("~/")?le(ce(),e.slice(2)):e}function Oe(e){if(!e)return null;let t=e.indexOf("/");if(t!==-1)return e.slice(t+1);return e.replace(/^bp:/,"")}class q{path;db;messageColumns;hasRecoverable;constructor(e=Z){this.path=e;try{this.db=new Te(e,{readonly:!0}),this.db.query("SELECT 1 FROM message LIMIT 1").all()}catch(t){throw new j(e,t)}this.messageColumns=new Set(this.db.query("PRAGMA table_info(message)").all().map((t)=>t.name)),this.hasRecoverable=!!this.db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'chat_recoverable_message_join'").get()}close(){this.db.close()}latestRowId(){return this.db.query("SELECT MAX(ROWID) AS id FROM message").get().id??0}column(e,t="NULL"){return this.messageColumns.has(e)?`m.${e}`:t}chats(){let e=this.hasRecoverable?"(SELECT COUNT(*) FROM chat_recoverable_message_join r WHERE r.chat_id = c.ROWID)":"0",t=this.db.query(`
2
+ var pe=Object.defineProperty;var fe=(e)=>e;function be(e,t){this[e]=fe.bind(null,t)}var A=(e,t)=>{for(var s in t)pe(e,s,{get:t[s],enumerable:!0,configurable:!0,set:be.bind(t,s)})};var U={};A(U,{DiscordDriver:()=>f,Driver:()=>f,configure:()=>p,default:()=>xe,driver:()=>ve,send:()=>R,sendEmbed:()=>te,sendWebhook:()=>b});import{log as ee}from"@stacksjs/logging";import{log as ye}from"@stacksjs/logging";class m{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 h={};function p(e){if(e.webhookUrl){let t;try{t=new URL(e.webhookUrl)}catch{throw Error(`[chat/discord] webhookUrl is not a valid URL: ${e.webhookUrl}`)}if(t.protocol!=="https:")throw Error("[chat/discord] webhookUrl must use https://");if(!/(?:^|\.)discord(?:app)?\.com$/i.test(t.hostname))throw Error(`[chat/discord] webhookUrl host "${t.hostname}" is not a Discord-owned domain.`)}h={...h,...e}}class f extends m{name="discord";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)p(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};ee.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 ee.warn(`[${this.name}] Message send failed, retrying (${s}/${this.config.maxRetries})`),await new Promise((n)=>setTimeout(n,this.config.retryTimeout)),this.sendWithRetry(e,t,s+1);throw r}}async sendMessage(e,t){if(h.webhookUrl)return this.sendViaWebhook(e);if(h.botToken)return this.sendViaBotToken(e);throw Error("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(h.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 ${h.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 R(e,t){return new f().send(e,t)}async function b(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 b(e,"",{...s,embeds:[t]})}var ve=new f,xe={send:R,sendWebhook:b,sendEmbed:te,configure:p,DiscordDriver:f};var W={};A(W,{Driver:()=>v,SlackDriver:()=>v,configure:()=>y,default:()=>Ce,driver:()=>_e,send:()=>T,sendWebhook:()=>D});import{log as se}from"@stacksjs/logging";var g={};function y(e){if(e.webhookUrl){let t;try{t=new URL(e.webhookUrl)}catch{throw Error(`[chat/slack] webhookUrl is not a valid URL: ${e.webhookUrl}`)}if(t.protocol!=="https:")throw Error("[chat/slack] webhookUrl must use https:// - Slack does not accept plain HTTP webhooks.");if(!/(?:^|\.)slack\.com$/i.test(t.hostname))throw Error(`[chat/slack] webhookUrl host "${t.hostname}" is not a Slack-owned domain.`)}g={...g,...e}}class v extends m{name="slack";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)y(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};se.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 se.warn(`[${this.name}] Message send failed, retrying (${s}/${this.config.maxRetries})`),await new Promise((n)=>setTimeout(n,this.config.retryTimeout)),this.sendWithRetry(e,t,s+1);throw r}}async sendMessage(e,t){if(g.webhookUrl)return this.sendViaWebhook(e);if(g.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(g.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 ${g.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 T(e,t){return new v().send(e,t)}async function D(e,t,s){try{let r={text:t,...s},n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});if(!n.ok){let i=await n.text();return{success:!1,provider:"slack",message:`Webhook failed: ${n.status} - ${i}`}}return{success:!0,provider:"slack",message:"Message sent successfully"}}catch(r){return{success:!1,provider:"slack",message:(r instanceof Error?r:Error(String(r))).message}}}var _e=new v,Ce={send:T,sendWebhook:D,configure:y,SlackDriver:v};var z={};A(z,{Driver:()=>_,TeamsDriver:()=>_,configure:()=>x,default:()=>we,driver:()=>ke,send:()=>I,sendCard:()=>ne,sendWebhook:()=>E});import{log as re}from"@stacksjs/logging";var H={};function x(e){H={...H,...e}}class _ extends m{name="teams";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)x(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};re.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 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){let r=H.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 I(e,t){return new _().send(e,t)}async function E(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 ne(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 ke=new _,we={send:I,sendWebhook:E,sendCard:ne,configure:x,TeamsDriver:_};var ge={};A(ge,{DiscordInboxDriver:()=>C,IMessageDriver:()=>w,SlackApiError:()=>B,SlackInboxDriver:()=>M,createInboxDriver:()=>Ge,decodeAttributedBody:()=>P,formatHandle:()=>O,normalizeHandle:()=>d,snowflakeTime:()=>J});var Me=new Set([0,5]),G=new Set([10,11,12]);function J(e){if(!e)return null;return Number((BigInt(e)>>22n)+1420070400000n)}class C{provider="discord";label="Discord";token;userId;fetch;apiBase;concurrency;guilds=new Map;channels=new Map;constructor(e){this.token=e.token,this.userId=e.userId,this.fetch=e.fetch??((t,s)=>fetch(t,s)),this.apiBase=(e.apiBase??"https://discord.com/api/v10").replace(/\/$/,""),this.concurrency=e.concurrency??3}async api(e,t={}){for(let s=0;;s++){let r=await this.fetch(`${this.apiBase}${e}`,{...t,headers:{authorization:`Bot ${this.token}`,"content-type":"application/json",...t.headers??{}}});if(r.status===429&&s<3){let n=await r.json().catch(()=>({}));await Bun.sleep(Math.min(30,n.retry_after??1)*1000);continue}if(!r.ok){let n=await r.json().catch(()=>({}));throw Error(`Discord ${t.method??"GET"} ${e} failed: ${n.message??r.status}`)}return r.status===204?void 0:await r.json()}}async status(){if(!this.token)return{connected:!1,detail:"Add a Discord bot token.",needs:"token"};try{return{connected:!0,detail:`Reading the servers ${(await this.api("/users/@me")).username} is in.`}}catch(e){return{connected:!1,detail:e instanceof Error?e.message:String(e),needs:"token"}}}archiveSupport(e){if(G.has(e.type))return{mode:"native",detail:"Archives the thread in Discord. It reopens when someone posts."};return{mode:"unsupported",detail:"Discord cannot hide a channel for one person, so it stays in Discord."}}async conversations(){let e=await this.api("/users/@me/guilds"),t=[];for(let r of e){this.guilds.set(r.id,r.name);let[n,i]=await Promise.all([this.api(`/guilds/${r.id}/channels`).catch(()=>[]),this.api(`/guilds/${r.id}/threads/active`).catch(()=>({threads:[]}))]);t.push(...n.filter((a)=>Me.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=G.has(e.type);return{provider:"discord",id:e.id,kind:s?"thread":"channel",title:s?e.name??"Thread":`#${e.name}`,participants:[],workspace:e.guild_id?this.guilds.get(e.guild_id)??null:null,lastMessageAt:t?Date.parse(t.timestamp):J(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},targetId:null,reaction:null,reactionRemoved:!1,replyToId:a.message_reference?.message_id??null,editedAt:a.edited_timestamp?Date.parse(a.edited_timestamp):null,unsent:!1,attachments:(a.attachments??[]).map((c)=>({id:c.id,name:c.filename,mimeType:c.content_type??null,bytes:c.size,path:null,url:c.url})),cursor:a.id});for(let c of a.reactions??[])i.push({id:`${a.id}:${c.emoji.id??c.emoji.name}`,conversationId:e,kind:"reaction",text:c.count>1?String(c.count):null,fromMe:!!c.me,sentAt:Date.parse(a.timestamp),sender:null,targetId:a.id,reaction:c.emoji.name??"\u2022",reactionRemoved:!1,replyToId:null,editedAt:null,unsent:!1,attachments:[],cursor:a.id})}return i}async channel(e){let t=this.channels.get(e);if(t)return t;let s=await this.api(`/channels/${e}`);return this.channels.set(e,s),s}async archive(e){let t=await this.channel(e),s=this.archiveSupport(t);if(s.mode!=="native")return{mode:s.mode,removed:!1,detail:s.detail};return await this.api(`/channels/${e}`,{method:"PATCH",body:JSON.stringify({archived:!0})}),this.channels.set(e,{...t,thread_metadata:{archived:!0}}),{mode:"native",removed:!0,detail:s.detail}}async unarchive(e){let t=await this.channel(e);if(!G.has(t.type))return;await this.api(`/channels/${e}`,{method:"PATCH",body:JSON.stringify({archived:!1})}),this.channels.set(e,{...t,thread_metadata:{archived:!1}})}async attachment(e){return e.url?this.fetch(e.url):new Response("Not found",{status:404})}}import{existsSync as Y}from"fs";import{dirname as Be}from"path";import Ue from"process";import{Database as De}from"bun:sqlite";import{homedir as le}from"os";import{join as de}from"path";function d(e){let t=e.trim();if(t.includes("@"))return t.toLowerCase();let s=t.replace(/[^\d+]/g,"");if(!/^\+?\d{7,15}$/.test(s))return t;if(s.startsWith("+"))return s;if(s.length===10)return`+1${s}`;return`+${s}`}function ie(e){return/^\+\d{7,15}$/.test(e)||/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)}function O(e){let t=e.match(/^\+1(\d{3})(\d{3})(\d{4})$/);if(t)return`+1 (${t[1]}) ${t[2]}-${t[3]}`;return e}var ae=new TextEncoder,Se=new TextDecoder("utf-8",{fatal:!1}),Ae=[ae.encode("NSString"),ae.encode("NSMutableString")];function Re(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 Te(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 P(e){if(!e||e.length===0)return null;for(let t of Ae){let s=Re(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=Te(e,i+1);if(!a||a.start+a.length>e.length)continue;return Se.decode(e.subarray(a.start,a.start+a.length))}}return null}var Z=de(le(),"Library","Messages","chat.db"),ue=978307200,Ie=45,N=43;function K(e){if(!e)return null;let t=e>1000000000000?e/1e9:e;return Math.round((t+ue)*1000)}function Ee(e){return Math.round((e/1000-ue)*1e9)}function oe(e,t){let r=(e?.trim()?e:P(t))?.replace(/\uFFFC/g,"").trim();return r?r:null}class j extends Error{path;needsFullDiskAccess;constructor(e,t){let s=t instanceof Error?t.message:String(t),r=/authorization denied|not permitted|unable to open/i.test(s);super(`Cannot read ${e}: ${s}`);this.path=e;this.name="MessagesAccessError",this.needsFullDiskAccess=r}}var Oe=(e)=>e>=2000&&e<4000;function ce(e){if(!e)return null;return e.startsWith("~/")?de(le(),e.slice(2)):e}function Pe(e){if(!e)return null;let t=e.indexOf("/");if(t!==-1)return e.slice(t+1);return e.replace(/^bp:/,"")}class q{path;db;messageColumns;hasRecoverable;constructor(e=Z){this.path=e;try{this.db=new De(e,{readonly:!0}),this.db.query("SELECT 1 FROM message LIMIT 1").all()}catch(t){throw new j(e,t)}this.messageColumns=new Set(this.db.query("PRAGMA table_info(message)").all().map((t)=>t.name)),this.hasRecoverable=!!this.db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'chat_recoverable_message_join'").get()}close(){this.db.close()}latestRowId(){return this.db.query("SELECT MAX(ROWID) AS id FROM message").get().id??0}column(e,t="NULL"){return this.messageColumns.has(e)?`m.${e}`:t}chats(){let e=this.hasRecoverable?"(SELECT COUNT(*) FROM chat_recoverable_message_join r WHERE r.chat_id = c.ROWID)":"0",t=this.db.query(`
3
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
5
  (SELECT COUNT(*) FROM chat_message_join j WHERE j.chat_id = c.ROWID) AS message_count,
@@ -9,9 +9,9 @@ var ge=Object.defineProperty;var pe=(e)=>e;function fe(e,t){this[e]=pe.bind(null
9
9
  AND (m.associated_message_type IS NULL OR m.associated_message_type = 0)) AS unread_count,
10
10
  (SELECT MAX(j.message_id) FROM chat_message_join j WHERE j.chat_id = c.ROWID) AS last_rowid
11
11
  FROM chat c
12
- `).all(),s=this.participantsByChat(),r=this.db.query("SELECT m.text, m.attributedBody, m.date, m.is_from_me FROM message m WHERE m.ROWID = ?");return t.map((i)=>{let a=i.last_rowid?r.get(i.last_rowid):null;return{rowid:i.rowid,guid:i.guid,identifier:i.chat_identifier?d(i.chat_identifier):i.guid,style:i.style??De,service:i.service_name??"iMessage",displayName:i.display_name?.trim()||null,groupId:i.group_id||null,participants:s.get(i.rowid)??[],messageCount:i.message_count,recoverableCount:i.recoverable_count,unreadCount:i.unread_count,lastMessageAt:a?K(a.date):null,lastMessageRowId:i.last_rowid??0,lastMessageText:a?a.text?.trim()?a.text:k(a.attributedBody):null,lastMessageFromMe:a?.is_from_me===1}}).sort((i,a)=>(a.lastMessageAt??0)-(i.lastMessageAt??0))}participantsByChat(){let e=this.db.query(`
12
+ `).all(),s=this.participantsByChat(),r=this.db.query("SELECT m.text, m.attributedBody, m.date, m.is_from_me FROM message m WHERE m.ROWID = ?");return t.map((i)=>{let a=i.last_rowid?r.get(i.last_rowid):null;return{rowid:i.rowid,guid:i.guid,identifier:i.chat_identifier?d(i.chat_identifier):i.guid,style:i.style??Ie,service:i.service_name??"iMessage",displayName:i.display_name?.trim()||null,groupId:i.group_id||null,participants:s.get(i.rowid)??[],messageCount:i.message_count,recoverableCount:i.recoverable_count,unreadCount:i.unread_count,lastMessageAt:a?K(a.date):null,lastMessageRowId:i.last_rowid??0,lastMessageText:a?oe(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
13
  SELECT chj.chat_id, h.id FROM chat_handle_join chj JOIN handle h ON h.ROWID = chj.handle_id
14
- `).all(),t=new Map;for(let s of e){let r=t.get(s.chat_id)??[],n=d(s.id);if(!r.includes(n))r.push(n);t.set(s.chat_id,r)}return t}messages(e,t={}){if(e.length===0)return[];let s=e.map(()=>"?").join(", "),r=t.newestFirst?"m.date DESC, m.ROWID DESC":t.byRowId?"m.ROWID ASC":"m.date ASC, m.ROWID ASC",n=t.beforeMs?`AND m.date < ${Ie(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(`
14
+ `).all(),t=new Map;for(let s of e){let r=t.get(s.chat_id)??[],n=d(s.id);if(!r.includes(n))r.push(n);t.set(s.chat_id,r)}return t}messages(e,t={}){if(e.length===0)return[];let s=e.map(()=>"?").join(", "),r=t.newestFirst?"m.date DESC, m.ROWID DESC":t.byRowId?"m.ROWID ASC":"m.date ASC, m.ROWID ASC",n=t.beforeMs?`AND m.date < ${Ee(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
15
  SELECT
16
16
  m.ROWID AS rowid, m.guid, m.text, m.attributedBody, m.is_from_me, m.date, m.service,
17
17
  m.associated_message_guid, m.associated_message_type, m.item_type, m.group_title,
@@ -28,11 +28,11 @@ var ge=Object.defineProperty;var pe=(e)=>e;function fe(e,t){this[e]=pe.bind(null
28
28
  WHERE c.guid IN (${s}) AND m.ROWID > ? ${n}
29
29
  ORDER BY ${r}
30
30
  LIMIT ?
31
- `).all(...e,t.afterRowId??0,t.limit??1e6),o=this.attachmentsFor(a.filter((c)=>c.cache_has_attachments===1).map((c)=>c.rowid));return a.map((c)=>{let A=c.associated_message_type??0,l=c.item_type??0,u=Ee(A)?"reaction":l!==0?"event":"message";return{rowid:c.rowid,guid:c.guid,chatGuid:c.chat_guid,kind:u,text:c.text?.trim()?c.text:k(c.attributedBody),isFromMe:c.is_from_me===1,sentAt:K(c.date)??0,sender:c.sender?d(c.sender):null,service:c.service??"iMessage",targetGuid:u==="reaction"?Oe(c.associated_message_guid):null,reactionType:u==="reaction"?A:0,replyToGuid:c.reply_to_guid||null,editedAt:K(c.date_edited),unsent:!!c.date_retracted,eventType:l,groupTitle:c.group_title||null,attachments:o.get(c.rowid)??[]}})}attachmentPath(e){let t=this.db.query("SELECT filename, mime_type, transfer_name FROM attachment WHERE guid = ?").get(e);if(!t)return null;let s=oe(t.filename);return{path:s,mimeType:t.mime_type,name:t.transfer_name||(s?s.split("/").pop():e)}}attachmentsFor(e){let t=new Map;for(let s=0;s<e.length;s+=500){let r=e.slice(s,s+500),n=this.db.query(`
31
+ `).all(...e,t.afterRowId??0,t.limit??1e6),o=this.attachmentsFor(a.filter((c)=>c.cache_has_attachments===1).map((c)=>c.rowid));return a.map((c)=>{let S=c.associated_message_type??0,l=c.item_type??0,u=Oe(S)?"reaction":l!==0?"event":"message";return{rowid:c.rowid,guid:c.guid,chatGuid:c.chat_guid,kind:u,text:oe(c.text,c.attributedBody),isFromMe:c.is_from_me===1,sentAt:K(c.date)??0,sender:c.sender?d(c.sender):null,service:c.service??"iMessage",targetGuid:u==="reaction"?Pe(c.associated_message_guid):null,reactionType:u==="reaction"?S:0,replyToGuid:c.reply_to_guid||null,editedAt:K(c.date_edited),unsent:!!c.date_retracted,eventType:l,groupTitle:c.group_title||null,attachments:o.get(c.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=ce(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
32
  SELECT maj.message_id, a.guid, a.filename, a.transfer_name, a.mime_type, a.uti, a.total_bytes, a.is_sticker
33
33
  FROM message_attachment_join maj JOIN attachment a ON a.ROWID = maj.attachment_id
34
34
  WHERE maj.message_id IN (${r.map(()=>"?").join(", ")})
35
- `).all(...r);for(let i of n){let a=t.get(i.message_id)??[],o=oe(i.filename);a.push({guid:i.guid,path:o,name:i.transfer_name||(o?o.split("/").pop():i.guid),mimeType:i.mime_type,uti:i.uti,bytes:i.total_bytes??0,isSticker:i.is_sticker===1}),t.set(i.message_id,a)}}return t}}import{Database as Pe}from"bun:sqlite";import{existsSync as ue,readdirSync as $e}from"fs";import{homedir as Ne}from"os";import{join as F}from"path";var Q=F(Ne(),"Library","Application Support","AddressBook");function je(e){let t=[],s=F(e,"AddressBook-v22.abcddb");if(ue(s))t.push(s);let r=F(e,"Sources");try{for(let n of $e(r)){let i=F(r,n,"AddressBook-v22.abcddb");if(ue(i))t.push(i)}}catch{}return t}function me(e){let t=e.replace(/[^\p{L}\p{N}\s]/gu," ").trim().split(/\s+/).filter(Boolean);if(t.length===0)return"?";let s=t[0][0],r=t.length>1?t[t.length-1][0]:"";return(s+r).toUpperCase()}class w{byHandle=new Map;static load(e=Q){let t=new w;for(let s of je(e))try{t.read(s)}catch{}return t}get size(){return this.byHandle.size}read(e){let t=new Pe(e,{readonly:!0});try{let s=t.query(`
35
+ `).all(...r);for(let i of n){let a=t.get(i.message_id)??[],o=ce(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 $e}from"bun:sqlite";import{existsSync as me,readdirSync as Ne}from"fs";import{homedir as je}from"os";import{join as F}from"path";var Q=F(je(),"Library","Application Support","AddressBook");function Fe(e){let t=[],s=F(e,"AddressBook-v22.abcddb");if(me(s))t.push(s);let r=F(e,"Sources");try{for(let n of Ne(r)){let i=F(r,n,"AddressBook-v22.abcddb");if(me(i))t.push(i)}}catch{}return t}function he(e){let t=e.replace(/[^\p{L}\p{N}\s]/gu," ").trim().split(/\s+/).filter(Boolean);if(t.length===0)return"?";let s=t[0][0],r=t.length>1?t[t.length-1][0]:"";return(s+r).toUpperCase()}class k{byHandle=new Map;static load(e=Q){let t=new k;for(let s of Fe(e))try{t.read(s)}catch{}return t}get size(){return this.byHandle.size}read(e){let t=new $e(e,{readonly:!0});try{let s=t.query(`
36
36
  SELECT Z_PK AS id, ZFIRSTNAME AS first, ZLASTNAME AS last, ZNICKNAME AS nick, ZORGANIZATION AS org
37
37
  FROM ZABCDRECORD
38
- `).all(),r=new Map;for(let a of s){let c=[a.first,a.last].filter(Boolean).join(" ").trim()||a.nick?.trim()||a.org?.trim();if(c)r.set(a.id,c)}let n=t.query("SELECT ZOWNER AS owner, ZFULLNUMBER AS value FROM ZABCDPHONENUMBER").all(),i=t.query("SELECT ZOWNER AS owner, ZADDRESS AS value FROM ZABCDEMAILADDRESS").all();for(let a of[...n,...i]){let o=r.get(a.owner);if(!o||!a.value)continue;let c=d(a.value);if(!this.byHandle.has(c))this.byHandle.set(c,{name:o,initials:me(o)})}}finally{t.close()}}set(e,t){this.byHandle.set(d(e),{name:t,initials:me(t)})}lookup(e){return this.byHandle.get(d(e))??null}nameFor(e){return this.lookup(e)?.name??null}}function Fe(e){if(e.style===N)return`group:${e.groupId||e.guid}`;return`direct:${e.identifier}`}function V(e){let t=new Map;for(let s of e){let r=Fe(s),n=t.get(r);if(!n){t.set(r,{key:r,kind:s.style===N?"group":"direct",chatGuids:[s.guid],displayName:s.displayName,groupId:s.groupId,participants:s.style===N?[...s.participants]:[s.identifier],messageCount:s.messageCount,recoverableCount:s.recoverableCount,unreadCount:s.unreadCount,lastMessageAt:s.lastMessageAt,lastMessageRowId:s.lastMessageRowId,lastMessageText:s.lastMessageText,lastMessageFromMe:s.lastMessageFromMe,services:[s.service]});continue}if(n.chatGuids.push(s.guid),n.messageCount+=s.messageCount,n.recoverableCount+=s.recoverableCount,n.unreadCount+=s.unreadCount,!n.services.includes(s.service))n.services.push(s.service);for(let i of s.participants)if(n.kind==="group"&&!n.participants.includes(i))n.participants.push(i);if((s.lastMessageAt??0)>(n.lastMessageAt??0))n.lastMessageAt=s.lastMessageAt,n.lastMessageRowId=s.lastMessageRowId,n.lastMessageText=s.lastMessageText,n.lastMessageFromMe=s.lastMessageFromMe,n.displayName=s.displayName??n.displayName}return[...t.values()].sort((s,r)=>(r.lastMessageAt??0)-(s.lastMessageAt??0))}function X(e){if(e.kind==="group")return e.groupId?`sms://open?groupid=${encodeURIComponent(e.groupId)}`:null;let t=e.participants[0];if(!t||!ie(t))return null;return`sms:${t}`}var Ue={2000:"\u2764\uFE0F",2001:"\uD83D\uDC4D",2002:"\uD83D\uDC4E",2003:"\uD83D\uDE02",2004:"\u203C\uFE0F",2005:"\u2753",2006:"\uD83D\uDE00",2007:"\uD83D\uDD16"};function We(e){Bun.spawn(["/usr/bin/open",e],{stdio:["ignore","ignore","ignore"]})}class M{provider="imessage";label="iMessage";databasePath;addressBookDir;openUrl;controller;confirmDeletes;platform;contacts=null;contactsLoadedAt=0;constructor(e={}){this.databasePath=e.databasePath??Z,this.addressBookDir=e.addressBookDir??Q,this.openUrl=e.openUrl??We,this.controller=e.controller,this.confirmDeletes=e.confirmDeletes??!1,this.platform=e.platform??Be.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]),P(e.participants[0]),e.participants[0]);return[...new Set(t.filter((s)=>!!s))]}open(e){let t=new q(this.databasePath);try{return e(t)}finally{t.close()}}names(){if(this.addressBookDir===!1)return this.contacts??=new w;if(!this.contacts||Date.now()-this.contactsLoadedAt>600000)this.contacts=w.load(this.addressBookDir),this.contactsLoadedAt=Date.now();return this.contacts}person(e){return e?{id:e,name:this.names().nameFor(e)}:null}find(e,t){return V(e.chats()).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 j&&e.needsFullDiskAccess&&Y(Le(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)=>V(e.chats()).map((t)=>this.toConversation(t)))}toConversation(e){let t=X(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((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"?Ue[s?t.reactionType-1000:t.reactionType]??"\u2022":null,reactionRemoved:s,replyToId:t.replyToGuid,editedAt:t.editedAt,unsent:t.unsent,attachments:t.attachments.map((r)=>({id:r.guid,name:r.name,mimeType:r.mimeType,bytes:r.bytes,path:r.path&&Y(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?X(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||!Y(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 He={"+1":"\uD83D\uDC4D",thumbsup:"\uD83D\uDC4D","-1":"\uD83D\uDC4E",heart:"\u2764\uFE0F",joy:"\uD83D\uDE02",laughing:"\uD83D\uDE06",tada:"\uD83C\uDF89",eyes:"\uD83D\uDC40",fire:"\uD83D\uDD25",white_check_mark:"\u2705",pray:"\uD83D\uDE4F",raised_hands:"\uD83D\uDE4C",rocket:"\uD83D\uDE80"},L=(e)=>Math.round(Number(e)*1000);class B extends Error{method;code;constructor(e,t){super(`Slack ${e} failed: ${t}`);this.method=e;this.code=t;this.name="SlackApiError"}}class S{provider="slack";label="Slack";token;fetch;apiBase;concurrency;self=null;users=new Map;channels=new Map;constructor(e){this.token=e.token,this.fetch=e.fetch??((t,s)=>fetch(t,s)),this.apiBase=(e.apiBase??"https://slack.com/api").replace(/\/$/,""),this.concurrency=e.concurrency??4}async api(e,t={},s=!1){for(let r=0;;r++){let n=new URLSearchParams(Object.entries(t).map(([o,c])=>[o,String(c)])),i=s?await this.fetch(`${this.apiBase}/${e}`,{method:"POST",headers:{authorization:`Bearer ${this.token}`,"content-type":"application/x-www-form-urlencoded"},body:n.toString()}):await this.fetch(`${this.apiBase}/${e}?${n}`,{headers:{authorization:`Bearer ${this.token}`}});if(i.status===429&&r<3){await Bun.sleep(Math.min(30,Number(i.headers.get("retry-after"))||1)*1000);continue}let a=await i.json();if(!a.ok)throw new B(e,a.error??`http_${i.status}`);return a}}async whoami(){if(!this.self){let e=await this.api("auth.test");this.self={userId:e.user_id,team:e.team,teamId:e.team_id}}return this.self}async userName(e){if(!e)return null;if(!this.users.has(e))try{let{user:t}=await this.api("users.info",{user:e});this.users.set(e,t.profile?.display_name||t.profile?.real_name||t.real_name||t.name||null)}catch{this.users.set(e,null)}return this.users.get(e)??null}async person(e){return e?{id:e,name:await this.userName(e)}:null}async status(){if(!this.token)return{connected:!1,detail:"Add a Slack user token.",needs:"token"};try{return{connected:!0,detail:`Signed in to ${(await this.whoami()).team}.`}}catch(e){return{connected:!1,detail:e instanceof Error?e.message:String(e),needs:"token"}}}archiveSupport(e){if(e.is_im||e.is_mpim)return{mode:"native",detail:"Closes the conversation in Slack. Slack reopens it when someone writes."};if(!e.is_private)return{mode:"native",detail:"Leaves the channel in Slack, and rejoins it if you unarchive."};return{mode:"unsupported",detail:"Leaving a private channel would need someone to invite you back, so it stays in Slack."}}async list(){let e=[],t="";do{let s=await this.api("users.conversations",{types:"im,mpim,public_channel,private_channel",exclude_archived:!0,limit:200,...t?{cursor:t}:{}});e.push(...s.channels),t=s.response_metadata?.next_cursor??""}while(t);return e}async conversations(){let e=await this.whoami(),t=await this.list(),s=[];for(let r=0;r<t.length;r+=this.concurrency){let n=t.slice(r,r+this.concurrency);s.push(...await Promise.all(n.map((i)=>this.describe(i,e))))}return s.sort((r,n)=>(n.lastMessageAt??0)-(r.lastMessageAt??0))}async describe(e,t){let[{channel:s},r]=await Promise.all([this.api("conversations.info",{channel:e.id}).catch(()=>({channel:e})),this.api("conversations.history",{channel:e.id,limit:1}).catch(()=>({messages:[]}))]),n={...e,...s};this.channels.set(n.id,n);let i=r.messages[0],a=[];if(n.is_im)a=[await this.person(n.user)];else if(n.is_mpim){let o=await this.api("conversations.members",{channel:n.id,limit:50}).catch(()=>({members:[]}));a=await Promise.all(o.members.filter((c)=>c!==t.userId).map(async(c)=>await this.person(c)))}return{provider:"slack",id:n.id,kind:n.is_im?"direct":n.is_mpim?"group":"channel",title:n.is_im||n.is_mpim?null:`#${n.name}`,participants:a,workspace:t.team,lastMessageAt:i?L(i.ts):null,preview:i?.text||(i?.files?.length?"Attachment":null),previewFromMe:i?.user===t.userId,unread:n.unread_count_display??0,cursor:i?.ts??"0",visible:n.is_im||n.is_mpim?n.is_open!==!1:!0,archive:this.archiveSupport(n),url:`slack://channel?team=${t.teamId}&id=${n.id}`}}async messages(e,t={}){let s=await this.whoami(),r=[],n="",i=t.limit??1000;do{let o=await this.api("conversations.history",{channel:e,limit:Math.min(200,i),...t.after?{oldest:t.after,inclusive:!1}:{},...n?{cursor:n}:{}});r.push(...o.messages),n=o.has_more?o.response_metadata?.next_cursor??"":""}while(n&&r.length<i);r.sort((o,c)=>Number(o.ts)-Number(c.ts));let a=[];for(let o of r.slice(-i)){let c=await this.person(o.user),A=!!o.subtype&&!["file_share","thread_broadcast","me_message"].includes(o.subtype);a.push({id:o.ts,conversationId:e,kind:A?"event":"message",text:o.text??null,fromMe:o.user===s.userId,sentAt:L(o.ts),sender:o.user===s.userId?null:c??(o.username?{id:o.bot_id??o.username,name:o.username}:null),targetId:null,reaction:null,reactionRemoved:!1,replyToId:o.thread_ts&&o.thread_ts!==o.ts?o.thread_ts:null,editedAt:o.edited?L(o.edited.ts):null,unsent:!1,attachments:(o.files??[]).map((l)=>({id:l.id,name:l.name??l.title??l.id,mimeType:l.mimetype??null,bytes:l.size??0,path:null,url:l.url_private??null})),cursor:o.ts});for(let l of o.reactions??[])for(let u of l.users??[])a.push({id:`${o.ts}:${l.name}:${u}`,conversationId:e,kind:"reaction",text:null,fromMe:u===s.userId,sentAt:L(o.ts),sender:u===s.userId?null:await this.person(u),targetId:o.ts,reaction:He[l.name]??`:${l.name}:`,reactionRemoved:!1,replyToId:null,editedAt:null,unsent:!1,attachments:[],cursor:o.ts})}return a}async channel(e){let t=this.channels.get(e);if(t)return t;let{channel:s}=await this.api("conversations.info",{channel:e});return this.channels.set(e,s),s}async archive(e){let t=await this.channel(e),s=this.archiveSupport(t);if(s.mode!=="native")return{mode:s.mode,removed:!1,detail:s.detail};if(t.is_im||t.is_mpim)await this.api("conversations.close",{channel:e},!0);else await this.api("conversations.leave",{channel:e},!0);return this.channels.set(e,{...t,is_open:!1}),{mode:"native",removed:!0,detail:s.detail}}async unarchive(e){let t=await this.channel(e);if(t.is_im||t.is_mpim)await this.api("conversations.open",{channel:e},!0);else if(!t.is_private)await this.api("conversations.join",{channel:e},!0);this.channels.set(e,{...t,is_open:!0})}async attachment(e){if(!e.url)return new Response("Not found",{status:404});return this.fetch(e.url,{headers:{authorization:`Bearer ${this.token}`}})}}function ze(e,t){switch(e){case"imessage":return new M(t);case"slack":return new S(t);case"discord":return new C(t);default:throw Error(`Unknown inbox driver: ${String(e)}`)}}async function Nt(e,t={}){let s=t.driver??"slack";switch(s){case"slack":return D(e);case"discord":return T(e);case"teams":return E(e);default:return{success:!1,provider:s,message:`Unknown chat driver: ${s}`}}}async function jt(e,t,s){return I(e,t,s)}async function Ft(e,t,s){return b(e,t,s)}async function Lt(e,t){return O(e,t)}function Bt(e){y(e)}function Ut(e){p(e)}function Wt(e){x(e)}export{Ut as configureDiscord,Bt as configureSlack,Wt as configureTeams,U as discord,he as inbox,Nt as send,Ft as sendToDiscord,jt as sendToSlack,Lt as sendToTeams,W as slack,z as teams};
38
+ `).all(),r=new Map;for(let a of s){let c=[a.first,a.last].filter(Boolean).join(" ").trim()||a.nick?.trim()||a.org?.trim();if(c)r.set(a.id,c)}let n=t.query("SELECT ZOWNER AS owner, ZFULLNUMBER AS value FROM ZABCDPHONENUMBER").all(),i=t.query("SELECT ZOWNER AS owner, ZADDRESS AS value FROM ZABCDEMAILADDRESS").all();for(let a of[...n,...i]){let o=r.get(a.owner);if(!o||!a.value)continue;let c=d(a.value);if(!this.byHandle.has(c))this.byHandle.set(c,{name:o,initials:he(o)})}}finally{t.close()}}set(e,t){this.byHandle.set(d(e),{name:t,initials:he(t)})}lookup(e){return this.byHandle.get(d(e))??null}nameFor(e){return this.lookup(e)?.name??null}}function Le(e){if(e.style===N)return`group:${e.groupId||e.guid}`;return`direct:${e.identifier}`}function V(e){let t=new Map;for(let s of e){let r=Le(s),n=t.get(r);if(!n){t.set(r,{key:r,kind:s.style===N?"group":"direct",chatGuids:[s.guid],displayName:s.displayName,groupId:s.groupId,participants:s.style===N?[...s.participants]:[s.identifier],messageCount:s.messageCount,recoverableCount:s.recoverableCount,unreadCount:s.unreadCount,lastMessageAt:s.lastMessageAt,lastMessageRowId:s.lastMessageRowId,lastMessageText:s.lastMessageText,lastMessageFromMe:s.lastMessageFromMe,services:[s.service]});continue}if(n.chatGuids.push(s.guid),n.messageCount+=s.messageCount,n.recoverableCount+=s.recoverableCount,n.unreadCount+=s.unreadCount,!n.services.includes(s.service))n.services.push(s.service);for(let i of s.participants)if(n.kind==="group"&&!n.participants.includes(i))n.participants.push(i);if((s.lastMessageAt??0)>(n.lastMessageAt??0))n.lastMessageAt=s.lastMessageAt,n.lastMessageRowId=s.lastMessageRowId,n.lastMessageText=s.lastMessageText,n.lastMessageFromMe=s.lastMessageFromMe,n.displayName=s.displayName??n.displayName}return[...t.values()].sort((s,r)=>(r.lastMessageAt??0)-(s.lastMessageAt??0))}function X(e){if(e.kind==="group")return e.groupId?`sms://open?groupid=${encodeURIComponent(e.groupId)}`:null;let t=e.participants[0];if(!t||!ie(t))return null;return`sms:${t}`}var We={2000:"\u2764\uFE0F",2001:"\uD83D\uDC4D",2002:"\uD83D\uDC4E",2003:"\uD83D\uDE02",2004:"\u203C\uFE0F",2005:"\u2753",2006:"\uD83D\uDE00",2007:"\uD83D\uDD16"};function He(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??Z,this.addressBookDir=e.addressBookDir??Q,this.openUrl=e.openUrl??He,this.controller=e.controller,this.confirmDeletes=e.confirmDeletes??!1,this.platform=e.platform??Ue.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]),O(e.participants[0]),e.participants[0]);return[...new Set(t.filter((s)=>!!s))]}open(e){let t=new q(this.databasePath);try{return e(t)}finally{t.close()}}names(){if(this.addressBookDir===!1)return this.contacts??=new k;if(!this.contacts||Date.now()-this.contactsLoadedAt>600000)this.contacts=k.load(this.addressBookDir),this.contactsLoadedAt=Date.now();return this.contacts}person(e){return e?{id:e,name:this.names().nameFor(e)}:null}find(e,t){return V(e.chats()).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 j&&e.needsFullDiskAccess&&Y(Be(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)=>V(e.chats()).map((t)=>this.toConversation(t)))}toConversation(e){let t=X(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((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"?We[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&&Y(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?X(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||!Y(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 ze={"+1":"\uD83D\uDC4D",thumbsup:"\uD83D\uDC4D","-1":"\uD83D\uDC4E",heart:"\u2764\uFE0F",joy:"\uD83D\uDE02",laughing:"\uD83D\uDE06",tada:"\uD83C\uDF89",eyes:"\uD83D\uDC40",fire:"\uD83D\uDD25",white_check_mark:"\u2705",pray:"\uD83D\uDE4F",raised_hands:"\uD83D\uDE4C",rocket:"\uD83D\uDE80"},L=(e)=>Math.round(Number(e)*1000);class B extends Error{method;code;constructor(e,t){super(`Slack ${e} failed: ${t}`);this.method=e;this.code=t;this.name="SlackApiError"}}class M{provider="slack";label="Slack";token;fetch;apiBase;concurrency;self=null;users=new Map;channels=new Map;constructor(e){this.token=e.token,this.fetch=e.fetch??((t,s)=>fetch(t,s)),this.apiBase=(e.apiBase??"https://slack.com/api").replace(/\/$/,""),this.concurrency=e.concurrency??4}async api(e,t={},s=!1){for(let r=0;;r++){let n=new URLSearchParams(Object.entries(t).map(([o,c])=>[o,String(c)])),i=s?await this.fetch(`${this.apiBase}/${e}`,{method:"POST",headers:{authorization:`Bearer ${this.token}`,"content-type":"application/x-www-form-urlencoded"},body:n.toString()}):await this.fetch(`${this.apiBase}/${e}?${n}`,{headers:{authorization:`Bearer ${this.token}`}});if(i.status===429&&r<3){await Bun.sleep(Math.min(30,Number(i.headers.get("retry-after"))||1)*1000);continue}let a=await i.json();if(!a.ok)throw new B(e,a.error??`http_${i.status}`);return a}}async whoami(){if(!this.self){let e=await this.api("auth.test");this.self={userId:e.user_id,team:e.team,teamId:e.team_id}}return this.self}async userName(e){if(!e)return null;if(!this.users.has(e))try{let{user:t}=await this.api("users.info",{user:e});this.users.set(e,t.profile?.display_name||t.profile?.real_name||t.real_name||t.name||null)}catch{this.users.set(e,null)}return this.users.get(e)??null}async person(e){return e?{id:e,name:await this.userName(e)}:null}async status(){if(!this.token)return{connected:!1,detail:"Add a Slack user token.",needs:"token"};try{return{connected:!0,detail:`Signed in to ${(await this.whoami()).team}.`}}catch(e){return{connected:!1,detail:e instanceof Error?e.message:String(e),needs:"token"}}}archiveSupport(e){if(e.is_im||e.is_mpim)return{mode:"native",detail:"Closes the conversation in Slack. Slack reopens it when someone writes."};if(!e.is_private)return{mode:"native",detail:"Leaves the channel in Slack, and rejoins it if you unarchive."};return{mode:"unsupported",detail:"Leaving a private channel would need someone to invite you back, so it stays in Slack."}}async list(){let e=[],t="";do{let s=await this.api("users.conversations",{types:"im,mpim,public_channel,private_channel",exclude_archived:!0,limit:200,...t?{cursor:t}:{}});e.push(...s.channels),t=s.response_metadata?.next_cursor??""}while(t);return e}async conversations(){let e=await this.whoami(),t=await this.list(),s=[];for(let r=0;r<t.length;r+=this.concurrency){let n=t.slice(r,r+this.concurrency);s.push(...await Promise.all(n.map((i)=>this.describe(i,e))))}return s.sort((r,n)=>(n.lastMessageAt??0)-(r.lastMessageAt??0))}async describe(e,t){let[{channel:s},r]=await Promise.all([this.api("conversations.info",{channel:e.id}).catch(()=>({channel:e})),this.api("conversations.history",{channel:e.id,limit:1}).catch(()=>({messages:[]}))]),n={...e,...s};this.channels.set(n.id,n);let i=r.messages[0],a=[];if(n.is_im)a=[await this.person(n.user)];else if(n.is_mpim){let o=await this.api("conversations.members",{channel:n.id,limit:50}).catch(()=>({members:[]}));a=await Promise.all(o.members.filter((c)=>c!==t.userId).map(async(c)=>await this.person(c)))}return{provider:"slack",id:n.id,kind:n.is_im?"direct":n.is_mpim?"group":"channel",title:n.is_im||n.is_mpim?null:`#${n.name}`,participants:a,workspace:t.team,lastMessageAt:i?L(i.ts):null,preview:i?.text||(i?.files?.length?"Attachment":null),previewFromMe:i?.user===t.userId,unread:n.unread_count_display??0,cursor:i?.ts??"0",visible:n.is_im||n.is_mpim?n.is_open!==!1:!0,archive:this.archiveSupport(n),url:`slack://channel?team=${t.teamId}&id=${n.id}`}}async messages(e,t={}){let s=await this.whoami(),r=[],n="",i=t.limit??1000;do{let o=await this.api("conversations.history",{channel:e,limit:Math.min(200,i),...t.after?{oldest:t.after,inclusive:!1}:{},...n?{cursor:n}:{}});r.push(...o.messages),n=o.has_more?o.response_metadata?.next_cursor??"":""}while(n&&r.length<i);r.sort((o,c)=>Number(o.ts)-Number(c.ts));let a=[];for(let o of r.slice(-i)){let c=await this.person(o.user),S=!!o.subtype&&!["file_share","thread_broadcast","me_message"].includes(o.subtype);a.push({id:o.ts,conversationId:e,kind:S?"event":"message",text:o.text??null,fromMe:o.user===s.userId,sentAt:L(o.ts),sender:o.user===s.userId?null:c??(o.username?{id:o.bot_id??o.username,name:o.username}:null),targetId:null,reaction:null,reactionRemoved:!1,replyToId:o.thread_ts&&o.thread_ts!==o.ts?o.thread_ts:null,editedAt:o.edited?L(o.edited.ts):null,unsent:!1,attachments:(o.files??[]).map((l)=>({id:l.id,name:l.name??l.title??l.id,mimeType:l.mimetype??null,bytes:l.size??0,path:null,url:l.url_private??null})),cursor:o.ts});for(let l of o.reactions??[])for(let u of l.users??[])a.push({id:`${o.ts}:${l.name}:${u}`,conversationId:e,kind:"reaction",text:null,fromMe:u===s.userId,sentAt:L(o.ts),sender:u===s.userId?null:await this.person(u),targetId:o.ts,reaction:ze[l.name]??`:${l.name}:`,reactionRemoved:!1,replyToId:null,editedAt:null,unsent:!1,attachments:[],cursor:o.ts})}return a}async channel(e){let t=this.channels.get(e);if(t)return t;let{channel:s}=await this.api("conversations.info",{channel:e});return this.channels.set(e,s),s}async archive(e){let t=await this.channel(e),s=this.archiveSupport(t);if(s.mode!=="native")return{mode:s.mode,removed:!1,detail:s.detail};if(t.is_im||t.is_mpim)await this.api("conversations.close",{channel:e},!0);else await this.api("conversations.leave",{channel:e},!0);return this.channels.set(e,{...t,is_open:!1}),{mode:"native",removed:!0,detail:s.detail}}async unarchive(e){let t=await this.channel(e);if(t.is_im||t.is_mpim)await this.api("conversations.open",{channel:e},!0);else if(!t.is_private)await this.api("conversations.join",{channel:e},!0);this.channels.set(e,{...t,is_open:!0})}async attachment(e){if(!e.url)return new Response("Not found",{status:404});return this.fetch(e.url,{headers:{authorization:`Bearer ${this.token}`}})}}function Ge(e,t){switch(e){case"imessage":return new w(t);case"slack":return new M(t);case"discord":return new C(t);default:throw Error(`Unknown inbox driver: ${String(e)}`)}}async function jt(e,t={}){let s=t.driver??"slack";switch(s){case"slack":return T(e);case"discord":return R(e);case"teams":return I(e);default:return{success:!1,provider:s,message:`Unknown chat driver: ${s}`}}}async function Ft(e,t,s){return D(e,t,s)}async function Lt(e,t,s){return b(e,t,s)}async function Bt(e,t){return E(e,t)}function Ut(e){y(e)}function Wt(e){p(e)}function Ht(e){x(e)}export{Wt as configureDiscord,Ut as configureSlack,Ht as configureTeams,U as discord,ge as inbox,jt as send,Lt as sendToDiscord,Ft as sendToSlack,Bt as sendToTeams,W as slack,z 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.48",
5
+ "version": "0.75.49",
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.48"
61
+ "@stacksjs/logging": "0.75.49"
62
62
  },
63
63
  "devDependencies": {
64
- "@stacksjs/cli": "0.75.48",
65
- "@stacksjs/config": "0.75.48",
66
- "@stacksjs/error-handling": "0.75.48",
67
- "@stacksjs/types": "0.75.48",
64
+ "@stacksjs/cli": "0.75.49",
65
+ "@stacksjs/config": "0.75.49",
66
+ "@stacksjs/error-handling": "0.75.49",
67
+ "@stacksjs/types": "0.75.49",
68
68
  "better-dx": "^0.2.24"
69
69
  }
70
70
  }