@stacksjs/chat 0.75.43 → 0.75.44
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/imessage/driver.d.ts +2 -1
- package/dist/index.js +4 -10
- package/package.json +6 -6
|
@@ -16,7 +16,6 @@ export declare interface IMessageConfig {
|
|
|
16
16
|
databasePath?: string
|
|
17
17
|
addressBookDir?: string | false
|
|
18
18
|
openUrl?: (url: string) => void
|
|
19
|
-
runAppleScript?: (script: string) => Promise<boolean>
|
|
20
19
|
}
|
|
21
20
|
/**
|
|
22
21
|
* iMessage and SMS, read from the Messages database on this Mac.
|
|
@@ -28,6 +27,8 @@ export declare interface IMessageConfig {
|
|
|
28
27
|
* archiving opens the conversation in Messages and invokes its own Delete
|
|
29
28
|
* Conversation, which the person confirms. Messages keeps it in Recently
|
|
30
29
|
* Deleted for 30 days, and a new message brings the thread back on its own.
|
|
30
|
+
* Give it a `controller` to have that done by name; without one it only
|
|
31
|
+
* opens the conversation for the person to delete.
|
|
31
32
|
*/
|
|
32
33
|
export declare class IMessageDriver implements InboxDriver {
|
|
33
34
|
readonly provider: 'imessage';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var pe=Object.defineProperty;var ge=(e)=>e;function fe(e,t){this[e]=ge.bind(null,t)}var R=(e,t)=>{for(var s in t)pe(e,s,{get:t[s],enumerable:!0,configurable:!0,set:fe.bind(t,s)})};var U={};R(U,{DiscordDriver:()=>g,Driver:()=>g,configure:()=>p,default:()=>ve,driver:()=>be,send:()=>T,sendEmbed:()=>te,sendWebhook:()=>f});import{log as ee}from"@stacksjs/logging";import{log as ye}from"@stacksjs/logging";class u{config;constructor(e){this.config={maxRetries:e?.maxRetries||3,retryTimeout:e?.retryTimeout||1000,...e}}configure(e){this.config={...this.config,...e}}validateMessage(e){if(!e.to)throw Error("Message recipient is required");if(!e.content&&!e.template)throw Error("Either message content or template is required");return!0}async handleError(e,t){let s=e instanceof Error?e:Error(String(e));ye.error(`[${this.name}] Message sending failed`,{error:s.message,stack:s.stack?.replace(/https?:\/\/[^\s)\]"']+/g,"[REDACTED-URL]"),to:t.to,subject:t.subject});let r={message:`Message sending failed: ${s.message}`,success:!1,provider:this.name};if(t.onError){let n=t.onError(s),i=n instanceof Promise?await n:n;r={...r,...i,success:!1,provider:this.name}}return r}async handleSuccess(e,t){let s={message:"Message sent successfully",success:!0,provider:this.name,messageId:t};try{if(e.handle){let r=e.handle(),n=r instanceof Promise?await r:r;s={...s,...n,success:!0,provider:this.name,messageId:t}}if(e.onSuccess){let r=e.onSuccess(),n=r instanceof Promise?await r:r;s={...s,...n,success:!0,provider:this.name,messageId:t}}}catch(r){return this.handleError(r,e)}return s}}var m={};function p(e){if(e.webhookUrl){let t;try{t=new URL(e.webhookUrl)}catch{throw Error(`[chat/discord] webhookUrl is not a valid URL: ${e.webhookUrl}`)}if(t.protocol!=="https:")throw Error("[chat/discord] webhookUrl must use https://");if(!/(?:^|\.)discord(?:app)?\.com$/i.test(t.hostname))throw Error(`[chat/discord] webhookUrl host "${t.hostname}" is not a Discord-owned domain.`)}m={...m,...e}}class g extends u{name="discord";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)p(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};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(m.webhookUrl)return this.sendViaWebhook(e);if(m.botToken)return this.sendViaBotToken(e);throw Error("Discord not configured: provide webhookUrl or botToken")}async sendViaWebhook(e){let t={content:e.content,username:e.from?.name};if(e.subject||e.template)t.embeds=[this.buildEmbed(e)];let s=await fetch(m.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!s.ok){let n=await s.text();throw Error(`Discord webhook failed: ${s.status} - ${n}`)}if(s.status===204)return{};return{id:(await s.json()).id}}async sendViaBotToken(e){let t=e.to,s={content:e.content};if(e.subject||e.template)s.embeds=[this.buildEmbed(e)];let r=await fetch(`https://discord.com/api/v10/channels/${t}/messages`,{method:"POST",headers:{Authorization:`Bot ${m.botToken}`,"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){let i=await r.json();throw Error(`Discord API failed: ${r.status} - ${i.message}`)}return{id:(await r.json()).id}}buildEmbed(e){let t={};if(e.subject)t.title=e.subject;if(e.content)t.description=e.content;return t.color=5793266,t.timestamp=new Date().toISOString(),t}}async function T(e,t){return new g().send(e,t)}async function f(e,t,s){try{let r={content:t,...s},n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});if(!n.ok){let i=await n.text();return{success:!1,provider:"discord",message:`Webhook failed: ${n.status} - ${i}`}}return{success:!0,provider:"discord",message:"Message sent successfully"}}catch(r){return{success:!1,provider:"discord",message:(r instanceof Error?r:Error(String(r))).message}}}async function te(e,t,s){return f(e,"",{...s,embeds:[t]})}var be=new g,ve={send:T,sendWebhook:f,sendEmbed:te,configure:p,DiscordDriver:g};var W={};R(W,{Driver:()=>b,SlackDriver:()=>b,configure:()=>y,default:()=>_e,driver:()=>xe,send:()=>D,sendWebhook:()=>I});import{log as se}from"@stacksjs/logging";var h={};function y(e){if(e.webhookUrl){let t;try{t=new URL(e.webhookUrl)}catch{throw Error(`[chat/slack] webhookUrl is not a valid URL: ${e.webhookUrl}`)}if(t.protocol!=="https:")throw Error("[chat/slack] webhookUrl must use https:// - Slack does not accept plain HTTP webhooks.");if(!/(?:^|\.)slack\.com$/i.test(t.hostname))throw Error(`[chat/slack] webhookUrl host "${t.hostname}" is not a Slack-owned domain.`)}h={...h,...e}}class b extends u{name="slack";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)y(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};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(h.webhookUrl)return this.sendViaWebhook(e);if(h.botToken)return this.sendViaBotToken(e);throw Error("Slack not configured: provide webhookUrl or botToken")}async sendViaWebhook(e){let s=e.content||e.subject||"",n={text:s.length>40000?`${s.slice(0,39986)}\u2026 [truncated]`:s,username:e.from?.name,mrkdwn:!0};if(e.template)n.blocks=this.buildBlocks(e);let i=await fetch(h.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!i.ok){let o=await i.text();throw Error(`Slack webhook failed: ${i.status} - ${o}`)}return{}}async sendViaBotToken(e){let t={channel:e.to,text:e.content||e.subject,mrkdwn:!0};if(e.template)t.blocks=this.buildBlocks(e);let s=await fetch("https://slack.com/api/chat.postMessage",{method:"POST",headers:{Authorization:`Bearer ${h.botToken}`,"Content-Type":"application/json"},body:JSON.stringify(t)});if(!s.ok)throw Error(`Slack API failed: ${s.status}`);let r=await s.json();if(!r.ok)throw Error(`Slack API error: ${r.error}`);return{ts:r.ts}}buildBlocks(e){let t=[];if(e.subject)t.push({type:"header",text:{type:"plain_text",text:e.subject,emoji:!0}});if(e.content)t.push({type:"section",text:{type:"mrkdwn",text:e.content}});return t}}async function D(e,t){return new b().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 b,_e={send:D,sendWebhook:I,configure:y,SlackDriver:b};var z={};R(z,{Driver:()=>x,TeamsDriver:()=>x,configure:()=>v,default:()=>ke,driver:()=>Ce,send:()=>E,sendCard:()=>ne,sendWebhook:()=>P});import{log as re}from"@stacksjs/logging";var H={};function v(e){H={...H,...e}}class x extends u{name="teams";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)v(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};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 o=await i.text();throw Error(`Teams webhook failed: ${i.status} - ${o}`)}}buildPayload(e){if(!e.subject&&!e.template)return{type:"message",text:e.content||""};let t=this.buildAdaptiveCard(e);return{type:"message",summary:e.subject||e.content?.substring(0,50)||"Notification",attachments:[{contentType:"application/vnd.microsoft.card.adaptive",content:t}]}}buildAdaptiveCard(e){let t=[];if(e.subject)t.push({type:"TextBlock",text:e.subject,size:"Large",weight:"Bolder",wrap:!0});if(e.content)t.push({type:"TextBlock",text:e.content,wrap:!0});return t.push({type:"TextBlock",text:new Date().toLocaleString(),size:"Small",color:"Dark",wrap:!0}),{type:"AdaptiveCard",version:"1.4",$schema:"http://adaptivecards.io/schemas/adaptive-card.json",body:t}}}async function E(e,t){return new x().send(e,t)}async function P(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 x,ke={send:E,sendWebhook:P,sendCard:ne,configure:v,TeamsDriver:x};var he={};R(he,{DiscordInboxDriver:()=>_,IMessageDriver:()=>w,SlackApiError:()=>B,SlackInboxDriver:()=>M,createInboxDriver:()=>Je,decodeAttributedBody:()=>C,formatHandle:()=>O,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 _{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((o)=>we.has(o.type)).map((o)=>({...o,guild_id:r.id}))),t.push(...i.threads.map((o)=>({...o,guild_id:o.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=(o,a)=>BigInt(o.id)<BigInt(a.id)?-1:1;if(t.after){let o=t.after;while(r.length<s){let a=await this.api(`/channels/${e}/messages?limit=100&after=${o}`);if(a.length===0)break;if(a.sort(n),r.push(...a),o=a[a.length-1].id,a.length<100)break}}else{let o="";while(r.length<s){let a=await this.api(`/channels/${e}/messages?limit=${Math.min(100,s-r.length)}${o?`&before=${o}`:""}`);if(a.length===0)break;if(a.sort(n),r.unshift(...a),o=a[0].id,a.length<100)break}}let i=[];for(let o of r.slice(0,s)){let a=o.author.id===this.userId;i.push({id:o.id,conversationId:e,kind:[0,19,20,21].includes(o.type)?"message":"event",text:o.content||null,fromMe:a,sentAt:Date.parse(o.timestamp),sender:a?null:{id:o.author.id,name:o.author.global_name||o.author.username},targetId:null,reaction:null,reactionRemoved:!1,replyToId:o.message_reference?.message_id??null,editedAt:o.edited_timestamp?Date.parse(o.edited_timestamp):null,unsent:!1,attachments:(o.attachments??[]).map((c)=>({id:c.id,name:c.filename,mimeType:c.content_type??null,bytes:c.size,path:null,url:c.url})),cursor:o.id});for(let c of o.reactions??[])i.push({id:`${o.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(o.timestamp),sender:null,targetId:o.id,reaction:c.emoji.name??"\u2022",reactionRemoved:!1,replyToId:null,editedAt:null,unsent:!1,attachments:[],cursor:o.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 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,Me=new TextDecoder("utf-8",{fatal:!1}),Se=[ae.encode("NSString"),ae.encode("NSMutableString")];function Ae(e,t,s=0){e:for(let r=s;r<=e.length-t.length;r++){for(let n=0;n<t.length;n++)if(e[r+n]!==t[n])continue e;return r}return-1}function Re(e,t){let s=e[t];if(s===void 0)return null;let r=new DataView(e.buffer,e.byteOffset,e.byteLength);if(s===129){if(t+3>e.length)return null;return{length:r.getUint16(t+1,!0),start:t+3}}if(s===130){if(t+5>e.length)return null;return{length:r.getUint32(t+1,!0),start:t+5}}return{length:s,start:t+1}}function C(e){if(!e||e.length===0)return null;for(let t of Se){let s=Ae(e,t);if(s===-1)continue;let r=s+t.length,n=Math.min(e.length,r+16);for(let i=r;i<n;i++){if(e[i]!==43)continue;let o=Re(e,i+1);if(!o||o.start+o.length>e.length)continue;return Me.decode(e.subarray(o.start,o.start+o.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 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 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 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:()=>p,Driver:()=>p,configure:()=>g,default:()=>ve,driver:()=>be,send:()=>T,sendEmbed:()=>te,sendWebhook:()=>f});import{log as ee}from"@stacksjs/logging";import{log as ye}from"@stacksjs/logging";class u{config;constructor(e){this.config={maxRetries:e?.maxRetries||3,retryTimeout:e?.retryTimeout||1000,...e}}configure(e){this.config={...this.config,...e}}validateMessage(e){if(!e.to)throw Error("Message recipient is required");if(!e.content&&!e.template)throw Error("Either message content or template is required");return!0}async handleError(e,t){let s=e instanceof Error?e:Error(String(e));ye.error(`[${this.name}] Message sending failed`,{error:s.message,stack:s.stack?.replace(/https?:\/\/[^\s)\]"']+/g,"[REDACTED-URL]"),to:t.to,subject:t.subject});let r={message:`Message sending failed: ${s.message}`,success:!1,provider:this.name};if(t.onError){let n=t.onError(s),i=n instanceof Promise?await n:n;r={...r,...i,success:!1,provider:this.name}}return r}async handleSuccess(e,t){let s={message:"Message sent successfully",success:!0,provider:this.name,messageId:t};try{if(e.handle){let r=e.handle(),n=r instanceof Promise?await r:r;s={...s,...n,success:!0,provider:this.name,messageId:t}}if(e.onSuccess){let r=e.onSuccess(),n=r instanceof Promise?await r:r;s={...s,...n,success:!0,provider:this.name,messageId:t}}}catch(r){return this.handleError(r,e)}return s}}var m={};function g(e){if(e.webhookUrl){let t;try{t=new URL(e.webhookUrl)}catch{throw Error(`[chat/discord] webhookUrl is not a valid URL: ${e.webhookUrl}`)}if(t.protocol!=="https:")throw Error("[chat/discord] webhookUrl must use https://");if(!/(?:^|\.)discord(?:app)?\.com$/i.test(t.hostname))throw Error(`[chat/discord] webhookUrl host "${t.hostname}" is not a Discord-owned domain.`)}m={...m,...e}}class p extends u{name="discord";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)g(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(m.webhookUrl)return this.sendViaWebhook(e);if(m.botToken)return this.sendViaBotToken(e);throw Error("Discord not configured: provide webhookUrl or botToken")}async sendViaWebhook(e){let t={content:e.content,username:e.from?.name};if(e.subject||e.template)t.embeds=[this.buildEmbed(e)];let s=await fetch(m.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!s.ok){let n=await s.text();throw Error(`Discord webhook failed: ${s.status} - ${n}`)}if(s.status===204)return{};return{id:(await s.json()).id}}async sendViaBotToken(e){let t=e.to,s={content:e.content};if(e.subject||e.template)s.embeds=[this.buildEmbed(e)];let r=await fetch(`https://discord.com/api/v10/channels/${t}/messages`,{method:"POST",headers:{Authorization:`Bot ${m.botToken}`,"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){let i=await r.json();throw Error(`Discord API failed: ${r.status} - ${i.message}`)}return{id:(await r.json()).id}}buildEmbed(e){let t={};if(e.subject)t.title=e.subject;if(e.content)t.description=e.content;return t.color=5793266,t.timestamp=new Date().toISOString(),t}}async function T(e,t){return new p().send(e,t)}async function f(e,t,s){try{let r={content:t,...s},n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});if(!n.ok){let i=await n.text();return{success:!1,provider:"discord",message:`Webhook failed: ${n.status} - ${i}`}}return{success:!0,provider:"discord",message:"Message sent successfully"}}catch(r){return{success:!1,provider:"discord",message:(r instanceof Error?r:Error(String(r))).message}}}async function te(e,t,s){return f(e,"",{...s,embeds:[t]})}var be=new p,ve={send:T,sendWebhook:f,sendEmbed:te,configure:g,DiscordDriver:p};var W={};R(W,{Driver:()=>b,SlackDriver:()=>b,configure:()=>y,default:()=>_e,driver:()=>xe,send:()=>D,sendWebhook:()=>I});import{log as se}from"@stacksjs/logging";var h={};function y(e){if(e.webhookUrl){let t;try{t=new URL(e.webhookUrl)}catch{throw Error(`[chat/slack] webhookUrl is not a valid URL: ${e.webhookUrl}`)}if(t.protocol!=="https:")throw Error("[chat/slack] webhookUrl must use https:// - Slack does not accept plain HTTP webhooks.");if(!/(?:^|\.)slack\.com$/i.test(t.hostname))throw Error(`[chat/slack] webhookUrl host "${t.hostname}" is not a Slack-owned domain.`)}h={...h,...e}}class b extends u{name="slack";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)y(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};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(h.webhookUrl)return this.sendViaWebhook(e);if(h.botToken)return this.sendViaBotToken(e);throw Error("Slack not configured: provide webhookUrl or botToken")}async sendViaWebhook(e){let s=e.content||e.subject||"",n={text:s.length>40000?`${s.slice(0,39986)}\u2026 [truncated]`:s,username:e.from?.name,mrkdwn:!0};if(e.template)n.blocks=this.buildBlocks(e);let i=await fetch(h.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!i.ok){let o=await i.text();throw Error(`Slack webhook failed: ${i.status} - ${o}`)}return{}}async sendViaBotToken(e){let t={channel:e.to,text:e.content||e.subject,mrkdwn:!0};if(e.template)t.blocks=this.buildBlocks(e);let s=await fetch("https://slack.com/api/chat.postMessage",{method:"POST",headers:{Authorization:`Bearer ${h.botToken}`,"Content-Type":"application/json"},body:JSON.stringify(t)});if(!s.ok)throw Error(`Slack API failed: ${s.status}`);let r=await s.json();if(!r.ok)throw Error(`Slack API error: ${r.error}`);return{ts:r.ts}}buildBlocks(e){let t=[];if(e.subject)t.push({type:"header",text:{type:"plain_text",text:e.subject,emoji:!0}});if(e.content)t.push({type:"section",text:{type:"mrkdwn",text:e.content}});return t}}async function D(e,t){return new b().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 b,_e={send:D,sendWebhook:I,configure:y,SlackDriver:b};var z={};R(z,{Driver:()=>x,TeamsDriver:()=>x,configure:()=>v,default:()=>ke,driver:()=>Ce,send:()=>E,sendCard:()=>ne,sendWebhook:()=>O});import{log as re}from"@stacksjs/logging";var H={};function v(e){H={...H,...e}}class x extends u{name="teams";constructor(e){super({maxRetries:e?.maxRetries??3,retryTimeout:e?.retryTimeout??1000});if(e)v(e)}async send(e,t){let s={provider:this.name,to:e.to,subject:e.subject||"No subject"};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 o=await i.text();throw Error(`Teams webhook failed: ${i.status} - ${o}`)}}buildPayload(e){if(!e.subject&&!e.template)return{type:"message",text:e.content||""};let t=this.buildAdaptiveCard(e);return{type:"message",summary:e.subject||e.content?.substring(0,50)||"Notification",attachments:[{contentType:"application/vnd.microsoft.card.adaptive",content:t}]}}buildAdaptiveCard(e){let t=[];if(e.subject)t.push({type:"TextBlock",text:e.subject,size:"Large",weight:"Bolder",wrap:!0});if(e.content)t.push({type:"TextBlock",text:e.content,wrap:!0});return t.push({type:"TextBlock",text:new Date().toLocaleString(),size:"Small",color:"Dark",wrap:!0}),{type:"AdaptiveCard",version:"1.4",$schema:"http://adaptivecards.io/schemas/adaptive-card.json",body:t}}}async function E(e,t){return new x().send(e,t)}async function O(e,t){try{let r=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"message",text:t})});if(!r.ok){let n=await r.text();return{success:!1,provider:"teams",message:`Webhook failed: ${r.status} - ${n}`}}return{success:!0,provider:"teams",message:"Message sent successfully"}}catch(s){return{success:!1,provider:"teams",message:(s instanceof Error?s:Error(String(s))).message}}}async function 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 x,ke={send:E,sendWebhook:O,sendCard:ne,configure:v,TeamsDriver:x};var he={};R(he,{DiscordInboxDriver:()=>_,IMessageDriver:()=>w,SlackApiError:()=>B,SlackInboxDriver:()=>M,createInboxDriver:()=>ze,decodeAttributedBody:()=>C,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 _{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((o)=>we.has(o.type)).map((o)=>({...o,guild_id:r.id}))),t.push(...i.threads.map((o)=>({...o,guild_id:o.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=(o,a)=>BigInt(o.id)<BigInt(a.id)?-1:1;if(t.after){let o=t.after;while(r.length<s){let a=await this.api(`/channels/${e}/messages?limit=100&after=${o}`);if(a.length===0)break;if(a.sort(n),r.push(...a),o=a[a.length-1].id,a.length<100)break}}else{let o="";while(r.length<s){let a=await this.api(`/channels/${e}/messages?limit=${Math.min(100,s-r.length)}${o?`&before=${o}`:""}`);if(a.length===0)break;if(a.sort(n),r.unshift(...a),o=a[0].id,a.length<100)break}}let i=[];for(let o of r.slice(0,s)){let a=o.author.id===this.userId;i.push({id:o.id,conversationId:e,kind:[0,19,20,21].includes(o.type)?"message":"event",text:o.content||null,fromMe:a,sentAt:Date.parse(o.timestamp),sender:a?null:{id:o.author.id,name:o.author.global_name||o.author.username},targetId:null,reaction:null,reactionRemoved:!1,replyToId:o.message_reference?.message_id??null,editedAt:o.edited_timestamp?Date.parse(o.edited_timestamp):null,unsent:!1,attachments:(o.attachments??[]).map((c)=>({id:c.id,name:c.filename,mimeType:c.content_type??null,bytes:c.size,path:null,url:c.url})),cursor:o.id});for(let c of o.reactions??[])i.push({id:`${o.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(o.timestamp),sender:null,targetId:o.id,reaction:c.emoji.name??"\u2022",reactionRemoved:!1,replyToId:null,editedAt:null,unsent:!1,attachments:[],cursor:o.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 C(e){if(!e||e.length===0)return null;for(let t of Se){let s=Ae(e,t);if(s===-1)continue;let r=s+t.length,n=Math.min(e.length,r+16);for(let i=r;i<n;i++){if(e[i]!==43)continue;let o=Re(e,i+1);if(!o||o.start+o.length>e.length)continue;return Me.decode(e.subarray(o.start,o.start+o.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(`
|
|
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,
|
|
@@ -28,17 +28,11 @@ var pe=Object.defineProperty;var ge=(e)=>e;function fe(e,t){this[e]=ge.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(i.filter((a)=>a.cache_has_attachments===1).map((a)=>a.rowid));return i.map((a)=>{let c=a.associated_message_type??0,S=a.item_type??0,l=Ee(c)?"reaction":S!==0?"event":"message";return{rowid:a.rowid,guid:a.guid,chatGuid:a.chat_guid,kind:l,text:a.text?.trim()?a.text:C(a.attributedBody),isFromMe:a.is_from_me===1,sentAt:K(a.date)??0,sender:a.sender?d(a.sender):null,service:a.service??"iMessage",targetGuid:l==="reaction"?
|
|
31
|
+
`).all(...e,t.afterRowId??0,t.limit??1e6),o=this.attachmentsFor(i.filter((a)=>a.cache_has_attachments===1).map((a)=>a.rowid));return i.map((a)=>{let c=a.associated_message_type??0,S=a.item_type??0,l=Ee(c)?"reaction":S!==0?"event":"message";return{rowid:a.rowid,guid:a.guid,chatGuid:a.chat_guid,kind:l,text:a.text?.trim()?a.text:C(a.attributedBody),isFromMe:a.is_from_me===1,sentAt:K(a.date)??0,sender:a.sender?d(a.sender):null,service:a.service??"iMessage",targetGuid:l==="reaction"?Oe(a.associated_message_guid):null,reactionType:l==="reaction"?c:0,replyToGuid:a.reply_to_guid||null,editedAt:K(a.date_edited),unsent:!!a.date_retracted,eventType:S,groupTitle:a.group_title||null,attachments:o.get(a.rowid)??[]}})}attachmentPath(e){let t=this.db.query("SELECT filename, mime_type, transfer_name FROM attachment WHERE guid = ?").get(e);if(!t)return null;let s=oe(t.filename);return{path:s,mimeType:t.mime_type,name:t.transfer_name||(s?s.split("/").pop():e)}}attachmentsFor(e){let t=new Map;for(let s=0;s<e.length;s+=500){let r=e.slice(s,s+500),n=this.db.query(`
|
|
32
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 o=t.get(i.message_id)??[],a=oe(i.filename);o.push({guid:i.guid,path:a,name:i.transfer_name||(a?a.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,o)}}return t}}import{Database as
|
|
35
|
+
`).all(...r);for(let i of n){let o=t.get(i.message_id)??[],a=oe(i.filename);o.push({guid:i.guid,path:a,name:i.transfer_name||(a?a.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,o)}}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 k{byHandle=new Map;static load(e=Q){let t=new k;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(`
|
|
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 o of s){let c=[o.first,o.last].filter(Boolean).join(" ").trim()||o.nick?.trim()||o.org?.trim();if(c)r.set(o.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 o of[...n,...i]){let a=r.get(o.owner);if(!a||!o.value)continue;let c=d(o.value);if(!this.byHandle.has(c))this.byHandle.set(c,{name:a,initials:me(a)})}}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"},We=`
|
|
39
|
-
tell application "Messages" to activate
|
|
40
|
-
delay 0.8
|
|
41
|
-
tell application "System Events" to tell process "Messages"
|
|
42
|
-
click menu item "Delete Conversation\u2026" of menu "Conversation" of menu bar 1
|
|
43
|
-
end tell
|
|
44
|
-
`;function He(e){Bun.spawn(["/usr/bin/open",e],{stdio:["ignore","ignore","ignore"]})}async function ze(e){return await Bun.spawn(["/usr/bin/osascript","-e",e],{stdio:["ignore","ignore","ignore"]}).exited===0}class w{provider="imessage";label="iMessage";databasePath;addressBookDir;openUrl;runAppleScript;controller;confirmDeletes;contacts=null;contactsLoadedAt=0;constructor(e={}){this.databasePath=e.databasePath??Z,this.addressBookDir=e.addressBookDir??Q,this.openUrl=e.openUrl??He,this.runAppleScript=e.runAppleScript??ze,this.controller=e.controller,this.confirmDeletes=e.confirmDeletes??!1}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(Be.platform!=="darwin")return{connected:!1,detail:"iMessage is only available on a Mac.",needs:"unsupported-platform"};try{return this.open(()=>{return}),{connected:!0,detail:"Reading Messages on this Mac."}}catch(e){if(e instanceof j&&e.needsFullDiskAccess&&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,archive:t?{mode:"confirm",detail:"Opens it in Messages at Delete Conversation for you to confirm. Messages keeps it in Recently Deleted for 30 days."}:{mode:"unsupported",detail:"Messages cannot open this conversation by address, so delete it there yourself."},url:t}}async messages(e,t={}){return this.open((s)=>{let r=this.find(s,e);if(!r)return[];return(t.after||!t.limit?s.messages(r.chatGuids,{afterRowId:Number(t.after)||0,byRowId:!0,limit:t.limit}):s.messages(r.chatGuids,{newestFirst:!0,limit:t.limit}).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"?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((i)=>this.find(i,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 i=await this.controller.remove(r,s,this.confirmDeletes);return{mode:"confirm",removed:!1,detail:i.ok?this.confirmDeletes?"Deleted in Messages.":"Messages is asking you to confirm the delete.":i.detail}}return this.openUrl(s),{mode:"confirm",removed:!1,detail:await this.runAppleScript(We)?"Messages is asking you to confirm the delete.":"Messages is open on the conversation: choose Conversation > Delete Conversation to finish. (Allow Accessibility access to have this done for you.)"}}async unarchive(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 Ge={"+1":"\uD83D\uDC4D",thumbsup:"\uD83D\uDC4D","-1":"\uD83D\uDC4E",heart:"\u2764\uFE0F",joy:"\uD83D\uDE02",laughing:"\uD83D\uDE06",tada:"\uD83C\uDF89",eyes:"\uD83D\uDC40",fire:"\uD83D\uDD25",white_check_mark:"\u2705",pray:"\uD83D\uDE4F",raised_hands:"\uD83D\uDE4C",rocket:"\uD83D\uDE80"},L=(e)=>Math.round(Number(e)*1000);class 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(([a,c])=>[a,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 o=await i.json();if(!o.ok)throw new B(e,o.error??`http_${i.status}`);return o}}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],o=[];if(n.is_im)o=[await this.person(n.user)];else if(n.is_mpim){let a=await this.api("conversations.members",{channel:n.id,limit:50}).catch(()=>({members:[]}));o=await Promise.all(a.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:o,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 a=await this.api("conversations.history",{channel:e,limit:Math.min(200,i),...t.after?{oldest:t.after,inclusive:!1}:{},...n?{cursor:n}:{}});r.push(...a.messages),n=a.has_more?a.response_metadata?.next_cursor??"":""}while(n&&r.length<i);r.sort((a,c)=>Number(a.ts)-Number(c.ts));let o=[];for(let a of r.slice(-i)){let c=await this.person(a.user),S=!!a.subtype&&!["file_share","thread_broadcast","me_message"].includes(a.subtype);o.push({id:a.ts,conversationId:e,kind:S?"event":"message",text:a.text??null,fromMe:a.user===s.userId,sentAt:L(a.ts),sender:a.user===s.userId?null:c??(a.username?{id:a.bot_id??a.username,name:a.username}:null),targetId:null,reaction:null,reactionRemoved:!1,replyToId:a.thread_ts&&a.thread_ts!==a.ts?a.thread_ts:null,editedAt:a.edited?L(a.edited.ts):null,unsent:!1,attachments:(a.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:a.ts});for(let l of a.reactions??[])for(let A of l.users??[])o.push({id:`${a.ts}:${l.name}:${A}`,conversationId:e,kind:"reaction",text:null,fromMe:A===s.userId,sentAt:L(a.ts),sender:A===s.userId?null:await this.person(A),targetId:a.ts,reaction:Ge[l.name]??`:${l.name}:`,reactionRemoved:!1,replyToId:null,editedAt:null,unsent:!1,attachments:[],cursor:a.ts})}return o}async channel(e){let t=this.channels.get(e);if(t)return t;let{channel:s}=await this.api("conversations.info",{channel:e});return this.channels.set(e,s),s}async archive(e){let t=await this.channel(e),s=this.archiveSupport(t);if(s.mode!=="native")return{mode:s.mode,removed:!1,detail:s.detail};if(t.is_im||t.is_mpim)await this.api("conversations.close",{channel:e},!0);else await this.api("conversations.leave",{channel:e},!0);return this.channels.set(e,{...t,is_open:!1}),{mode:"native",removed:!0,detail:s.detail}}async unarchive(e){let t=await this.channel(e);if(t.is_im||t.is_mpim)await this.api("conversations.open",{channel:e},!0);else if(!t.is_private)await this.api("conversations.join",{channel:e},!0);this.channels.set(e,{...t,is_open:!0})}async attachment(e){if(!e.url)return new Response("Not found",{status:404});return this.fetch(e.url,{headers:{authorization:`Bearer ${this.token}`}})}}function Je(e,t){switch(e){case"imessage":return new w(t);case"slack":return new M(t);case"discord":return new _(t);default:throw Error(`Unknown inbox driver: ${String(e)}`)}}async function Ft(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 Lt(e,t,s){return I(e,t,s)}async function Bt(e,t,s){return f(e,t,s)}async function Ut(e,t){return P(e,t)}function Wt(e){y(e)}function Ht(e){p(e)}function zt(e){v(e)}export{Ht as configureDiscord,Wt as configureSlack,zt as configureTeams,U as discord,he as inbox,Ft as send,Bt as sendToDiscord,Lt as sendToSlack,Ut as sendToTeams,W as slack,z as teams};
|
|
38
|
+
`).all(),r=new Map;for(let o of s){let c=[o.first,o.last].filter(Boolean).join(" ").trim()||o.nick?.trim()||o.org?.trim();if(c)r.set(o.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 o of[...n,...i]){let a=r.get(o.owner);if(!a||!o.value)continue;let c=d(o.value);if(!this.byHandle.has(c))this.byHandle.set(c,{name:a,initials:me(a)})}}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 w{provider="imessage";label="iMessage";databasePath;addressBookDir;openUrl;controller;confirmDeletes;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}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 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(Be.platform!=="darwin")return{connected:!1,detail:"iMessage is only available on a Mac.",needs:"unsupported-platform"};try{return this.open(()=>{return}),{connected:!0,detail:"Reading Messages on this Mac."}}catch(e){if(e instanceof j&&e.needsFullDiskAccess&&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,archive:t?{mode:"confirm",detail:"Opens it in Messages at Delete Conversation for you to confirm. Messages keeps it in Recently Deleted for 30 days."}:{mode:"unsupported",detail:"Messages cannot open this conversation by address, so delete it there yourself."},url:t}}async messages(e,t={}){return this.open((s)=>{let r=this.find(s,e);if(!r)return[];return(t.after||!t.limit?s.messages(r.chatGuids,{afterRowId:Number(t.after)||0,byRowId:!0,limit:t.limit}):s.messages(r.chatGuids,{newestFirst:!0,limit:t.limit}).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"?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 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(([a,c])=>[a,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 o=await i.json();if(!o.ok)throw new B(e,o.error??`http_${i.status}`);return o}}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],o=[];if(n.is_im)o=[await this.person(n.user)];else if(n.is_mpim){let a=await this.api("conversations.members",{channel:n.id,limit:50}).catch(()=>({members:[]}));o=await Promise.all(a.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:o,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 a=await this.api("conversations.history",{channel:e,limit:Math.min(200,i),...t.after?{oldest:t.after,inclusive:!1}:{},...n?{cursor:n}:{}});r.push(...a.messages),n=a.has_more?a.response_metadata?.next_cursor??"":""}while(n&&r.length<i);r.sort((a,c)=>Number(a.ts)-Number(c.ts));let o=[];for(let a of r.slice(-i)){let c=await this.person(a.user),S=!!a.subtype&&!["file_share","thread_broadcast","me_message"].includes(a.subtype);o.push({id:a.ts,conversationId:e,kind:S?"event":"message",text:a.text??null,fromMe:a.user===s.userId,sentAt:L(a.ts),sender:a.user===s.userId?null:c??(a.username?{id:a.bot_id??a.username,name:a.username}:null),targetId:null,reaction:null,reactionRemoved:!1,replyToId:a.thread_ts&&a.thread_ts!==a.ts?a.thread_ts:null,editedAt:a.edited?L(a.edited.ts):null,unsent:!1,attachments:(a.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:a.ts});for(let l of a.reactions??[])for(let A of l.users??[])o.push({id:`${a.ts}:${l.name}:${A}`,conversationId:e,kind:"reaction",text:null,fromMe:A===s.userId,sentAt:L(a.ts),sender:A===s.userId?null:await this.person(A),targetId:a.ts,reaction:He[l.name]??`:${l.name}:`,reactionRemoved:!1,replyToId:null,editedAt:null,unsent:!1,attachments:[],cursor:a.ts})}return o}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 w(t);case"slack":return new M(t);case"discord":return new _(t);default:throw Error(`Unknown inbox driver: ${String(e)}`)}}async function Nt(e,t={}){let s=t.driver??"slack";switch(s){case"slack":return 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 f(e,t,s)}async function Lt(e,t){return O(e,t)}function Bt(e){y(e)}function Ut(e){g(e)}function Wt(e){v(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};
|
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.44",
|
|
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.44"
|
|
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.44",
|
|
65
|
+
"@stacksjs/config": "0.75.44",
|
|
66
|
+
"@stacksjs/error-handling": "0.75.44",
|
|
67
|
+
"@stacksjs/types": "0.75.44",
|
|
68
68
|
"better-dx": "^0.2.24"
|
|
69
69
|
}
|
|
70
70
|
}
|