-
Notifications
You must be signed in to change notification settings - Fork 237
/
alertsms.go
135 lines (112 loc) · 3.02 KB
/
alertsms.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
package twilio
import (
"bytes"
"context"
"strings"
"text/template"
"unicode"
"github.com/pkg/errors"
"github.com/target/goalert/config"
)
// 160 GSM characters (140 bytes) is the max for a single segment message.
// Multi-segment messages include a 6-byte header limiting to 153 GSM characters
// per segment.
//
// Non-GSM will use UCS-2 encoding, using 2-bytes per character. The max would
// then be 70 or 67 characters for single or multi-segmented messages, respectively.
const maxGSMLen = 160
type alertSMS struct {
ID int
Count int
Body string
Link string
Code int
}
var smsTmpl = template.Must(template.New("alertSMS").Parse(`
{{- if .ID}}Alert #{{.ID}}: {{.Body}}
{{- else if .Count}}Svc '{{.Body}}': {{.Count}} unacked alert{{if gt .Count 1}}s{{end}}
{{- end}}
{{- if .Link }}
{{.Link}}
{{- end}}
{{- if and .Count .ID }}
{{.Count}} other alert{{if gt .Count 1}}s have{{else}} has{{end}} been updated.
{{- end}}
{{- if .Code}}
Reply '{{.Code}}a{{if .Count}}a{{end}}' to ack{{if .Count}} all{{end}}, '{{.Code}}c{{if .Count}}c{{end}}' to close{{if .Count}} all{{end}}.
{{- end}}`,
))
const gsmAlphabet = "@∆ 0¡P¿p£!1AQaq$Φ\"2BRbr¥Γ#3CScsèΛ¤4DTdtéΩ%5EUeuùΠ&6FVfvìΨ'7GWgwòΣ(8HXhxÇΘ)9IYiy\n Ξ *:JZjzØ+;KÄkäøÆ,<LÖlö\ræ-=MÑmñÅß.>NÜnüåÉ/?O§oà"
var gsmChr = make(map[rune]bool, len(gsmAlphabet))
func init() {
for _, r := range gsmAlphabet {
gsmChr[r] = true
}
}
func mapGSM(r rune) rune {
if unicode.IsSpace(r) {
return ' '
}
if !unicode.IsPrint(r) {
return -1
}
if gsmChr[r] {
return r
}
// Map similar characters to keep as much meaning as possible.
switch r {
case '_', '|', '~':
return '-'
case '[', '{':
return '('
case ']', '}':
return ')'
case '»':
return '>'
case '`', '’', '‘':
return '\''
}
switch {
case unicode.Is(unicode.Dash, r):
return '-'
case unicode.Is(unicode.Quotation_Mark, r):
return '"'
}
// If no substitute, replace with '?'
return '?'
}
// hasTwoWaySMSSupport returns true if a number supports 2-way SMS messaging (replies).
func hasTwoWaySMSSupport(ctx context.Context, number string) bool {
if config.FromContext(ctx).Twilio.DisableTwoWaySMS {
return false
}
// India numbers do not support SMS replies.
return !strings.HasPrefix(number, "+91")
}
// Render will render a single-segment SMS.
//
// Non-GSM characters will be replaced with '?' and Body will be
// truncated (if needed) until the output is <= 160 characters.
func (a alertSMS) Render() (string, error) {
a.Body = strings.Map(mapGSM, a.Body)
a.Body = strings.Replace(a.Body, " ", " ", -1)
a.Body = strings.TrimSpace(a.Body)
var buf bytes.Buffer
err := smsTmpl.Execute(&buf, a)
if err != nil {
return "", err
}
if buf.Len() > maxGSMLen {
newBodyLen := len(a.Body) - (buf.Len() - maxGSMLen)
if newBodyLen <= 0 {
return "", errors.New("message too long to include body")
}
a.Body = strings.TrimSpace(a.Body[:newBodyLen])
buf.Reset()
err = smsTmpl.Execute(&buf, a)
if err != nil {
return "", err
}
}
return buf.String(), nil
}