sentdm 0.32.0 → 0.34.0

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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/README.md +1 -1
  4. data/lib/sentdm/client.rb +6 -0
  5. data/lib/sentdm/models/channel_event_payload.rb +154 -7
  6. data/lib/sentdm/models/contact_event.rb +15 -7
  7. data/lib/sentdm/models/contact_event_payload.rb +77 -16
  8. data/lib/sentdm/models/conversation_messages_list.rb +214 -6
  9. data/lib/sentdm/models/inbound_message_event_payload.rb +70 -3
  10. data/lib/sentdm/models/me_retrieve_response.rb +21 -1
  11. data/lib/sentdm/models/message_event_payload.rb +42 -1
  12. data/lib/sentdm/models/message_retrieve_activities_response.rb +42 -5
  13. data/lib/sentdm/models/message_retrieve_status_response.rb +222 -8
  14. data/lib/sentdm/models/message_send_params.rb +67 -3
  15. data/lib/sentdm/models/message_send_response.rb +13 -4
  16. data/lib/sentdm/models/template_body.rb +82 -1
  17. data/lib/sentdm/models/template_create_params.rb +11 -1
  18. data/lib/sentdm/models/template_header.rb +91 -3
  19. data/lib/sentdm/models/template_variable.rb +15 -1
  20. data/lib/sentdm/models/webhook_create_params.rb +31 -1
  21. data/lib/sentdm/models/webhook_list_events_response.rb +497 -9
  22. data/lib/sentdm/models/webhook_update_params.rb +31 -1
  23. data/lib/sentdm/resources/me.rb +5 -0
  24. data/lib/sentdm/resources/messages.rb +33 -5
  25. data/lib/sentdm/resources/templates.rb +3 -1
  26. data/lib/sentdm/resources/webhooks.rb +9 -3
  27. data/lib/sentdm/version.rb +1 -1
  28. data/lib/sentdm.rb +1 -1
  29. data/rbi/sentdm/client.rbi +6 -0
  30. data/rbi/sentdm/models/channel_event_payload.rbi +273 -10
  31. data/rbi/sentdm/models/contact_event.rbi +28 -12
  32. data/rbi/sentdm/models/contact_event_payload.rbi +118 -28
  33. data/rbi/sentdm/models/conversation_messages_list.rbi +308 -10
  34. data/rbi/sentdm/models/inbound_message_event_payload.rbi +110 -2
  35. data/rbi/sentdm/models/me_retrieve_response.rbi +35 -0
  36. data/rbi/sentdm/models/message_event_payload.rbi +50 -0
  37. data/rbi/sentdm/models/message_retrieve_activities_response.rbi +49 -5
  38. data/rbi/sentdm/models/message_retrieve_status_response.rbi +320 -12
  39. data/rbi/sentdm/models/message_send_params.rbi +100 -2
  40. data/rbi/sentdm/models/message_send_response.rbi +15 -5
  41. data/rbi/sentdm/models/template_body.rbi +133 -0
  42. data/rbi/sentdm/models/template_create_params.rbi +15 -0
  43. data/rbi/sentdm/models/template_header.rbi +143 -2
  44. data/rbi/sentdm/models/template_variable.rbi +10 -0
  45. data/rbi/sentdm/models/webhook_create_params.rbi +61 -0
  46. data/rbi/sentdm/models/webhook_list_events_response.rbi +747 -12
  47. data/rbi/sentdm/models/webhook_update_params.rbi +61 -0
  48. data/rbi/sentdm/resources/me.rbi +5 -0
  49. data/rbi/sentdm/resources/messages.rbi +58 -3
  50. data/rbi/sentdm/resources/templates.rbi +6 -0
  51. data/rbi/sentdm/resources/webhooks.rbi +15 -1
  52. data/sig/sentdm/models/channel_event_payload.rbs +62 -0
  53. data/sig/sentdm/models/contact_event_payload.rbs +24 -9
  54. data/sig/sentdm/models/conversation_messages_list.rbs +99 -6
  55. data/sig/sentdm/models/inbound_message_event_payload.rbs +37 -0
  56. data/sig/sentdm/models/me_retrieve_response.rbs +7 -0
  57. data/sig/sentdm/models/message_event_payload.rbs +20 -0
  58. data/sig/sentdm/models/message_retrieve_activities_response.rbs +15 -0
  59. data/sig/sentdm/models/message_retrieve_status_response.rbs +99 -6
  60. data/sig/sentdm/models/message_send_params.rbs +15 -0
  61. data/sig/sentdm/models/template_body.rbs +44 -0
  62. data/sig/sentdm/models/template_create_params.rbs +7 -0
  63. data/sig/sentdm/models/template_header.rbs +44 -0
  64. data/sig/sentdm/models/webhook_create_params.rbs +29 -0
  65. data/sig/sentdm/models/webhook_list_events_response.rbs +252 -0
  66. data/sig/sentdm/models/webhook_update_params.rbs +29 -0
  67. data/sig/sentdm/resources/messages.rbs +3 -0
  68. data/sig/sentdm/resources/templates.rbs +1 -0
  69. data/sig/sentdm/resources/webhooks.rbs +2 -0
  70. metadata +2 -2
@@ -9,8 +9,9 @@ module Sentdm
9
9
  end
10
10
 
11
11
  # Whether the contact is opted out after this signal — the state to write to your
12
- # own record. Same meaning as opt_out on the contact resource. On contact.help
13
- # this reports the contact's existing state, which help does not change.
12
+ # own record. Same meaning as opt_out on the contact resource. On contact.help and
13
+ # contact.custom_keyword this reports the contact's existing state, which neither
14
+ # changes.
14
15
  #
15
16
  # Two signals from the same contact can arrive out of order, because each one is
16
17
  # queued on its own rather than against the contact. Compare the envelope's
@@ -35,6 +36,16 @@ module Sentdm
35
36
  sig { params(account_id: String).void }
36
37
  attr_writer :account_id
37
38
 
39
+ # The RCS agent the signal reached, when it reached one.
40
+ #
41
+ # Omitted entirely on channels that have no agent, rather than sent as null — an
42
+ # SMS or WhatsApp payload does not carry this key at all. On RCS it is the
43
+ # counterpart to To: a contact reaches an agent rather than a number, so exactly
44
+ # one of the two is populated and never both. If you run more than one agent, this
45
+ # is what tells you which of them the contact acted on.
46
+ sig { returns(T.nilable(String)) }
47
+ attr_accessor :agent_id
48
+
38
49
  # The channel the signal arrived on, for example sms or whatsapp.
39
50
  sig { returns(T.nilable(String)) }
40
51
  attr_reader :channel
@@ -43,14 +54,23 @@ module Sentdm
43
54
  attr_writer :channel
44
55
 
45
56
  # The contact who raised the signal. Always populated, including for contact.help
46
- # from a number you have not messaged before — the contact is created if it does
47
- # not exist yet, so this identifier is always resolvable against the contacts API.
57
+ # or contact.custom_keyword from a number you have not messaged before — the
58
+ # contact is created if it does not exist yet, so this identifier is always
59
+ # resolvable against the contacts API.
48
60
  sig { returns(T.nilable(String)) }
49
61
  attr_reader :contact_id
50
62
 
51
63
  sig { params(contact_id: String).void }
52
64
  attr_writer :contact_id
53
65
 
66
+ # The contact's number, in E.164 format with the leading + — who raised the
67
+ # signal. The same party message.received publishes as inbound_number.
68
+ sig { returns(T.nilable(String)) }
69
+ attr_reader :from
70
+
71
+ sig { params(from: String).void }
72
+ attr_writer :from
73
+
54
74
  # The inbound message that carried the signal, matching message_id on the
55
75
  # corresponding message.received event so the two can be joined.
56
76
  #
@@ -62,13 +82,19 @@ module Sentdm
62
82
  sig { returns(T.nilable(String)) }
63
83
  attr_accessor :message_id
64
84
 
65
- # The contact's number in E.164 format. Same value as phone_number on the contact
66
- # resource.
85
+ # The auto-reply template whose keyword the contact matched, joinable against the
86
+ # templates API.
87
+ #
88
+ # This is what identifies which signal arrived on contact.custom_keyword: every
89
+ # custom template reports the same event name, so the event alone cannot tell your
90
+ # booking keyword from your opening-hours one. One template holds as many keywords
91
+ # as you configured, so this is steadier to switch on than text.
92
+ #
93
+ # Populated on the compliance sub-types too, where it names the template that
94
+ # replied. Sent as null when no template was involved — a network-reported opt-out
95
+ # matches no keyword. The field is always present, so read it and check for null.
67
96
  sig { returns(T.nilable(String)) }
68
- attr_reader :phone_number
69
-
70
- sig { params(phone_number: String).void }
71
- attr_writer :phone_number
97
+ attr_accessor :template_id
72
98
 
73
99
  # The text the contact sent, for example STOP or UNSUBSCRIBE. Sent as null when
74
100
  # the signal did not arrive as text. The field is always present, so read it and
@@ -76,33 +102,60 @@ module Sentdm
76
102
  sig { returns(T.nilable(String)) }
77
103
  attr_accessor :text
78
104
 
79
- # Body of a contact.opt_in, contact.opt_out or contact.help event. Delivered when
80
- # a contact signals a consent change or asks for help.
105
+ # The number of yours that received the signal, in E.164 format with the leading
106
+ # +. Tells a multi-number account which of its senders the contact acted on, which
107
+ # nothing else on this payload answers.
108
+ #
109
+ # This is your number, not the contact's. That is the opposite of what to means on
110
+ # POST /v3/messages, where it is the list of recipients you are sending to. Reply
111
+ # to From, not to this field, or the message goes back to yourself.
112
+ #
113
+ # Sent as null when the signal did not arrive at a number of yours — an RCS signal
114
+ # terminates at an agent rather than a number, and a provider-reported opt-out may
115
+ # name no receiving number at all. The field is always present, so read it and
116
+ # check for null rather than checking whether the key exists.
117
+ sig { returns(T.nilable(String)) }
118
+ attr_accessor :to
119
+
120
+ # Body of a contact.opt_in, contact.opt_out, contact.help or
121
+ # contact.custom_keyword event. Delivered when a contact signals a consent change,
122
+ # asks for help, or sends one of your own auto-reply keywords.
81
123
  #
82
124
  # These events state the signal outright, so you do not have to recognise keywords
83
125
  # in the text of a message.received event. They also cover cases that produce no
84
126
  # inbound message at all, such as a network handling an opt-out on your behalf.
85
127
  #
86
- # Fields are ordered identity → resulting state → provenance → join key. Nothing
87
- # here restates the envelope: which of the three signals occurred is the
88
- # envelope's event, and when it was emitted is its timestamp. Retries carry the
89
- # same X-Webhook-Event-ID header, which is what to deduplicate on.
128
+ # Two of the four change consent and two do not: contact.help and
129
+ # contact.custom_keyword report the state the contact already had. Read opt_out
130
+ # for the state and the envelope's event for what happened, rather than inferring
131
+ # one from the other.
132
+ #
133
+ # Fields are ordered identity → resulting state → provenance → join keys. The two
134
+ # parties are from and to. Note that the message family has not moved to those
135
+ # names yet — message.received still calls the same two parties inbound_number and
136
+ # outbound_number. Nothing here restates the envelope: which signal occurred is
137
+ # the envelope's event, and when it was emitted is its timestamp. Retries carry
138
+ # the same X-Webhook-Event-ID header, which is what to deduplicate on.
90
139
  sig do
91
140
  params(
92
141
  opt_out: T::Boolean,
93
142
  source: String,
94
143
  account_id: String,
144
+ agent_id: T.nilable(String),
95
145
  channel: String,
96
146
  contact_id: String,
147
+ from: String,
97
148
  message_id: T.nilable(String),
98
- phone_number: String,
99
- text: T.nilable(String)
149
+ template_id: T.nilable(String),
150
+ text: T.nilable(String),
151
+ to: T.nilable(String)
100
152
  ).returns(T.attached_class)
101
153
  end
102
154
  def self.new(
103
155
  # Whether the contact is opted out after this signal — the state to write to your
104
- # own record. Same meaning as opt_out on the contact resource. On contact.help
105
- # this reports the contact's existing state, which help does not change.
156
+ # own record. Same meaning as opt_out on the contact resource. On contact.help and
157
+ # contact.custom_keyword this reports the contact's existing state, which neither
158
+ # changes.
106
159
  #
107
160
  # Two signals from the same contact can arrive out of order, because each one is
108
161
  # queued on its own rather than against the contact. Compare the envelope's
@@ -118,12 +171,24 @@ module Sentdm
118
171
  # The account the contact belongs to. Present so one endpoint can serve several
119
172
  # accounts.
120
173
  account_id: nil,
174
+ # The RCS agent the signal reached, when it reached one.
175
+ #
176
+ # Omitted entirely on channels that have no agent, rather than sent as null — an
177
+ # SMS or WhatsApp payload does not carry this key at all. On RCS it is the
178
+ # counterpart to To: a contact reaches an agent rather than a number, so exactly
179
+ # one of the two is populated and never both. If you run more than one agent, this
180
+ # is what tells you which of them the contact acted on.
181
+ agent_id: nil,
121
182
  # The channel the signal arrived on, for example sms or whatsapp.
122
183
  channel: nil,
123
184
  # The contact who raised the signal. Always populated, including for contact.help
124
- # from a number you have not messaged before — the contact is created if it does
125
- # not exist yet, so this identifier is always resolvable against the contacts API.
185
+ # or contact.custom_keyword from a number you have not messaged before — the
186
+ # contact is created if it does not exist yet, so this identifier is always
187
+ # resolvable against the contacts API.
126
188
  contact_id: nil,
189
+ # The contact's number, in E.164 format with the leading + — who raised the
190
+ # signal. The same party message.received publishes as inbound_number.
191
+ from: nil,
127
192
  # The inbound message that carried the signal, matching message_id on the
128
193
  # corresponding message.received event so the two can be joined.
129
194
  #
@@ -133,13 +198,35 @@ module Sentdm
133
198
  # number. The field is always present, so read it and check for null rather than
134
199
  # checking whether the key exists.
135
200
  message_id: nil,
136
- # The contact's number in E.164 format. Same value as phone_number on the contact
137
- # resource.
138
- phone_number: nil,
201
+ # The auto-reply template whose keyword the contact matched, joinable against the
202
+ # templates API.
203
+ #
204
+ # This is what identifies which signal arrived on contact.custom_keyword: every
205
+ # custom template reports the same event name, so the event alone cannot tell your
206
+ # booking keyword from your opening-hours one. One template holds as many keywords
207
+ # as you configured, so this is steadier to switch on than text.
208
+ #
209
+ # Populated on the compliance sub-types too, where it names the template that
210
+ # replied. Sent as null when no template was involved — a network-reported opt-out
211
+ # matches no keyword. The field is always present, so read it and check for null.
212
+ template_id: nil,
139
213
  # The text the contact sent, for example STOP or UNSUBSCRIBE. Sent as null when
140
214
  # the signal did not arrive as text. The field is always present, so read it and
141
215
  # check for null rather than checking whether the key exists.
142
- text: nil
216
+ text: nil,
217
+ # The number of yours that received the signal, in E.164 format with the leading
218
+ # +. Tells a multi-number account which of its senders the contact acted on, which
219
+ # nothing else on this payload answers.
220
+ #
221
+ # This is your number, not the contact's. That is the opposite of what to means on
222
+ # POST /v3/messages, where it is the list of recipients you are sending to. Reply
223
+ # to From, not to this field, or the message goes back to yourself.
224
+ #
225
+ # Sent as null when the signal did not arrive at a number of yours — an RCS signal
226
+ # terminates at an agent rather than a number, and a provider-reported opt-out may
227
+ # name no receiving number at all. The field is always present, so read it and
228
+ # check for null rather than checking whether the key exists.
229
+ to: nil
143
230
  )
144
231
  end
145
232
 
@@ -149,11 +236,14 @@ module Sentdm
149
236
  opt_out: T::Boolean,
150
237
  source: String,
151
238
  account_id: String,
239
+ agent_id: T.nilable(String),
152
240
  channel: String,
153
241
  contact_id: String,
242
+ from: String,
154
243
  message_id: T.nilable(String),
155
- phone_number: String,
156
- text: T.nilable(String)
244
+ template_id: T.nilable(String),
245
+ text: T.nilable(String),
246
+ to: T.nilable(String)
157
247
  }
158
248
  )
159
249
  end
@@ -112,7 +112,14 @@ module Sentdm
112
112
  attr_accessor :events
113
113
 
114
114
  # Structured message body format for database storage. Preserves channel-specific
115
- # components (header, body, footer, buttons).
115
+ # components (header, header media, body, footer, buttons, MMS subject and media).
116
+ #
117
+ # Persisted as the messageBody jsonb column on Messages. Every write path goes
118
+ # through MessageUtils.MessageBodyJsonOptions, which writes nulls, so the envelope
119
+ # shape is stable regardless of channel or status. Anything that rebuilds this
120
+ # object field by field — the four IMessageBodyStrategy implementations and
121
+ # MessageUtils.BuildSegmentBody — has to carry every member, or that member is
122
+ # silently dropped on whichever path forgot it.
116
123
  sig do
117
124
  returns(
118
125
  T.nilable(Sentdm::ConversationMessagesList::Message::MessageBody)
@@ -145,6 +152,20 @@ module Sentdm
145
152
  sig { returns(T.nilable(Float)) }
146
153
  attr_accessor :price
147
154
 
155
+ # A human-readable sentence for reason_code, for example "Insufficient balance".
156
+ # Omitted whenever reason_code is.
157
+ sig { returns(T.nilable(String)) }
158
+ attr_accessor :reason
159
+
160
+ # Why the message is at its current status, as a stable platform code such as
161
+ # DELIVERY_007, BUSINESS_003 or DELIVERY_003. Present when the current status is
162
+ # FAILED, FILTERED or BLOCKED and the lifecycle was loaded; omitted otherwise.
163
+ # Switch on this rather than on reason: the code is stable, the wording may be
164
+ # improved. It is the platform's classification of the outcome, never a carrier or
165
+ # vendor code.
166
+ sig { returns(T.nilable(String)) }
167
+ attr_accessor :reason_code
168
+
148
169
  sig { returns(T.nilable(String)) }
149
170
  attr_reader :region_code
150
171
 
@@ -166,7 +187,12 @@ module Sentdm
166
187
  sig { returns(T.nilable(String)) }
167
188
  attr_accessor :template_name
168
189
 
169
- # Message response for v3 API — same shape as v2 with snake_case JSON conventions
190
+ # Message response for v3 API — same shape as v2 with snake_case JSON conventions.
191
+ #
192
+ # The shape of a message that was sent immediately: it never has a scheduled_at
193
+ # key. A message that is or was held for a later instant is a
194
+ # ScheduledMessageResponse, and the endpoint decides which of the two to answer
195
+ # with. From always returns this type.
170
196
  sig do
171
197
  params(
172
198
  id: String,
@@ -189,6 +215,8 @@ module Sentdm
189
215
  phone: String,
190
216
  phone_international: String,
191
217
  price: T.nilable(Float),
218
+ reason: T.nilable(String),
219
+ reason_code: T.nilable(String),
192
220
  region_code: String,
193
221
  status: String,
194
222
  template_category: T.nilable(String),
@@ -206,11 +234,28 @@ module Sentdm
206
234
  direction: nil,
207
235
  events: nil,
208
236
  # Structured message body format for database storage. Preserves channel-specific
209
- # components (header, body, footer, buttons).
237
+ # components (header, header media, body, footer, buttons, MMS subject and media).
238
+ #
239
+ # Persisted as the messageBody jsonb column on Messages. Every write path goes
240
+ # through MessageUtils.MessageBodyJsonOptions, which writes nulls, so the envelope
241
+ # shape is stable regardless of channel or status. Anything that rebuilds this
242
+ # object field by field — the four IMessageBodyStrategy implementations and
243
+ # MessageUtils.BuildSegmentBody — has to carry every member, or that member is
244
+ # silently dropped on whichever path forgot it.
210
245
  message_body: nil,
211
246
  phone: nil,
212
247
  phone_international: nil,
213
248
  price: nil,
249
+ # A human-readable sentence for reason_code, for example "Insufficient balance".
250
+ # Omitted whenever reason_code is.
251
+ reason: nil,
252
+ # Why the message is at its current status, as a stable platform code such as
253
+ # DELIVERY_007, BUSINESS_003 or DELIVERY_003. Present when the current status is
254
+ # FAILED, FILTERED or BLOCKED and the lifecycle was loaded; omitted otherwise.
255
+ # Switch on this rather than on reason: the code is stable, the wording may be
256
+ # improved. It is the platform's classification of the outcome, never a carrier or
257
+ # vendor code.
258
+ reason_code: nil,
214
259
  region_code: nil,
215
260
  status: nil,
216
261
  template_category: nil,
@@ -240,6 +285,8 @@ module Sentdm
240
285
  phone: String,
241
286
  phone_international: String,
242
287
  price: T.nilable(Float),
288
+ reason: T.nilable(String),
289
+ reason_code: T.nilable(String),
243
290
  region_code: String,
244
291
  status: String,
245
292
  template_category: T.nilable(String),
@@ -269,15 +316,39 @@ module Sentdm
269
316
  sig { returns(T.nilable(String)) }
270
317
  attr_accessor :description
271
318
 
319
+ # A human-readable sentence for reason_code. Omitted whenever reason_code is.
320
+ sig { returns(T.nilable(String)) }
321
+ attr_accessor :reason
322
+
323
+ # Why the message reached this status, as a stable platform code such as
324
+ # DELIVERY_007. Present on FAILED, FILTERED and BLOCKED events; omitted on every
325
+ # status that needs no explanation. Same wire name and vocabulary as on the
326
+ # activities list and the webhook.
327
+ sig { returns(T.nilable(String)) }
328
+ attr_accessor :reason_code
329
+
272
330
  # Represents a status change event in a message's lifecycle (v3)
273
331
  sig do
274
332
  params(
275
333
  status: String,
276
334
  timestamp: Time,
277
- description: T.nilable(String)
335
+ description: T.nilable(String),
336
+ reason: T.nilable(String),
337
+ reason_code: T.nilable(String)
278
338
  ).returns(T.attached_class)
279
339
  end
280
- def self.new(status:, timestamp:, description: nil)
340
+ def self.new(
341
+ status:,
342
+ timestamp:,
343
+ description: nil,
344
+ # A human-readable sentence for reason_code. Omitted whenever reason_code is.
345
+ reason: nil,
346
+ # Why the message reached this status, as a stable platform code such as
347
+ # DELIVERY_007. Present on FAILED, FILTERED and BLOCKED events; omitted on every
348
+ # status that needs no explanation. Same wire name and vocabulary as on the
349
+ # activities list and the webhook.
350
+ reason_code: nil
351
+ )
281
352
  end
282
353
 
283
354
  sig do
@@ -285,7 +356,9 @@ module Sentdm
285
356
  {
286
357
  status: String,
287
358
  timestamp: Time,
288
- description: T.nilable(String)
359
+ description: T.nilable(String),
360
+ reason: T.nilable(String),
361
+ reason_code: T.nilable(String)
289
362
  }
290
363
  )
291
364
  end
@@ -325,8 +398,57 @@ module Sentdm
325
398
  sig { returns(T.nilable(String)) }
326
399
  attr_accessor :header
327
400
 
401
+ # The media asset that rode a message's header, recorded as sent.
402
+ sig do
403
+ returns(
404
+ T.nilable(
405
+ Sentdm::ConversationMessagesList::Message::MessageBody::HeaderMedia
406
+ )
407
+ )
408
+ end
409
+ attr_reader :header_media
410
+
411
+ sig do
412
+ params(
413
+ header_media:
414
+ T.nilable(
415
+ Sentdm::ConversationMessagesList::Message::MessageBody::HeaderMedia::OrHash
416
+ )
417
+ ).void
418
+ end
419
+ attr_writer :header_media
420
+
421
+ # MMS attachments, as the publicly fetchable URLs handed to the carrier. Null on
422
+ # every other channel.
423
+ #
424
+ # Persisted rather than derived because a resend and a curfew release rebuild the
425
+ # send from the stored row — MessageReplayCommandBuilder reads templateId and
426
+ # templateVariables and nothing else — so media that lives only on the original
427
+ # request would silently turn a replayed MMS into a text message.
428
+ sig do
429
+ returns(
430
+ T.nilable(
431
+ T::Array[
432
+ Sentdm::ConversationMessagesList::Message::MessageBody::Media
433
+ ]
434
+ )
435
+ )
436
+ end
437
+ attr_accessor :media
438
+
439
+ # MMS subject line. Null on every other channel.
440
+ sig { returns(T.nilable(String)) }
441
+ attr_accessor :subject
442
+
328
443
  # Structured message body format for database storage. Preserves channel-specific
329
- # components (header, body, footer, buttons).
444
+ # components (header, header media, body, footer, buttons, MMS subject and media).
445
+ #
446
+ # Persisted as the messageBody jsonb column on Messages. Every write path goes
447
+ # through MessageUtils.MessageBodyJsonOptions, which writes nulls, so the envelope
448
+ # shape is stable regardless of channel or status. Anything that rebuilds this
449
+ # object field by field — the four IMessageBodyStrategy implementations and
450
+ # MessageUtils.BuildSegmentBody — has to carry every member, or that member is
451
+ # silently dropped on whichever path forgot it.
330
452
  sig do
331
453
  params(
332
454
  buttons:
@@ -337,10 +459,38 @@ module Sentdm
337
459
  ),
338
460
  content: String,
339
461
  footer: T.nilable(String),
340
- header: T.nilable(String)
462
+ header: T.nilable(String),
463
+ header_media:
464
+ T.nilable(
465
+ Sentdm::ConversationMessagesList::Message::MessageBody::HeaderMedia::OrHash
466
+ ),
467
+ media:
468
+ T.nilable(
469
+ T::Array[
470
+ Sentdm::ConversationMessagesList::Message::MessageBody::Media::OrHash
471
+ ]
472
+ ),
473
+ subject: T.nilable(String)
341
474
  ).returns(T.attached_class)
342
475
  end
343
- def self.new(buttons: nil, content: nil, footer: nil, header: nil)
476
+ def self.new(
477
+ buttons: nil,
478
+ content: nil,
479
+ footer: nil,
480
+ header: nil,
481
+ # The media asset that rode a message's header, recorded as sent.
482
+ header_media: nil,
483
+ # MMS attachments, as the publicly fetchable URLs handed to the carrier. Null on
484
+ # every other channel.
485
+ #
486
+ # Persisted rather than derived because a resend and a curfew release rebuild the
487
+ # send from the stored row — MessageReplayCommandBuilder reads templateId and
488
+ # templateVariables and nothing else — so media that lives only on the original
489
+ # request would silently turn a replayed MMS into a text message.
490
+ media: nil,
491
+ # MMS subject line. Null on every other channel.
492
+ subject: nil
493
+ )
344
494
  end
345
495
 
346
496
  sig do
@@ -354,7 +504,18 @@ module Sentdm
354
504
  ),
355
505
  content: String,
356
506
  footer: T.nilable(String),
357
- header: T.nilable(String)
507
+ header: T.nilable(String),
508
+ header_media:
509
+ T.nilable(
510
+ Sentdm::ConversationMessagesList::Message::MessageBody::HeaderMedia
511
+ ),
512
+ media:
513
+ T.nilable(
514
+ T::Array[
515
+ Sentdm::ConversationMessagesList::Message::MessageBody::Media
516
+ ]
517
+ ),
518
+ subject: T.nilable(String)
358
519
  }
359
520
  )
360
521
  end
@@ -412,6 +573,143 @@ module Sentdm
412
573
  def to_hash
413
574
  end
414
575
  end
576
+
577
+ class HeaderMedia < Sentdm::Internal::Type::BaseModel
578
+ OrHash =
579
+ T.type_alias do
580
+ T.any(
581
+ Sentdm::ConversationMessagesList::Message::MessageBody::HeaderMedia,
582
+ Sentdm::Internal::AnyHash
583
+ )
584
+ end
585
+
586
+ # "image", "video" or "document" — taken from the header's media variable.
587
+ sig { returns(T.nilable(String)) }
588
+ attr_reader :type
589
+
590
+ sig { params(type: String).void }
591
+ attr_writer :type
592
+
593
+ # The https URL the caller supplied for this send. Never the template's stored
594
+ # props.sample, which is Meta's expiring header_handle rather than what was
595
+ # delivered.
596
+ sig { returns(T.nilable(String)) }
597
+ attr_reader :url
598
+
599
+ sig { params(url: String).void }
600
+ attr_writer :url
601
+
602
+ # The media asset that rode a message's header, recorded as sent.
603
+ sig { params(type: String, url: String).returns(T.attached_class) }
604
+ def self.new(
605
+ # "image", "video" or "document" — taken from the header's media variable.
606
+ type: nil,
607
+ # The https URL the caller supplied for this send. Never the template's stored
608
+ # props.sample, which is Meta's expiring header_handle rather than what was
609
+ # delivered.
610
+ url: nil
611
+ )
612
+ end
613
+
614
+ sig { override.returns({ type: String, url: String }) }
615
+ def to_hash
616
+ end
617
+ end
618
+
619
+ class Media < Sentdm::Internal::Type::BaseModel
620
+ OrHash =
621
+ T.type_alias do
622
+ T.any(
623
+ Sentdm::ConversationMessagesList::Message::MessageBody::Media,
624
+ Sentdm::Internal::AnyHash
625
+ )
626
+ end
627
+
628
+ # One of MmsMediaTypes when the content type is known. Advisory — a reader should
629
+ # trust the fetched object's own Content-Type.
630
+ sig { returns(T.nilable(String)) }
631
+ attr_accessor :media_type
632
+
633
+ # Content type as the provider declared it. Null when it declared none.
634
+ sig { returns(T.nilable(String)) }
635
+ attr_accessor :mime_type
636
+
637
+ # Size as the provider declared it. Never measured here — nothing downloads the
638
+ # file.
639
+ sig { returns(T.nilable(Integer)) }
640
+ attr_accessor :size_bytes
641
+
642
+ # Inbound only: the SHA-256 the provider declared alongside the attachment, when
643
+ # it declared one. Relayed to the customer so they can verify what they fetch
644
+ # matches what the carrier said it sent. It is the only integrity signal available
645
+ # on an attachment nobody here has read.
646
+ sig { returns(T.nilable(String)) }
647
+ attr_accessor :source_hash_sha256
648
+
649
+ # Where the file lives. Outbound: the URL the customer gave us and the carrier
650
+ # fetched. Inbound: the URL the carrier hosts it at, relayed unchanged.
651
+ sig { returns(T.nilable(String)) }
652
+ attr_accessor :url
653
+
654
+ # One attachment on a message, in either direction — and in both, a URL somebody
655
+ # else hosts.
656
+ #
657
+ # Outbound: the customer supplied a public URL and we handed it to the carrier.
658
+ # Inbound: the carrier hosts the file and we record where. sent.dm never holds the
659
+ # bytes, so there is no key, no expiry bookkeeping and nothing minted per read —
660
+ # what is stored is what is served.
661
+ #
662
+ # An inbound link expires on the carrier's own schedule and is unauthenticated.
663
+ # That is the customer's to manage, and it is documented where they will see it
664
+ # rather than only here — a recipient who needs an attachment to outlive that
665
+ # window copies it on receipt.
666
+ #
667
+ # Storing a presigned URL is the specific mistake this shape still avoids:
668
+ # M260826130000 and M260826140000 exist because RCS assets were stored as signed
669
+ # URLs and went stale. Nothing here is signed.
670
+ sig do
671
+ params(
672
+ media_type: T.nilable(String),
673
+ mime_type: T.nilable(String),
674
+ size_bytes: T.nilable(Integer),
675
+ source_hash_sha256: T.nilable(String),
676
+ url: T.nilable(String)
677
+ ).returns(T.attached_class)
678
+ end
679
+ def self.new(
680
+ # One of MmsMediaTypes when the content type is known. Advisory — a reader should
681
+ # trust the fetched object's own Content-Type.
682
+ media_type: nil,
683
+ # Content type as the provider declared it. Null when it declared none.
684
+ mime_type: nil,
685
+ # Size as the provider declared it. Never measured here — nothing downloads the
686
+ # file.
687
+ size_bytes: nil,
688
+ # Inbound only: the SHA-256 the provider declared alongside the attachment, when
689
+ # it declared one. Relayed to the customer so they can verify what they fetch
690
+ # matches what the carrier said it sent. It is the only integrity signal available
691
+ # on an attachment nobody here has read.
692
+ source_hash_sha256: nil,
693
+ # Where the file lives. Outbound: the URL the customer gave us and the carrier
694
+ # fetched. Inbound: the URL the carrier hosts it at, relayed unchanged.
695
+ url: nil
696
+ )
697
+ end
698
+
699
+ sig do
700
+ override.returns(
701
+ {
702
+ media_type: T.nilable(String),
703
+ mime_type: T.nilable(String),
704
+ size_bytes: T.nilable(Integer),
705
+ source_hash_sha256: T.nilable(String),
706
+ url: T.nilable(String)
707
+ }
708
+ )
709
+ end
710
+ def to_hash
711
+ end
712
+ end
415
713
  end
416
714
  end
417
715
  end