1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one or more
3
+ * contributor license agreements. See the NOTICE file distributed with
4
+ * this work for additional information regarding copyright ownership.
5
+ * The ASF licenses this file to You under the Apache License, Version 2.0
6
+ * (the "License"); you may not use this file except in compliance with
7
+ * the License. You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ package org .apache .hertzbeat .alert .service .impl ;
19
+
20
+ import com .fasterxml .jackson .databind .JsonNode ;
21
+ import lombok .extern .slf4j .Slf4j ;
22
+ import org .apache .hertzbeat .alert .config .TwilioSmsProperties ;
23
+ import org .apache .hertzbeat .alert .service .SmsClient ;
24
+ import org .apache .hertzbeat .common .entity .alerter .GroupAlert ;
25
+ import org .apache .hertzbeat .common .entity .alerter .NoticeReceiver ;
26
+ import org .apache .hertzbeat .common .entity .alerter .NoticeTemplate ;
27
+ import org .apache .hertzbeat .common .support .exception .SendMessageException ;
28
+ import org .apache .hertzbeat .common .util .JsonUtil ;
29
+ import org .apache .http .client .methods .CloseableHttpResponse ;
30
+ import org .apache .http .client .methods .HttpPost ;
31
+ import org .apache .http .impl .client .CloseableHttpClient ;
32
+ import org .apache .http .impl .client .HttpClients ;
33
+ import org .apache .http .message .BasicNameValuePair ;
34
+ import org .apache .http .util .EntityUtils ;
35
+ import org .apache .http .client .entity .UrlEncodedFormEntity ;
36
+
37
+ import java .net .URI ;
38
+ import java .util .ArrayList ;
39
+ import java .util .Base64 ;
40
+ import java .util .List ;
41
+
42
+ import static org .apache .hertzbeat .common .constants .SmsConstants .TWILIO ;
43
+
44
+ /**
45
+ * Twilio SMS Client Implementation<br>
46
+ * API doc: <a href=
47
+ * "https://www.twilio.com/docs/sms/api">https://www.twilio.com/docs/sms/api</a>
48
+ */
49
+ @ Slf4j
50
+ public class TwilioSmsClientImpl implements SmsClient {
51
+
52
+ private static final String API_URL_FORMAT = "https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json" ;
53
+ private static final String MESSAGE_TEMPLATE = "Instance: %s, Priority: %s, Content: %s" ;
54
+
55
+ private final String accountSid ;
56
+ private final String authToken ;
57
+ private final String twilioPhoneNumber ;
58
+
59
+ public TwilioSmsClientImpl (TwilioSmsProperties config ) {
60
+ if (config != null ) {
61
+ this .accountSid = config .getAccountSid ();
62
+ this .authToken = config .getAuthToken ();
63
+ this .twilioPhoneNumber = config .getTwilioPhoneNumber ();
64
+ } else {
65
+ this .accountSid = "" ;
66
+ this .authToken = "" ;
67
+ this .twilioPhoneNumber = "" ;
68
+ }
69
+ }
70
+
71
+ @ Override
72
+ public void sendMessage (NoticeReceiver receiver , NoticeTemplate noticeTemplate , GroupAlert alert ) {
73
+ String instance = null ;
74
+ String priority = null ;
75
+ String content = null ;
76
+ if (alert .getCommonLabels () != null ) {
77
+ instance = alert .getCommonLabels ().get ("instance" ) == null ? alert .getGroupKey ()
78
+ : alert .getCommonLabels ().get ("instance" );
79
+ priority = alert .getCommonLabels ().get ("priority" ) == null ? "unknown"
80
+ : alert .getCommonLabels ().get ("priority" );
81
+ content = alert .getCommonAnnotations ().get ("summary" );
82
+ content = content == null ? alert .getCommonAnnotations ().get ("description" ) : content ;
83
+ if (content == null ) {
84
+ content = alert .getCommonAnnotations ().values ().stream ().findFirst ().orElse (null );
85
+ }
86
+ }
87
+ this .send (receiver .getPhone (), createMessage (instance , priority , content ));
88
+ }
89
+
90
+ private String createMessage (String instance , String priority , String content ) {
91
+ return String .format (MESSAGE_TEMPLATE , instance , priority , content );
92
+ }
93
+
94
+ private void send (String phoneNumber , String message ) {
95
+ try (CloseableHttpClient httpClient = HttpClients .createDefault ()) {
96
+ String endpoint = String .format (API_URL_FORMAT , accountSid );
97
+ URI requestUri = new URI (endpoint );
98
+
99
+ HttpPost httpPost = createHttpPost (requestUri , phoneNumber , message );
100
+ log .info ("Sending Twilio SMS request to {}" , requestUri );
101
+ executeRequest (httpClient , httpPost , phoneNumber );
102
+ } catch (Exception e ) {
103
+ log .warn ("Failed to send SMS: {}" , e .getMessage ());
104
+ throw new SendMessageException (e .getMessage ());
105
+ }
106
+ }
107
+
108
+ private HttpPost createHttpPost (URI requestUri , String toNumber , String message ) {
109
+ HttpPost httpPost = new HttpPost (requestUri );
110
+
111
+ String auth = accountSid + ":" + authToken ;
112
+ String encodedAuth = Base64 .getEncoder ().encodeToString (auth .getBytes ());
113
+ httpPost .setHeader ("Authorization" , "Basic " + encodedAuth );
114
+
115
+ List <BasicNameValuePair > parameters = new ArrayList <>();
116
+ parameters .add (new BasicNameValuePair ("To" , toNumber ));
117
+ parameters .add (new BasicNameValuePair ("From" , twilioPhoneNumber ));
118
+ parameters .add (new BasicNameValuePair ("Body" , message ));
119
+
120
+ try {
121
+ httpPost .setEntity (new UrlEncodedFormEntity (parameters ));
122
+ return httpPost ;
123
+ } catch (Exception e ) {
124
+ log .error ("Failed to create HTTP request: {}" , e .getMessage ());
125
+ throw new SendMessageException (e .getMessage ());
126
+ }
127
+ }
128
+
129
+ private void executeRequest (CloseableHttpClient httpClient , HttpPost httpPost , String phoneNumber )
130
+ throws Exception {
131
+ try (CloseableHttpResponse response = httpClient .execute (httpPost )) {
132
+ int statusCode = response .getStatusLine ().getStatusCode ();
133
+ String responseBody = EntityUtils .toString (response .getEntity ());
134
+ log .info ("SMS response status: {}, body: {}" , statusCode , responseBody );
135
+
136
+ if (statusCode < 200 || statusCode >= 300 ) {
137
+
138
+ if (responseBody .contains ("21608" )) {
139
+ throw new SendMessageException (
140
+ "The Twilio trial account can only send SMS to verified phone numbers" );
141
+ } else {
142
+ throw new SendMessageException (
143
+ "HTTP request failed with status code: " + statusCode + ", response: " + responseBody );
144
+ }
145
+ }
146
+
147
+ JsonNode jsonResponse = JsonUtil .fromJson (responseBody );
148
+ if (jsonResponse == null ) {
149
+ throw new SendMessageException (statusCode + ":" + responseBody );
150
+ }
151
+
152
+ JsonNode sidNode = jsonResponse .get ("sid" );
153
+ if (sidNode == null ) {
154
+ throw new SendMessageException (statusCode + ":" + responseBody );
155
+ }
156
+
157
+ String sid = sidNode .asText ();
158
+ log .info ("Successfully sent SMS to phone: {}, sid: {}" , phoneNumber , sid );
159
+ }
160
+ }
161
+
162
+ @ Override
163
+ public String getType () {
164
+ return TWILIO ;
165
+ }
166
+
167
+ @ Override
168
+ public boolean checkConfig () {
169
+ return !(accountSid .isBlank () || authToken .isBlank () || twilioPhoneNumber .isBlank ());
170
+ }
171
+ }
0 commit comments