Added routing key and secret
This commit is contained in:
		
							parent
							
								
									4327f4d3a6
								
							
						
					
					
						commit
						d3bda85a93
					
				@ -88,6 +88,10 @@
 | 
			
		||||
            <groupId>org.thingsboard.common</groupId>
 | 
			
		||||
            <artifactId>queue</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.thingsboard.common</groupId>
 | 
			
		||||
            <artifactId>edge-api</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.thingsboard</groupId>
 | 
			
		||||
            <artifactId>dao</artifactId>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,104 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2019 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.edge.rpc;
 | 
			
		||||
 | 
			
		||||
import com.google.common.io.Resources;
 | 
			
		||||
import io.grpc.Server;
 | 
			
		||||
import io.grpc.ServerBuilder;
 | 
			
		||||
import io.grpc.stub.StreamObserver;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Value;
 | 
			
		||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 | 
			
		||||
import org.springframework.stereotype.Service;
 | 
			
		||||
import org.thingsboard.server.common.edge.gen.EdgeProtos;
 | 
			
		||||
import org.thingsboard.server.common.edge.gen.EdgeRpcServiceGrpc;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.PostConstruct;
 | 
			
		||||
import javax.annotation.PreDestroy;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
 | 
			
		||||
@Service
 | 
			
		||||
@Slf4j
 | 
			
		||||
@ConditionalOnProperty(prefix = "edges.rpc", value = "enabled", havingValue = "true")
 | 
			
		||||
public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase {
 | 
			
		||||
 | 
			
		||||
    @Value("${edges.rpc.port}")
 | 
			
		||||
    private int rpcPort;
 | 
			
		||||
    @Value("${edges.rpc.ssl.enabled}")
 | 
			
		||||
    private boolean sslEnabled;
 | 
			
		||||
    @Value("${edges.rpc.ssl.cert}")
 | 
			
		||||
    private String certFileResource;
 | 
			
		||||
    @Value("${edges.rpc.ssl.privateKey}")
 | 
			
		||||
    private String privateKeyResource;
 | 
			
		||||
 | 
			
		||||
    private Server server;
 | 
			
		||||
 | 
			
		||||
    @PostConstruct
 | 
			
		||||
    public void init() {
 | 
			
		||||
        log.info("Initializing Edge RPC service!");
 | 
			
		||||
        ServerBuilder builder = ServerBuilder.forPort(rpcPort).addService(this);
 | 
			
		||||
        if (sslEnabled) {
 | 
			
		||||
            try {
 | 
			
		||||
                File certFile = new File(Resources.getResource(certFileResource).toURI());
 | 
			
		||||
                File privateKeyFile = new File(Resources.getResource(privateKeyResource).toURI());
 | 
			
		||||
                builder.useTransportSecurity(certFile, privateKeyFile);
 | 
			
		||||
            } catch (Exception e) {
 | 
			
		||||
                log.error("Unable to set up SSL context. Reason: " + e.getMessage(), e);
 | 
			
		||||
                throw new RuntimeException("Unable to set up SSL context!", e);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        server = builder.build();
 | 
			
		||||
        log.info("Going to start Edge RPC server using port: {}", rpcPort);
 | 
			
		||||
        try {
 | 
			
		||||
            server.start();
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            log.error("Failed to start Edge RPC server!", e);
 | 
			
		||||
            throw new RuntimeException("Failed to start Edge RPC server!");
 | 
			
		||||
        }
 | 
			
		||||
        log.info("Edge RPC service initialized!");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @PreDestroy
 | 
			
		||||
    public void destroy() {
 | 
			
		||||
        if (server != null) {
 | 
			
		||||
            server.shutdownNow();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public StreamObserver<EdgeProtos.UplinkMsg> sendUplink(StreamObserver<EdgeProtos.DownlinkMsg> responseObserver) {
 | 
			
		||||
        log.info("sendUplink [{}]", responseObserver);
 | 
			
		||||
        return new StreamObserver<EdgeProtos.UplinkMsg>() {
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onNext(EdgeProtos.UplinkMsg uplinkMsg) {
 | 
			
		||||
                log.info("onNext [{}]", uplinkMsg);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onError(Throwable throwable) {
 | 
			
		||||
                log.info("onError", throwable);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onCompleted() {
 | 
			
		||||
                log.info("onCompleted");
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -495,6 +495,17 @@ transport:
 | 
			
		||||
    bind_port: "${COAP_BIND_PORT:5683}"
 | 
			
		||||
    timeout: "${COAP_TIMEOUT:10000}"
 | 
			
		||||
 | 
			
		||||
# Edges parameters
 | 
			
		||||
edges:
 | 
			
		||||
  rpc:
 | 
			
		||||
    enabled: "${EDGES_RPC_ENABLED:true}"
 | 
			
		||||
    port: "${EDGES_RPC_PORT:60061}"
 | 
			
		||||
    ssl:
 | 
			
		||||
      # Enable/disable SSL support
 | 
			
		||||
      enabled: "${EDGES_RPC_SSL_ENABLED:false}"
 | 
			
		||||
      cert: "${EDGES_RPC_SSL_CERT:certChainFile.pem}"
 | 
			
		||||
      privateKey: "${EDGES_RPC_SSL_PRIVATE_KEY:privateKeyFile.pem}"
 | 
			
		||||
 | 
			
		||||
swagger:
 | 
			
		||||
  api_path_regex: "${SWAGGER_API_PATH_REGEX:/api.*}"
 | 
			
		||||
  security_path_regex: "${SWAGGER_SECURITY_PATH_REGEX:/api.*}"
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.page.TextPageData;
 | 
			
		||||
import org.thingsboard.server.common.data.page.TextPageLink;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
public interface EdgeService {
 | 
			
		||||
 | 
			
		||||
@ -35,6 +36,8 @@ public interface EdgeService {
 | 
			
		||||
 | 
			
		||||
    Edge findEdgeByTenantIdAndName(TenantId tenantId, String name);
 | 
			
		||||
 | 
			
		||||
    Optional<Edge> findEdgeByRoutingKey(TenantId tenantId, String routingKey);
 | 
			
		||||
 | 
			
		||||
    Edge saveEdge(Edge edge);
 | 
			
		||||
 | 
			
		||||
    Edge assignEdgeToCustomer(TenantId tenantId, EdgeId edgeId, CustomerId customerId);
 | 
			
		||||
 | 
			
		||||
@ -44,6 +44,8 @@ public class Edge extends SearchTextBasedWithAdditionalInfo<EdgeId> implements H
 | 
			
		||||
    private String name;
 | 
			
		||||
    private String type;
 | 
			
		||||
    private String label;
 | 
			
		||||
    private String routingKey;
 | 
			
		||||
    private String secret;
 | 
			
		||||
    private transient JsonNode configuration;
 | 
			
		||||
 | 
			
		||||
    public Edge() {
 | 
			
		||||
@ -60,6 +62,8 @@ public class Edge extends SearchTextBasedWithAdditionalInfo<EdgeId> implements H
 | 
			
		||||
        this.customerId = edge.getCustomerId();
 | 
			
		||||
        this.type = edge.getType();
 | 
			
		||||
        this.name = edge.getName();
 | 
			
		||||
        this.routingKey = edge.getRoutingKey();
 | 
			
		||||
        this.secret = edge.getSecret();
 | 
			
		||||
        this.configuration = edge.getConfiguration();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										129
									
								
								common/edge-api/pom.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								common/edge-api/pom.xml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,129 @@
 | 
			
		||||
<!--
 | 
			
		||||
 | 
			
		||||
    Copyright © 2016-2019 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.
 | 
			
		||||
 | 
			
		||||
-->
 | 
			
		||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
			
		||||
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 | 
			
		||||
    <modelVersion>4.0.0</modelVersion>
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.thingsboard</groupId>
 | 
			
		||||
        <version>2.4.1-SNAPSHOT</version>
 | 
			
		||||
        <artifactId>common</artifactId>
 | 
			
		||||
    </parent>
 | 
			
		||||
    <groupId>org.thingsboard.common</groupId>
 | 
			
		||||
    <artifactId>edge-api</artifactId>
 | 
			
		||||
    <packaging>jar</packaging>
 | 
			
		||||
 | 
			
		||||
    <name>Thingsboard Server Remote Edge wrapper</name>
 | 
			
		||||
    <url>https://thingsboard.io</url>
 | 
			
		||||
 | 
			
		||||
    <properties>
 | 
			
		||||
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 | 
			
		||||
        <main.dir>${basedir}/../..</main.dir>
 | 
			
		||||
    </properties>
 | 
			
		||||
 | 
			
		||||
    <dependencies>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.thingsboard.common</groupId>
 | 
			
		||||
            <artifactId>data</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.thingsboard.common</groupId>
 | 
			
		||||
            <artifactId>message</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.google.code.gson</groupId>
 | 
			
		||||
            <artifactId>gson</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.slf4j</groupId>
 | 
			
		||||
            <artifactId>slf4j-api</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.slf4j</groupId>
 | 
			
		||||
            <artifactId>log4j-over-slf4j</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>ch.qos.logback</groupId>
 | 
			
		||||
            <artifactId>logback-core</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>ch.qos.logback</groupId>
 | 
			
		||||
            <artifactId>logback-classic</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.springframework</groupId>
 | 
			
		||||
            <artifactId>spring-context</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.springframework.boot</groupId>
 | 
			
		||||
            <artifactId>spring-boot-starter-web</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>io.netty</groupId>
 | 
			
		||||
            <artifactId>netty-all</artifactId>
 | 
			
		||||
            <scope>provided</scope>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.google.guava</groupId>
 | 
			
		||||
            <artifactId>guava</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>io.grpc</groupId>
 | 
			
		||||
            <artifactId>grpc-netty</artifactId>
 | 
			
		||||
            <exclusions>
 | 
			
		||||
                <exclusion>
 | 
			
		||||
                    <artifactId>netty-transport</artifactId>
 | 
			
		||||
                    <groupId>io.netty</groupId>
 | 
			
		||||
                </exclusion>
 | 
			
		||||
                <exclusion>
 | 
			
		||||
                    <artifactId>netty-common</artifactId>
 | 
			
		||||
                    <groupId>io.netty</groupId>
 | 
			
		||||
                </exclusion>
 | 
			
		||||
            </exclusions>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>io.grpc</groupId>
 | 
			
		||||
            <artifactId>grpc-protobuf</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>io.grpc</groupId>
 | 
			
		||||
            <artifactId>grpc-stub</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.google.protobuf</groupId>
 | 
			
		||||
            <artifactId>protobuf-java</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
    </dependencies>
 | 
			
		||||
 | 
			
		||||
    <build>
 | 
			
		||||
        <plugins>
 | 
			
		||||
            <plugin>
 | 
			
		||||
                <groupId>org.xolstice.maven.plugins</groupId>
 | 
			
		||||
                <artifactId>protobuf-maven-plugin</artifactId>
 | 
			
		||||
            </plugin>
 | 
			
		||||
        </plugins>
 | 
			
		||||
    </build>
 | 
			
		||||
 | 
			
		||||
    <distributionManagement>
 | 
			
		||||
        <repository>
 | 
			
		||||
            <id>thingsboard-repo-deploy</id>
 | 
			
		||||
            <name>ThingsBoard Repo Deployment</name>
 | 
			
		||||
            <url>https://repo.thingsboard.io/artifactory/libs-release-public</url>
 | 
			
		||||
        </repository>
 | 
			
		||||
    </distributionManagement>
 | 
			
		||||
 | 
			
		||||
</project>
 | 
			
		||||
@ -0,0 +1,84 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2019 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.edge.rpc;
 | 
			
		||||
 | 
			
		||||
import com.google.common.io.Resources;
 | 
			
		||||
import io.grpc.ManagedChannel;
 | 
			
		||||
import io.grpc.netty.GrpcSslContexts;
 | 
			
		||||
import io.grpc.netty.NettyChannelBuilder;
 | 
			
		||||
import io.grpc.stub.StreamObserver;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Value;
 | 
			
		||||
import org.springframework.stereotype.Service;
 | 
			
		||||
import org.thingsboard.server.common.edge.gen.EdgeProtos;
 | 
			
		||||
import org.thingsboard.server.common.edge.gen.EdgeRpcServiceGrpc;
 | 
			
		||||
 | 
			
		||||
import javax.net.ssl.SSLException;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.net.URISyntaxException;
 | 
			
		||||
 | 
			
		||||
@Service
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class EdgeGrpcClient implements EdgeRpcClient {
 | 
			
		||||
 | 
			
		||||
    @Value("${cloud.rpc.host}")
 | 
			
		||||
    private String rpcHost;
 | 
			
		||||
    @Value("${cloud.rpc.port}")
 | 
			
		||||
    private int rpcPort;
 | 
			
		||||
    @Value("${cloud.rpc.timeout}")
 | 
			
		||||
    private int timeoutSecs;
 | 
			
		||||
    @Value("${cloud.rpc.ssl.enabled}")
 | 
			
		||||
    private boolean sslEnabled;
 | 
			
		||||
    @Value("${cloud.rpc.ssl.cert}")
 | 
			
		||||
    private String certResource;
 | 
			
		||||
 | 
			
		||||
    private ManagedChannel channel;
 | 
			
		||||
 | 
			
		||||
    private StreamObserver<EdgeProtos.UplinkMsg> inputStream;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void connect() {
 | 
			
		||||
        NettyChannelBuilder builder = NettyChannelBuilder.forAddress(rpcHost, rpcPort).usePlaintext();
 | 
			
		||||
        if (sslEnabled) {
 | 
			
		||||
            try {
 | 
			
		||||
                builder.sslContext(GrpcSslContexts.forClient().trustManager(new File(Resources.getResource(certResource).toURI())).build());
 | 
			
		||||
            } catch (URISyntaxException | SSLException e) {
 | 
			
		||||
                log.error("Failed to initialize channel!", e);
 | 
			
		||||
                throw new RuntimeException(e);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        channel = builder.build();
 | 
			
		||||
        EdgeRpcServiceGrpc.EdgeRpcServiceStub stub = EdgeRpcServiceGrpc.newStub(channel);
 | 
			
		||||
        StreamObserver<EdgeProtos.DownlinkMsg> responseObserver = new StreamObserver<EdgeProtos.DownlinkMsg>() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onNext(EdgeProtos.DownlinkMsg downlinkMsg) {
 | 
			
		||||
                log.info("onNext [{}]", downlinkMsg);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onError(Throwable throwable) {
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onCompleted() {
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        inputStream = stub.sendUplink(responseObserver);
 | 
			
		||||
        inputStream.onNext(EdgeProtos.UplinkMsg.newBuilder().setMsgType(EdgeProtos.UplinkMsgType.DELETE_DEVICE_MESSAGE).build());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,21 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2019 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.edge.rpc;
 | 
			
		||||
 | 
			
		||||
public interface EdgeRpcClient {
 | 
			
		||||
 | 
			
		||||
    void connect();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								common/edge-api/src/main/proto/edge.proto
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								common/edge-api/src/main/proto/edge.proto
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2019 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.
 | 
			
		||||
 */
 | 
			
		||||
syntax = "proto3";
 | 
			
		||||
 | 
			
		||||
option java_package = "org.thingsboard.server.common.edge.gen";
 | 
			
		||||
option java_outer_classname = "EdgeProtos";
 | 
			
		||||
 | 
			
		||||
package edge;
 | 
			
		||||
 | 
			
		||||
// Interface exported by the ThingsBoard PRC Edge.
 | 
			
		||||
service EdgeRpcService {
 | 
			
		||||
 | 
			
		||||
  rpc sendUplink(stream UplinkMsg) returns (stream DownlinkMsg) {}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data Structures;
 | 
			
		||||
 */
 | 
			
		||||
message UplinkMsg {
 | 
			
		||||
  UplinkMsgType msgType = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message DownlinkMsg {
 | 
			
		||||
  DownlinkMsgType msgType = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum UplinkMsgType {
 | 
			
		||||
  SAVE_DEVICE_MESSAGE = 0;
 | 
			
		||||
  DELETE_DEVICE_MESSAGE = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum DownlinkMsgType {
 | 
			
		||||
  SAVE_ENTITY_MESSAGE = 0;
 | 
			
		||||
  DELETE_ENTITY_MESSAGE = 1;
 | 
			
		||||
}
 | 
			
		||||
@ -40,6 +40,7 @@
 | 
			
		||||
        <module>queue</module>
 | 
			
		||||
        <module>transport</module>
 | 
			
		||||
        <module>dao-api</module>
 | 
			
		||||
        <module>edge-api</module>
 | 
			
		||||
    </modules>
 | 
			
		||||
 | 
			
		||||
</project>
 | 
			
		||||
 | 
			
		||||
@ -46,6 +46,7 @@ import org.thingsboard.server.dao.entity.AbstractEntityService;
 | 
			
		||||
import org.thingsboard.server.dao.exception.DataValidationException;
 | 
			
		||||
import org.thingsboard.server.dao.service.DataValidator;
 | 
			
		||||
import org.thingsboard.server.dao.service.PaginatedRemover;
 | 
			
		||||
import org.thingsboard.server.dao.service.Validator;
 | 
			
		||||
import org.thingsboard.server.dao.tenant.TenantDao;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
@ -111,6 +112,13 @@ public class BaseEdgeService extends AbstractEntityService implements EdgeServic
 | 
			
		||||
        return edgeOpt.orElse(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Optional<Edge> findEdgeByRoutingKey(TenantId tenantId, String routingKey) {
 | 
			
		||||
        log.trace("Executing findEdgeByRoutingKey [{}]", routingKey);
 | 
			
		||||
        Validator.validateString(routingKey, "Incorrect edge routingKey for search request.");
 | 
			
		||||
        return edgeDao.findByRoutingKey(tenantId.getId(), routingKey);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @CacheEvict(cacheNames = EDGE_CACHE, key = "{#edge.tenantId, #edge.name}")
 | 
			
		||||
    @Override
 | 
			
		||||
    public Edge saveEdge(Edge edge) {
 | 
			
		||||
 | 
			
		||||
@ -86,4 +86,9 @@ public class CassandraEdgeDao extends CassandraAbstractSearchTextDao<EdgeEntity,
 | 
			
		||||
    public ListenableFuture<List<EntitySubtype>> findTenantEdgeTypesAsync(UUID tenantId) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Optional<Edge> findByRoutingKey(UUID tenantId, String routingKey) {
 | 
			
		||||
        return Optional.empty();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -116,5 +116,12 @@ public interface EdgeDao extends Dao<Edge> {
 | 
			
		||||
     */
 | 
			
		||||
    ListenableFuture<List<EntitySubtype>> findTenantEdgeTypesAsync(UUID tenantId);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find edge by routing Key.
 | 
			
		||||
     *
 | 
			
		||||
     * @param routingKey the edge routingKey
 | 
			
		||||
     * @return the optional edge object
 | 
			
		||||
     */
 | 
			
		||||
    Optional<Edge> findByRoutingKey(UUID tenantId, String routingKey);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -361,7 +361,8 @@ public class ModelConstants {
 | 
			
		||||
    public static final String EDGE_CONFIGURATION_PROPERTY = "configuration";
 | 
			
		||||
    public static final String EDGE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
 | 
			
		||||
 | 
			
		||||
    public static final String EDGE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "edge_by_tenant_and_search_text";
 | 
			
		||||
    public static final String EDGE_ROUTING_KEY_PROPERTY = "routing_key";
 | 
			
		||||
    public static final String EDGE_SECRET_PROPERTY = "secret";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -37,6 +37,8 @@ import static org.thingsboard.server.dao.model.ModelConstants.EDGE_CONFIGURATION
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_CUSTOMER_ID_PROPERTY;
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_LABEL_PROPERTY;
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_NAME_PROPERTY;
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_ROUTING_KEY_PROPERTY;
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_SECRET_PROPERTY;
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_TENANT_ID_PROPERTY;
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_TYPE_PROPERTY;
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
 | 
			
		||||
@ -70,6 +72,12 @@ public class EdgeEntity implements SearchTextEntity<Edge> {
 | 
			
		||||
    @Column(name = SEARCH_TEXT_PROPERTY)
 | 
			
		||||
    private String searchText;
 | 
			
		||||
 | 
			
		||||
    @Column(name = EDGE_ROUTING_KEY_PROPERTY)
 | 
			
		||||
    private String routingKey;
 | 
			
		||||
 | 
			
		||||
    @Column(name = EDGE_SECRET_PROPERTY)
 | 
			
		||||
    private String secret;
 | 
			
		||||
 | 
			
		||||
    @Column(name = EDGE_CONFIGURATION_PROPERTY, codec = JsonCodec.class)
 | 
			
		||||
    private JsonNode configuration;
 | 
			
		||||
 | 
			
		||||
@ -90,6 +98,8 @@ public class EdgeEntity implements SearchTextEntity<Edge> {
 | 
			
		||||
        this.type = edge.getType();
 | 
			
		||||
        this.name = edge.getName();
 | 
			
		||||
        this.label = edge.getLabel();
 | 
			
		||||
        this.routingKey = edge.getRoutingKey();
 | 
			
		||||
        this.secret = edge.getSecret();
 | 
			
		||||
        this.configuration = edge.getConfiguration();
 | 
			
		||||
        this.additionalInfo = edge.getAdditionalInfo();
 | 
			
		||||
    }
 | 
			
		||||
@ -112,6 +122,8 @@ public class EdgeEntity implements SearchTextEntity<Edge> {
 | 
			
		||||
        edge.setType(type);
 | 
			
		||||
        edge.setName(name);
 | 
			
		||||
        edge.setLabel(label);
 | 
			
		||||
        edge.setRoutingKey(routingKey);
 | 
			
		||||
        edge.setSecret(secret);
 | 
			
		||||
        edge.setConfiguration(configuration);
 | 
			
		||||
        edge.setAdditionalInfo(additionalInfo);
 | 
			
		||||
        return edge;
 | 
			
		||||
 | 
			
		||||
@ -35,11 +35,12 @@ import javax.persistence.Column;
 | 
			
		||||
import javax.persistence.Entity;
 | 
			
		||||
import javax.persistence.Table;
 | 
			
		||||
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.ASSET_CUSTOMER_ID_PROPERTY;
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_COLUMN_FAMILY_NAME;
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_CUSTOMER_ID_PROPERTY;
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_LABEL_PROPERTY;
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_NAME_PROPERTY;
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_ROUTING_KEY_PROPERTY;
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_SECRET_PROPERTY;
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_TENANT_ID_PROPERTY;
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_TYPE_PROPERTY;
 | 
			
		||||
import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
 | 
			
		||||
@ -69,6 +70,12 @@ public class EdgeEntity extends BaseSqlEntity<Edge> implements SearchTextEntity<
 | 
			
		||||
    @Column(name = SEARCH_TEXT_PROPERTY)
 | 
			
		||||
    private String searchText;
 | 
			
		||||
 | 
			
		||||
    @Column(name = EDGE_ROUTING_KEY_PROPERTY)
 | 
			
		||||
    private String routingKey;
 | 
			
		||||
 | 
			
		||||
    @Column(name = EDGE_SECRET_PROPERTY)
 | 
			
		||||
    private String secret;
 | 
			
		||||
 | 
			
		||||
    @Type(type = "json")
 | 
			
		||||
    @Column(name = ModelConstants.EDGE_CONFIGURATION_PROPERTY)
 | 
			
		||||
    private JsonNode configuration;
 | 
			
		||||
@ -94,6 +101,8 @@ public class EdgeEntity extends BaseSqlEntity<Edge> implements SearchTextEntity<
 | 
			
		||||
        this.type = edge.getType();
 | 
			
		||||
        this.name = edge.getName();
 | 
			
		||||
        this.label = edge.getLabel();
 | 
			
		||||
        this.routingKey = edge.getRoutingKey();
 | 
			
		||||
        this.secret = edge.getSecret();
 | 
			
		||||
        this.configuration = edge.getConfiguration();
 | 
			
		||||
        this.additionalInfo = edge.getAdditionalInfo();
 | 
			
		||||
    }
 | 
			
		||||
@ -125,6 +134,8 @@ public class EdgeEntity extends BaseSqlEntity<Edge> implements SearchTextEntity<
 | 
			
		||||
        edge.setType(type);
 | 
			
		||||
        edge.setName(name);
 | 
			
		||||
        edge.setLabel(label);
 | 
			
		||||
        edge.setRoutingKey(routingKey);
 | 
			
		||||
        edge.setSecret(secret);
 | 
			
		||||
        edge.setConfiguration(configuration);
 | 
			
		||||
        edge.setAdditionalInfo(additionalInfo);
 | 
			
		||||
        return edge;
 | 
			
		||||
 | 
			
		||||
@ -76,4 +76,5 @@ public interface EdgeRepository extends CrudRepository<EdgeEntity, String> {
 | 
			
		||||
 | 
			
		||||
    List<EdgeEntity> findEdgesByTenantIdAndIdIn(String tenantId, List<String> edgeIds);
 | 
			
		||||
 | 
			
		||||
    EdgeEntity findByRoutingKey(String routingKey);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -126,6 +126,12 @@ public class JpaEdgeDao extends JpaAbstractSearchTextDao<EdgeEntity, Edge> imple
 | 
			
		||||
        return service.submit(() -> convertTenantEdgeTypesToDto(tenantId, edgeRepository.findTenantEdgeTypes(fromTimeUUID(tenantId))));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Optional<Edge> findByRoutingKey(UUID tenantId, String routingKey) {
 | 
			
		||||
        Edge edge = DaoUtil.getData(edgeRepository.findByRoutingKey(routingKey));
 | 
			
		||||
        return Optional.ofNullable(edge);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private List<EntitySubtype> convertTenantEdgeTypesToDto(UUID tenantId, List<String> types) {
 | 
			
		||||
        List<EntitySubtype> list = Collections.emptyList();
 | 
			
		||||
        if (types != null && !types.isEmpty()) {
 | 
			
		||||
 | 
			
		||||
@ -257,6 +257,8 @@ CREATE TABLE IF NOT EXISTS edge (
 | 
			
		||||
    type varchar(255),
 | 
			
		||||
    name varchar(255),
 | 
			
		||||
    label varchar(255),
 | 
			
		||||
    routing_key varchar(255),
 | 
			
		||||
    secret varchar(255),
 | 
			
		||||
    search_text varchar(255),
 | 
			
		||||
    tenant_id varchar(31)
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										5
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								pom.xml
									
									
									
									
									
								
							@ -406,6 +406,11 @@
 | 
			
		||||
                <artifactId>coap</artifactId>
 | 
			
		||||
                <version>${project.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>org.thingsboard.common</groupId>
 | 
			
		||||
                <artifactId>edge-api</artifactId>
 | 
			
		||||
                <version>${project.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>org.thingsboard</groupId>
 | 
			
		||||
                <artifactId>dao</artifactId>
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,7 @@
 | 
			
		||||
 | 
			
		||||
<div layout="row">
 | 
			
		||||
    <md-button ngclipboard data-clipboard-action="copy"
 | 
			
		||||
               ngclipboard-success="onEdgeIdCopied(e)"
 | 
			
		||||
               ngclipboard-success="onEdgeIdCopied()"
 | 
			
		||||
               data-clipboard-text="{{edge.id.id}}" ng-show="!isEdit"
 | 
			
		||||
               class="md-raised">
 | 
			
		||||
        <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
 | 
			
		||||
@ -69,4 +69,34 @@
 | 
			
		||||
            <textarea ng-model="edge.additionalInfo.description" rows="2"></textarea>
 | 
			
		||||
        </md-input-container>
 | 
			
		||||
    </fieldset>
 | 
			
		||||
    <div layout="row">
 | 
			
		||||
        <md-input-container class="md-block" flex>
 | 
			
		||||
            <label translate>edge.edge-key</label>
 | 
			
		||||
            <input ng-model="edge.routingKey" disabled>
 | 
			
		||||
        </md-input-container>
 | 
			
		||||
        <md-button class="md-icon-button" style="margin-top: 14px;"
 | 
			
		||||
                   ngclipboard data-clipboard-action="copy"
 | 
			
		||||
                   ngclipboard-success="onEdgeInfoCopied('key')"
 | 
			
		||||
                   data-clipboard-text="{{edge.routingKey}}">
 | 
			
		||||
            <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
 | 
			
		||||
            <md-tooltip md-direction="top">
 | 
			
		||||
                {{ 'edge.copy-edge-key' | translate }}
 | 
			
		||||
            </md-tooltip>
 | 
			
		||||
        </md-button>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div layout="row">
 | 
			
		||||
        <md-input-container class="md-block" flex>
 | 
			
		||||
            <label translate>edge.edge-secret</label>
 | 
			
		||||
            <input ng-model="edge.secret" disabled>
 | 
			
		||||
        </md-input-container>
 | 
			
		||||
        <md-button class="md-icon-button" style="margin-top: 14px;"
 | 
			
		||||
                   ngclipboard data-clipboard-action="copy"
 | 
			
		||||
                   data-clipboard-text="{{edge.secret}}"
 | 
			
		||||
                   ngclipboard-success="onEdgeInfoCopied('secret')">
 | 
			
		||||
            <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
 | 
			
		||||
            <md-tooltip md-direction="top">
 | 
			
		||||
                {{ 'edge.copy-edge-secret' | translate }}
 | 
			
		||||
            </md-tooltip>
 | 
			
		||||
        </md-button>
 | 
			
		||||
    </div>
 | 
			
		||||
</md-content>
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ import edgeFieldsetTemplate from './edge-fieldset.tpl.html';
 | 
			
		||||
/* eslint-enable import/no-unresolved, import/default */
 | 
			
		||||
 | 
			
		||||
/*@ngInject*/
 | 
			
		||||
export default function EdgeDirective($compile, $templateCache, $translate, $mdDialog, $document, toast, types, customerService) {
 | 
			
		||||
export default function EdgeDirective($compile, $templateCache, $translate, $mdDialog, $document, utils, toast, types, customerService) {
 | 
			
		||||
    var linker = function (scope, element) {
 | 
			
		||||
        var template = $templateCache.get(edgeFieldsetTemplate);
 | 
			
		||||
        element.html(template);
 | 
			
		||||
@ -32,6 +32,10 @@ export default function EdgeDirective($compile, $templateCache, $translate, $mdD
 | 
			
		||||
 | 
			
		||||
        scope.$watch('edge', function(newVal) {
 | 
			
		||||
            if (newVal) {
 | 
			
		||||
                if (!scope.edge.id) {
 | 
			
		||||
                    scope.edge.routingKey = utils.guid('');
 | 
			
		||||
                    scope.edge.secret = generateSecret(20);
 | 
			
		||||
                }
 | 
			
		||||
                if (scope.edge.customerId && scope.edge.customerId.id !== types.id.nullUid) {
 | 
			
		||||
                    scope.isAssignedToCustomer = true;
 | 
			
		||||
                    customerService.getShortCustomerInfo(scope.edge.customerId.id).then(
 | 
			
		||||
@ -48,12 +52,38 @@ export default function EdgeDirective($compile, $templateCache, $translate, $mdD
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        function generateSecret(length) {
 | 
			
		||||
            if (angular.isUndefined(length) || length == null) {
 | 
			
		||||
                length = 1;
 | 
			
		||||
            }
 | 
			
		||||
            var l = length > 10 ? 10 : length;
 | 
			
		||||
            var str = Math.random().toString(36).substr(2, l);
 | 
			
		||||
            if(str.length >= length){
 | 
			
		||||
                return str;
 | 
			
		||||
            }
 | 
			
		||||
            return str.concat(generateSecret(length - str.length));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        scope.onEdgeIdCopied = function() {
 | 
			
		||||
            toast.showSuccess($translate.instant('edge.id-copied-message'), 750, angular.element(element).parent().parent(), 'bottom left');
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        $compile(element.contents())(scope);
 | 
			
		||||
 | 
			
		||||
        scope.onEdgeInfoCopied = function(type) {
 | 
			
		||||
            let translateInstant = "";
 | 
			
		||||
            switch (type) {
 | 
			
		||||
                case 'key':
 | 
			
		||||
                    translateInstant = "edge.edge-key-copied-message";
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'secret':
 | 
			
		||||
                    translateInstant = "edge.edge-secret-copied-message";
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
            toast.showSuccess($translate.instant(translateInstant), 750, angular.element(element).parent().parent(), 'top left');
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    };
 | 
			
		||||
    return {
 | 
			
		||||
        restrict: "E",
 | 
			
		||||
 | 
			
		||||
@ -602,6 +602,16 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
 | 
			
		||||
                    }
 | 
			
		||||
                );
 | 
			
		||||
                return deferred.promise;
 | 
			
		||||
            case types.entityType.edge:
 | 
			
		||||
                openImportDialogCSV($event, entityType, 'edge.import', 'edge.edge-file').then(
 | 
			
		||||
                    function success() {
 | 
			
		||||
                        deferred.resolve();
 | 
			
		||||
                    },
 | 
			
		||||
                    function fail() {
 | 
			
		||||
                        deferred.reject();
 | 
			
		||||
                    }
 | 
			
		||||
                );
 | 
			
		||||
                return deferred.promise;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -777,7 +777,13 @@
 | 
			
		||||
        "unassign-from-edge": "Unassign from edge",
 | 
			
		||||
        "dashboards": "Edge Dashboards",
 | 
			
		||||
        "manage-edge-rulechains": "Manage edge rule chains",
 | 
			
		||||
        "rulechains": "Edge Rule Chains"
 | 
			
		||||
        "rulechains": "Edge Rule Chains",
 | 
			
		||||
        "edge-key": "Edge key",
 | 
			
		||||
        "copy-edge-key": "Copy edge key",
 | 
			
		||||
        "edge-key-copied-message": "Edge key has been copied to clipboard",
 | 
			
		||||
        "edge-secret": "Edge secret",
 | 
			
		||||
        "copy-edge-secret": "Copy edge secret",
 | 
			
		||||
        "edge-secret-copied-message": "Edge secret has been copied to clipboard"
 | 
			
		||||
    },
 | 
			
		||||
    "error": {
 | 
			
		||||
        "unable-to-connect": "Unable to connect to the server! Please check your internet connection.",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user