{
12 |
13 | /**
14 | * get user by username and password, return user(id can not be null)
15 | * if username and password are right, else return null.
16 | *
17 | * be sure that your password has been properly encrypted
18 | *
19 | * @param username
20 | * @param pwd
21 | * @return
22 | */
23 | T getUser(String username, String pwd);
24 |
25 | /**
26 | * get user by id, if id not exist then return null.
27 | *
28 | * @param id
29 | * @return
30 | */
31 | T getById(String id);
32 | }
--------------------------------------------------------------------------------
/rest/rest-spi/src/main/java/com/github/yuanrw/im/rest/spi/domain/LdapUser.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.spi.domain;
2 |
3 | /**
4 | * Date: 2019-07-07
5 | * Time: 17:08
6 | *
7 | * @author yrw
8 | */
9 | public class LdapUser extends UserBase {
10 |
11 | private String email;
12 |
13 | public String getEmail() {
14 | return email;
15 | }
16 |
17 | public void setEmail(String email) {
18 | this.email = email;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/rest/rest-spi/src/main/java/com/github/yuanrw/im/rest/spi/domain/UserBase.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.spi.domain;
2 |
3 | /**
4 | * Date: 2019-07-07
5 | * Time: 13:15
6 | *
7 | * @author yrw
8 | */
9 | public class UserBase {
10 |
11 | private String id;
12 | private String username;
13 |
14 | public String getId() {
15 | return id;
16 | }
17 |
18 | public void setId(String id) {
19 | this.id = id;
20 | }
21 |
22 | public String getUsername() {
23 | return username;
24 | }
25 |
26 | public void setUsername(String username) {
27 | this.username = username;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/rest/rest-web/Dockerfile:
--------------------------------------------------------------------------------
1 | # Dockerfile for rest-web
2 | # docker build -t yuanrw/rest-web:$VERSION .
3 | # docker run -p 8082:8082 -d -v /tmp/IM_logs:/tmp/IM_logs --name rest-web rest-web
4 |
5 | FROM adoptopenjdk/openjdk11:alpine-jre
6 | MAINTAINER yuanrw <295415537@qq.com>
7 |
8 | ENV SERVICE_NAME rest-web
9 | ENV VERSION 1.0.0
10 |
11 | EXPOSE 8082
12 |
13 | RUN echo "http://mirrors.aliyun.com/alpine/v3.8/main" > /etc/apk/repositories \
14 | && echo "http://mirrors.aliyun.com/alpine/v3.8/community" >> /etc/apk/repositories \
15 | && apk update upgrade \
16 | && apk add --no-cache procps unzip curl bash tzdata \
17 | && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
18 | && echo "Asia/Shanghai" > /etc/timezone
19 |
20 | COPY target/${SERVICE_NAME}-${VERSION}-bin.zip /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip
21 |
22 | RUN unzip /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip -d /${SERVICE_NAME} \
23 | && rm -rf /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip \
24 | && cd /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION} \
25 | && echo "tail -f /dev/null" >> start-docker.sh
26 |
27 | WORKDIR /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}
28 |
29 | COPY src/main/resources/application-docker.properties .
30 | COPY src/main/bin/start-docker.sh .
31 | COPY src/main/bin/wait-for-it.sh .
32 |
33 | CMD /bin/bash wait-for-it.sh -t 0 im-mysql:3306 --strict -- \
34 | /bin/bash wait-for-it.sh -t 0 im-redis:6379 --strict -- \
35 | /bin/bash wait-for-it.sh -t 0 im-rabbit:5672 --strict -- \
36 | /bin/bash start-docker.sh
--------------------------------------------------------------------------------
/rest/rest-web/src/assembly/assembly.xml:
--------------------------------------------------------------------------------
1 |
2 | bin
3 |
4 | zip
5 |
6 |
7 |
8 | true
9 | lib
10 |
11 |
12 |
13 |
14 | src/main/resources
15 | /
16 |
17 | logback*.xml
18 | *-docker.*
19 |
20 | unix
21 |
22 |
23 | target
24 | /
25 |
26 | ${project.artifactId}-*.jar
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/bin/start-docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | SERVICE_NAME="rest-web"
3 | VERSION="1.0.0"
4 |
5 | LOG_DIR=/tmp/IM_logs
6 | mkdir -p $LOG_DIR
7 |
8 | # Find Java
9 | if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then
10 | java="$JAVA_HOME/bin/java"
11 | elif type -p java > /dev/null 2>&1; then
12 | java=$(type -p java)
13 | elif [[ -x "/usr/bin/java" ]]; then
14 | java="/usr/bin/java"
15 | else
16 | echo "Unable to find Java"
17 | exit 1
18 | fi
19 |
20 | JAVA_OPTS="-Xms512m -Xmx512m -Xmn256m -XX:PermSize=128m -XX:MaxPermSize=128m"
21 |
22 | echo "JAVA_HOME: $JAVA_HOME"
23 | $java $JAVA_OPTS -jar $SERVICE_NAME-$VERSION.jar --spring.config.location=application-docker.properties
24 | echo "SERVICE_NAME started...."
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/RestStarter.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.context.annotation.ComponentScan;
6 | import org.springframework.scheduling.annotation.EnableScheduling;
7 |
8 | /**
9 | * Date: 2019-02-11
10 | * Time: 12:09
11 | *
12 | * @author yrw
13 | */
14 | @EnableScheduling
15 | @ComponentScan(basePackages = {"com.github.yuanrw.im.rest"})
16 | @SpringBootApplication
17 | public class RestStarter {
18 | public static void main(String[] args) {
19 | SpringApplication.run(RestStarter.class, args);
20 | }
21 | }
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/config/RestConfig.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.config;
2 |
3 | import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties;
4 | import com.baomidou.mybatisplus.core.config.GlobalConfig;
5 | import com.github.yuanrw.im.common.domain.constant.ImConstant;
6 | import com.github.yuanrw.im.common.domain.po.DbModel;
7 | import com.github.yuanrw.im.rest.web.handler.ValidHandler;
8 | import org.mybatis.spring.annotation.MapperScan;
9 | import org.springframework.amqp.core.AcknowledgeMode;
10 | import org.springframework.amqp.core.Queue;
11 | import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
12 | import org.springframework.amqp.rabbit.connection.ConnectionFactory;
13 | import org.springframework.context.annotation.Bean;
14 | import org.springframework.context.annotation.ComponentScan;
15 | import org.springframework.context.annotation.Configuration;
16 | import org.springframework.context.annotation.Primary;
17 | import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
18 | import org.springframework.data.redis.core.ReactiveRedisTemplate;
19 | import org.springframework.data.redis.core.RedisTemplate;
20 | import org.springframework.data.redis.serializer.RedisSerializationContext;
21 |
22 | import javax.validation.Validator;
23 |
24 | /**
25 | * Date: 2019-04-21
26 | * Time: 15:08
27 | *
28 | * @author yrw
29 | */
30 | @Configuration
31 | @MapperScan(value = "com.github.yuanrw.im.rest.web.mapper")
32 | @ComponentScan(basePackages = "com.github.yuanrw.im.rest.web.service")
33 | public class RestConfig {
34 |
35 | @Bean
36 | @Primary
37 | public MybatisPlusProperties mybatisPlusProperties() {
38 | MybatisPlusProperties properties = new MybatisPlusProperties();
39 | GlobalConfig globalConfig = new GlobalConfig();
40 |
41 | properties.setTypeAliasesSuperType(DbModel.class);
42 | properties.setMapperLocations(new String[]{"classpath*:/mapper/**/*.xml"});
43 | properties.setGlobalConfig(globalConfig);
44 |
45 | GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
46 | dbConfig.setTablePrefix("im_");
47 | globalConfig.setDbConfig(dbConfig);
48 |
49 | return properties;
50 | }
51 |
52 | @Bean
53 | public Integer init(Validator validator, RedisTemplate redisTemplate) {
54 | ValidHandler.setValidator(validator);
55 | return 1;
56 | }
57 |
58 | @Bean
59 | public SimpleRabbitListenerContainerFactory listenerFactory(ConnectionFactory connectionFactory) {
60 | SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
61 | factory.setConnectionFactory(connectionFactory);
62 | factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
63 | return factory;
64 | }
65 |
66 | @Bean
67 | public Queue offlineQueue() {
68 | return new Queue(ImConstant.MQ_OFFLINE_QUEUE);
69 | }
70 |
71 | @Bean
72 | public ReactiveRedisTemplate reactiveRedisTemplateString
73 | (ReactiveRedisConnectionFactory connectionFactory) {
74 | return new ReactiveRedisTemplate<>(connectionFactory, RedisSerializationContext.string());
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/filter/GlobalErrorAttributes.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.filter;
2 |
3 | import com.github.yuanrw.im.common.exception.ImException;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import org.springframework.boot.web.reactive.error.DefaultErrorAttributes;
7 | import org.springframework.context.annotation.Configuration;
8 | import org.springframework.web.reactive.function.server.ServerRequest;
9 | import org.springframework.web.server.ResponseStatusException;
10 |
11 | import java.util.Date;
12 | import java.util.LinkedHashMap;
13 | import java.util.Map;
14 |
15 | /**
16 | * Date: 2019-02-12
17 | * Time: 10:43
18 | *
19 | * @author yrw
20 | */
21 | @Configuration
22 | public class GlobalErrorAttributes extends DefaultErrorAttributes {
23 |
24 | private Logger logger = LoggerFactory.getLogger(GlobalErrorWebExceptionHandler.class);
25 |
26 | @Override
27 | public Map getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
28 | Map errorAttributes = new LinkedHashMap<>();
29 | errorAttributes.put("timestamp", new Date());
30 | errorAttributes.put("path", request.path());
31 | errorAttributes.put("status", 500);
32 | Throwable error = this.getError(request);
33 |
34 | logger.error("[rest] unknown error", this.getError(request));
35 |
36 | if (error instanceof ResponseStatusException) {
37 | return super.getErrorAttributes(request, includeStackTrace);
38 | }
39 | if (error instanceof ImException) {
40 | ImException e = (ImException) error;
41 | errorAttributes.put("msg", e.getMessage());
42 | } else if (error instanceof IllegalArgumentException) {
43 | IllegalArgumentException e = (IllegalArgumentException) error;
44 | errorAttributes.put("msg", e.getMessage());
45 | } else {
46 | errorAttributes.put("msg", "服务器繁忙,请稍后再试!");
47 | }
48 | return errorAttributes;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/filter/GlobalErrorWebExceptionHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.filter;
2 |
3 | import org.springframework.boot.autoconfigure.web.ResourceProperties;
4 | import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler;
5 | import org.springframework.boot.web.reactive.error.ErrorAttributes;
6 | import org.springframework.context.ApplicationContext;
7 | import org.springframework.core.annotation.Order;
8 | import org.springframework.http.HttpStatus;
9 | import org.springframework.http.MediaType;
10 | import org.springframework.http.codec.ServerCodecConfigurer;
11 | import org.springframework.stereotype.Component;
12 | import org.springframework.web.reactive.function.BodyInserters;
13 | import org.springframework.web.reactive.function.server.*;
14 | import reactor.core.publisher.Mono;
15 |
16 | import java.util.Map;
17 |
18 | /**
19 | * Date: 2019-02-12
20 | * Time: 10:37
21 | *
22 | * @author yrw
23 | */
24 | @Component
25 | @Order(-2)
26 | public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
27 |
28 | public GlobalErrorWebExceptionHandler(
29 | ErrorAttributes errorAttributes,
30 | ResourceProperties resourceProperties,
31 | ApplicationContext applicationContext,
32 | ServerCodecConfigurer serverCodecConfigurer
33 | ) {
34 | super(errorAttributes, resourceProperties, applicationContext);
35 | this.setMessageWriters(serverCodecConfigurer.getWriters());
36 | this.setMessageReaders(serverCodecConfigurer.getReaders());
37 | }
38 |
39 | @Override
40 | protected RouterFunction getRoutingFunction(ErrorAttributes errorAttributes) {
41 | return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
42 | }
43 |
44 | private Mono renderErrorResponse(ServerRequest request) {
45 |
46 | Map errorPropertiesMap = getErrorAttributes(request, false);
47 |
48 | return ServerResponse.status(HttpStatus.OK)
49 | .contentType(MediaType.APPLICATION_JSON_UTF8)
50 | .body(BodyInserters.fromObject(errorPropertiesMap));
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/filter/HeaderFilter.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.filter;
2 |
3 | import com.github.yuanrw.im.common.exception.ImException;
4 | import org.springframework.stereotype.Component;
5 | import org.springframework.web.server.ServerWebExchange;
6 | import org.springframework.web.server.WebFilter;
7 | import org.springframework.web.server.WebFilterChain;
8 | import reactor.core.publisher.Mono;
9 |
10 | /**
11 | * Date: 2019-04-21
12 | * Time: 15:51
13 | *
14 | * @author yrw
15 | */
16 | @Component
17 | public class HeaderFilter implements WebFilter {
18 |
19 | private TokenManager tokenManager;
20 |
21 | public HeaderFilter(TokenManager tokenManager) {
22 | this.tokenManager = tokenManager;
23 | }
24 |
25 | @Override
26 | public Mono filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
27 | String path = serverWebExchange.getRequest().getPath().value();
28 |
29 | if ("/user/login".equals(path) || path.startsWith("/offline")) {
30 | return webFilterChain.filter(serverWebExchange);
31 | }
32 | if (!serverWebExchange.getRequest().getHeaders().containsKey("token")) {
33 | return Mono.error(new ImException("[rest] user is not login"));
34 | }
35 |
36 | String token = serverWebExchange.getRequest().getHeaders().getFirst("token");
37 |
38 | return tokenManager.validateToken(token).flatMap(b -> b != null ? webFilterChain.filter(serverWebExchange) :
39 | Mono.error(new ImException("[rest] user is not login")));
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/filter/TokenManager.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.filter;
2 |
3 | import com.github.yuanrw.im.common.util.TokenGenerator;
4 | import org.springframework.data.redis.core.ReactiveRedisTemplate;
5 | import org.springframework.stereotype.Service;
6 | import reactor.core.publisher.Mono;
7 |
8 | import java.time.Duration;
9 |
10 | /**
11 | * Date: 2019-07-04
12 | * Time: 15:17
13 | *
14 | * @author yrw
15 | */
16 | @Service
17 | public class TokenManager {
18 |
19 | private static final String SESSION_KEY = "IM:TOKEN:";
20 | private ReactiveRedisTemplate template;
21 |
22 | public TokenManager(ReactiveRedisTemplate template) {
23 | this.template = template;
24 | }
25 |
26 | public Mono validateToken(String token) {
27 | return template.opsForValue().get(SESSION_KEY + token).map(id -> {
28 | template.expire(SESSION_KEY + token, Duration.ofMinutes(30));
29 | return id;
30 | }).switchIfEmpty(Mono.empty());
31 | }
32 |
33 | public Mono createNewToken(String userId) {
34 | String token = TokenGenerator.generate();
35 | return template.opsForValue().set(SESSION_KEY + token, userId)
36 | .flatMap(b -> b ? template.expire(SESSION_KEY + token, Duration.ofMinutes(30)) : Mono.just(false))
37 | .flatMap(b -> b ? Mono.just(token) : Mono.empty());
38 | }
39 |
40 | public Mono expire(String token) {
41 | return template.delete(SESSION_KEY + token).map(l -> l > 0);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/handler/OfflineHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.handler;
2 |
3 | import com.fasterxml.jackson.core.JsonProcessingException;
4 | import com.github.yuanrw.im.common.domain.ResultWrapper;
5 | import com.github.yuanrw.im.common.exception.ImException;
6 | import com.github.yuanrw.im.rest.web.service.OfflineService;
7 | import org.springframework.stereotype.Component;
8 | import org.springframework.web.reactive.function.server.ServerRequest;
9 | import org.springframework.web.reactive.function.server.ServerResponse;
10 | import reactor.core.publisher.Mono;
11 |
12 | import static org.springframework.http.MediaType.APPLICATION_JSON;
13 | import static org.springframework.web.reactive.function.BodyInserters.fromObject;
14 |
15 | /**
16 | * Date: 2019-05-27
17 | * Time: 09:52
18 | *
19 | * @author yrw
20 | */
21 | @Component
22 | public class OfflineHandler {
23 |
24 | private OfflineService offlineService;
25 |
26 | public OfflineHandler(OfflineService offlineService) {
27 | this.offlineService = offlineService;
28 | }
29 |
30 | public Mono pollOfflineMsg(ServerRequest request) {
31 |
32 | String id = request.pathVariable("id");
33 |
34 | return Mono.fromSupplier(() -> {
35 | try {
36 | return offlineService.pollOfflineMsg(id);
37 | } catch (JsonProcessingException e) {
38 | throw new ImException(e);
39 | }
40 | }).map(ResultWrapper::success).flatMap(res ->
41 | ServerResponse.ok().contentType(APPLICATION_JSON).body(fromObject(res)));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/handler/ValidHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.handler;
2 |
3 | import com.github.yuanrw.im.common.exception.ImException;
4 | import com.google.common.collect.Iterables;
5 | import org.springframework.web.reactive.function.server.ServerRequest;
6 | import org.springframework.web.reactive.function.server.ServerResponse;
7 | import reactor.core.publisher.Mono;
8 |
9 | import javax.validation.ConstraintViolation;
10 | import javax.validation.Validator;
11 | import java.util.Set;
12 | import java.util.function.Function;
13 |
14 | /**
15 | * Date: 2019-03-01
16 | * Time: 14:51
17 | *
18 | * @author yrw
19 | */
20 | public class ValidHandler {
21 |
22 | private static Validator validator;
23 |
24 | public static Mono requireValidBody(
25 | Function, Mono> block,
26 | ServerRequest request, Class bodyClass) {
27 |
28 | return request
29 | .bodyToMono(bodyClass)
30 | .flatMap(body -> {
31 | Set> msg = validator.validate(body);
32 | if (msg.isEmpty()) {
33 | return block.apply(Mono.just(body));
34 | } else {
35 | ConstraintViolation v = Iterables.get(msg, 0);
36 | throw new ImException(v.getPropertyPath() + " " + v.getMessage());
37 | }
38 | }
39 | );
40 | }
41 |
42 | public static void setValidator(Validator validator) {
43 | ValidHandler.validator = validator;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/mapper/OfflineMapper.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.mapper;
2 |
3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4 | import com.github.yuanrw.im.common.domain.po.Offline;
5 |
6 | /**
7 | * Date: 2019-05-05
8 | * Time: 09:46
9 | *
10 | * @author yrw
11 | */
12 | public interface OfflineMapper extends BaseMapper {
13 |
14 | /**
15 | * read offline msg from db, cas
16 | *
17 | * @param msgId
18 | * @return
19 | */
20 | int readMsg(Long msgId);
21 | }
22 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/mapper/RelationMapper.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.mapper;
2 |
3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4 | import com.github.yuanrw.im.common.domain.po.Relation;
5 | import com.github.yuanrw.im.common.domain.po.RelationDetail;
6 | import org.apache.ibatis.annotations.Param;
7 |
8 | import java.util.List;
9 |
10 | /**
11 | * Date: 2019-02-11
12 | * Time: 17:21
13 | *
14 | * @author yrw
15 | */
16 | public interface RelationMapper extends BaseMapper {
17 |
18 | /**
19 | * list user's friends
20 | *
21 | * @param userId
22 | * @return
23 | */
24 | List listFriends(@Param("userId") String userId);
25 | }
26 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/mapper/UserMapper.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.mapper;
2 |
3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4 | import com.github.yuanrw.im.common.domain.po.User;
5 |
6 | /**
7 | * Date: 2019-02-09
8 | * Time: 19:11
9 | *
10 | * @author yrw
11 | */
12 | public interface UserMapper extends BaseMapper {
13 | }
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/router/RestRouter.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.router;
2 |
3 | import com.github.yuanrw.im.rest.web.handler.OfflineHandler;
4 | import com.github.yuanrw.im.rest.web.handler.RelationHandler;
5 | import com.github.yuanrw.im.rest.web.handler.UserHandler;
6 | import org.springframework.context.annotation.Bean;
7 | import org.springframework.context.annotation.Configuration;
8 | import org.springframework.web.reactive.function.server.RouterFunction;
9 | import org.springframework.web.reactive.function.server.RouterFunctions;
10 | import org.springframework.web.reactive.function.server.ServerResponse;
11 |
12 | import static org.springframework.http.MediaType.APPLICATION_JSON;
13 | import static org.springframework.web.reactive.function.server.RequestPredicates.*;
14 |
15 | /**
16 | * Date: 2019-02-09
17 | * Time: 12:56
18 | *
19 | * @author yrw
20 | */
21 | @Configuration
22 | public class RestRouter {
23 |
24 | @Bean
25 | public RouterFunction userRoutes(UserHandler userHandler) {
26 | return RouterFunctions
27 | .route(POST("/user/login").and(contentType(APPLICATION_JSON)).and(accept(APPLICATION_JSON)),
28 | userHandler::login)
29 | .andRoute(GET("/user/logout").and(accept(APPLICATION_JSON)),
30 | userHandler::logout);
31 | }
32 |
33 | @Bean
34 | public RouterFunction relationRoutes(RelationHandler relationHandler) {
35 | return RouterFunctions
36 | .route(GET("/relation/{id}").and(accept(APPLICATION_JSON)),
37 | relationHandler::listFriends)
38 | .andRoute(GET("/relation").and(accept(APPLICATION_JSON)),
39 | relationHandler::getRelation)
40 | .andRoute(POST("/relation").and(contentType(APPLICATION_JSON)).and(accept(APPLICATION_JSON)),
41 | relationHandler::saveRelation)
42 | .andRoute(DELETE("/relation/{id}").and(accept(APPLICATION_JSON)),
43 | relationHandler::deleteRelation);
44 | }
45 |
46 | @Bean
47 | public RouterFunction offlineRoutes(OfflineHandler offlineHandler) {
48 | //only for connector
49 | return RouterFunctions
50 | .route(GET("/offline/poll/{id}").and(accept(APPLICATION_JSON)),
51 | offlineHandler::pollOfflineMsg);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/service/OfflineService.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.service;
2 |
3 | import com.baomidou.mybatisplus.extension.service.IService;
4 | import com.fasterxml.jackson.core.JsonProcessingException;
5 | import com.github.yuanrw.im.common.domain.po.Offline;
6 | import com.github.yuanrw.im.protobuf.generate.Ack;
7 | import com.github.yuanrw.im.protobuf.generate.Chat;
8 |
9 | import java.util.List;
10 |
11 | /**
12 | * Date: 2019-05-05
13 | * Time: 09:48
14 | *
15 | * @author yrw
16 | */
17 | public interface OfflineService extends IService {
18 |
19 | /**
20 | * save offline chat msg
21 | *
22 | * @param msg
23 | * @return
24 | */
25 | void saveChat(Chat.ChatMsg msg);
26 |
27 | /**
28 | * save offline ack msg
29 | *
30 | * @param msg
31 | * @return
32 | */
33 | void saveAck(Ack.AckMsg msg);
34 |
35 | /**
36 | * get a user's all offline msgs
37 | *
38 | * @param userId
39 | * @return
40 | * @throws JsonProcessingException
41 | */
42 | List pollOfflineMsg(String userId) throws JsonProcessingException;
43 | }
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/service/RelationService.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.service;
2 |
3 | import com.baomidou.mybatisplus.extension.service.IService;
4 | import com.github.yuanrw.im.common.domain.po.Relation;
5 | import com.github.yuanrw.im.common.domain.po.RelationDetail;
6 |
7 | import java.util.List;
8 |
9 | /**
10 | * Date: 2019-04-07
11 | * Time: 18:47
12 | *
13 | * @author yrw
14 | */
15 | public interface RelationService extends IService {
16 |
17 | /**
18 | * return the friends list of the user
19 | *
20 | * @param id userId
21 | * @return
22 | */
23 | List friends(String id);
24 |
25 | /**
26 | * add an relation between user1 and user2
27 | * insure that the same relation can only be saved once.
28 | * by default, use mysql union unique index.
29 | * if the db don't support a union unique index, then you need to check the exist relation
30 | *
31 | * @param userId1 id of user1l
32 | * @param userId2 id of user2
33 | * @return if success, return relation id, else return Mono.empty()
34 | */
35 | Long saveRelation(String userId1, String userId2);
36 | }
37 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/service/UserService.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.service;
2 |
3 | import com.baomidou.mybatisplus.extension.service.IService;
4 | import com.github.yuanrw.im.common.domain.po.User;
5 |
6 | /**
7 | * Date: 2019-04-07
8 | * Time: 18:35
9 | *
10 | * @author yrw
11 | */
12 | public interface UserService extends IService {
13 |
14 | /**
15 | * 验证用户密码,成功则返回用户,失败返回null
16 | *
17 | * @param username 用户名
18 | * @param pwd 密码
19 | * @return
20 | */
21 | User verifyAndGet(String username, String pwd);
22 | }
23 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/service/impl/OfflineServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.service.impl;
2 |
3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
5 | import com.github.yuanrw.im.common.domain.po.DbModel;
6 | import com.github.yuanrw.im.common.domain.po.Offline;
7 | import com.github.yuanrw.im.common.exception.ImException;
8 | import com.github.yuanrw.im.protobuf.constant.MsgTypeEnum;
9 | import com.github.yuanrw.im.protobuf.generate.Ack;
10 | import com.github.yuanrw.im.protobuf.generate.Chat;
11 | import com.github.yuanrw.im.rest.web.mapper.OfflineMapper;
12 | import com.github.yuanrw.im.rest.web.service.OfflineService;
13 | import org.springframework.stereotype.Service;
14 | import org.springframework.transaction.annotation.Transactional;
15 |
16 | import java.util.Comparator;
17 | import java.util.List;
18 | import java.util.stream.Collectors;
19 |
20 | /**
21 | * Date: 2019-05-05
22 | * Time: 09:49
23 | *
24 | * @author yrw
25 | */
26 | @Service
27 | public class OfflineServiceImpl extends ServiceImpl implements OfflineService {
28 |
29 | @Override
30 | public void saveChat(Chat.ChatMsg msg) {
31 | Offline offline = new Offline();
32 | offline.setMsgId(msg.getId());
33 | offline.setMsgCode(MsgTypeEnum.CHAT.getCode());
34 | offline.setToUserId(msg.getDestId());
35 | offline.setContent(msg.toByteArray());
36 |
37 | saveOffline(offline);
38 | }
39 |
40 | @Override
41 | public void saveAck(Ack.AckMsg msg) {
42 | Offline offline = new Offline();
43 | offline.setMsgId(msg.getId());
44 | offline.setMsgCode(MsgTypeEnum.getByClass(Ack.AckMsg.class).getCode());
45 | offline.setToUserId(msg.getDestId());
46 | offline.setContent(msg.toByteArray());
47 |
48 | saveOffline(offline);
49 | }
50 |
51 | private void saveOffline(Offline offline) {
52 | if (!save(offline)) {
53 | throw new ImException("[offline] save chat msg failed");
54 | }
55 | }
56 |
57 | @Override
58 | @Transactional(rollbackFor = Exception.class)
59 | public List pollOfflineMsg(String userId) {
60 | List unreadList = list(new LambdaQueryWrapper()
61 | .eq(Offline::getToUserId, userId));
62 |
63 | return unreadList.stream().filter(offline ->
64 | baseMapper.readMsg(offline.getId()) > 0)
65 | .sorted(Comparator.comparing(DbModel::getId))
66 | .collect(Collectors.toList());
67 | }
68 | }
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/service/impl/RelationServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.service.impl;
2 |
3 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
4 | import com.github.yuanrw.im.common.domain.po.Relation;
5 | import com.github.yuanrw.im.common.domain.po.RelationDetail;
6 | import com.github.yuanrw.im.common.exception.ImException;
7 | import com.github.yuanrw.im.rest.spi.UserSpi;
8 | import com.github.yuanrw.im.rest.spi.domain.UserBase;
9 | import com.github.yuanrw.im.rest.web.mapper.RelationMapper;
10 | import com.github.yuanrw.im.rest.web.service.RelationService;
11 | import com.github.yuanrw.im.rest.web.spi.SpiFactory;
12 | import org.apache.commons.lang3.RandomStringUtils;
13 | import org.springframework.stereotype.Service;
14 |
15 | import java.util.List;
16 |
17 | /**
18 | * Date: 2019-04-07
19 | * Time: 18:48
20 | *
21 | * @author yrw
22 | */
23 | @Service
24 | public class RelationServiceImpl extends ServiceImpl implements RelationService {
25 |
26 | private UserSpi extends UserBase> userSpi;
27 |
28 | public RelationServiceImpl(SpiFactory spiFactory) {
29 | this.userSpi = spiFactory.getUserSpi();
30 | }
31 |
32 | @Override
33 | public List friends(String id) {
34 | return baseMapper.listFriends(id);
35 | }
36 |
37 | @Override
38 | public Long saveRelation(String userId1, String userId2) {
39 | if (userId1.equals(userId2)) {
40 | throw new ImException("[rest] userId1 and userId2 can not be same");
41 | }
42 | if (userSpi.getById(userId1 + "") == null || userSpi.getById(userId2 + "") == null) {
43 | throw new ImException("[rest] user not exist");
44 | }
45 | String max = userId1.compareTo(userId2) >= 0 ? userId1 : userId2;
46 | String min = max.equals(userId1) ? userId2 : userId1;
47 |
48 | Relation relation = new Relation();
49 | relation.setUserId1(min);
50 | relation.setUserId2(max);
51 | relation.setEncryptKey(RandomStringUtils.randomAlphanumeric(16) + "|" + RandomStringUtils.randomNumeric(16));
52 |
53 | if (save(relation)) {
54 | return relation.getId();
55 | } else {
56 | throw new ImException("[rest] save relation failed");
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/service/impl/UserServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.service.impl;
2 |
3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
5 | import com.github.yuanrw.im.common.domain.po.User;
6 | import com.github.yuanrw.im.rest.web.mapper.UserMapper;
7 | import com.github.yuanrw.im.rest.web.service.UserService;
8 | import org.apache.commons.codec.digest.DigestUtils;
9 | import org.springframework.stereotype.Service;
10 |
11 | /**
12 | * Date: 2019-04-07
13 | * Time: 18:36
14 | *
15 | * @author yrw
16 | */
17 | @Service
18 | public class UserServiceImpl extends ServiceImpl implements UserService {
19 |
20 | @Override
21 | public User verifyAndGet(String username, String pwd) {
22 | User user = getOne(new LambdaQueryWrapper().eq(User::getUsername, username));
23 | return user != null ? verityPassword(pwd, user.getSalt(), user.getPwdHash()) ? user : null : null;
24 | }
25 |
26 | private boolean verityPassword(String pwd, String salt, String pwdHash) {
27 | String hashRes = DigestUtils.sha256Hex(pwd + salt);
28 | return hashRes.equals(pwdHash);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/spi/SpiFactory.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.spi;
2 |
3 | import com.github.yuanrw.im.common.exception.ImException;
4 | import com.github.yuanrw.im.rest.spi.UserSpi;
5 | import com.github.yuanrw.im.rest.spi.domain.UserBase;
6 | import com.github.yuanrw.im.rest.web.spi.impl.DefaultUserSpiImpl;
7 | import org.springframework.beans.factory.annotation.Value;
8 | import org.springframework.context.ApplicationContext;
9 | import org.springframework.context.ApplicationContextAware;
10 | import org.springframework.stereotype.Component;
11 | import org.springframework.util.StringUtils;
12 |
13 | /**
14 | * Date: 2019-07-03
15 | * Time: 17:50
16 | *
17 | * @author yrw
18 | */
19 | @Component
20 | public class SpiFactory implements ApplicationContextAware {
21 |
22 | private UserSpi extends UserBase> userSpi;
23 | private ApplicationContext applicationContext;
24 |
25 | @Value("${spi.user.impl.class}")
26 | private String userSpiImplClassName;
27 |
28 | @Override
29 | public void setApplicationContext(ApplicationContext applicationContext) {
30 | this.applicationContext = applicationContext;
31 | }
32 |
33 | public UserSpi extends UserBase> getUserSpi() {
34 | if (StringUtils.isEmpty(userSpiImplClassName)) {
35 | return applicationContext.getBean(DefaultUserSpiImpl.class);
36 | }
37 | try {
38 | if (userSpi == null) {
39 | Class> userSpiImplClass = Class.forName(userSpiImplClassName);
40 | userSpi = (UserSpi extends UserBase>) applicationContext.getBean(userSpiImplClass);
41 | }
42 | return userSpi;
43 | } catch (ClassNotFoundException e) {
44 | throw new ImException("can not find class: " + userSpiImplClassName);
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/spi/impl/DefaultUserSpiImpl.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.spi.impl;
2 |
3 | import com.github.yuanrw.im.common.domain.po.User;
4 | import com.github.yuanrw.im.rest.spi.UserSpi;
5 | import com.github.yuanrw.im.rest.spi.domain.UserBase;
6 | import com.github.yuanrw.im.rest.web.service.UserService;
7 | import org.springframework.stereotype.Service;
8 |
9 | /**
10 | * Date: 2019-07-03
11 | * Time: 17:49
12 | *
13 | * @author yrw
14 | */
15 | @Service
16 | public class DefaultUserSpiImpl implements UserSpi {
17 |
18 | private UserService userService;
19 |
20 | public DefaultUserSpiImpl(UserService userService) {
21 | this.userService = userService;
22 | }
23 |
24 | @Override
25 | public UserBase getUser(String username, String pwd) {
26 | User user = userService.verifyAndGet(username, pwd);
27 | if (user == null) {
28 | return null;
29 | }
30 |
31 | UserBase userBase = new UserBase();
32 | userBase.setId(user.getId() + "");
33 | userBase.setUsername(user.getUsername());
34 | return userBase;
35 | }
36 |
37 | @Override
38 | public UserBase getById(String id) {
39 | User user = userService.getById(Long.parseLong(id));
40 | if (user == null) {
41 | return null;
42 | }
43 |
44 | UserBase userBase = new UserBase();
45 | userBase.setId(userBase.getId());
46 | userBase.setUsername(userBase.getUsername());
47 | return userBase;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/spi/impl/LdapUserSpiImpl.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.spi.impl;
2 |
3 | import com.github.yuanrw.im.rest.spi.UserSpi;
4 | import com.github.yuanrw.im.rest.spi.domain.LdapUser;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.beans.factory.annotation.Value;
7 | import org.springframework.ldap.core.ContextMapper;
8 | import org.springframework.ldap.core.DirContextAdapter;
9 | import org.springframework.ldap.core.LdapTemplate;
10 | import org.springframework.ldap.filter.AndFilter;
11 | import org.springframework.ldap.filter.EqualsFilter;
12 | import org.springframework.ldap.query.ContainerCriteria;
13 | import org.springframework.ldap.query.SearchScope;
14 | import org.springframework.stereotype.Service;
15 |
16 | import static org.springframework.ldap.query.LdapQueryBuilder.query;
17 |
18 | /**
19 | * Date: 2019-07-06
20 | * Time: 22:01
21 | *
22 | * @author yrw
23 | */
24 | @Service
25 | public class LdapUserSpiImpl implements UserSpi {
26 |
27 | @Value("${ldap.searchBase}")
28 | private String searchBase;
29 |
30 | @Value("${ldap.mapping.objectClass}")
31 | private String objectClassAttrName;
32 |
33 | @Value("${ldap.mapping.loginId}")
34 | private String loginIdAttrName;
35 |
36 | @Value("${ldap.mapping.userDisplayName}")
37 | private String userDisplayNameAttrName;
38 |
39 | @Value("${ldap.mapping.email}")
40 | private String emailAttrName;
41 |
42 | @Autowired
43 | private LdapTemplate ldapTemplate;
44 |
45 | @Override
46 | public LdapUser getUser(String username, String pwd) {
47 | AndFilter filter = new AndFilter()
48 | .and(new EqualsFilter(emailAttrName, username));
49 | boolean authenticate = ldapTemplate.authenticate(searchBase, filter.encode(), pwd);
50 | return authenticate ? ldapTemplate.searchForObject(ldapQueryCriteria()
51 | .and(emailAttrName).is(username), ldapUserInfoMapper) : null;
52 | }
53 |
54 | @Override
55 | public LdapUser getById(String id) {
56 | return ldapTemplate.searchForObject(ldapQueryCriteria()
57 | .and(loginIdAttrName).is(id), ldapUserInfoMapper);
58 | }
59 |
60 | private ContextMapper ldapUserInfoMapper = (ctx) -> {
61 | DirContextAdapter contextAdapter = (DirContextAdapter) ctx;
62 | LdapUser ldapUser = new LdapUser();
63 | ldapUser.setId(contextAdapter.getStringAttribute(loginIdAttrName));
64 | ldapUser.setEmail(contextAdapter.getStringAttribute(emailAttrName));
65 | ldapUser.setUsername(contextAdapter.getStringAttribute(userDisplayNameAttrName));
66 | return ldapUser;
67 | };
68 |
69 | private ContainerCriteria ldapQueryCriteria() {
70 | return query().searchScope(SearchScope.SUBTREE)
71 | .where("objectClass").is(objectClassAttrName);
72 | }
73 | }
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/task/CleanOfflineMsgTask.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.task;
2 |
3 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4 | import com.github.yuanrw.im.common.domain.po.Offline;
5 | import com.github.yuanrw.im.rest.web.service.OfflineService;
6 | import com.google.common.collect.Lists;
7 | import org.slf4j.Logger;
8 | import org.slf4j.LoggerFactory;
9 | import org.springframework.scheduling.annotation.Scheduled;
10 | import org.springframework.stereotype.Component;
11 |
12 | import java.util.List;
13 | import java.util.stream.Collectors;
14 |
15 | /**
16 | * Date: 2019-09-02
17 | * Time: 18:24
18 | *
19 | * @author yrw
20 | */
21 | @Component
22 | public class CleanOfflineMsgTask {
23 | private static Logger logger = LoggerFactory.getLogger(CleanOfflineMsgTask.class);
24 |
25 | private OfflineService offlineService;
26 |
27 | public CleanOfflineMsgTask(OfflineService offlineService) {
28 | this.offlineService = offlineService;
29 | }
30 |
31 | @Scheduled(cron = "0 0/5 * * * *")
32 | public void cleanReadMsg() {
33 | List readIds = offlineService.list(new LambdaQueryWrapper()
34 | .select(Offline::getId)
35 | .eq(Offline::getHasRead, true)).stream()
36 | .map(Offline::getId).collect(Collectors.toList());
37 |
38 | logger.info("[clean task] clean read offline msg, size: {}", readIds.size());
39 |
40 | Lists.partition(readIds, 1000).forEach(offlineService::removeByIds);
41 | }
42 | }
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/task/OfflineListen.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.task;
2 |
3 | import com.github.yuanrw.im.common.domain.constant.ImConstant;
4 | import com.github.yuanrw.im.common.parse.ParseService;
5 | import com.github.yuanrw.im.protobuf.constant.MsgTypeEnum;
6 | import com.github.yuanrw.im.protobuf.generate.Ack;
7 | import com.github.yuanrw.im.protobuf.generate.Chat;
8 | import com.github.yuanrw.im.rest.web.service.OfflineService;
9 | import com.rabbitmq.client.Channel;
10 | import org.slf4j.Logger;
11 | import org.slf4j.LoggerFactory;
12 | import org.springframework.amqp.core.Message;
13 | import org.springframework.amqp.rabbit.annotation.RabbitHandler;
14 | import org.springframework.amqp.rabbit.annotation.RabbitListener;
15 | import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
16 | import org.springframework.stereotype.Component;
17 |
18 | import javax.annotation.PostConstruct;
19 |
20 | /**
21 | * Date: 2019-05-15
22 | * Time: 22:58
23 | *
24 | * @author yrw
25 | */
26 | @Component
27 | public class OfflineListen implements ChannelAwareMessageListener {
28 | private Logger logger = LoggerFactory.getLogger(OfflineListen.class);
29 |
30 | private ParseService parseService;
31 | private OfflineService offlineService;
32 |
33 | public OfflineListen(OfflineService offlineService) {
34 | this.parseService = new ParseService();
35 | this.offlineService = offlineService;
36 | }
37 |
38 | @PostConstruct
39 | public void init() {
40 | logger.info("[OfflineConsumer] Start listening Offline queue......");
41 | }
42 |
43 | @Override
44 | @RabbitHandler
45 | @RabbitListener(queues = ImConstant.MQ_OFFLINE_QUEUE, containerFactory = "listenerFactory")
46 | public void onMessage(Message message, Channel channel) throws Exception {
47 | logger.info("[OfflineConsumer] getUserSpi msg: {}", message.toString());
48 | try {
49 | int code = message.getBody()[0];
50 |
51 | byte[] msgBody = new byte[message.getBody().length - 1];
52 | System.arraycopy(message.getBody(), 1, msgBody, 0, message.getBody().length - 1);
53 |
54 | com.google.protobuf.Message msg = parseService.getMsgByCode(code, msgBody);
55 | if (code == MsgTypeEnum.CHAT.getCode()) {
56 | offlineService.saveChat((Chat.ChatMsg) msg);
57 | } else {
58 | offlineService.saveAck((Ack.AckMsg) msg);
59 | }
60 |
61 | } catch (Exception e) {
62 | logger.error("[OfflineConsumer] has error", e);
63 | } finally {
64 | channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/vo/RelationReq.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.vo;
2 |
3 | import javax.validation.constraints.NotEmpty;
4 |
5 | /**
6 | * Date: 2019-06-23
7 | * Time: 21:04
8 | *
9 | * @author yrw
10 | */
11 | public class RelationReq {
12 |
13 | @NotEmpty
14 | private String userId1;
15 |
16 | @NotEmpty
17 | private String userId2;
18 |
19 | public String getUserId1() {
20 | return userId1;
21 | }
22 |
23 | public void setUserId1(String userId1) {
24 | this.userId1 = userId1;
25 | }
26 |
27 | public String getUserId2() {
28 | return userId2;
29 | }
30 |
31 | public void setUserId2(String userId2) {
32 | this.userId2 = userId2;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/java/com/github/yuanrw/im/rest/web/vo/UserReq.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.vo;
2 |
3 | import javax.validation.constraints.NotEmpty;
4 |
5 | /**
6 | * Date: 2019-04-21
7 | * Time: 14:43
8 | *
9 | * @author yrw
10 | */
11 | public class UserReq {
12 |
13 | @NotEmpty
14 | // @Length(min = 6, max = 30)
15 | private String username;
16 |
17 | @NotEmpty
18 | private String pwd;
19 |
20 | public String getUsername() {
21 | return username;
22 | }
23 |
24 | public void setUsername(String username) {
25 | this.username = username;
26 | }
27 |
28 | public String getPwd() {
29 | return pwd;
30 | }
31 |
32 | public void setPwd(String pwd) {
33 | this.pwd = pwd;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/resources/application-docker.properties:
--------------------------------------------------------------------------------
1 | server.port=8082
2 |
3 | logging.level.root=info
4 | log.path=/tmp/IM_logs
5 |
6 | spring.datasource.url=jdbc:mysql://im-mysql:3306/im?useUnicode=true&characterEncoding=utf-8&useSSL=false
7 | spring.datasource.username=root
8 | spring.datasource.password=123456
9 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
10 |
11 | spring.redis.host=im-redis
12 | spring.redis.port=6379
13 | spring.redis.password=
14 |
15 | #rabbitmq
16 | spring.rabbitmq.host=im-rabbit
17 | spring.rabbitmq.port=5672
18 | spring.rabbitmq.username=rabbitmq
19 | spring.rabbitmq.password=rabbitmq
20 |
21 | spi.user.impl.class=
22 | #spi.user.spi.class=com.github.yuanrw.im.rest.web.spiLdapUserSpiImpl
23 |
24 | spring.ldap.base=dc=example,dc=org
25 | # admin
26 | spring.ldap.username=cn=admin,dc=example,dc=org
27 | spring.ldap.password=admin
28 | spring.ldap.urls=ldap://127.0.0.1:389
29 | # user filter,use the filter to search user when login in
30 | spring.ldap.searchFilter=
31 | # search base eg. ou=dev
32 | ldap.searchBase=
33 | # user objectClass
34 | ldap.mapping.objectClass=inetOrgPerson
35 | ldap.mapping.loginId=uid
36 | ldap.mapping.userDisplayName=gecos
37 | ldap.mapping.email=mail
--------------------------------------------------------------------------------
/rest/rest-web/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | server.port=8082
2 |
3 | logging.level.root=info
4 | log.path=/tmp/IM_logs
5 |
6 | spring.datasource.url=jdbc:mysql://127.0.0.1:3306/im?useUnicode=true&characterEncoding=utf-8&useSSL=false
7 | spring.datasource.username=root
8 | spring.datasource.password=123456
9 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
10 |
11 | spring.redis.host=127.0.0.1
12 | spring.redis.port=6379
13 | spring.redis.password=
14 |
15 | #rabbitmq
16 | spring.rabbitmq.host=127.0.0.1
17 | spring.rabbitmq.port=5672
18 | spring.rabbitmq.username=rabbitmq
19 | spring.rabbitmq.password=rabbitmq
20 |
21 | spi.user.impl.class=
22 | #spi.user.spi.class=com.github.yuanrw.im.rest.web.spiLdapUserSpiImpl
23 |
24 | spring.ldap.base=dc=example,dc=org
25 | # admin
26 | spring.ldap.username=cn=admin,dc=example,dc=org
27 | spring.ldap.password=admin
28 | spring.ldap.urls=ldap://127.0.0.1:389
29 | # user filter,use the filter to search user when login in
30 | spring.ldap.searchFilter=
31 | # search base eg. ou=dev
32 | ldap.searchBase=
33 | # user objectClass
34 | ldap.mapping.objectClass=inetOrgPerson
35 | ldap.mapping.loginId=uid
36 | ldap.mapping.userDisplayName=gecos
37 | ldap.mapping.email=mail
--------------------------------------------------------------------------------
/rest/rest-web/src/main/resources/logback-spring.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | rest
5 |
6 |
7 |
8 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n
9 |
10 |
11 |
12 |
13 | ${log.path}/rest.log
14 |
15 | rest.%d{yyyy-MM-dd}.log
16 |
17 |
18 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/resources/mapper/OfflineMapper.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | update im_offline set has_read = true
8 | where id = #{id} and has_read = false
9 |
10 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/resources/mapper/RelationMapper.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
23 |
--------------------------------------------------------------------------------
/rest/rest-web/src/main/resources/rest.sql:
--------------------------------------------------------------------------------
1 | CREATE DATABASE im;
2 | DROP TABLE IF EXISTS `im_user`;
3 | CREATE TABLE `im_user` (
4 | `id` bigint(20) NOT NULL,
5 | `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '用户名',
6 | `pwd_hash` char(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '密码加密后的hash值',
7 | `salt` char(16) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '盐',
8 | `gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
9 | `gmt_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
10 | `deleted` tinyint(1) NOT NULL DEFAULT '0',
11 | PRIMARY KEY (`id`),
12 | UNIQUE KEY `username` (`username`)
13 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
14 |
15 | DROP TABLE IF EXISTS `im_relation`;
16 | CREATE TABLE `im_relation` (
17 | `id` bigint(20) NOT NULL,
18 | `user_id1` varchar(100) NOT NULL COMMENT '用户1的id',
19 | `user_id2` varchar(100) NOT NULL COMMENT '用户2的id',
20 | `encrypt_key` char(33) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '密钥',
21 | `gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
22 | `gmt_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
23 | `deleted` tinyint(1) NOT NULL DEFAULT '0',
24 | PRIMARY KEY (`id`),
25 | UNIQUE KEY `USERID1_USERID2` (`user_id1`,`user_id2`)
26 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
27 |
28 | DROP TABLE IF EXISTS `im_offline`;
29 | CREATE TABLE `im_offline` (
30 | `id` bigint(20) NOT NULL,
31 | `msg_id` bigint(20) NOT NULL,
32 | `msg_code` int(2) NOT NULL,
33 | `to_user_id` varchar(100) NOT NULL,
34 | `content` varbinary(5000) NOT NULL DEFAULT '',
35 | `has_read` tinyint(1) NOT NULL DEFAULT '0',
36 | `gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
37 | `gmt_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
38 | `deleted` tinyint(1) NOT NULL DEFAULT '0'
39 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--------------------------------------------------------------------------------
/rest/rest-web/src/test/java/com/github/yuanrw/im/rest/web/test/OfflineTest.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.rest.web.test;
2 |
3 | import org.junit.Test;
4 | import org.junit.runner.RunWith;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
7 | import org.springframework.boot.test.context.SpringBootTest;
8 | import org.springframework.test.annotation.DirtiesContext;
9 | import org.springframework.test.context.junit4.SpringRunner;
10 | import org.springframework.test.web.reactive.server.WebTestClient;
11 |
12 | /**
13 | * Date: 2019-07-05
14 | * Time: 18:07
15 | *
16 | * @author yrw
17 | */
18 | @RunWith(SpringRunner.class)
19 | @SpringBootTest
20 | @AutoConfigureWebTestClient
21 | @DirtiesContext
22 | public class OfflineTest {
23 |
24 | @Autowired
25 | private WebTestClient webClient;
26 |
27 | @Test
28 | public void pollAllMsg() {
29 | webClient.get().uri("/offline/poll/1142773797275836418")
30 | .exchange()
31 | .expectStatus().isOk()
32 | .expectBody()
33 | .jsonPath("$.status").isEqualTo(200)
34 | .jsonPath("$.msg").isEqualTo("SUCCESS")
35 | .jsonPath("$.data.length()").isEqualTo(2)
36 | .jsonPath("$.data[0].id").isNotEmpty()
37 | .jsonPath("$.data[0].toUserId").isNotEmpty()
38 | .jsonPath("$.data[0].content").isNotEmpty()
39 | .jsonPath("$.data[1].id").isNotEmpty()
40 | .jsonPath("$.data[1].toUserId").isNotEmpty()
41 | .jsonPath("$.data[1].content").isNotEmpty();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/rest/rest-web/src/test/resources/application.properties:
--------------------------------------------------------------------------------
1 | server.port=8082
2 | logging.level.root=info
3 | spring.datasource.url=jdbc:h2:mem:im;INIT=runscript from 'src/test/resources/rest-test.sql';
4 | spring.datasource.driver-class-name=org.h2.Driver
5 | spring.redis.host=127.0.0.1
6 | spring.redis.port=6379
7 | spring.redis.password=
8 | #rabbitmq
9 | spring.rabbitmq.host=127.0.0.1
10 | spring.rabbitmq.port=5672
11 | spring.rabbitmq.username=rabbitmq
12 | spring.rabbitmq.password=rabbitmq
13 | spi.user.impl.class=
14 | #spi.user.spi.class=com.github.yuanrw.im.rest.web.spiLdapUserSpiImpl
15 | spring.ldap.base=dc=example,dc=org
16 | # admin
17 | spring.ldap.username=cn=admin,dc=example,dc=org
18 | spring.ldap.password=admin
19 | spring.ldap.urls=ldap://127.0.0.1:389
20 | # user filter,use the filter to search user when login in
21 | spring.ldap.searchFilter=
22 | # search base eg. ou=dev
23 | ldap.searchBase=
24 | # user objectClass
25 | ldap.mapping.objectClass=inetOrgPerson
26 | ldap.mapping.loginId=uid
27 | ldap.mapping.userDisplayName=gecos
28 | ldap.mapping.email=mail
--------------------------------------------------------------------------------
/rest/rest-web/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | rest
4 |
5 |
6 |
7 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/transfer/Dockerfile:
--------------------------------------------------------------------------------
1 | # Dockerfile for transfer
2 | # docker build -t yuanrw/transfer:$VERSION .
3 | # docker run -p 9082:9082 -d -v /tmp/IM_logs:/tmp/IM_logs --name transfer transfer
4 |
5 | FROM adoptopenjdk/openjdk11:alpine-jre
6 | MAINTAINER yuanrw <295415537@qq.com>
7 |
8 | ENV SERVICE_NAME transfer
9 | ENV VERSION 1.0.0
10 |
11 | EXPOSE 9082
12 |
13 | RUN echo "http://mirrors.aliyun.com/alpine/v3.8/main" > /etc/apk/repositories \
14 | && echo "http://mirrors.aliyun.com/alpine/v3.8/community" >> /etc/apk/repositories \
15 | && apk update upgrade \
16 | && apk add --no-cache procps unzip curl bash tzdata \
17 | && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
18 | && echo "Asia/Shanghai" > /etc/timezone
19 |
20 | COPY target/${SERVICE_NAME}-${VERSION}-bin.zip /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip
21 |
22 | RUN unzip /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip -d /${SERVICE_NAME} \
23 | && rm -rf /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}-bin.zip \
24 | && cd /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION} \
25 | && echo "tail -f /dev/null" >> start.sh
26 |
27 | WORKDIR /${SERVICE_NAME}/${SERVICE_NAME}-${VERSION}
28 |
29 | COPY src/main/resources/transfer-docker.properties .
30 | COPY src/main/bin/start-docker.sh .
31 | COPY src/main/bin/wait-for-it.sh .
32 |
33 | CMD /bin/bash wait-for-it.sh -t 0 rest-web:8082 --strict -- \
34 | /bin/bash start-docker.sh
--------------------------------------------------------------------------------
/transfer/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | IM
7 | com.github.yuanrw.im
8 | 1.0.0
9 |
10 | 4.0.0
11 |
12 | transfer
13 |
14 |
15 | com.github.yuanrw.im.transfer.start.TransferStarter
16 |
17 |
18 |
19 |
20 | io.netty
21 | netty-all
22 |
23 |
24 | commons-codec
25 | commons-codec
26 |
27 |
28 | org.apache.commons
29 | commons-lang3
30 |
31 |
32 | com.github.yuanrw.im
33 | common
34 |
35 |
36 | com.fasterxml.jackson.core
37 | jackson-databind
38 |
39 |
40 | com.rabbitmq
41 | amqp-client
42 |
43 |
44 | com.github.yuanrw.im
45 | user-status
46 | 1.0.0
47 |
48 |
49 |
50 |
51 |
52 |
53 | maven-jar-plugin
54 |
55 |
56 | maven-assembly-plugin
57 |
58 |
59 | org.codehaus.gmavenplus
60 | gmavenplus-plugin
61 |
62 |
63 | org.jacoco
64 | jacoco-maven-plugin
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/transfer/src/assembly/assembly.xml:
--------------------------------------------------------------------------------
1 |
2 | bin
3 |
4 | zip
5 |
6 |
7 |
8 | true
9 | lib
10 |
11 |
12 |
13 |
14 | src/main/resources
15 | /
16 |
17 | logback*.xml
18 | *-docker.*
19 |
20 | unix
21 |
22 |
23 | target
24 | /
25 |
26 | ${project.artifactId}-*.jar
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/transfer/src/main/bin/start-docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | SERVICE_NAME="transfer"
3 | VERSION="1.0.0"
4 |
5 | LOG_DIR=/tmp/IM_logs
6 | mkdir -p $LOG_DIR
7 |
8 | # Find Java
9 | if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then
10 | java="$JAVA_HOME/bin/java"
11 | elif type -p java > /dev/null 2>&1; then
12 | java=$(type -p java)
13 | elif [[ -x "/usr/bin/java" ]]; then
14 | java="/usr/bin/java"
15 | else
16 | echo "Unable to find Java"
17 | exit 1
18 | fi
19 |
20 | JAVA_OPTS="-Xms512m -Xmx512m -Xmn256m -XX:PermSize=128m -XX:MaxPermSize=128m"
21 |
22 | echo "JAVA_HOME: $JAVA_HOME"
23 | $java $JAVA_OPTS -Dconfig=$SERVICE_NAME-docker.properties -jar $SERVICE_NAME-$VERSION.jar
24 | echo "SERVICE_NAME started...."
--------------------------------------------------------------------------------
/transfer/src/main/java/com/github/yuanrw/im/transfer/config/TransferConfig.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.transfer.config;
2 |
3 | /**
4 | * Date: 2019-06-09
5 | * Time: 15:18
6 | *
7 | * @author yrw
8 | */
9 | public class TransferConfig {
10 |
11 | private Integer port;
12 |
13 | private String redisHost;
14 |
15 | private Integer redisPort;
16 |
17 | private String redisPassword;
18 |
19 | private String rabbitmqHost;
20 |
21 | private Integer rabbitmqPort;
22 |
23 | private String rabbitmqUsername;
24 |
25 | private String rabbitmqPassword;
26 |
27 | public Integer getPort() {
28 | return port;
29 | }
30 |
31 | public void setPort(Integer port) {
32 | this.port = port;
33 | }
34 |
35 | public String getRedisHost() {
36 | return redisHost;
37 | }
38 |
39 | public void setRedisHost(String redisHost) {
40 | this.redisHost = redisHost;
41 | }
42 |
43 | public Integer getRedisPort() {
44 | return redisPort;
45 | }
46 |
47 | public void setRedisPort(Integer redisPort) {
48 | this.redisPort = redisPort;
49 | }
50 |
51 | public String getRedisPassword() {
52 | return redisPassword;
53 | }
54 |
55 | public void setRedisPassword(String redisPassword) {
56 | this.redisPassword = redisPassword;
57 | }
58 |
59 | public String getRabbitmqHost() {
60 | return rabbitmqHost;
61 | }
62 |
63 | public void setRabbitmqHost(String rabbitmqHost) {
64 | this.rabbitmqHost = rabbitmqHost;
65 | }
66 |
67 | public Integer getRabbitmqPort() {
68 | return rabbitmqPort;
69 | }
70 |
71 | public void setRabbitmqPort(Integer rabbitmqPort) {
72 | this.rabbitmqPort = rabbitmqPort;
73 | }
74 |
75 | public String getRabbitmqUsername() {
76 | return rabbitmqUsername;
77 | }
78 |
79 | public void setRabbitmqUsername(String rabbitmqUsername) {
80 | this.rabbitmqUsername = rabbitmqUsername;
81 | }
82 |
83 | public String getRabbitmqPassword() {
84 | return rabbitmqPassword;
85 | }
86 |
87 | public void setRabbitmqPassword(String rabbitmqPassword) {
88 | this.rabbitmqPassword = rabbitmqPassword;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/transfer/src/main/java/com/github/yuanrw/im/transfer/config/TransferModule.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.transfer.config;
2 |
3 | import com.github.yuanrw.im.user.status.factory.UserStatusServiceFactory;
4 | import com.github.yuanrw.im.user.status.service.UserStatusService;
5 | import com.github.yuanrw.im.user.status.service.impl.RedisUserStatusServiceImpl;
6 | import com.google.inject.AbstractModule;
7 | import com.google.inject.assistedinject.FactoryModuleBuilder;
8 |
9 | /**
10 | * Date: 2019-06-09
11 | * Time: 15:52
12 | *
13 | * @author yrw
14 | */
15 | public class TransferModule extends AbstractModule {
16 |
17 | @Override
18 | protected void configure() {
19 | install(new FactoryModuleBuilder()
20 | .implement(UserStatusService.class, RedisUserStatusServiceImpl.class)
21 | .build(UserStatusServiceFactory.class));
22 | }
23 | }
--------------------------------------------------------------------------------
/transfer/src/main/java/com/github/yuanrw/im/transfer/domain/ConnectorConnContext.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.transfer.domain;
2 |
3 | import com.github.yuanrw.im.common.domain.conn.ConnectorConn;
4 | import com.github.yuanrw.im.common.domain.conn.MemoryConnContext;
5 | import com.github.yuanrw.im.user.status.factory.UserStatusServiceFactory;
6 | import com.github.yuanrw.im.user.status.service.UserStatusService;
7 | import com.google.inject.Inject;
8 | import com.google.inject.Singleton;
9 |
10 | import java.util.Properties;
11 |
12 | import static com.github.yuanrw.im.transfer.start.TransferStarter.TRANSFER_CONFIG;
13 |
14 | /**
15 | * 存储transfer和connector的连接
16 | * 以及用户和connector的关系
17 | * Date: 2019-04-12
18 | * Time: 18:22
19 | *
20 | * @author yrw
21 | */
22 | @Singleton
23 | public class ConnectorConnContext extends MemoryConnContext {
24 |
25 | private UserStatusService userStatusService;
26 |
27 | @Inject
28 | public ConnectorConnContext(UserStatusServiceFactory userStatusServiceFactory) {
29 | Properties properties = new Properties();
30 | properties.put("host", TRANSFER_CONFIG.getRedisHost());
31 | properties.put("port", TRANSFER_CONFIG.getRedisPort());
32 | properties.put("password", TRANSFER_CONFIG.getRedisPassword());
33 | this.userStatusService = userStatusServiceFactory.createService(properties);
34 | }
35 |
36 | public ConnectorConn getConnByUserId(String userId) {
37 | String connectorId = userStatusService.getConnectorId(userId);
38 | if (connectorId != null) {
39 | ConnectorConn conn = getConn(connectorId);
40 | if (conn != null) {
41 | return conn;
42 | } else {
43 | //connectorId已过时,而用户还没再次上线
44 | userStatusService.offline(userId);
45 | }
46 | }
47 | return null;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/transfer/src/main/java/com/github/yuanrw/im/transfer/handler/TransferConnectorHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.transfer.handler;
2 |
3 | import com.github.yuanrw.im.common.parse.AbstractMsgParser;
4 | import com.github.yuanrw.im.common.parse.InternalParser;
5 | import com.github.yuanrw.im.protobuf.generate.Ack;
6 | import com.github.yuanrw.im.protobuf.generate.Chat;
7 | import com.github.yuanrw.im.protobuf.generate.Internal;
8 | import com.github.yuanrw.im.transfer.domain.ConnectorConnContext;
9 | import com.github.yuanrw.im.transfer.service.TransferService;
10 | import com.google.inject.Inject;
11 | import com.google.protobuf.Message;
12 | import io.netty.channel.ChannelHandlerContext;
13 | import io.netty.channel.SimpleChannelInboundHandler;
14 | import org.slf4j.Logger;
15 | import org.slf4j.LoggerFactory;
16 |
17 | import static com.github.yuanrw.im.common.parse.AbstractMsgParser.checkDest;
18 | import static com.github.yuanrw.im.common.parse.AbstractMsgParser.checkFrom;
19 |
20 | /**
21 | * Date: 2019-04-12
22 | * Time: 18:17
23 | *
24 | * @author yrw
25 | */
26 | public class TransferConnectorHandler extends SimpleChannelInboundHandler {
27 | private Logger logger = LoggerFactory.getLogger(TransferConnectorHandler.class);
28 |
29 | private TransferService transferService;
30 | private ConnectorConnContext connectorConnContext;
31 | private FromConnectorParser fromConnectorParser;
32 |
33 | @Inject
34 | public TransferConnectorHandler(TransferService transferService, ConnectorConnContext connectorConnContext) {
35 | this.fromConnectorParser = new FromConnectorParser();
36 | this.transferService = transferService;
37 | this.connectorConnContext = connectorConnContext;
38 | }
39 |
40 | @Override
41 | protected void channelRead0(ChannelHandlerContext ctx, Message msg) throws Exception {
42 | logger.debug("[transfer] get msg: {}", msg.toString());
43 |
44 | checkFrom(msg, Internal.InternalMsg.Module.CONNECTOR);
45 | checkDest(msg, Internal.InternalMsg.Module.TRANSFER);
46 |
47 | fromConnectorParser.parse(msg, ctx);
48 | }
49 |
50 | @Override
51 | public void channelInactive(ChannelHandlerContext ctx) throws Exception {
52 | connectorConnContext.removeConn(ctx);
53 | }
54 |
55 | class FromConnectorParser extends AbstractMsgParser {
56 |
57 | @Override
58 | public void registerParsers() {
59 | InternalParser parser = new InternalParser(3);
60 | parser.register(Internal.InternalMsg.MsgType.GREET, (m, ctx) -> transferService.doGreet(m, ctx));
61 |
62 | register(Chat.ChatMsg.class, (m, ctx) -> transferService.doChat(m));
63 | register(Ack.AckMsg.class, (m, ctx) -> transferService.doSendAck(m));
64 | register(Internal.InternalMsg.class, parser.generateFun());
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/transfer/src/main/java/com/github/yuanrw/im/transfer/service/TransferService.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.transfer.service;
2 |
3 | import com.github.yuanrw.im.common.domain.conn.Conn;
4 | import com.github.yuanrw.im.common.domain.conn.ConnectorConn;
5 | import com.github.yuanrw.im.common.domain.constant.ImConstant;
6 | import com.github.yuanrw.im.common.domain.constant.MsgVersion;
7 | import com.github.yuanrw.im.common.util.IdWorker;
8 | import com.github.yuanrw.im.protobuf.generate.Ack;
9 | import com.github.yuanrw.im.protobuf.generate.Chat;
10 | import com.github.yuanrw.im.protobuf.generate.Internal;
11 | import com.github.yuanrw.im.transfer.domain.ConnectorConnContext;
12 | import com.github.yuanrw.im.transfer.start.TransferMqProducer;
13 | import com.github.yuanrw.im.transfer.start.TransferStarter;
14 | import com.google.inject.Inject;
15 | import com.google.protobuf.Message;
16 | import com.rabbitmq.client.MessageProperties;
17 | import io.netty.channel.ChannelHandlerContext;
18 |
19 | import java.io.IOException;
20 |
21 | /**
22 | * Date: 2019-05-04
23 | * Time: 13:47
24 | *
25 | * @author yrw
26 | */
27 | public class TransferService {
28 |
29 | private ConnectorConnContext connContext;
30 | private TransferMqProducer producer;
31 |
32 | @Inject
33 | public TransferService(ConnectorConnContext connContext) {
34 | this.connContext = connContext;
35 | this.producer = TransferStarter.producer;
36 | }
37 |
38 | public void doChat(Chat.ChatMsg msg) throws IOException {
39 | ConnectorConn conn = connContext.getConnByUserId(msg.getDestId());
40 |
41 | if (conn != null) {
42 | conn.getCtx().writeAndFlush(msg);
43 | } else {
44 | doOffline(msg);
45 | }
46 | }
47 |
48 | public void doSendAck(Ack.AckMsg msg) throws IOException {
49 | ConnectorConn conn = connContext.getConnByUserId(msg.getDestId());
50 |
51 | if (conn != null) {
52 | conn.getCtx().writeAndFlush(msg);
53 | } else {
54 | doOffline(msg);
55 | }
56 | }
57 |
58 | public void doGreet(Internal.InternalMsg msg, ChannelHandlerContext ctx) {
59 | ctx.channel().attr(Conn.NET_ID).set(msg.getMsgBody());
60 | ConnectorConn conn = new ConnectorConn(ctx);
61 | connContext.addConn(conn);
62 |
63 | ctx.writeAndFlush(getInternalAck(msg.getId()));
64 | }
65 |
66 | private Internal.InternalMsg getInternalAck(Long msgId) {
67 | return Internal.InternalMsg.newBuilder()
68 | .setVersion(MsgVersion.V1.getVersion())
69 | .setId(IdWorker.genId())
70 | .setFrom(Internal.InternalMsg.Module.TRANSFER)
71 | .setDest(Internal.InternalMsg.Module.CONNECTOR)
72 | .setCreateTime(System.currentTimeMillis())
73 | .setMsgType(Internal.InternalMsg.MsgType.ACK)
74 | .setMsgBody(msgId + "")
75 | .build();
76 | }
77 |
78 | private void doOffline(Message msg) throws IOException {
79 | producer.basicPublish(ImConstant.MQ_EXCHANGE, ImConstant.MQ_ROUTING_KEY,
80 | MessageProperties.PERSISTENT_TEXT_PLAIN, msg);
81 | }
82 | }
--------------------------------------------------------------------------------
/transfer/src/main/java/com/github/yuanrw/im/transfer/start/TransferMqProducer.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.transfer.start;
2 |
3 | import com.github.yuanrw.im.common.domain.constant.ImConstant;
4 | import com.github.yuanrw.im.protobuf.constant.MsgTypeEnum;
5 | import com.google.inject.Singleton;
6 | import com.google.protobuf.Message;
7 | import com.rabbitmq.client.*;
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 |
11 | import java.io.IOException;
12 | import java.util.concurrent.TimeoutException;
13 |
14 | /**
15 | * Date: 2019-05-06
16 | * Time: 14:27
17 | *
18 | * @author yrw
19 | */
20 | @Singleton
21 | public class TransferMqProducer {
22 | private static Logger logger = LoggerFactory.getLogger(TransferMqProducer.class);
23 |
24 | private Channel channel;
25 |
26 | public TransferMqProducer(String host, int port, String username, String password)
27 | throws IOException, TimeoutException {
28 | ConnectionFactory factory = new ConnectionFactory();
29 | factory.setHost(host);
30 | factory.setPort(port);
31 | factory.setUsername(username);
32 | factory.setPassword(password);
33 |
34 | Connection connection = factory.newConnection();
35 | Channel channel = connection.createChannel();
36 |
37 | channel.exchangeDeclare(ImConstant.MQ_EXCHANGE, BuiltinExchangeType.DIRECT, true, false, null);
38 | channel.queueDeclare(ImConstant.MQ_OFFLINE_QUEUE, true, false, false, null);
39 | channel.queueBind(ImConstant.MQ_OFFLINE_QUEUE, ImConstant.MQ_EXCHANGE, ImConstant.MQ_ROUTING_KEY);
40 |
41 | this.channel = channel;
42 | logger.info("[transfer] producer start success");
43 | }
44 |
45 | public void basicPublish(String exchange, String routingKey, AMQP.BasicProperties properties, Message message) throws IOException {
46 | int code = MsgTypeEnum.getByClass(message.getClass()).getCode();
47 |
48 | byte[] srcB = message.toByteArray();
49 | byte[] destB = new byte[srcB.length + 1];
50 | destB[0] = (byte) code;
51 |
52 | System.arraycopy(message.toByteArray(), 0, destB, 1, message.toByteArray().length);
53 |
54 | channel.basicPublish(exchange, routingKey, properties, destB);
55 | }
56 |
57 | public Channel getChannel() {
58 | return channel;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/transfer/src/main/java/com/github/yuanrw/im/transfer/start/TransferServer.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.transfer.start;
2 |
3 | import com.github.yuanrw.im.common.code.MsgDecoder;
4 | import com.github.yuanrw.im.common.code.MsgEncoder;
5 | import com.github.yuanrw.im.common.exception.ImException;
6 | import com.github.yuanrw.im.transfer.handler.TransferConnectorHandler;
7 | import io.netty.bootstrap.ServerBootstrap;
8 | import io.netty.channel.*;
9 | import io.netty.channel.nio.NioEventLoopGroup;
10 | import io.netty.channel.socket.SocketChannel;
11 | import io.netty.channel.socket.nio.NioServerSocketChannel;
12 | import org.slf4j.Logger;
13 | import org.slf4j.LoggerFactory;
14 |
15 | import java.net.InetSocketAddress;
16 | import java.util.concurrent.ExecutionException;
17 | import java.util.concurrent.TimeUnit;
18 | import java.util.concurrent.TimeoutException;
19 |
20 | /**
21 | * Date: 2019-04-12
22 | * Time: 18:16
23 | *
24 | * @author yrw
25 | */
26 | public class TransferServer {
27 | private static Logger logger = LoggerFactory.getLogger(TransferServer.class);
28 |
29 | static void startTransferServer(int port) {
30 | EventLoopGroup bossGroup = new NioEventLoopGroup();
31 | EventLoopGroup workGroup = new NioEventLoopGroup();
32 |
33 | ServerBootstrap bootstrap = new ServerBootstrap()
34 | .group(bossGroup, workGroup)
35 | .channel(NioServerSocketChannel.class)
36 | .childHandler(new ChannelInitializer() {
37 | @Override
38 | protected void initChannel(SocketChannel channel) throws Exception {
39 | ChannelPipeline pipeline = channel.pipeline();
40 | pipeline.addLast("MsgDecoder", TransferStarter.injector.getInstance(MsgDecoder.class));
41 | pipeline.addLast("MsgEncoder", TransferStarter.injector.getInstance(MsgEncoder.class));
42 | pipeline.addLast("TransferClientHandler", TransferStarter.injector.getInstance(TransferConnectorHandler.class));
43 | }
44 | });
45 |
46 | ChannelFuture f = bootstrap.bind(new InetSocketAddress(port)).addListener((ChannelFutureListener) future -> {
47 | if (future.isSuccess()) {
48 | logger.info("[transfer] start successful at port {}, waiting for connectors to connect...", port);
49 | } else {
50 | throw new ImException("[transfer] start failed");
51 | }
52 | });
53 |
54 | try {
55 | f.get(10, TimeUnit.SECONDS);
56 | } catch (InterruptedException | ExecutionException | TimeoutException e) {
57 | throw new ImException("[transfer] start failed");
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/transfer/src/main/java/com/github/yuanrw/im/transfer/start/TransferStarter.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.transfer.start;
2 |
3 | import com.github.yuanrw.im.common.exception.ImException;
4 | import com.github.yuanrw.im.transfer.config.TransferConfig;
5 | import com.github.yuanrw.im.transfer.config.TransferModule;
6 | import com.google.inject.Guice;
7 | import com.google.inject.Injector;
8 | import org.slf4j.LoggerFactory;
9 |
10 | import java.io.FileInputStream;
11 | import java.io.IOException;
12 | import java.io.InputStream;
13 | import java.util.Properties;
14 |
15 | /**
16 | * Date: 2019-05-07
17 | * Time: 20:39
18 | *
19 | * @author yrw
20 | */
21 | public class TransferStarter {
22 | public static TransferConfig TRANSFER_CONFIG = new TransferConfig();
23 | public static TransferMqProducer producer;
24 | static Injector injector = Guice.createInjector(new TransferModule());
25 |
26 | public static void main(String[] args) {
27 | try {
28 | //parse start parameter
29 | TransferStarter.TRANSFER_CONFIG = parseConfig();
30 |
31 | //start rabbitmq server
32 | producer = new TransferMqProducer(TRANSFER_CONFIG.getRabbitmqHost(), TRANSFER_CONFIG.getRabbitmqPort(),
33 | TRANSFER_CONFIG.getRabbitmqUsername(), TRANSFER_CONFIG.getRabbitmqPassword());
34 |
35 | //start transfer server
36 | TransferServer.startTransferServer(TRANSFER_CONFIG.getPort());
37 | } catch (Exception e) {
38 | LoggerFactory.getLogger(TransferStarter.class).error("[transfer] start failed", e);
39 | }
40 | }
41 |
42 | private static TransferConfig parseConfig() throws IOException {
43 | Properties properties = getProperties();
44 |
45 | TransferConfig transferConfig = new TransferConfig();
46 | try {
47 | transferConfig.setPort(Integer.parseInt((String) properties.get("port")));
48 | transferConfig.setRedisHost(properties.getProperty("redis.host"));
49 | transferConfig.setRedisPort(Integer.parseInt(properties.getProperty("redis.port")));
50 | transferConfig.setRedisPassword(properties.getProperty("redis.password"));
51 | transferConfig.setRabbitmqHost(properties.getProperty("rabbitmq.host"));
52 | transferConfig.setRabbitmqUsername(properties.getProperty("rabbitmq.username"));
53 | transferConfig.setRabbitmqPassword(properties.getProperty("rabbitmq.password"));
54 | transferConfig.setRabbitmqPort(Integer.parseInt(properties.getProperty("rabbitmq.port")));
55 | } catch (Exception e) {
56 | throw new ImException("there's a parse error, check your config properties");
57 | }
58 |
59 | System.setProperty("log.path", properties.getProperty("log.path"));
60 | System.setProperty("log.level", properties.getProperty("log.level"));
61 |
62 | return transferConfig;
63 | }
64 |
65 | private static Properties getProperties() throws IOException {
66 | InputStream inputStream;
67 | String path = System.getProperty("config");
68 | if (path == null) {
69 | throw new ImException("transfer.properties is not defined");
70 | } else {
71 | inputStream = new FileInputStream(path);
72 | }
73 |
74 | Properties properties = new Properties();
75 | properties.load(inputStream);
76 | return properties;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/transfer/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | transfer
4 |
5 |
6 |
7 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n
8 |
9 |
10 |
11 |
12 | ${log.path}/transfer.log
13 |
14 | rest.%d{yyyy-MM-dd}.log
15 |
16 |
17 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/transfer/src/main/resources/transfer-docker.properties:
--------------------------------------------------------------------------------
1 | port=9082
2 |
3 | redis.host=im-redis
4 | redis.port=6379
5 | redis.password=
6 |
7 | rabbitmq.host=im-rabbit
8 | rabbitmq.port=5672
9 | rabbitmq.username=rabbitmq
10 | rabbitmq.password=rabbitmq
11 |
12 | log.path=/tmp/IM_logs
13 | log.level=info
--------------------------------------------------------------------------------
/transfer/src/main/resources/transfer.properties:
--------------------------------------------------------------------------------
1 | port=9082
2 |
3 | redis.host=127.0.0.1
4 | redis.port=6379
5 | redis.password=
6 |
7 | rabbitmq.host=127.0.0.1
8 | rabbitmq.port=5672
9 | rabbitmq.username=rabbitmq
10 | rabbitmq.password=rabbitmq
11 |
12 | log.path=/tmp/IM_logs
13 | log.level=info
--------------------------------------------------------------------------------
/transfer/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | rest
4 |
5 |
6 |
7 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/user-status/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | IM
7 | com.github.yuanrw.im
8 | 1.0.0
9 |
10 | 4.0.0
11 |
12 | user-status
13 |
14 |
15 |
16 | com.github.yuanrw.im
17 | common
18 |
19 |
20 | redis.clients
21 | jedis
22 |
23 |
24 | org.yaml
25 | snakeyaml
26 |
27 |
28 | com.google.inject.extensions
29 | guice-assistedinject
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/user-status/src/main/java/com/github/yuanrw/im/user/status/factory/UserStatusServiceFactory.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.user.status.factory;
2 |
3 | import com.github.yuanrw.im.user.status.service.UserStatusService;
4 |
5 | import java.util.Properties;
6 |
7 | /**
8 | * Date: 2019-06-09
9 | * Time: 15:51
10 | *
11 | * @author yrw
12 | */
13 | public interface UserStatusServiceFactory {
14 |
15 | /**
16 | * create a userStatusService
17 | *
18 | * @param properties
19 | * @return
20 | */
21 | UserStatusService createService(Properties properties);
22 | }
23 |
--------------------------------------------------------------------------------
/user-status/src/main/java/com/github/yuanrw/im/user/status/service/UserStatusService.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.user.status.service;
2 |
3 | /**
4 | * Date: 2019-06-09
5 | * Time: 15:55
6 | *
7 | * @author yrw
8 | */
9 | public interface UserStatusService {
10 |
11 | /**
12 | * user online
13 | *
14 | * @param userId
15 | * @param connectorId
16 | * @return the user's previous connection id, if don't exist then return null
17 | */
18 | String online(String userId, String connectorId);
19 |
20 | /**
21 | * user offline
22 | *
23 | * @param userId
24 | */
25 | void offline(String userId);
26 |
27 | /**
28 | * get connector id by user id
29 | *
30 | * @param userId
31 | * @return
32 | */
33 | String getConnectorId(String userId);
34 | }
35 |
--------------------------------------------------------------------------------
/user-status/src/main/java/com/github/yuanrw/im/user/status/service/impl/MemoryUserStatusServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.user.status.service.impl;
2 |
3 | import com.github.yuanrw.im.user.status.service.UserStatusService;
4 |
5 | import java.util.concurrent.ConcurrentHashMap;
6 | import java.util.concurrent.ConcurrentMap;
7 |
8 | /**
9 | * it's for test
10 | * Date: 2019-09-02
11 | * Time: 13:33
12 | *
13 | * @author yrw
14 | */
15 | public class MemoryUserStatusServiceImpl implements UserStatusService {
16 |
17 | private ConcurrentMap userIdConnectorIdMap;
18 |
19 | public MemoryUserStatusServiceImpl() {
20 | this.userIdConnectorIdMap = new ConcurrentHashMap<>();
21 | }
22 |
23 | @Override
24 | public String online(String userId, String connectorId) {
25 | return userIdConnectorIdMap.put(userId, connectorId);
26 | }
27 |
28 | @Override
29 | public void offline(String userId) {
30 | userIdConnectorIdMap.remove(userId);
31 | }
32 |
33 | @Override
34 | public String getConnectorId(String userId) {
35 | return userIdConnectorIdMap.get(userId);
36 | }
37 | }
--------------------------------------------------------------------------------
/user-status/src/main/java/com/github/yuanrw/im/user/status/service/impl/RedisUserStatusServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.github.yuanrw.im.user.status.service.impl;
2 |
3 | import com.github.yuanrw.im.user.status.service.UserStatusService;
4 | import com.google.inject.Inject;
5 | import com.google.inject.assistedinject.Assisted;
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 | import redis.clients.jedis.Jedis;
9 | import redis.clients.jedis.JedisPool;
10 | import redis.clients.jedis.JedisPoolConfig;
11 |
12 | import java.util.Properties;
13 |
14 | /**
15 | * manage user status in redis
16 | * Date: 2019-05-19
17 | * Time: 21:14
18 | *
19 | * @author yrw
20 | */
21 | public class RedisUserStatusServiceImpl implements UserStatusService {
22 | private static final Logger logger = LoggerFactory.getLogger(RedisUserStatusServiceImpl.class);
23 | private static final String USER_CONN_STATUS_KEY = "IM:USER_CONN_STATUS:USERID:";
24 |
25 | private JedisPool jedisPool;
26 |
27 | @Inject
28 | public RedisUserStatusServiceImpl(@Assisted Properties properties) {
29 | JedisPoolConfig config = new JedisPoolConfig();
30 | config.setMaxWaitMillis(2 * 1000);
31 | String password = properties.getProperty("password");
32 | jedisPool = new JedisPool(config, properties.getProperty("host"), (Integer) properties.get("port"),
33 | 2 * 1000, password != null && !password.isEmpty() ? password : null);
34 | }
35 |
36 | @Override
37 | public String online(String userId, String connectorId) {
38 | logger.debug("[user status] user online: userId: {}, connectorId: {}", userId, connectorId);
39 |
40 | try (Jedis jedis = jedisPool.getResource()) {
41 | String oldConnectorId = jedis.hget(USER_CONN_STATUS_KEY, String.valueOf(userId));
42 | jedis.hset(USER_CONN_STATUS_KEY, String.valueOf(userId), connectorId);
43 | return oldConnectorId;
44 | } catch (Exception e) {
45 | logger.error(e.getMessage(), e);
46 | return null;
47 | }
48 | }
49 |
50 | @Override
51 | public void offline(String userId) {
52 | logger.debug("[user status] user offline: userId: {}", userId);
53 |
54 | try (Jedis jedis = jedisPool.getResource()) {
55 | jedis.hdel(USER_CONN_STATUS_KEY, String.valueOf(userId));
56 | } catch (Exception e) {
57 | logger.error(e.getMessage(), e);
58 | }
59 | }
60 |
61 | @Override
62 | public String getConnectorId(String userId) {
63 | try (Jedis jedis = jedisPool.getResource()) {
64 | return jedis.hget(USER_CONN_STATUS_KEY, userId);
65 | } catch (Exception e) {
66 | logger.error(e.getMessage(), e);
67 | return null;
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/user-status/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | rest
4 |
5 |
6 |
7 | %d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------