@@ -7,6 +7,8 @@ import { z } from "zod";
77import { syncBillingBridge } from "../billing" ;
88import { env } from "../env" ;
99import type { AppBindings } from "../hono-bindings" ;
10+ import { classifyContactStatus , getContactByEmail } from "../integration/loops" ;
11+ import { postThreadReply } from "../integration/slack" ;
1012import { stripeSync } from "../integration/stripe-sync" ;
1113import { API_TAGS } from "./constants" ;
1214
@@ -78,3 +80,126 @@ webhook.post(
7880 return c . json ( { ok : true } , 200 ) ;
7981 } ,
8082) ;
83+
84+ const SlackEventSchema = z . object ( {
85+ type : z . string ( ) ,
86+ challenge : z . string ( ) . optional ( ) ,
87+ event : z
88+ . object ( {
89+ type : z . string ( ) ,
90+ channel : z . string ( ) . optional ( ) ,
91+ ts : z . string ( ) . optional ( ) ,
92+ text : z . string ( ) . optional ( ) ,
93+ bot_id : z . string ( ) . optional ( ) ,
94+ user : z . string ( ) . optional ( ) ,
95+ } )
96+ . optional ( ) ,
97+ } ) ;
98+
99+ function extractEmailFromLoopsMessage ( text : string ) : string | null {
100+ const mailtoMatch = text . match ( / < m a i l t o : ( [ ^ | ] + ) \| / ) ;
101+ if ( mailtoMatch ) {
102+ return mailtoMatch [ 1 ] ;
103+ }
104+ const emailMatch = text . match (
105+ / [ A - Z a - z 0 - 9 . _ % + - ] + @ [ A - Z a - z 0 - 9 . - ] + \. [ A - Z a - z ] { 2 , } / ,
106+ ) ;
107+ return emailMatch ? emailMatch [ 0 ] : null ;
108+ }
109+
110+ webhook . post (
111+ "/slack/events" ,
112+ describeRoute ( {
113+ tags : [ API_TAGS . PRIVATE_SKIP_OPENAPI ] ,
114+ responses : {
115+ 200 : {
116+ description : "result" ,
117+ content : {
118+ "application/json" : {
119+ schema : resolver ( WebhookSuccessSchema ) ,
120+ } ,
121+ } ,
122+ } ,
123+ 400 : { description : "-" } ,
124+ 500 : { description : "-" } ,
125+ } ,
126+ } ) ,
127+ async ( c ) => {
128+ const rawBody = c . get ( "slackRawBody" ) ;
129+ const span = c . get ( "sentrySpan" ) ;
130+
131+ let payload : z . infer < typeof SlackEventSchema > ;
132+ try {
133+ payload = SlackEventSchema . parse ( JSON . parse ( rawBody ) ) ;
134+ } catch {
135+ return c . json ( { error : "invalid_payload" } , 400 ) ;
136+ }
137+
138+ if ( payload . type === "url_verification" && payload . challenge ) {
139+ return c . json ( { challenge : payload . challenge } , 200 ) ;
140+ }
141+
142+ if ( payload . type !== "event_callback" || ! payload . event ) {
143+ return c . json ( { ok : true } , 200 ) ;
144+ }
145+
146+ const event = payload . event ;
147+ span ?. setAttribute ( "slack.event_type" , event . type ) ;
148+
149+ if ( event . type !== "message" || ! event . bot_id ) {
150+ return c . json ( { ok : true } , 200 ) ;
151+ }
152+
153+ if (
154+ env . LOOPS_SLACK_CHANNEL_ID &&
155+ event . channel !== env . LOOPS_SLACK_CHANNEL_ID
156+ ) {
157+ return c . json ( { ok : true } , 200 ) ;
158+ }
159+
160+ if (
161+ ! event . text ||
162+ ! event . text . includes ( "was added to your account" ) ||
163+ ! event . ts ||
164+ ! event . channel
165+ ) {
166+ return c . json ( { ok : true } , 200 ) ;
167+ }
168+
169+ const email = extractEmailFromLoopsMessage ( event . text ) ;
170+ if ( ! email ) {
171+ return c . json ( { ok : true } , 200 ) ;
172+ }
173+
174+ try {
175+ const contact = await getContactByEmail ( email ) ;
176+ if ( ! contact ) {
177+ return c . json ( { ok : true } , 200 ) ;
178+ }
179+
180+ const status = classifyContactStatus ( contact ) ;
181+ const source = contact . source || "Unknown" ;
182+ const details = [
183+ `Source: ${ source } ` ,
184+ contact . intent ? `Intent: ${ contact . intent } ` : null ,
185+ contact . platform ? `Platform: ${ contact . platform } ` : null ,
186+ ]
187+ . filter ( Boolean )
188+ . join ( ", " ) ;
189+
190+ await postThreadReply (
191+ event . channel ,
192+ event . ts ,
193+ `Status: ${ status } (${ details } )` ,
194+ ) ;
195+ } catch ( error ) {
196+ Sentry . captureException ( error , {
197+ tags : { webhook : "slack" , step : "loops_source_thread" } ,
198+ extra : { email } ,
199+ } ) ;
200+ return c . json ( { error : "failed_to_process" } , 500 ) ;
201+ }
202+
203+ return c . json ( { ok : true } , 200 ) ;
204+ } ,
205+ ) ;
0 commit comments