47 changed files with 1888 additions and 41 deletions
@ -0,0 +1,54 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.sms; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.rule.engine.api.sms.SmsSender; |
|||
import org.thingsboard.rule.engine.api.sms.exception.SmsParseException; |
|||
import sun.misc.Regexp; |
|||
|
|||
import java.util.regex.Pattern; |
|||
|
|||
@Slf4j |
|||
public abstract class AbstractSmsSender implements SmsSender { |
|||
|
|||
private static final Pattern E_164_PHONE_NUMBER_PATTERN = Pattern.compile("^\\+[1-9]\\d{1,14}$"); |
|||
|
|||
private static final int MAX_SMS_MESSAGE_LENGTH = 1600; |
|||
private static final int MAX_SMS_SEGMENT_LENGTH = 70; |
|||
|
|||
protected String validatePhoneNumber(String phoneNumber) throws SmsParseException { |
|||
phoneNumber = phoneNumber.trim(); |
|||
if (!E_164_PHONE_NUMBER_PATTERN.matcher(phoneNumber).matches()) { |
|||
throw new SmsParseException("Invalid phone number format. Phone number must be in E.164 format."); |
|||
} |
|||
return phoneNumber; |
|||
} |
|||
|
|||
protected String prepareMessage(String message) { |
|||
message = message.replaceAll("^\"|\"$", "").replaceAll("\\\\n", "\n"); |
|||
if (message.length() > MAX_SMS_MESSAGE_LENGTH) { |
|||
log.warn("SMS message exceeds maximum symbols length and will be truncated"); |
|||
message = message.substring(0, MAX_SMS_MESSAGE_LENGTH); |
|||
} |
|||
return message; |
|||
} |
|||
|
|||
protected int countMessageSegments(String message) { |
|||
return (int)Math.ceil((double) message.length() / (double) MAX_SMS_SEGMENT_LENGTH); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.sms; |
|||
|
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.rule.engine.api.sms.SmsSender; |
|||
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; |
|||
import org.thingsboard.rule.engine.api.sms.config.AwsSnsSmsProviderConfiguration; |
|||
import org.thingsboard.rule.engine.api.sms.config.SmsProviderConfiguration; |
|||
import org.thingsboard.rule.engine.api.sms.config.TwilioSmsProviderConfiguration; |
|||
import org.thingsboard.server.service.sms.aws.AwsSmsSender; |
|||
import org.thingsboard.server.service.sms.twilio.TwilioSmsSender; |
|||
|
|||
@Component |
|||
public class DefaultSmsSenderFactory implements SmsSenderFactory { |
|||
|
|||
@Override |
|||
public SmsSender createSmsSender(SmsProviderConfiguration config) { |
|||
switch (config.getType()) { |
|||
case AWS_SNS: |
|||
return new AwsSmsSender((AwsSnsSmsProviderConfiguration)config); |
|||
case TWILIO: |
|||
return new TwilioSmsSender((TwilioSmsProviderConfiguration)config); |
|||
default: |
|||
throw new RuntimeException("Unknown SMS provider type " + config.getType()); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,127 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.sms; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.core.NestedRuntimeException; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.rule.engine.api.SmsService; |
|||
import org.thingsboard.rule.engine.api.sms.SmsSender; |
|||
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; |
|||
import org.thingsboard.rule.engine.api.sms.config.SmsProviderConfiguration; |
|||
import org.thingsboard.rule.engine.api.sms.config.TestSmsRequest; |
|||
import org.thingsboard.server.common.data.AdminSettings; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.dao.settings.AdminSettingsService; |
|||
import org.thingsboard.server.dao.util.mapping.JacksonUtil; |
|||
|
|||
import javax.annotation.PostConstruct; |
|||
import javax.annotation.PreDestroy; |
|||
|
|||
@Service |
|||
@Slf4j |
|||
public class DefaultSmsService implements SmsService { |
|||
|
|||
@Autowired |
|||
private SmsSenderFactory smsSenderFactory; |
|||
|
|||
@Autowired |
|||
private AdminSettingsService adminSettingsService; |
|||
|
|||
private SmsSender smsSender; |
|||
|
|||
@PostConstruct |
|||
private void init() { |
|||
updateSmsConfiguration(); |
|||
} |
|||
|
|||
@PreDestroy |
|||
private void destroy() { |
|||
if (this.smsSender != null) { |
|||
this.smsSender.destroy(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void updateSmsConfiguration() { |
|||
AdminSettings settings = adminSettingsService.findAdminSettingsByKey(new TenantId(EntityId.NULL_UUID), "sms"); |
|||
if (settings != null) { |
|||
try { |
|||
JsonNode jsonConfig = settings.getJsonValue(); |
|||
SmsProviderConfiguration configuration = JacksonUtil.convertValue(jsonConfig, SmsProviderConfiguration.class); |
|||
SmsSender newSmsSender = this.smsSenderFactory.createSmsSender(configuration); |
|||
if (this.smsSender != null) { |
|||
this.smsSender.destroy(); |
|||
} |
|||
this.smsSender = newSmsSender; |
|||
} catch (Exception e) { |
|||
log.error("Failed to create SMS sender", e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void sendSms(String numberTo, String message) throws ThingsboardException { |
|||
if (this.smsSender == null) { |
|||
throw new ThingsboardException("Unable to send SMS: no SMS provider configured!", ThingsboardErrorCode.GENERAL); |
|||
} |
|||
this.sendSms(this.smsSender, numberTo, message); |
|||
} |
|||
|
|||
@Override |
|||
public void sendSms(String[] numbersTo, String message) throws ThingsboardException { |
|||
for (String numberTo : numbersTo) { |
|||
this.sendSms(numberTo, message); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void sendTestSms(TestSmsRequest testSmsRequest) throws ThingsboardException { |
|||
SmsSender testSmsSender; |
|||
try { |
|||
testSmsSender = this.smsSenderFactory.createSmsSender(testSmsRequest.getProviderConfiguration()); |
|||
} catch (Exception e) { |
|||
throw handleException(e); |
|||
} |
|||
this.sendSms(testSmsSender, testSmsRequest.getNumberTo(), testSmsRequest.getMessage()); |
|||
testSmsSender.destroy(); |
|||
} |
|||
|
|||
private int sendSms(SmsSender smsSender, String numberTo, String message) throws ThingsboardException { |
|||
try { |
|||
return smsSender.sendSms(numberTo, message); |
|||
} catch (Exception e) { |
|||
throw handleException(e); |
|||
} |
|||
} |
|||
|
|||
private ThingsboardException handleException(Exception exception) { |
|||
String message; |
|||
if (exception instanceof NestedRuntimeException) { |
|||
message = ((NestedRuntimeException) exception).getMostSpecificCause().getMessage(); |
|||
} else { |
|||
message = exception.getMessage(); |
|||
} |
|||
log.warn("Unable to send SMS: {}", message); |
|||
return new ThingsboardException(String.format("Unable to send SMS: %s", message), |
|||
ThingsboardErrorCode.GENERAL); |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.sms; |
|||
|
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.common.util.AbstractListeningExecutor; |
|||
|
|||
@Component |
|||
public class SmsExecutorService extends AbstractListeningExecutor { |
|||
|
|||
@Value("${actors.rule.sms_thread_pool_size}") |
|||
private int smsExecutorThreadPoolSize; |
|||
|
|||
@Override |
|||
protected int getThreadPollSize() { |
|||
return smsExecutorThreadPoolSize; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.sms.aws; |
|||
|
|||
import com.amazonaws.auth.AWSCredentials; |
|||
import com.amazonaws.auth.AWSStaticCredentialsProvider; |
|||
import com.amazonaws.auth.BasicAWSCredentials; |
|||
import com.amazonaws.services.sns.AmazonSNS; |
|||
import com.amazonaws.services.sns.AmazonSNSClient; |
|||
import com.amazonaws.services.sns.model.PublishRequest; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.thingsboard.rule.engine.api.sms.config.AwsSnsSmsProviderConfiguration; |
|||
import org.thingsboard.rule.engine.api.sms.exception.SmsException; |
|||
import org.thingsboard.rule.engine.api.sms.exception.SmsSendException; |
|||
import org.thingsboard.server.service.sms.AbstractSmsSender; |
|||
|
|||
@Slf4j |
|||
public class AwsSmsSender extends AbstractSmsSender { |
|||
|
|||
private AmazonSNS snsClient; |
|||
|
|||
public AwsSmsSender(AwsSnsSmsProviderConfiguration config) { |
|||
if (StringUtils.isEmpty(config.getAccessKeyId()) || StringUtils.isEmpty(config.getSecretAccessKey()) || StringUtils.isEmpty(config.getRegion())) { |
|||
throw new IllegalArgumentException("Invalid AWS sms provider configuration: aws accessKeyId, aws secretAccessKey and aws region should be specified!"); |
|||
} |
|||
AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKeyId(), config.getSecretAccessKey()); |
|||
AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials); |
|||
this.snsClient = AmazonSNSClient.builder() |
|||
.withCredentials(credProvider) |
|||
.withRegion(config.getRegion()) |
|||
.build(); |
|||
} |
|||
|
|||
@Override |
|||
public int sendSms(String numberTo, String message) throws SmsException { |
|||
numberTo = this.validatePhoneNumber(numberTo); |
|||
message = this.prepareMessage(message); |
|||
try { |
|||
PublishRequest publishRequest = new PublishRequest() |
|||
.withPhoneNumber(numberTo) |
|||
.withMessage(message); |
|||
this.snsClient.publish(publishRequest); |
|||
return this.countMessageSegments(message); |
|||
} catch (Exception e) { |
|||
throw new SmsSendException("Failed to send SMS message - " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void destroy() { |
|||
if (this.snsClient != null) { |
|||
try { |
|||
this.snsClient.shutdown(); |
|||
} catch (Exception e) { |
|||
log.error("Failed to shutdown SNS client during destroy()", e); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.sms.twilio; |
|||
|
|||
import com.twilio.http.TwilioRestClient; |
|||
import com.twilio.rest.api.v2010.account.Message; |
|||
import com.twilio.type.PhoneNumber; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.thingsboard.rule.engine.api.sms.config.TwilioSmsProviderConfiguration; |
|||
import org.thingsboard.rule.engine.api.sms.exception.SmsException; |
|||
import org.thingsboard.rule.engine.api.sms.exception.SmsSendException; |
|||
import org.thingsboard.server.service.sms.AbstractSmsSender; |
|||
|
|||
public class TwilioSmsSender extends AbstractSmsSender { |
|||
|
|||
private TwilioRestClient twilioRestClient; |
|||
private String numberFrom; |
|||
|
|||
public TwilioSmsSender(TwilioSmsProviderConfiguration config) { |
|||
if (StringUtils.isEmpty(config.getAccountSid()) || StringUtils.isEmpty(config.getAccountToken()) || StringUtils.isEmpty(config.getNumberFrom())) { |
|||
throw new IllegalArgumentException("Invalid twilio sms provider configuration: accountSid, accountToken and numberFrom should be specified!"); |
|||
} |
|||
this.numberFrom = this.validatePhoneNumber(config.getNumberFrom()); |
|||
this.twilioRestClient = new TwilioRestClient.Builder(config.getAccountSid(), config.getAccountToken()).build(); |
|||
} |
|||
|
|||
@Override |
|||
public int sendSms(String numberTo, String message) throws SmsException { |
|||
numberTo = this.validatePhoneNumber(numberTo); |
|||
message = this.prepareMessage(message); |
|||
try { |
|||
String numSegments = Message.creator(new PhoneNumber(numberTo), new PhoneNumber(this.numberFrom), message).create(this.twilioRestClient).getNumSegments(); |
|||
return Integer.valueOf(numSegments); |
|||
} catch (Exception e) { |
|||
throw new SmsSendException("Failed to send SMS message - " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void destroy() { |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.rule.engine.api; |
|||
|
|||
import org.thingsboard.rule.engine.api.sms.config.TestSmsRequest; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
|
|||
public interface SmsService { |
|||
|
|||
void updateSmsConfiguration(); |
|||
|
|||
void sendSms(String numberTo, String message) throws ThingsboardException; |
|||
|
|||
void sendSms(String[] numbersTo, String message) throws ThingsboardException;; |
|||
|
|||
void sendTestSms(TestSmsRequest testSmsRequest) throws ThingsboardException; |
|||
|
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.rule.engine.api.sms; |
|||
|
|||
import org.thingsboard.rule.engine.api.sms.exception.SmsException; |
|||
|
|||
public interface SmsSender { |
|||
|
|||
int sendSms(String numberTo, String message) throws SmsException; |
|||
|
|||
void destroy(); |
|||
|
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.rule.engine.api.sms; |
|||
|
|||
import org.thingsboard.rule.engine.api.sms.config.SmsProviderConfiguration; |
|||
|
|||
public interface SmsSenderFactory { |
|||
|
|||
SmsSender createSmsSender(SmsProviderConfiguration config); |
|||
|
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.rule.engine.api.sms.config; |
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class AwsSnsSmsProviderConfiguration implements SmsProviderConfiguration { |
|||
|
|||
private String accessKeyId; |
|||
private String secretAccessKey; |
|||
private String region; |
|||
|
|||
@Override |
|||
public SmsProviderType getType() { |
|||
return SmsProviderType.AWS_SNS; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.rule.engine.api.sms.config; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonIgnore; |
|||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
|||
import com.fasterxml.jackson.annotation.JsonSubTypes; |
|||
import com.fasterxml.jackson.annotation.JsonTypeInfo; |
|||
|
|||
@JsonIgnoreProperties(ignoreUnknown = true) |
|||
@JsonTypeInfo( |
|||
use = JsonTypeInfo.Id.NAME, |
|||
include = JsonTypeInfo.As.PROPERTY, |
|||
property = "type") |
|||
@JsonSubTypes({ |
|||
@JsonSubTypes.Type(value = AwsSnsSmsProviderConfiguration.class, name = "AWS_SNS"), |
|||
@JsonSubTypes.Type(value = TwilioSmsProviderConfiguration.class, name = "TWILIO")}) |
|||
public interface SmsProviderConfiguration { |
|||
|
|||
@JsonIgnore |
|||
SmsProviderType getType(); |
|||
|
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.rule.engine.api.sms.config; |
|||
|
|||
public enum SmsProviderType { |
|||
AWS_SNS, |
|||
TWILIO |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.rule.engine.api.sms.config; |
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class TestSmsRequest { |
|||
|
|||
private SmsProviderConfiguration providerConfiguration; |
|||
private String numberTo; |
|||
private String message; |
|||
|
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.rule.engine.api.sms.config; |
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class TwilioSmsProviderConfiguration implements SmsProviderConfiguration { |
|||
|
|||
private String accountSid; |
|||
private String accountToken; |
|||
private String numberFrom; |
|||
|
|||
@Override |
|||
public SmsProviderType getType() { |
|||
return SmsProviderType.TWILIO; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.rule.engine.api.sms.exception; |
|||
|
|||
public abstract class SmsException extends RuntimeException { |
|||
|
|||
public SmsException(String msg) { |
|||
super(msg); |
|||
} |
|||
|
|||
public SmsException(String msg, Throwable cause) { |
|||
super(msg, cause); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.rule.engine.api.sms.exception; |
|||
|
|||
public class SmsParseException extends SmsException { |
|||
|
|||
public SmsParseException(String msg) { |
|||
super(msg); |
|||
} |
|||
|
|||
public SmsParseException(String msg, Throwable cause) { |
|||
super(msg, cause); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.rule.engine.api.sms.exception; |
|||
|
|||
public class SmsSendException extends SmsException { |
|||
|
|||
public SmsSendException(String msg) { |
|||
super(msg); |
|||
} |
|||
|
|||
public SmsSendException(String msg, Throwable cause) { |
|||
super(msg, cause); |
|||
} |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.rule.engine.sms; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.rule.engine.api.RuleNode; |
|||
import org.thingsboard.rule.engine.api.TbContext; |
|||
import org.thingsboard.rule.engine.api.TbNode; |
|||
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
|||
import org.thingsboard.rule.engine.api.TbNodeException; |
|||
import org.thingsboard.rule.engine.api.sms.SmsSender; |
|||
import org.thingsboard.rule.engine.api.util.TbNodeUtils; |
|||
import org.thingsboard.server.common.data.plugin.ComponentType; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
|
|||
import static org.thingsboard.common.util.DonAsynchron.withCallback; |
|||
|
|||
@Slf4j |
|||
@RuleNode( |
|||
type = ComponentType.EXTERNAL, |
|||
name = "send sms", |
|||
configClazz = TbSendSmsNodeConfiguration.class, |
|||
nodeDescription = "Sends SMS message via SMS provider.", |
|||
nodeDetails = "Will send SMS message by populating target phone numbers and sms message fields using values derived from message metadata.", |
|||
uiResources = {"static/rulenode/rulenode-core-config.js"}, |
|||
configDirective = "tbActionNodeSendSmsConfig", |
|||
icon = "sms" |
|||
) |
|||
public class TbSendSmsNode implements TbNode { |
|||
|
|||
private TbSendSmsNodeConfiguration config; |
|||
private SmsSender smsSender; |
|||
|
|||
@Override |
|||
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
|||
try { |
|||
this.config = TbNodeUtils.convert(configuration, TbSendSmsNodeConfiguration.class); |
|||
if (!this.config.isUseSystemSmsSettings()) { |
|||
smsSender = createSmsSender(ctx); |
|||
} |
|||
} catch (Exception e) { |
|||
throw new TbNodeException(e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onMsg(TbContext ctx, TbMsg msg) { |
|||
try { |
|||
withCallback(ctx.getSmsExecutor().executeAsync(() -> { |
|||
sendSms(ctx, msg); |
|||
return null; |
|||
}), |
|||
ok -> ctx.tellSuccess(msg), |
|||
fail -> ctx.tellFailure(msg, fail)); |
|||
} catch (Exception ex) { |
|||
ctx.tellFailure(msg, ex); |
|||
} |
|||
} |
|||
|
|||
private void sendSms(TbContext ctx, TbMsg msg) throws Exception { |
|||
String numbersTo = TbNodeUtils.processPattern(this.config.getNumbersToTemplate(), msg.getMetaData()); |
|||
String message = TbNodeUtils.processPattern(this.config.getSmsMessageTemplate(), msg.getMetaData()); |
|||
String[] numbersToList = numbersTo.split(","); |
|||
if (this.config.isUseSystemSmsSettings()) { |
|||
ctx.getSmsService().sendSms(numbersToList, message); |
|||
} else { |
|||
for (String numberTo : numbersToList) { |
|||
this.smsSender.sendSms(numberTo, message); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void destroy() { |
|||
if (this.smsSender != null) { |
|||
this.smsSender.destroy(); |
|||
} |
|||
} |
|||
|
|||
private SmsSender createSmsSender(TbContext ctx) { |
|||
return ctx.getSmsSenderFactory().createSmsSender(this.config.getSmsProviderConfiguration()); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.rule.engine.sms; |
|||
|
|||
import lombok.Data; |
|||
import org.thingsboard.rule.engine.api.NodeConfiguration; |
|||
import org.thingsboard.rule.engine.api.sms.config.SmsProviderConfiguration; |
|||
|
|||
@Data |
|||
public class TbSendSmsNodeConfiguration implements NodeConfiguration { |
|||
|
|||
private String numbersToTemplate; |
|||
private String smsMessageTemplate; |
|||
private boolean useSystemSmsSettings; |
|||
private SmsProviderConfiguration smsProviderConfiguration; |
|||
|
|||
@Override |
|||
public NodeConfiguration defaultConfiguration() { |
|||
TbSendSmsNodeConfiguration configuration = new TbSendSmsNodeConfiguration(); |
|||
configuration.numbersToTemplate = "${userPhone}"; |
|||
configuration.smsMessageTemplate = "Device ${deviceName} has high temperature ${temp}"; |
|||
configuration.setUseSystemSmsSettings(true); |
|||
return configuration; |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2020 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<form [formGroup]="awsSnsProviderConfigurationFormGroup" style="padding-bottom: 16px;"> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>admin.aws-access-key-id</mat-label> |
|||
<input required matInput formControlName="accessKeyId"> |
|||
<mat-error *ngIf="awsSnsProviderConfigurationFormGroup.get('accessKeyId').hasError('required')"> |
|||
{{ 'admin.aws-access-key-id-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>admin.aws-secret-access-key</mat-label> |
|||
<input required type="password" matInput formControlName="secretAccessKey"> |
|||
<mat-error *ngIf="awsSnsProviderConfigurationFormGroup.get('secretAccessKey').hasError('required')"> |
|||
{{ 'admin.aws-secret-access-key-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>admin.aws-region</mat-label> |
|||
<input required matInput formControlName="region"> |
|||
<mat-error *ngIf="awsSnsProviderConfigurationFormGroup.get('region').hasError('required')"> |
|||
{{ 'admin.aws-region-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</form> |
|||
@ -0,0 +1,99 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
|||
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@app/core/core.state'; |
|||
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
|||
import { isDefinedAndNotNull } from '@core/utils'; |
|||
import { AwsSnsSmsProviderConfiguration } from '@shared/models/settings.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-aws-sns-provider-configuration', |
|||
templateUrl: './aws-sns-provider-configuration.component.html', |
|||
styleUrls: [], |
|||
providers: [{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => AwsSnsProviderConfigurationComponent), |
|||
multi: true |
|||
}] |
|||
}) |
|||
export class AwsSnsProviderConfigurationComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
awsSnsProviderConfigurationFormGroup: FormGroup; |
|||
|
|||
private requiredValue: boolean; |
|||
|
|||
get required(): boolean { |
|||
return this.requiredValue; |
|||
} |
|||
|
|||
@Input() |
|||
set required(value: boolean) { |
|||
this.requiredValue = coerceBooleanProperty(value); |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
private fb: FormBuilder) { |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.awsSnsProviderConfigurationFormGroup = this.fb.group({ |
|||
accessKeyId: [null, [Validators.required]], |
|||
secretAccessKey: [null, [Validators.required]], |
|||
region: [null, [Validators.required]] |
|||
}); |
|||
this.awsSnsProviderConfigurationFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.awsSnsProviderConfigurationFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.awsSnsProviderConfigurationFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: AwsSnsSmsProviderConfiguration | null): void { |
|||
if (isDefinedAndNotNull(value)) { |
|||
this.awsSnsProviderConfigurationFormGroup.patchValue(value, {emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
private updateModel() { |
|||
let configuration: AwsSnsSmsProviderConfiguration = null; |
|||
if (this.awsSnsProviderConfigurationFormGroup.valid) { |
|||
configuration = this.awsSnsProviderConfigurationFormGroup.value; |
|||
} |
|||
this.propagateChange(configuration); |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2020 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div [formGroup]="smsProviderConfigurationFormGroup"> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>admin.sms-provider-type</mat-label> |
|||
<mat-select formControlName="type" [required]="required"> |
|||
<mat-option *ngFor="let type of smsProviderTypes" [value]="type"> |
|||
{{smsProviderTypeTranslations.get(type) | translate}} |
|||
</mat-option> |
|||
</mat-select> |
|||
<mat-error *ngIf="smsProviderConfigurationFormGroup.get('type').hasError('required')"> |
|||
{{ 'admin.sms-provider-type-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<div [ngSwitch]="smsProviderConfigurationFormGroup.get('type').value"> |
|||
<ng-template [ngSwitchCase]="smsProviderType.AWS_SNS"> |
|||
<tb-aws-sns-provider-configuration |
|||
[required]="required" |
|||
formControlName="configuration"> |
|||
</tb-aws-sns-provider-configuration> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="smsProviderType.TWILIO"> |
|||
<tb-twilio-sms-provider-configuration |
|||
[required]="required" |
|||
formControlName="configuration"> |
|||
</tb-twilio-sms-provider-configuration> |
|||
</ng-template> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,123 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
|||
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@app/core/core.state'; |
|||
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
|||
import { |
|||
DeviceProfileTransportConfiguration, |
|||
DeviceTransportType, |
|||
deviceTransportTypeTranslationMap |
|||
} from '@shared/models/device.models'; |
|||
import { deepClone } from '@core/utils'; |
|||
import { |
|||
createSmsProviderConfiguration, |
|||
SmsProviderConfiguration, |
|||
SmsProviderType, |
|||
smsProviderTypeTranslationMap |
|||
} from '@shared/models/settings.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-sms-provider-configuration', |
|||
templateUrl: './sms-provider-configuration.component.html', |
|||
styleUrls: [], |
|||
providers: [{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => SmsProviderConfigurationComponent), |
|||
multi: true |
|||
}] |
|||
}) |
|||
export class SmsProviderConfigurationComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
smsProviderType = SmsProviderType; |
|||
smsProviderTypes = Object.keys(SmsProviderType); |
|||
smsProviderTypeTranslations = smsProviderTypeTranslationMap; |
|||
|
|||
smsProviderConfigurationFormGroup: FormGroup; |
|||
|
|||
private requiredValue: boolean; |
|||
get required(): boolean { |
|||
return this.requiredValue; |
|||
} |
|||
@Input() |
|||
set required(value: boolean) { |
|||
this.requiredValue = coerceBooleanProperty(value); |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
private fb: FormBuilder) { |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.smsProviderConfigurationFormGroup = this.fb.group({ |
|||
type: [null, Validators.required], |
|||
configuration: [null, Validators.required] |
|||
}); |
|||
this.smsProviderConfigurationFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
this.smsProviderConfigurationFormGroup.get('type').valueChanges.subscribe(() => { |
|||
this.smsProviderTypeChanged(); |
|||
}); |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.smsProviderConfigurationFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.smsProviderConfigurationFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: SmsProviderConfiguration | null): void { |
|||
const configuration = deepClone(value); |
|||
const type = configuration?.type; |
|||
if (configuration) { |
|||
delete configuration.type; |
|||
} |
|||
this.smsProviderConfigurationFormGroup.patchValue({type}, {emitEvent: false}); |
|||
this.smsProviderConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); |
|||
} |
|||
|
|||
private smsProviderTypeChanged() { |
|||
const type: SmsProviderType = this.smsProviderConfigurationFormGroup.get('type').value; |
|||
this.smsProviderConfigurationFormGroup.patchValue({configuration: createSmsProviderConfiguration(type)}, {emitEvent: false}); |
|||
} |
|||
|
|||
private updateModel() { |
|||
let configuration: SmsProviderConfiguration = null; |
|||
if (this.smsProviderConfigurationFormGroup.valid) { |
|||
configuration = this.smsProviderConfigurationFormGroup.getRawValue().configuration; |
|||
configuration.type = this.smsProviderConfigurationFormGroup.getRawValue().type; |
|||
} |
|||
this.propagateChange(configuration); |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2020 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<form [formGroup]="twilioSmsProviderConfigurationFormGroup" style="padding-bottom: 16px;"> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>admin.number-from</mat-label> |
|||
<input type="tel" required [pattern]="phoneNumberPattern" matInput formControlName="numberFrom"> |
|||
<mat-error *ngIf="twilioSmsProviderConfigurationFormGroup.get('numberFrom').hasError('required')"> |
|||
{{ 'admin.number-from-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="twilioSmsProviderConfigurationFormGroup.get('numberFrom').hasError('pattern')"> |
|||
{{ 'admin.phone-number-pattern' | translate }} |
|||
</mat-error> |
|||
<mat-hint innerHTML="{{ 'admin.phone-number-hint' | translate }}"></mat-hint> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>admin.twilio-account-sid</mat-label> |
|||
<input required matInput formControlName="accountSid"> |
|||
<mat-error *ngIf="twilioSmsProviderConfigurationFormGroup.get('accountSid').hasError('required')"> |
|||
{{ 'admin.twilio-account-sid-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>admin.twilio-account-token</mat-label> |
|||
<input required type="password" matInput formControlName="accountToken"> |
|||
<mat-error *ngIf="twilioSmsProviderConfigurationFormGroup.get('accountToken').hasError('required')"> |
|||
{{ 'admin.twilio-account-token-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</form> |
|||
@ -0,0 +1,101 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
|||
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@app/core/core.state'; |
|||
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
|||
import { isDefinedAndNotNull } from '@core/utils'; |
|||
import { phoneNumberPattern, TwilioSmsProviderConfiguration } from '@shared/models/settings.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-twilio-sms-provider-configuration', |
|||
templateUrl: './twilio-sms-provider-configuration.component.html', |
|||
styleUrls: [], |
|||
providers: [{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => TwilioSmsProviderConfigurationComponent), |
|||
multi: true |
|||
}] |
|||
}) |
|||
export class TwilioSmsProviderConfigurationComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
twilioSmsProviderConfigurationFormGroup: FormGroup; |
|||
|
|||
phoneNumberPattern = phoneNumberPattern; |
|||
|
|||
private requiredValue: boolean; |
|||
|
|||
get required(): boolean { |
|||
return this.requiredValue; |
|||
} |
|||
|
|||
@Input() |
|||
set required(value: boolean) { |
|||
this.requiredValue = coerceBooleanProperty(value); |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
private fb: FormBuilder) { |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.twilioSmsProviderConfigurationFormGroup = this.fb.group({ |
|||
numberFrom: [null, [Validators.required, Validators.pattern(phoneNumberPattern)]], |
|||
accountSid: [null, [Validators.required]], |
|||
accountToken: [null, [Validators.required]] |
|||
}); |
|||
this.twilioSmsProviderConfigurationFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.twilioSmsProviderConfigurationFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.twilioSmsProviderConfigurationFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: TwilioSmsProviderConfiguration | null): void { |
|||
if (isDefinedAndNotNull(value)) { |
|||
this.twilioSmsProviderConfigurationFormGroup.patchValue(value, {emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
private updateModel() { |
|||
let configuration: TwilioSmsProviderConfiguration = null; |
|||
if (this.twilioSmsProviderConfigurationFormGroup.valid) { |
|||
configuration = this.twilioSmsProviderConfigurationFormGroup.value; |
|||
} |
|||
this.propagateChange(configuration); |
|||
} |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2020 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<form [formGroup]="sendTestSmsFormGroup" style="min-width: 500px; position: relative;"> |
|||
<mat-toolbar color="primary"> |
|||
<h2 translate>admin.send-test-sms</h2> |
|||
<span fxFlex></span> |
|||
<button mat-icon-button |
|||
(click)="close()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
|||
<div mat-dialog-content tb-toast toastTarget="sendTestSmsDialogContent"> |
|||
<fieldset [disabled]="(isLoading$ | async)"> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>admin.number-to</mat-label> |
|||
<input type="tel" required [pattern]="phoneNumberPattern" matInput formControlName="numberTo"> |
|||
<mat-error *ngIf="sendTestSmsFormGroup.get('numberTo').hasError('required')"> |
|||
{{ 'admin.number-to-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="sendTestSmsFormGroup.get('numberTo').hasError('pattern')"> |
|||
{{ 'admin.phone-number-pattern' | translate }} |
|||
</mat-error> |
|||
<mat-hint innerHTML="{{ 'admin.phone-number-hint' | translate }}"></mat-hint> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>admin.sms-message</mat-label> |
|||
<textarea required matInput rows="3" [maxLength]="1600" formControlName="message"></textarea> |
|||
<mat-error *ngIf="sendTestSmsFormGroup.get('message').hasError('required')"> |
|||
{{ 'admin.sms-message-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="sendTestSmsFormGroup.get('message').hasError('maxLength')"> |
|||
{{ 'admin.sms-message-max-length' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</fieldset> |
|||
</div> |
|||
<div mat-dialog-actions fxLayoutAlign="end center"> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="close()" cdkFocusInitial> |
|||
{{ 'action.close' | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
type="button" |
|||
(click)="sendTestSms()" |
|||
[disabled]="(isLoading$ | async) || sendTestSmsFormGroup.invalid"> |
|||
{{ 'admin.send-test-sms' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,87 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, Inject, OnInit } from '@angular/core'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { Router } from '@angular/router'; |
|||
import { phoneNumberPattern, SmsProviderConfiguration, TestSmsRequest } from '@shared/models/settings.models'; |
|||
import { AdminService } from '@core/http/admin.service'; |
|||
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
|
|||
export interface SendTestSmsDialogData { |
|||
smsProviderConfiguration: SmsProviderConfiguration; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-send-test-sms-dialog', |
|||
templateUrl: './send-test-sms-dialog.component.html', |
|||
styleUrls: [] |
|||
}) |
|||
export class SendTestSmsDialogComponent extends |
|||
DialogComponent<SendTestSmsDialogComponent> implements OnInit { |
|||
|
|||
phoneNumberPattern = phoneNumberPattern; |
|||
|
|||
sendTestSmsFormGroup: FormGroup; |
|||
|
|||
smsProviderConfiguration = this.data.smsProviderConfiguration; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: SendTestSmsDialogData, |
|||
private adminService: AdminService, |
|||
private translate: TranslateService, |
|||
public dialogRef: MatDialogRef<SendTestSmsDialogComponent>, |
|||
public fb: FormBuilder) { |
|||
super(store, router, dialogRef); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.sendTestSmsFormGroup = this.fb.group({ |
|||
numberTo: [null, [Validators.required, Validators.pattern(phoneNumberPattern)]], |
|||
message: [null, [Validators.required, Validators.maxLength(1600)]] |
|||
}); |
|||
} |
|||
|
|||
close(): void { |
|||
this.dialogRef.close(); |
|||
} |
|||
|
|||
sendTestSms(): void { |
|||
const request: TestSmsRequest = { |
|||
providerConfiguration: this.smsProviderConfiguration, |
|||
numberTo: this.sendTestSmsFormGroup.value.numberTo, |
|||
message: this.sendTestSmsFormGroup.value.message |
|||
}; |
|||
this.adminService.sendTestSms(request).subscribe( |
|||
() => { |
|||
this.store.dispatch(new ActionNotificationShow( |
|||
{ |
|||
message: this.translate.instant('admin.test-sms-sent'), |
|||
target: 'sendTestSmsDialogContent', |
|||
verticalPosition: 'bottom', |
|||
horizontalPosition: 'left', |
|||
type: 'success' |
|||
})); |
|||
} |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2020 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div> |
|||
<mat-card class="settings-card"> |
|||
<mat-card-title> |
|||
<div fxLayout="row"> |
|||
<span class="mat-headline" translate>admin.sms-provider-settings</span> |
|||
<span fxFlex></span> |
|||
<div tb-help="smsProviderSettings"></div> |
|||
</div> |
|||
</mat-card-title> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
|||
<mat-card-content style="padding-top: 16px;"> |
|||
<form [formGroup]="smsProvider" (ngSubmit)="save()"> |
|||
<fieldset [disabled]="isLoading$ | async"> |
|||
<tb-sms-provider-configuration |
|||
required |
|||
formControlName="configuration"> |
|||
</tb-sms-provider-configuration> |
|||
<div fxLayout="row" fxLayoutAlign="end center" fxLayout.xs="column" fxLayoutAlign.xs="end" fxLayoutGap="16px"> |
|||
<button mat-raised-button type="button" |
|||
[disabled]="(isLoading$ | async) || smsProvider.invalid" (click)="sendTestSms()"> |
|||
{{'admin.send-test-sms' | translate}} |
|||
</button> |
|||
<button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || smsProvider.invalid || !smsProvider.dirty" |
|||
type="submit">{{'action.save' | translate}} |
|||
</button> |
|||
</div> |
|||
</fieldset> |
|||
</form> |
|||
</mat-card-content> |
|||
</mat-card> |
|||
</div> |
|||
@ -0,0 +1,18 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
|
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
///
|
|||
/// Copyright © 2016-2020 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, OnInit } from '@angular/core'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Router } from '@angular/router'; |
|||
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
|||
import { AdminSettings, SmsProviderConfiguration } from '@shared/models/settings.models'; |
|||
import { AdminService } from '@core/http/admin.service'; |
|||
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { SendTestSmsDialogComponent, SendTestSmsDialogData } from '@home/pages/admin/send-test-sms-dialog.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-sms-provider', |
|||
templateUrl: './sms-provider.component.html', |
|||
styleUrls: ['./sms-provider.component.scss', './settings-card.scss'] |
|||
}) |
|||
export class SmsProviderComponent extends PageComponent implements OnInit, HasConfirmForm { |
|||
|
|||
smsProvider: FormGroup; |
|||
adminSettings: AdminSettings<SmsProviderConfiguration>; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private router: Router, |
|||
private adminService: AdminService, |
|||
private dialog: MatDialog, |
|||
public fb: FormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.buildSmsProviderForm(); |
|||
this.adminService.getAdminSettings<SmsProviderConfiguration>('sms', {ignoreErrors: true}).subscribe( |
|||
(adminSettings) => { |
|||
this.adminSettings = adminSettings; |
|||
this.smsProvider.reset({configuration: this.adminSettings.jsonValue}); |
|||
}, |
|||
() => { |
|||
this.adminSettings = { |
|||
key: 'sms', |
|||
jsonValue: null |
|||
}; |
|||
this.smsProvider.reset({configuration: this.adminSettings.jsonValue}); |
|||
} |
|||
); |
|||
} |
|||
|
|||
buildSmsProviderForm() { |
|||
this.smsProvider = this.fb.group({ |
|||
configuration: [null, [Validators.required]] |
|||
}); |
|||
this.registerDisableOnLoadFormControl(this.smsProvider.get('configuration')); |
|||
} |
|||
|
|||
sendTestSms(): void { |
|||
this.dialog.open<SendTestSmsDialogComponent, SendTestSmsDialogData>(SendTestSmsDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
smsProviderConfiguration: this.smsProvider.value.configuration |
|||
} |
|||
}); |
|||
} |
|||
|
|||
save(): void { |
|||
this.adminSettings.jsonValue = this.smsProvider.value.configuration; |
|||
this.adminService.saveAdminSettings(this.adminSettings).subscribe( |
|||
(adminSettings) => { |
|||
this.adminSettings = adminSettings; |
|||
this.smsProvider.reset({configuration: this.adminSettings.jsonValue}); |
|||
} |
|||
); |
|||
} |
|||
|
|||
confirmForm(): FormGroup { |
|||
return this.smsProvider; |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue