-
Notifications
You must be signed in to change notification settings - Fork 20
/
msg.go
245 lines (199 loc) · 8.07 KB
/
msg.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
package flows
import (
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"slices"
"github.com/go-playground/validator/v10"
"github.com/nyaruka/gocommon/i18n"
"github.com/nyaruka/gocommon/urns"
"github.com/nyaruka/gocommon/uuids"
"github.com/nyaruka/goflow/assets"
"github.com/nyaruka/goflow/envs"
"github.com/nyaruka/goflow/utils"
)
func init() {
utils.RegisterValidatorAlias("msg_topic", "eq=event|eq=account|eq=purchase|eq=agent", func(validator.FieldError) string {
return "is not a valid message topic"
})
}
type UnsendableReason string
const (
NilUnsendableReason UnsendableReason = ""
UnsendableReasonNoDestination UnsendableReason = "no_destination" // no sendable channel+URN pair
UnsendableReasonContactStatus UnsendableReason = "contact_status" // contact is blocked or stopped or archived
)
// MsgTopic is the topic, as required by some channel types
type MsgTopic string
// possible msg topic values
const (
NilMsgTopic MsgTopic = ""
MsgTopicEvent MsgTopic = "event"
MsgTopicAccount MsgTopic = "account"
MsgTopicPurchase MsgTopic = "purchase"
MsgTopicAgent MsgTopic = "agent"
)
// BaseMsg represents a incoming or outgoing message with the session contact
type BaseMsg struct {
UUID_ MsgUUID `json:"uuid"`
ID_ MsgID `json:"id,omitempty"`
URN_ urns.URN `json:"urn,omitempty" validate:"omitempty,urn"`
Channel_ *assets.ChannelReference `json:"channel,omitempty"`
Text_ string `json:"text"`
Attachments_ []utils.Attachment `json:"attachments,omitempty"`
}
// MsgIn represents a incoming message from the session contact
type MsgIn struct {
BaseMsg
ExternalID_ string `json:"external_id,omitempty"`
}
// MsgOut represents a outgoing message to the session contact
type MsgOut struct {
BaseMsg
QuickReplies_ []string `json:"quick_replies,omitempty"`
Templating_ *MsgTemplating `json:"templating,omitempty"`
Topic_ MsgTopic `json:"topic,omitempty"`
Locale_ i18n.Locale `json:"locale,omitempty"`
UnsendableReason_ UnsendableReason `json:"unsendable_reason,omitempty"`
}
// NewMsgIn creates a new incoming message
func NewMsgIn(uuid MsgUUID, urn urns.URN, channel *assets.ChannelReference, text string, attachments []utils.Attachment) *MsgIn {
return &MsgIn{
BaseMsg: BaseMsg{
UUID_: uuid,
URN_: urn,
Channel_: channel,
Text_: text,
Attachments_: attachments,
},
}
}
// NewMsgOut creates a new outgoing message
func NewMsgOut(urn urns.URN, channel *assets.ChannelReference, text string, attachments []utils.Attachment, quickReplies []string, templating *MsgTemplating, topic MsgTopic, locale i18n.Locale, reason UnsendableReason) *MsgOut {
return &MsgOut{
BaseMsg: BaseMsg{
UUID_: MsgUUID(uuids.New()),
URN_: urn,
Channel_: channel,
Text_: text,
Attachments_: attachments,
},
QuickReplies_: quickReplies,
Templating_: templating,
Topic_: topic,
Locale_: locale,
UnsendableReason_: reason,
}
}
// NewIVRMsgOut creates a new outgoing message for IVR
func NewIVRMsgOut(urn urns.URN, channel *assets.ChannelReference, text string, audioURL string, locale i18n.Locale) *MsgOut {
var attachments []utils.Attachment
if audioURL != "" {
attachments = []utils.Attachment{utils.Attachment(fmt.Sprintf("audio:%s", audioURL))}
}
return &MsgOut{
BaseMsg: BaseMsg{
UUID_: MsgUUID(uuids.New()),
URN_: urn,
Channel_: channel,
Text_: text,
Attachments_: attachments,
},
QuickReplies_: nil,
Templating_: nil,
Topic_: NilMsgTopic,
Locale_: locale,
}
}
// UUID returns the UUID of this message
func (m *BaseMsg) UUID() MsgUUID { return m.UUID_ }
// ID returns the internal ID of this message
func (m *BaseMsg) ID() MsgID { return m.ID_ }
// SetID sets the internal ID of this message
func (m *BaseMsg) SetID(id MsgID) { m.ID_ = id }
// URN returns the URN of this message
func (m *BaseMsg) URN() urns.URN { return m.URN_ }
// SetURN returns the URN of this message
func (m *BaseMsg) SetURN(urn urns.URN) { m.URN_ = urn }
// Channel returns the channel of this message
func (m *BaseMsg) Channel() *assets.ChannelReference { return m.Channel_ }
// Text returns the text of this message
func (m *BaseMsg) Text() string { return m.Text_ }
// Attachments returns the attachments of this message
func (m *BaseMsg) Attachments() []utils.Attachment { return m.Attachments_ }
// ExternalID returns the optional external ID of this incoming message
func (m *MsgIn) ExternalID() string { return m.ExternalID_ }
// SetExternalID sets the external ID of this message
func (m *MsgIn) SetExternalID(id string) { m.ExternalID_ = id }
// QuickReplies returns the quick replies of this outgoing message
func (m *MsgOut) QuickReplies() []string { return m.QuickReplies_ }
// Templating returns the templating to use to send this message (if any)
func (m *MsgOut) Templating() *MsgTemplating { return m.Templating_ }
// Topic returns the topic to use to send this message (if any)
func (m *MsgOut) Topic() MsgTopic { return m.Topic_ }
// Locale returns the locale of this message (if any)
func (m *MsgOut) Locale() i18n.Locale { return m.Locale_ }
// UnsendableReason returns the reason this message can't be sent (if any)
func (m *MsgOut) UnsendableReason() UnsendableReason { return m.UnsendableReason_ }
type TemplateParam struct {
Type string `json:"type"`
Value string `json:"value"`
}
// MsgTemplating represents any substituted message template that should be applied when sending this message
type MsgTemplating struct {
Template_ *assets.TemplateReference `json:"template"`
Params_ map[string][]TemplateParam `json:"params,omitempty"`
Namespace_ string `json:"namespace"`
}
// Template returns the template this msg template is for
func (t MsgTemplating) Template() *assets.TemplateReference { return t.Template_ }
// Namespace returns the namespace that should be for the template
func (t MsgTemplating) Namespace() string { return t.Namespace_ }
// Params returns the params that should be used for the template
func (t MsgTemplating) Params() map[string][]TemplateParam { return t.Params_ }
// NewMsgTemplating creates and returns a new msg template
func NewMsgTemplating(template *assets.TemplateReference, variables []string, namespace string) *MsgTemplating {
params := make(map[string][]TemplateParam, 1)
// TODO add support for params in other components besides body
if len(variables) > 0 {
params["body"] = make([]TemplateParam, len(variables))
for i, v := range variables {
params["body"][i] = TemplateParam{Type: "text", Value: v}
}
}
return &MsgTemplating{Template_: template, Namespace_: namespace, Params_: params}
}
// BroadcastTranslation is the broadcast content in a particular language
type BroadcastTranslation struct {
Text string `json:"text"`
Attachments []utils.Attachment `json:"attachments,omitempty"`
QuickReplies []string `json:"quick_replies,omitempty"`
}
type BroadcastTranslations map[i18n.Language]*BroadcastTranslation
// ForContact is a utility to help callers select the translation for a contact
func (b BroadcastTranslations) ForContact(e envs.Environment, c *Contact, baseLanguage i18n.Language) (*BroadcastTranslation, i18n.Language) {
// first try the contact language if it is valid
if c.Language() != i18n.NilLanguage && slices.Contains(e.AllowedLanguages(), c.Language()) {
t := b[c.Language()]
if t != nil {
return t, c.Language()
}
}
// second try the default flow language
t := b[e.DefaultLanguage()]
if t != nil {
return t, e.DefaultLanguage()
}
// finally return the base language
return b[baseLanguage], baseLanguage
}
// Scan supports reading translation values from JSON in database
func (t *BroadcastTranslations) Scan(value any) error {
b, ok := value.([]byte)
if !ok {
return errors.New("failed type assertion to []byte")
}
return json.Unmarshal(b, &t)
}
func (t BroadcastTranslations) Value() (driver.Value, error) { return json.Marshal(t) }