@@ -191,8 +191,17 @@ type SubDigest struct {
191191}
192192
193193type SlackMessage struct {
194- Text string `json:"text,omitempty"`
195- Blocks []Block `json:"blocks,omitempty"`
194+ Text string `json:"text,omitempty"`
195+ Blocks []Block `json:"blocks,omitempty"`
196+ Channel string `json:"channel,omitempty"`
197+ ThreadTS string `json:"thread_ts,omitempty"`
198+ }
199+
200+ type SlackResponse struct {
201+ OK bool `json:"ok"`
202+ Error string `json:"error,omitempty"`
203+ TS string `json:"ts,omitempty"`
204+ Channel string `json:"channel,omitempty"`
196205}
197206
198207type Block struct {
@@ -995,14 +1004,7 @@ func digestHandler(w http.ResponseWriter, r *http.Request) {
9951004}
9961005
9971006func sendDigestToSlack (customers map [string ]* CustomerDigest , order []string , quotes []QuoteAPIData ) error {
998- message := formatDigestMessage (customers , order , quotes )
999-
1000- // Slack supports up to 40,000 chars, but for readability split at ~10k
1001- if len (message .Text ) <= 10000 {
1002- return sendSlackNotification (message )
1003- }
1004-
1005- // If too long, send header + quotes first, then customers in batches
1007+ // Send header message first (this becomes the parent for threading)
10061008 header := fmt .Sprintf (":bar_chart: *Weekly Payment Digest* — %s\n \n *%d customers* with subscriptions" ,
10071009 time .Now ().Format ("Jan 2, 2006" ), len (customers ))
10081010
@@ -1011,35 +1013,55 @@ func sendDigestToSlack(customers map[string]*CustomerDigest, order []string, quo
10111013 header += "\n \n " + formatQuotesSection (quotes )
10121014 }
10131015
1014- if err := sendSlackNotification (SlackMessage {Text : header }); err != nil {
1015- return err
1016- }
1017-
1018- // Send customers in batches to avoid hitting limits
1019- var batch strings.Builder
1020- for i , accountID := range order {
1021- cd := customers [accountID ]
1022- section := formatCustomerSection (cd )
1016+ parentTS , err := sendSlackMessage (SlackMessage {Text : header })
1017+ if err != nil {
1018+ return fmt .Errorf ("failed to send header: %w" , err )
1019+ }
1020+
1021+ // If no threading support (no bot token), fall back to simple messages
1022+ if parentTS == "" {
1023+ // Send customers in batches without threading
1024+ var batch strings.Builder
1025+ for i , accountID := range order {
1026+ cd := customers [accountID ]
1027+ section := formatCustomerSection (cd )
1028+
1029+ if batch .Len ()+ len (section ) > 8000 {
1030+ if err := sendSlackNotification (SlackMessage {Text : batch .String ()}); err != nil {
1031+ log .Printf ("Failed to send digest batch: %v" , err )
1032+ }
1033+ batch .Reset ()
1034+ }
10231035
1024- if batch .Len ()+ len (section ) > 8000 {
1025- // Send current batch and start new one
1026- if err := sendSlackNotification (SlackMessage {Text : batch .String ()}); err != nil {
1027- log .Printf ("Failed to send digest batch: %v" , err )
1036+ if i > 0 && batch .Len () > 0 {
1037+ batch .WriteString ("\n \n " )
10281038 }
1029- batch .Reset ( )
1039+ batch .WriteString ( section )
10301040 }
10311041
1032- if i > 0 && batch .Len () > 0 {
1033- batch .WriteString ("\n \n " )
1042+ if batch .Len () > 0 {
1043+ if err := sendSlackNotification (SlackMessage {Text : batch .String ()}); err != nil {
1044+ log .Printf ("Failed to send final digest batch: %v" , err )
1045+ }
10341046 }
1035- batch . WriteString ( section )
1047+ return nil
10361048 }
10371049
1038- // Send remaining batch
1039- if batch .Len () > 0 {
1040- if err := sendSlackNotification (SlackMessage {Text : batch .String ()}); err != nil {
1041- log .Printf ("Failed to send final digest batch: %v" , err )
1050+ // Send each customer as a threaded reply
1051+ for _ , accountID := range order {
1052+ cd := customers [accountID ]
1053+ section := formatCustomerSection (cd )
1054+
1055+ _ , err := sendSlackMessage (SlackMessage {
1056+ Text : section ,
1057+ ThreadTS : parentTS ,
1058+ })
1059+ if err != nil {
1060+ log .Printf ("Failed to send threaded message for %s: %v" , accountID , err )
10421061 }
1062+
1063+ // Small delay to avoid rate limits
1064+ time .Sleep (100 * time .Millisecond )
10431065 }
10441066
10451067 return nil
@@ -1158,6 +1180,53 @@ func formatQuotesSection(quotes []QuoteAPIData) string {
11581180 return section
11591181}
11601182
1183+ // sendSlackMessage sends a message and returns the timestamp (for threading)
1184+ func sendSlackMessage (message SlackMessage ) (string , error ) {
1185+ botToken := os .Getenv ("SLACK_BOT_TOKEN" )
1186+ channel := os .Getenv ("SLACK_CHANNEL_ID" )
1187+
1188+ // If bot token is configured, use Web API for threading support
1189+ if botToken != "" && channel != "" {
1190+ message .Channel = channel
1191+
1192+ payload , err := json .Marshal (message )
1193+ if err != nil {
1194+ return "" , fmt .Errorf ("failed to marshal slack message: %w" , err )
1195+ }
1196+
1197+ req , err := http .NewRequest ("POST" , "https://slack.com/api/chat.postMessage" , bytes .NewBuffer (payload ))
1198+ if err != nil {
1199+ return "" , fmt .Errorf ("failed to create request: %w" , err )
1200+ }
1201+
1202+ req .Header .Set ("Content-Type" , "application/json" )
1203+ req .Header .Set ("Authorization" , "Bearer " + botToken )
1204+
1205+ client := & http.Client {Timeout : 10 * time .Second }
1206+ resp , err := client .Do (req )
1207+ if err != nil {
1208+ return "" , fmt .Errorf ("failed to send slack message: %w" , err )
1209+ }
1210+ defer resp .Body .Close ()
1211+
1212+ var slackResp SlackResponse
1213+ if err := json .NewDecoder (resp .Body ).Decode (& slackResp ); err != nil {
1214+ return "" , fmt .Errorf ("failed to decode slack response: %w" , err )
1215+ }
1216+
1217+ if ! slackResp .OK {
1218+ return "" , fmt .Errorf ("slack API error: %s" , slackResp .Error )
1219+ }
1220+
1221+ log .Printf ("Slack message sent successfully (ts: %s)" , slackResp .TS )
1222+ return slackResp .TS , nil
1223+ }
1224+
1225+ // Fall back to webhook (no threading support)
1226+ err := sendSlackNotification (message )
1227+ return "" , err
1228+ }
1229+
11611230func sendSlackNotification (message SlackMessage ) error {
11621231 webhookURL := os .Getenv ("SLACK_WEBHOOK_URL" )
11631232 if webhookURL == "" {
0 commit comments