diff --git a/hs-im-app/App.vue b/hs-im-app/App.vue new file mode 100644 index 0000000..8c2b732 --- /dev/null +++ b/hs-im-app/App.vue @@ -0,0 +1,17 @@ + + + diff --git a/hs-im-app/index.html b/hs-im-app/index.html new file mode 100644 index 0000000..c3ff205 --- /dev/null +++ b/hs-im-app/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + +
+ + + diff --git a/hs-im-app/main.js b/hs-im-app/main.js new file mode 100644 index 0000000..c1caf36 --- /dev/null +++ b/hs-im-app/main.js @@ -0,0 +1,22 @@ +import App from './App' + +// #ifndef VUE3 +import Vue from 'vue' +import './uni.promisify.adaptor' +Vue.config.productionTip = false +App.mpType = 'app' +const app = new Vue({ + ...App +}) +app.$mount() +// #endif + +// #ifdef VUE3 +import { createSSRApp } from 'vue' +export function createApp() { + const app = createSSRApp(App) + return { + app + } +} +// #endif \ No newline at end of file diff --git a/hs-im-app/manifest.json b/hs-im-app/manifest.json new file mode 100644 index 0000000..f877d65 --- /dev/null +++ b/hs-im-app/manifest.json @@ -0,0 +1,72 @@ +{ + "name" : "hs-im-app", + "appid" : "__UNI__1D29CD0", + "description" : "", + "versionName" : "1.0.0", + "versionCode" : "100", + "transformPx" : false, + /* 5+App特有相关 */ + "app-plus" : { + "usingComponents" : true, + "nvueStyleCompiler" : "uni-app", + "compilerVersion" : 3, + "splashscreen" : { + "alwaysShowBeforeRender" : true, + "waiting" : true, + "autoclose" : true, + "delay" : 0 + }, + /* 模块配置 */ + "modules" : {}, + /* 应用发布信息 */ + "distribute" : { + /* android打包配置 */ + "android" : { + "permissions" : [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + /* ios打包配置 */ + "ios" : {}, + /* SDK配置 */ + "sdkConfigs" : {} + } + }, + /* 快应用特有相关 */ + "quickapp" : {}, + /* 小程序特有相关 */ + "mp-weixin" : { + "appid" : "", + "setting" : { + "urlCheck" : false + }, + "usingComponents" : true + }, + "mp-alipay" : { + "usingComponents" : true + }, + "mp-baidu" : { + "usingComponents" : true + }, + "mp-toutiao" : { + "usingComponents" : true + }, + "uniStatistics" : { + "enable" : false + }, + "vueVersion" : "2" +} diff --git a/hs-im-app/pages.json b/hs-im-app/pages.json new file mode 100644 index 0000000..abe78c6 --- /dev/null +++ b/hs-im-app/pages.json @@ -0,0 +1,23 @@ +{ + "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages + + { + "path": "pages/index/index", + "style": { + "navigationBarTitleText": "uni-app" + } + }, { + "path": "pages/index/index", + "style": { + "navigationBarTitleText": "uni-app" + } + } + ], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "uni-app", + "navigationBarBackgroundColor": "#F8F8F8", + "backgroundColor": "#F8F8F8" + }, + "uniIdRouter": {} +} \ No newline at end of file diff --git a/hs-im-app/pages/index/index.vue b/hs-im-app/pages/index/index.vue new file mode 100644 index 0000000..ec0ec26 --- /dev/null +++ b/hs-im-app/pages/index/index.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/hs-im-app/pages/login/login.vue b/hs-im-app/pages/login/login.vue new file mode 100644 index 0000000..8183fd0 --- /dev/null +++ b/hs-im-app/pages/login/login.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/hs-im-app/static/logo.png b/hs-im-app/static/logo.png new file mode 100644 index 0000000..b5771e2 Binary files /dev/null and b/hs-im-app/static/logo.png differ diff --git a/hs-im-app/uni.promisify.adaptor.js b/hs-im-app/uni.promisify.adaptor.js new file mode 100644 index 0000000..47fbce1 --- /dev/null +++ b/hs-im-app/uni.promisify.adaptor.js @@ -0,0 +1,10 @@ +uni.addInterceptor({ + returnValue (res) { + if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) { + return res; + } + return new Promise((resolve, reject) => { + res.then((res) => res[0] ? reject(res[0]) : resolve(res[1])); + }); + }, +}); \ No newline at end of file diff --git a/hs-im-app/uni.scss b/hs-im-app/uni.scss new file mode 100644 index 0000000..a05adb4 --- /dev/null +++ b/hs-im-app/uni.scss @@ -0,0 +1,76 @@ +/** + * 这里是uni-app内置的常用样式变量 + * + * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 + * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App + * + */ + +/** + * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 + * + * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 + */ + +/* 颜色变量 */ + +/* 行为相关颜色 */ +$uni-color-primary: #007aff; +$uni-color-success: #4cd964; +$uni-color-warning: #f0ad4e; +$uni-color-error: #dd524d; + +/* 文字基本颜色 */ +$uni-text-color:#333;//基本色 +$uni-text-color-inverse:#fff;//反色 +$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息 +$uni-text-color-placeholder: #808080; +$uni-text-color-disable:#c0c0c0; + +/* 背景颜色 */ +$uni-bg-color:#ffffff; +$uni-bg-color-grey:#f8f8f8; +$uni-bg-color-hover:#f1f1f1;//点击状态颜色 +$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色 + +/* 边框颜色 */ +$uni-border-color:#c8c7cc; + +/* 尺寸变量 */ + +/* 文字尺寸 */ +$uni-font-size-sm:12px; +$uni-font-size-base:14px; +$uni-font-size-lg:16; + +/* 图片尺寸 */ +$uni-img-size-sm:20px; +$uni-img-size-base:26px; +$uni-img-size-lg:40px; + +/* Border Radius */ +$uni-border-radius-sm: 2px; +$uni-border-radius-base: 3px; +$uni-border-radius-lg: 6px; +$uni-border-radius-circle: 50%; + +/* 水平间距 */ +$uni-spacing-row-sm: 5px; +$uni-spacing-row-base: 10px; +$uni-spacing-row-lg: 15px; + +/* 垂直间距 */ +$uni-spacing-col-sm: 4px; +$uni-spacing-col-base: 8px; +$uni-spacing-col-lg: 12px; + +/* 透明度 */ +$uni-opacity-disabled: 0.3; // 组件禁用态的透明度 + +/* 文章场景相关 */ +$uni-color-title: #2C405A; // 文章标题颜色 +$uni-font-size-title:20px; +$uni-color-subtitle: #555555; // 二级标题颜色 +$uni-font-size-subtitle:26px; +$uni-color-paragraph: #3F536E; // 文章段落颜色 +$uni-font-size-paragraph:15px; diff --git a/hs-im-server/README.md b/hs-im-server/README.md index e69de29..e657816 100644 --- a/hs-im-server/README.md +++ b/hs-im-server/README.md @@ -0,0 +1,15 @@ +rabitMQTT + +```bash + +docker run -d -it -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq:management + +``` +在web管理后台新建Exchanges : +`messageService2Pipeline` +`pipeline2UserService` + + +zookeeper搭建教程 + +https://blog.csdn.net/weixin_43559374/article/details/131588024 diff --git a/hs-im-server/doc/privateSend.py b/hs-im-server/doc/privateSend.py new file mode 100644 index 0000000..06695c8 --- /dev/null +++ b/hs-im-server/doc/privateSend.py @@ -0,0 +1,42 @@ +import socket +import uuid +import json + + +s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) +s.connect(("127.0.0.1",9000)) + +imei = str(uuid.uuid4()) + +print(imei) + +## 基础数据 +command = 9888 +version = 1 +clientType = 4 +messageType= 0x0 +appId = 10000 +name ='rowger' + +## 数据转换为bytes +commandByte = command.to_bytes(4, "big") +versionByte = version.to_bytes(4,'big') +messageTypeByte = messageType.to_bytes(4,'big') +clientTypeByte = clientType.to_bytes(4,'big') +appIdByte = appId.to_bytes(4,"big") +clientTypeByte = clientType.to_bytes(4, 'big') +imeiBytes = bytes(imei,"utf-8") +imeiLength = len(imeiBytes) +imeiLengthByte = imeiLength.to_bytes(4, 'big') + +data = {"name": name, "appId": appId, "clientType": clientType, "imei": imei} + + +jsonData = json.dumps(data) +body = bytes(jsonData, 'utf-8') +body_len = len(body) +bodyLenBytes = body_len.to_bytes(4,"big") + +s.sendall(commandByte + versionByte + clientTypeByte + messageTypeByte + appIdByte + imeiLengthByte + bodyLenBytes + imeiBytes + body) +print("Sending end") + diff --git a/hs-im-server/im-codec/pom.xml b/hs-im-server/im-codec/pom.xml new file mode 100644 index 0000000..7160237 --- /dev/null +++ b/hs-im-server/im-codec/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + com.lld + im-system + 1.0.0-SNAPSHOT + + + im-codec + + + 8 + 8 + UTF-8 + + + + + + + + io.netty + netty-all + 4.1.35.Final + + + + + com.alibaba + fastjson + 1.2.51 + + + + + com.lld + common + 1.0.0-SNAPSHOT + + + + + diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/MessageDecoder.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/MessageDecoder.java new file mode 100644 index 0000000..fa9b010 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/MessageDecoder.java @@ -0,0 +1,43 @@ +package com.lld.im.codec; + +import com.alibaba.fastjson.JSONObject; +import com.lld.im.codec.proto.Message; +import com.lld.im.codec.proto.MessageHeader; +import com.lld.im.codec.utils.ByteBufToMessageUtils; +import com.sun.org.apache.bcel.internal.generic.NEW; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; + +/** + * @description: 消息解码类 + * @author: lld + * @version: 1.0 + */ +public class MessageDecoder extends ByteToMessageDecoder { + + @Override + protected void decode(ChannelHandlerContext ctx, + ByteBuf in, List out) throws Exception { + //请求头(指令 + // 版本 + // clientType + // 消息解析类型 + // appId + // imei长度 + // bodylen)+ imei号 + 请求体 + + if(in.readableBytes() < 28){ + return; + } + + Message message = ByteBufToMessageUtils.transition(in); + if(message == null){ + return; + } + + out.add(message); + } +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/MessageEncoder.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/MessageEncoder.java new file mode 100644 index 0000000..35792be --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/MessageEncoder.java @@ -0,0 +1,27 @@ +package com.lld.im.codec; + +import com.alibaba.fastjson.JSONObject; +import com.lld.im.codec.proto.MessagePack; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +/** + * @author: Chackylee + * @description: 消息编码类,私有协议规则,前4位表示长度,接着command4位,后面是数据 + **/ +public class MessageEncoder extends MessageToByteEncoder { + + @Override + protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception { + if(msg instanceof MessagePack){ + MessagePack msgBody = (MessagePack) msg; + String s = JSONObject.toJSONString(msgBody.getData()); + byte[] bytes = s.getBytes(); + out.writeInt(msgBody.getCommand()); + out.writeInt(bytes.length); + out.writeBytes(bytes); + } + } + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/WebSocketMessageDecoder.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/WebSocketMessageDecoder.java new file mode 100644 index 0000000..65afe8d --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/WebSocketMessageDecoder.java @@ -0,0 +1,32 @@ +package com.lld.im.codec; + +import com.lld.im.codec.proto.Message; +import com.lld.im.codec.utils.ByteBufToMessageUtils; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; + +import java.util.List; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +public class WebSocketMessageDecoder extends MessageToMessageDecoder { + @Override + protected void decode(ChannelHandlerContext ctx, BinaryWebSocketFrame msg, List out) throws Exception { + System.out.println("ws解码器收到信息"); + + ByteBuf content = msg.content(); + if (content.readableBytes() < 28) { + return; + } + Message message = ByteBufToMessageUtils.transition(content); + if(message == null){ + return; + } + out.add(message); + } +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/WebSocketMessageEncoder.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/WebSocketMessageEncoder.java new file mode 100644 index 0000000..49fe765 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/WebSocketMessageEncoder.java @@ -0,0 +1,40 @@ +package com.lld.im.codec; + + +import com.alibaba.fastjson.JSONObject; +import com.lld.im.codec.proto.MessagePack; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * @author: Chackylee + * @description: + **/ +public class WebSocketMessageEncoder extends MessageToMessageEncoder { + + private static Logger log = LoggerFactory.getLogger(WebSocketMessageEncoder.class); + + @Override + protected void encode(ChannelHandlerContext ctx, MessagePack msg, List out) { + + try { + String s = JSONObject.toJSONString(msg); + ByteBuf byteBuf = Unpooled.directBuffer(8+s.length()); + byte[] bytes = s.getBytes(); + byteBuf.writeInt(msg.getCommand()); + byteBuf.writeInt(bytes.length); + byteBuf.writeBytes(bytes); + out.add(new BinaryWebSocketFrame(byteBuf)); + }catch (Exception e){ + e.printStackTrace(); + } + + } +} \ No newline at end of file diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/config/BootstrapConfig.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/config/BootstrapConfig.java new file mode 100644 index 0000000..9bf6465 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/config/BootstrapConfig.java @@ -0,0 +1,146 @@ +package com.lld.im.codec.config; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: Chackylee + * @description: + **/ +@Data +public class BootstrapConfig { + + private TcpConfig lim; + + + @Data + public static class TcpConfig { + private Integer tcpPort;// tcp 绑定的端口号 + + private Integer webSocketPort; // webSocket 绑定的端口号 + + private boolean enableWebSocket; //是否启用webSocket + + private Integer bossThreadSize; // boss线程 默认=1 + + private Integer workThreadSize; //work线程 + + private Long heartBeatTime; //心跳超时时间 单位毫秒 + + private Integer loginModel; + + /** + * redis配置 + */ + private RedisConfig redis; + + /** + * rabbitmq配置 + */ + private Rabbitmq rabbitmq; + + /** + * zk配置 + */ + private ZkConfig zkConfig; + + /** + * brokerId + */ + private Integer brokerId; + + private String logicUrl; + + } + + @Data + public static class ZkConfig { + /** + * zk连接地址 + */ + private String zkAddr; + + /** + * zk连接超时时间 + */ + private Integer zkConnectTimeOut; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class RedisConfig { + + /** + * 单机模式:single 哨兵模式:sentinel 集群模式:cluster + */ + private String mode; + /** + * 数据库 + */ + private Integer database; + /** + * 密码 + */ + private String password; + /** + * 超时时间 + */ + private Integer timeout; + /** + * 最小空闲数 + */ + private Integer poolMinIdle; + /** + * 连接超时时间(毫秒) + */ + private Integer poolConnTimeout; + /** + * 连接池大小 + */ + private Integer poolSize; + + /** + * redis单机配置 + */ + private RedisSingle single; + + } + + /** + * redis单机配置 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class RedisSingle { + /** + * 地址 + */ + private String address; + } + + /** + * rabbitmq哨兵模式配置 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Rabbitmq { + private String host; + + private Integer port; + + private String virtualHost; + + private String userName; + + private String password; + } + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/LoginPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/LoginPack.java new file mode 100644 index 0000000..8198fb8 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/LoginPack.java @@ -0,0 +1,15 @@ +package com.lld.im.codec.pack; + +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class LoginPack { + + private String userId; + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/conversation/DeleteConversationPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/conversation/DeleteConversationPack.java new file mode 100644 index 0000000..dd9d3ca --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/conversation/DeleteConversationPack.java @@ -0,0 +1,15 @@ +package com.lld.im.codec.pack.conversation; + +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class DeleteConversationPack { + + private String conversationId; + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/conversation/UpdateConversationPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/conversation/UpdateConversationPack.java new file mode 100644 index 0000000..f3a1cb2 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/conversation/UpdateConversationPack.java @@ -0,0 +1,23 @@ +package com.lld.im.codec.pack.conversation; + +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class UpdateConversationPack { + + private String conversationId; + + private Integer isMute; + + private Integer isTop; + + private Integer conversationType; + + private Long sequence; + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/AddFriendBlackPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/AddFriendBlackPack.java new file mode 100644 index 0000000..9319b68 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/AddFriendBlackPack.java @@ -0,0 +1,16 @@ +package com.lld.im.codec.pack.friendship; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 用户添加黑名单以后tcp通知数据包 + **/ +@Data +public class AddFriendBlackPack { + private String fromId; + + private String toId; + + private Long sequence; +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/AddFriendGroupMemberPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/AddFriendGroupMemberPack.java new file mode 100644 index 0000000..bbd02f8 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/AddFriendGroupMemberPack.java @@ -0,0 +1,22 @@ +package com.lld.im.codec.pack.friendship; + +import lombok.Data; + +import java.util.List; + +/** + * @author: Chackylee + * @description: 好友分组添加成员通知包 + **/ +@Data +public class AddFriendGroupMemberPack { + + public String fromId; + + private String groupName; + + private List toIds; + + /** 序列号*/ + private Long sequence; +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/AddFriendGroupPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/AddFriendGroupPack.java new file mode 100644 index 0000000..e073e73 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/AddFriendGroupPack.java @@ -0,0 +1,17 @@ +package com.lld.im.codec.pack.friendship; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 用户创建好友分组通知包 + **/ +@Data +public class AddFriendGroupPack { + public String fromId; + + private String groupName; + + /** 序列号*/ + private Long sequence; +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/AddFriendPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/AddFriendPack.java new file mode 100644 index 0000000..53e43a2 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/AddFriendPack.java @@ -0,0 +1,28 @@ +package com.lld.im.codec.pack.friendship; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 添加好友通知报文 + **/ +@Data +public class AddFriendPack { + private String fromId; + + /** + * 备注 + */ + private String remark; + private String toId; + /** + * 好友来源 + */ + private String addSource; + /** + * 添加好友时的描述信息(用于打招呼) + */ + private String addWording; + + private Long sequence; +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/ApproverFriendRequestPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/ApproverFriendRequestPack.java new file mode 100644 index 0000000..ba2b2d7 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/ApproverFriendRequestPack.java @@ -0,0 +1,18 @@ +package com.lld.im.codec.pack.friendship; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 审批好友申请通知报文 + **/ +@Data +public class ApproverFriendRequestPack { + + private Long id; + + //1同意 2拒绝 + private Integer status; + + private Long sequence; +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/DeleteAllFriendPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/DeleteAllFriendPack.java new file mode 100644 index 0000000..c56b923 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/DeleteAllFriendPack.java @@ -0,0 +1,14 @@ +package com.lld.im.codec.pack.friendship; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 删除黑名单通知报文 + **/ +@Data +public class DeleteAllFriendPack { + + private String fromId; + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/DeleteBlackPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/DeleteBlackPack.java new file mode 100644 index 0000000..b9b76ec --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/DeleteBlackPack.java @@ -0,0 +1,17 @@ +package com.lld.im.codec.pack.friendship; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 删除黑名单通知报文 + **/ +@Data +public class DeleteBlackPack { + + private String fromId; + + private String toId; + + private Long sequence; +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/DeleteFriendGroupMemberPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/DeleteFriendGroupMemberPack.java new file mode 100644 index 0000000..ea346df --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/DeleteFriendGroupMemberPack.java @@ -0,0 +1,22 @@ +package com.lld.im.codec.pack.friendship; + +import lombok.Data; + +import java.util.List; + +/** + * @author: Chackylee + * @description: 删除好友分组成员通知报文 + **/ +@Data +public class DeleteFriendGroupMemberPack { + + public String fromId; + + private String groupName; + + private List toIds; + + /** 序列号*/ + private Long sequence; +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/DeleteFriendGroupPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/DeleteFriendGroupPack.java new file mode 100644 index 0000000..b07a9f6 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/DeleteFriendGroupPack.java @@ -0,0 +1,17 @@ +package com.lld.im.codec.pack.friendship; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 删除好友分组通知报文 + **/ +@Data +public class DeleteFriendGroupPack { + public String fromId; + + private String groupName; + + /** 序列号*/ + private Long sequence; +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/DeleteFriendPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/DeleteFriendPack.java new file mode 100644 index 0000000..f5d542d --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/DeleteFriendPack.java @@ -0,0 +1,17 @@ +package com.lld.im.codec.pack.friendship; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 删除好友通知报文 + **/ +@Data +public class DeleteFriendPack { + + private String fromId; + + private String toId; + + private Long sequence; +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/ReadAllFriendRequestPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/ReadAllFriendRequestPack.java new file mode 100644 index 0000000..eeb3bf2 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/ReadAllFriendRequestPack.java @@ -0,0 +1,15 @@ +package com.lld.im.codec.pack.friendship; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 已读好友申请通知报文 + **/ +@Data +public class ReadAllFriendRequestPack { + + private String fromId; + + private Long sequence; +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/UpdateFriendPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/UpdateFriendPack.java new file mode 100644 index 0000000..3989bf8 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/friendship/UpdateFriendPack.java @@ -0,0 +1,20 @@ +package com.lld.im.codec.pack.friendship; + +import lombok.Data; + + +/** + * @author: Chackylee + * @description: 修改好友通知报文 + **/ +@Data +public class UpdateFriendPack { + + public String fromId; + + private String toId; + + private String remark; + + private Long sequence; +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/AddGroupMemberPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/AddGroupMemberPack.java new file mode 100644 index 0000000..211d696 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/AddGroupMemberPack.java @@ -0,0 +1,18 @@ +package com.lld.im.codec.pack.group; + +import lombok.Data; + +import java.util.List; + +/** + * @author: Chackylee + * @description: 群内添加群成员通知报文 + **/ +@Data +public class AddGroupMemberPack { + + private String groupId; + + private List members; + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/CreateGroupPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/CreateGroupPack.java new file mode 100644 index 0000000..5803607 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/CreateGroupPack.java @@ -0,0 +1,48 @@ +package com.lld.im.codec.pack.group; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 创建群组通知报文 + **/ +@Data +public class CreateGroupPack { + + private String groupId; + + private Integer appId; + + //群主id + private String ownerId; + + //群类型 1私有群(类似微信) 2公开群(类似qq) + private Integer groupType; + + private String groupName; + + private Integer mute;// 是否全员禁言,0 不禁言;1 全员禁言。 + + // 申请加群选项包括如下几种: +// 0 表示禁止任何人申请加入 +// 1 表示需要群主或管理员审批 +// 2 表示允许无需审批自由加入群组 + private Integer applyJoinType; + + private Integer privateChat; //是否禁止私聊,0 允许群成员发起私聊;1 不允许群成员发起私聊。 + + private String introduction;//群简介 + + private String notification;//群公告 + + private String photo;//群头像 + + private Integer status;//群状态 0正常 1解散 + + private Long sequence; + + private Long createTime; + + private String extra; + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/DestroyGroupPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/DestroyGroupPack.java new file mode 100644 index 0000000..7884c9f --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/DestroyGroupPack.java @@ -0,0 +1,16 @@ +package com.lld.im.codec.pack.group; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 解散群通知报文 + **/ +@Data +public class DestroyGroupPack { + + private String groupId; + + private Long sequence; + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/GroupMemberSpeakPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/GroupMemberSpeakPack.java new file mode 100644 index 0000000..f4a2550 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/GroupMemberSpeakPack.java @@ -0,0 +1,18 @@ +package com.lld.im.codec.pack.group; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 群成员禁言通知报文 + **/ +@Data +public class GroupMemberSpeakPack { + + private String groupId; + + private String memberId; + + private Long speakDate; + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/GroupMessagePack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/GroupMessagePack.java new file mode 100644 index 0000000..be5542b --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/GroupMessagePack.java @@ -0,0 +1,37 @@ +package com.lld.im.codec.pack.group; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 群聊消息分发报文 + **/ +@Data +public class GroupMessagePack { + + //客户端传的messageId + private String messageId; + + private String messageKey; + + private String fromId; + + private String groupId; + + private int messageRandom; + + private long messageTime; + + private long messageSequence; + + private String messageBody; + /** + * 这个字段缺省或者为 0 表示需要计数,为 1 表示本条消息不需要计数,即右上角图标数字不增加 + */ + private int badgeMode; + + private Long messageLifeTime; + + private Integer appId; + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/MuteGroupPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/MuteGroupPack.java new file mode 100644 index 0000000..5723adb --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/MuteGroupPack.java @@ -0,0 +1,14 @@ +package com.lld.im.codec.pack.group; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 禁言群tcp通知 + **/ +@Data +public class MuteGroupPack { + + private String groupId; + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/RemoveGroupMemberPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/RemoveGroupMemberPack.java new file mode 100644 index 0000000..877a444 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/RemoveGroupMemberPack.java @@ -0,0 +1,16 @@ +package com.lld.im.codec.pack.group; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 踢人出群通知报文 + **/ +@Data +public class RemoveGroupMemberPack { + + private String groupId; + + private String member; + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/TransferGroupPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/TransferGroupPack.java new file mode 100644 index 0000000..08e2ef5 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/TransferGroupPack.java @@ -0,0 +1,16 @@ +package com.lld.im.codec.pack.group; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 转让群主通知报文 + **/ +@Data +public class TransferGroupPack { + + private String groupId; + + private String ownerId; + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/UpdateGroupInfoPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/UpdateGroupInfoPack.java new file mode 100644 index 0000000..ac06f09 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/UpdateGroupInfoPack.java @@ -0,0 +1,29 @@ +package com.lld.im.codec.pack.group; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 修改群信息通知报文 + **/ +@Data +public class UpdateGroupInfoPack { + + private String groupId; + + private String groupName; + + private Integer mute;// 是否全员禁言,0 不禁言;1 全员禁言。 + + private Integer joinType;//加入群权限,0 所有人可以加入;1 群成员可以拉人;2 群管理员或群组可以拉人。 + + private String introduction;//群简介 + + private String notification;//群公告 + + private String photo;//群头像 + + private Integer maxMemberCount;//群成员上限 + + private Long sequence; +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/UpdateGroupMemberPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/UpdateGroupMemberPack.java new file mode 100644 index 0000000..59852f6 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/group/UpdateGroupMemberPack.java @@ -0,0 +1,19 @@ +package com.lld.im.codec.pack.group; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: 修改群成员通知报文 + **/ +@Data +public class UpdateGroupMemberPack { + + private String groupId; + + private String memberId; + + private String alias; + + private String extra; +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/message/ChatMessageAck.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/message/ChatMessageAck.java new file mode 100644 index 0000000..0f2df7d --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/message/ChatMessageAck.java @@ -0,0 +1,25 @@ +package com.lld.im.codec.pack.message; + +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class ChatMessageAck { + + private String messageId; + private Long messageSequence; + + public ChatMessageAck(String messageId) { + this.messageId = messageId; + } + + public ChatMessageAck(String messageId,Long messageSequence) { + this.messageId = messageId; + this.messageSequence = messageSequence; + } + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/message/MessageReadedPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/message/MessageReadedPack.java new file mode 100644 index 0000000..71fd66b --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/message/MessageReadedPack.java @@ -0,0 +1,22 @@ +package com.lld.im.codec.pack.message; + +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class MessageReadedPack { + + private long messageSequence; + + private String fromId; + + private String groupId; + + private String toId; + + private Integer conversationType; +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/message/MessageReciveServerAckPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/message/MessageReciveServerAckPack.java new file mode 100644 index 0000000..0d55aae --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/message/MessageReciveServerAckPack.java @@ -0,0 +1,22 @@ +package com.lld.im.codec.pack.message; + +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class MessageReciveServerAckPack { + + private Long messageKey; + + private String fromId; + + private String toId; + + private Long messageSequence; + + private Boolean serverSend; +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/message/RecallMessageNotifyPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/message/RecallMessageNotifyPack.java new file mode 100644 index 0000000..330af07 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/message/RecallMessageNotifyPack.java @@ -0,0 +1,21 @@ +package com.lld.im.codec.pack.message; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: Chackylee + * @description: 撤回消息通知报文 + **/ +@Data +@NoArgsConstructor +public class RecallMessageNotifyPack { + + private String fromId; + + private String toId; + + private Long messageKey; + + private Long messageSequence; +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/user/LoginAckPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/user/LoginAckPack.java new file mode 100644 index 0000000..505682c --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/user/LoginAckPack.java @@ -0,0 +1,15 @@ +package com.lld.im.codec.pack.user; + +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class LoginAckPack { + + private String userId; + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/user/UserCustomStatusChangeNotifyPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/user/UserCustomStatusChangeNotifyPack.java new file mode 100644 index 0000000..6172baf --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/user/UserCustomStatusChangeNotifyPack.java @@ -0,0 +1,22 @@ +package com.lld.im.codec.pack.user; + +import com.lld.im.common.model.UserSession; +import lombok.Data; + +import java.util.List; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class UserCustomStatusChangeNotifyPack { + + private String customText; + + private Integer customStatus; + + private String userId; + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/user/UserModifyPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/user/UserModifyPack.java new file mode 100644 index 0000000..78bb4d7 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/user/UserModifyPack.java @@ -0,0 +1,32 @@ +package com.lld.im.codec.pack.user; + +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class UserModifyPack { + // 用户id + private String userId; + + // 用户名称 + private String nickName; + + private String password; + + // 头像 + private String photo; + + // 性别 + private String userSex; + + // 个性签名 + private String selfSignature; + + // 加好友验证类型(Friend_AllowType) 1需要验证 + private Integer friendAllowType; + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/user/UserStatusChangeNotifyPack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/user/UserStatusChangeNotifyPack.java new file mode 100644 index 0000000..7b95fd2 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/pack/user/UserStatusChangeNotifyPack.java @@ -0,0 +1,25 @@ +package com.lld.im.codec.pack.user; + +import com.lld.im.common.model.UserSession; +import lombok.Data; +import sun.dc.pr.PRError; + +import java.util.List; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class UserStatusChangeNotifyPack { + + private Integer appId; + + private String userId; + + private Integer status; + + private List client; + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/proto/Message.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/proto/Message.java new file mode 100644 index 0000000..9a30dec --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/proto/Message.java @@ -0,0 +1,24 @@ +package com.lld.im.codec.proto; + +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class Message { + + private MessageHeader messageHeader; + + private Object messagePack; + + @Override + public String toString() { + return "Message{" + + "messageHeader=" + messageHeader + + ", messagePack=" + messagePack + + '}'; + } +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/proto/MessageHeader.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/proto/MessageHeader.java new file mode 100644 index 0000000..7966cd4 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/proto/MessageHeader.java @@ -0,0 +1,39 @@ +package com.lld.im.codec.proto; + +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class MessageHeader { + + //消息操作指令 十六进制 一个消息的开始通常以0x开头 + //4字节 + private Integer command; + //4字节 版本号 + private Integer version; + //4字节 端类型 + private Integer clientType; + /** + * 应用ID + */ +// 4字节 appId + private Integer appId; + /** + * 数据解析类型 和具体业务无关,后续根据解析类型解析data数据 0x0:Json,0x1:ProtoBuf,0x2:Xml,默认:0x0 + */ + //4字节 解析类型 + private Integer messageType = 0x0; + + //4字节 imel长度 + private Integer imeiLength; + + //4字节 包体长度 + private int length; + + //imei号 + private String imei; +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/proto/MessagePack.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/proto/MessagePack.java new file mode 100644 index 0000000..99cf4b3 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/proto/MessagePack.java @@ -0,0 +1,48 @@ +package com.lld.im.codec.proto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @author: Chackylee + * @description: 消息服务发送给tcp的包体,tcp再根据改包体解析成Message发给客户端 + **/ +@Data +public class MessagePack implements Serializable { + + private String userId; + + private Integer appId; + + /** + * 接收方 + */ + private String toId; + + /** + * 客户端标识 + */ + private int clientType; + + /** + * 消息ID + */ + private String messageId; + + /** + * 客户端设备唯一标识 + */ + private String imei; + + private Integer command; + + /** + * 业务数据对象,如果是聊天消息则不需要解析直接透传 + */ + private T data; + +// /** 用户签名*/ +// private String userSign; + +} diff --git a/hs-im-server/im-codec/src/main/java/com/lld/im/codec/utils/ByteBufToMessageUtils.java b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/utils/ByteBufToMessageUtils.java new file mode 100644 index 0000000..fb0eea9 --- /dev/null +++ b/hs-im-server/im-codec/src/main/java/com/lld/im/codec/utils/ByteBufToMessageUtils.java @@ -0,0 +1,84 @@ +package com.lld.im.codec.utils; + +import com.alibaba.fastjson.JSONObject; +import com.lld.im.codec.proto.Message; +import com.lld.im.codec.proto.MessageHeader; +import io.netty.buffer.ByteBuf; + +/** + * @author: rowger + * @description: 将ByteBuf转化为Message实体,根据私有协议转换 + * 私有协议规则, + * 4位表示Command表示消息的开始, + * 4位表示version + * 4位表示clientType + * 4位表示messageType + * 4位表示appId + * 4位表示imei长度 + * imei + * 4位表示数据长度 + * data + * 后续将解码方式加到数据头根据不同的解码方式解码,如pb,json,现在用json字符串 + * @version: 1.0 + */ +public class ByteBufToMessageUtils { + + public static Message transition(ByteBuf in){ + + /** 获取command*/ + int command = in.readInt(); + + /** 获取version*/ + int version = in.readInt(); + + /** 获取clientType*/ + int clientType = in.readInt(); + + /** 获取clientType*/ + int messageType = in.readInt(); + + /** 获取appId*/ + int appId = in.readInt(); + + /** 获取imeiLength*/ + int imeiLength = in.readInt(); + + /** 获取bodyLen*/ + int bodyLen = in.readInt(); + + if(in.readableBytes() < bodyLen + imeiLength){ + in.resetReaderIndex(); + return null; + } + + byte [] imeiData = new byte[imeiLength]; + in.readBytes(imeiData); + String imei = new String(imeiData); + + byte [] bodyData = new byte[bodyLen]; + in.readBytes(bodyData); + + + MessageHeader messageHeader = new MessageHeader(); + messageHeader.setAppId(appId); + messageHeader.setClientType(clientType); + messageHeader.setCommand(command); + messageHeader.setLength(bodyLen); + messageHeader.setVersion(version); + messageHeader.setMessageType(messageType); + messageHeader.setImei(imei); + + Message message = new Message(); + message.setMessageHeader(messageHeader); + + if(messageType == 0x0){ + String body = new String(bodyData); + JSONObject parse = (JSONObject) JSONObject.parse(body); + message.setMessagePack(parse); + } + + in.markReaderIndex(); + return message; + } + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/config/AppConfig.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/config/AppConfig.java new file mode 100644 index 0000000..661a3b5 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/config/AppConfig.java @@ -0,0 +1,70 @@ +package com.lld.im.common.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @author: Chackylee + * @description: + **/ +@Data +@Component +@ConfigurationProperties(prefix = "appconfig") +public class AppConfig { + + private String privateKey; + + /** zk连接地址*/ + private String zkAddr; + + /** zk连接超时时间*/ + private Integer zkConnectTimeOut; + + /** im管道地址路由策略*/ + private Integer imRouteWay; + + private boolean sendMessageCheckFriend; //发送消息是否校验关系链 + + private boolean sendMessageCheckBlack; //发送消息是否校验黑名单 + + /** 如果选用一致性hash的话具体hash算法*/ + private Integer consistentHashWay; + + private String callbackUrl; + + private boolean modifyUserAfterCallback; //用户资料变更之后回调开关 + + private boolean addFriendAfterCallback; //添加好友之后回调开关 + + private boolean addFriendBeforeCallback; //添加好友之前回调开关 + + private boolean modifyFriendAfterCallback; //修改好友之后回调开关 + + private boolean deleteFriendAfterCallback; //删除好友之后回调开关 + + private boolean addFriendShipBlackAfterCallback; //添加黑名单之后回调开关 + + private boolean deleteFriendShipBlackAfterCallback; //删除黑名单之后回调开关 + + private boolean createGroupAfterCallback; //创建群聊之后回调开关 + + private boolean modifyGroupAfterCallback; //修改群聊之后回调开关 + + private boolean destroyGroupAfterCallback;//解散群聊之后回调开关 + + private boolean deleteGroupMemberAfterCallback;//删除群成员之后回调 + + private boolean addGroupMemberBeforeCallback;//拉人入群之前回调 + + private boolean addGroupMemberAfterCallback;//拉人入群之后回调 + + private boolean sendMessageAfterCallback;//发送单聊消息之后 + + private boolean sendMessageBeforeCallback;//发送单聊消息之前 + + private Integer deleteConversationSyncMode; + + private Integer offlineMessageCount;//离线消息最大条数 + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/config/GlobalHttpClientConfig.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/config/GlobalHttpClientConfig.java new file mode 100644 index 0000000..6946345 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/config/GlobalHttpClientConfig.java @@ -0,0 +1,150 @@ +package com.lld.im.common.config; + +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "httpclient") +public class GlobalHttpClientConfig { + private Integer maxTotal; // 最大连接数 + private Integer defaultMaxPerRoute; // 最大并发链接数 + private Integer connectTimeout; // 创建链接的最大时间 + private Integer connectionRequestTimeout; // 链接获取超时时间 + private Integer socketTimeout; // 数据传输最长时间 + private boolean staleConnectionCheckEnabled; // 提交时检查链接是否可用 + + PoolingHttpClientConnectionManager manager = null; + HttpClientBuilder httpClientBuilder = null; + + // 定义httpClient链接池 + @Bean(name = "httpClientConnectionManager") + public PoolingHttpClientConnectionManager getPoolingHttpClientConnectionManager() { + return getManager(); + } + + private PoolingHttpClientConnectionManager getManager() { + if (manager != null) { + return manager; + } + manager = new PoolingHttpClientConnectionManager(); + manager.setMaxTotal(maxTotal); // 设定最大链接数 + manager.setDefaultMaxPerRoute(defaultMaxPerRoute); // 设定并发链接数 + return manager; + } + + /** + * 实例化连接池,设置连接池管理器。 这里需要以参数形式注入上面实例化的连接池管理器 + * + * @Qualifier 指定bean标签进行注入 + */ + @Bean(name = "httpClientBuilder") + public HttpClientBuilder getHttpClientBuilder( + @Qualifier("httpClientConnectionManager") PoolingHttpClientConnectionManager httpClientConnectionManager) { + + // HttpClientBuilder中的构造方法被protected修饰,所以这里不能直接使用new来实例化一个HttpClientBuilder,可以使用HttpClientBuilder提供的静态方法create()来获取HttpClientBuilder对象 + httpClientBuilder = HttpClientBuilder.create(); + httpClientBuilder.setConnectionManager(httpClientConnectionManager); + return httpClientBuilder; + } + + + /** + * 注入连接池,用于获取httpClient + * + * @param httpClientBuilder + * @return + */ + @Bean + public CloseableHttpClient getCloseableHttpClient( + @Qualifier("httpClientBuilder") HttpClientBuilder httpClientBuilder) { + + return httpClientBuilder.build(); + } + + public CloseableHttpClient getCloseableHttpClient() { + if (httpClientBuilder != null) { + return httpClientBuilder.build(); + } + httpClientBuilder = HttpClientBuilder.create(); + httpClientBuilder.setConnectionManager(getManager()); + return httpClientBuilder.build(); + } + + /** + * Builder是RequestConfig的一个内部类 通过RequestConfig的custom方法来获取到一个Builder对象 + * 设置builder的连接信息 + * + * @return + */ + @Bean(name = "builder") + public RequestConfig.Builder getBuilder() { + RequestConfig.Builder builder = RequestConfig.custom(); + return builder.setConnectTimeout(connectTimeout).setConnectionRequestTimeout(connectionRequestTimeout) + .setSocketTimeout(socketTimeout).setStaleConnectionCheckEnabled(staleConnectionCheckEnabled); + } + + /** + * 使用builder构建一个RequestConfig对象 + * + * @param builder + * @return + */ + @Bean + public RequestConfig getRequestConfig(@Qualifier("builder") RequestConfig.Builder builder) { + return builder.build(); + } + + public Integer getMaxTotal() { + return maxTotal; + } + + public void setMaxTotal(Integer maxTotal) { + this.maxTotal = maxTotal; + } + + public Integer getDefaultMaxPerRoute() { + return defaultMaxPerRoute; + } + + public void setDefaultMaxPerRoute(Integer defaultMaxPerRoute) { + this.defaultMaxPerRoute = defaultMaxPerRoute; + } + + public Integer getConnectTimeout() { + return connectTimeout; + } + + public void setConnectTimeout(Integer connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public Integer getConnectionRequestTimeout() { + return connectionRequestTimeout; + } + + public void setConnectionRequestTimeout(Integer connectionRequestTimeout) { + this.connectionRequestTimeout = connectionRequestTimeout; + } + + public Integer getSocketTimeout() { + return socketTimeout; + } + + public void setSocketTimeout(Integer socketTimeout) { + this.socketTimeout = socketTimeout; + } + + public boolean isStaleConnectionCheckEnabled() { + return staleConnectionCheckEnabled; + } + + public void setStaleConnectionCheckEnabled(boolean staleConnectionCheckEnabled) { + this.staleConnectionCheckEnabled = staleConnectionCheckEnabled; + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/constant/Constants.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/constant/Constants.java new file mode 100644 index 0000000..0edf1eb --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/constant/Constants.java @@ -0,0 +1,155 @@ +package com.lld.im.common.constant; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +public class Constants { + + /** channel绑定的userId Key*/ + public static final String UserId = "userId"; + + /** channel绑定的appId */ + public static final String AppId = "appId"; + + public static final String ClientType = "clientType"; + + public static final String Imei = "imei"; + + /** channel绑定的clientType 和 imel Key*/ + public static final String ClientImei = "clientImei"; + + public static final String ReadTime = "readTime"; + + public static final String ImCoreZkRoot = "/im-coreRoot"; + + public static final String ImCoreZkRootTcp = "/tcp"; + + public static final String ImCoreZkRootWeb = "/web"; + + + public static class RedisConstants{ + + /** + * userSign,格式:appId:userSign: + */ + public static final String userSign = "userSign"; + + /** + * 用户上线通知channel + */ + public static final String UserLoginChannel + = "signal/channel/LOGIN_USER_INNER_QUEUE"; + + + /** + * 用户session,appId + UserSessionConstants + 用户id 例如10000:userSession:lld + */ + public static final String UserSessionConstants = ":userSession:"; + + /** + * 缓存客户端消息防重,格式: appId + :cacheMessage: + messageId + */ + public static final String cacheMessage = "cacheMessage"; + + public static final String OfflineMessage = "offlineMessage"; + + /** + * seq 前缀 + */ + public static final String SeqPrefix = "seq"; + + /** + * 用户订阅列表,格式 :appId + :subscribe: + userId。Hash结构,filed为订阅自己的人 + */ + public static final String subscribe = "subscribe"; + + /** + * 用户自定义在线状态,格式 :appId + :userCustomerStatus: + userId。set,value为用户id + */ + public static final String userCustomerStatus = "userCustomerStatus"; + + } + + public static class RabbitConstants{ + + public static final String Im2UserService = "pipeline2UserService"; + + public static final String Im2MessageService = "pipeline2MessageService"; + + public static final String Im2GroupService = "pipeline2GroupService"; + + public static final String Im2FriendshipService = "pipeline2FriendshipService"; + + public static final String MessageService2Im = "messageService2Pipeline"; + + public static final String GroupService2Im = "GroupService2Pipeline"; + + public static final String FriendShip2Im = "friendShip2Pipeline"; + + public static final String StoreP2PMessage = "storeP2PMessage"; + + public static final String StoreGroupMessage = "storeGroupMessage"; + + + } + + public static class CallbackCommand{ + public static final String ModifyUserAfter = "user.modify.after"; + + public static final String CreateGroupAfter = "group.create.after"; + + public static final String UpdateGroupAfter = "group.update.after"; + + public static final String DestoryGroupAfter = "group.destory.after"; + + public static final String TransferGroupAfter = "group.transfer.after"; + + public static final String GroupMemberAddBefore = "group.member.add.before"; + + public static final String GroupMemberAddAfter = "group.member.add.after"; + + public static final String GroupMemberDeleteAfter = "group.member.delete.after"; + + public static final String AddFriendBefore = "friend.add.before"; + + public static final String AddFriendAfter = "friend.add.after"; + + public static final String UpdateFriendBefore = "friend.update.before"; + + public static final String UpdateFriendAfter = "friend.update.after"; + + public static final String DeleteFriendAfter = "friend.delete.after"; + + public static final String AddBlackAfter = "black.add.after"; + + public static final String DeleteBlack = "black.delete"; + + public static final String SendMessageAfter = "message.send.after"; + + public static final String SendMessageBefore = "message.send.before"; + + } + + public static class SeqConstants { + public static final String Message = "messageSeq"; + + public static final String GroupMessage = "groupMessageSeq"; + + + public static final String Friendship = "friendshipSeq"; + +// public static final String FriendshipBlack = "friendshipBlackSeq"; + + public static final String FriendshipRequest = "friendshipRequestSeq"; + + public static final String FriendshipGroup = "friendshipGrouptSeq"; + + public static final String Group = "groupSeq"; + + public static final String Conversation = "conversationSeq"; + + } + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/ConversationErrorCode.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/ConversationErrorCode.java new file mode 100644 index 0000000..e2ff7db --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/ConversationErrorCode.java @@ -0,0 +1,31 @@ +package com.lld.im.common.enums; + +import com.lld.im.common.exception.ApplicationExceptionEnum; + +/** + * @author: Chackylee + * @description: + **/ +public enum ConversationErrorCode implements ApplicationExceptionEnum { + + CONVERSATION_UPDATE_PARAM_ERROR(50000,"會話修改參數錯誤"), + + + ; + + private int code; + private String error; + + ConversationErrorCode(int code, String error){ + this.code = code; + this.error = error; + } + public int getCode() { + return this.code; + } + + public String getError() { + return this.error; + } + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/ConversationTypeEnum.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/ConversationTypeEnum.java new file mode 100644 index 0000000..764308b --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/ConversationTypeEnum.java @@ -0,0 +1,24 @@ +package com.lld.im.common.enums; + +public enum ConversationTypeEnum { + + /** + * 0 单聊 1群聊 2机器人 3公众号 + */ + P2P(0), + + GROUP(1), + + ROBOT(2), + ; + + private int code; + + ConversationTypeEnum(int code){ + this.code=code; + } + + public int getCode() { + return code; + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/DeviceMultiLoginEnum.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/DeviceMultiLoginEnum.java new file mode 100644 index 0000000..7aa983a --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/DeviceMultiLoginEnum.java @@ -0,0 +1,63 @@ +package com.lld.im.common.enums; + +public enum DeviceMultiLoginEnum { + + /** + * 单端登录 仅允许 Windows、Web、Android 或 iOS 单端登录。 + */ + ONE(1,"DeviceMultiLoginEnum_ONE"), + + /** + * 双端登录 允许 Windows、Mac、Android 或 iOS 单端登录,同时允许与 Web 端同时在线。 + */ + TWO(2,"DeviceMultiLoginEnum_TWO"), + + /** + * 三端登录 允许 Android 或 iOS 单端登录(互斥),Windows 或者 Mac 单聊登录(互斥),同时允许 Web 端同时在线 + */ + THREE(3,"DeviceMultiLoginEnum_THREE"), + + /** + * 多端同时在线 允许 Windows、Mac、Web、Android 或 iOS 多端或全端同时在线登录 + */ + ALL(4,"DeviceMultiLoginEnum_ALL"); + + private int loginMode; + private String loginDesc; + + /** + * 不能用 默认的 enumType b= enumType.values()[i]; 因为本枚举是类形式封装 + * @param ordinal + * @return + */ + public static DeviceMultiLoginEnum getMember(int ordinal) { + for (int i = 0; i < DeviceMultiLoginEnum.values().length; i++) { + if (DeviceMultiLoginEnum.values()[i].getLoginMode() == ordinal) { + return DeviceMultiLoginEnum.values()[i]; + } + } + return THREE; + } + + DeviceMultiLoginEnum(int loginMode, String loginDesc){ + this.loginMode=loginMode; + this.loginDesc=loginDesc; + } + + public int getLoginMode() { + return loginMode; + } + + public void setLoginMode(int loginMode) { + this.loginMode = loginMode; + } + + public String getLoginDesc() { + return loginDesc; + } + + public void setLoginDesc(String loginDesc) { + this.loginDesc = loginDesc; + } + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/GateWayErrorCode.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/GateWayErrorCode.java new file mode 100644 index 0000000..1b9057c --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/GateWayErrorCode.java @@ -0,0 +1,40 @@ +package com.lld.im.common.enums; + +import com.lld.im.common.exception.ApplicationExceptionEnum; + +/** + * @author: Chackylee + * @description: 6 + **/ +public enum GateWayErrorCode implements ApplicationExceptionEnum { + + USERSIGN_NOT_EXIST(60000,"用户签名不存在"), + + APPID_NOT_EXIST(60001,"appId不存在"), + + OPERATER_NOT_EXIST(60002,"操作人不存在"), + + USERSIGN_IS_ERROR(60003,"用户签名不正确"), + + USERSIGN_OPERATE_NOT_MATE(60005,"用户签名与操作人不匹配"), + + USERSIGN_IS_EXPIRED(60004,"用户签名已过期"), + + ; + + private int code; + private String error; + + GateWayErrorCode(int code, String error){ + this.code = code; + this.error = error; + } + public int getCode() { + return this.code; + } + + public String getError() { + return this.error; + } + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/ImConnectStatusEnum.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/ImConnectStatusEnum.java new file mode 100644 index 0000000..2039480 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/ImConnectStatusEnum.java @@ -0,0 +1,22 @@ +package com.lld.im.common.enums; + +public enum ImConnectStatusEnum { + + /** + * 管道链接状态,1=在线,2=离线。。 + */ + ONLINE_STATUS(1), + + OFFLINE_STATUS(2), + ; + + private Integer code; + + ImConnectStatusEnum(Integer code){ + this.code=code; + } + + public Integer getCode() { + return code; + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/ImUrlRouteWayEnum.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/ImUrlRouteWayEnum.java new file mode 100644 index 0000000..4e41c1a --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/ImUrlRouteWayEnum.java @@ -0,0 +1,52 @@ +package com.lld.im.common.enums; + +public enum ImUrlRouteWayEnum { + + /** + * 随机 + */ + RAMDOM(1,"com.lld.im.common.route.algorithm.random.RandomHandle"), + + + /** + * 1.轮训 + */ + LOOP(2,"com.lld.im.common.route.algorithm.loop.LoopHandle"), + + /** + * HASH + */ + HASH(3,"com.lld.im.common.route.algorithm.consistenthash.ConsistentHashHandle"), + ; + + + private int code; + private String clazz; + + /** + * 不能用 默认的 enumType b= enumType.values()[i]; 因为本枚举是类形式封装 + * @param ordinal + * @return + */ + public static ImUrlRouteWayEnum getHandler(int ordinal) { + for (int i = 0; i < ImUrlRouteWayEnum.values().length; i++) { + if (ImUrlRouteWayEnum.values()[i].getCode() == ordinal) { + return ImUrlRouteWayEnum.values()[i]; + } + } + return null; + } + + ImUrlRouteWayEnum(int code, String clazz){ + this.code=code; + this.clazz=clazz; + } + + public String getClazz() { + return clazz; + } + + public int getCode() { + return code; + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/ImUserTypeEnum.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/ImUserTypeEnum.java new file mode 100644 index 0000000..3d8a7f4 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/ImUserTypeEnum.java @@ -0,0 +1,19 @@ +package com.lld.im.common.enums; + +public enum ImUserTypeEnum { + + IM_USER(1), + + APP_ADMIN(100), + ; + + private int code; + + ImUserTypeEnum(int code){ + this.code=code; + } + + public int getCode() { + return code; + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/MessageErrorCode.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/MessageErrorCode.java new file mode 100644 index 0000000..18398fa --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/MessageErrorCode.java @@ -0,0 +1,36 @@ +package com.lld.im.common.enums; + +import com.lld.im.common.exception.ApplicationExceptionEnum; + +public enum MessageErrorCode implements ApplicationExceptionEnum { + + + FROMER_IS_MUTE(50002,"发送方被禁言"), + + FROMER_IS_FORBIBBEN(50003,"发送方被禁用"), + + + MESSAGEBODY_IS_NOT_EXIST(50003,"消息体不存在"), + + MESSAGE_RECALL_TIME_OUT(50004,"消息已超过可撤回时间"), + + MESSAGE_IS_RECALLED(50005,"消息已被撤回"), + + ; + + private int code; + private String error; + + MessageErrorCode(int code, String error){ + this.code = code; + this.error = error; + } + public int getCode() { + return this.code; + } + + public String getError() { + return this.error; + } + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/RouteHashMethodEnum.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/RouteHashMethodEnum.java new file mode 100644 index 0000000..7ce3e80 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/RouteHashMethodEnum.java @@ -0,0 +1,48 @@ +package com.lld.im.common.enums; + +public enum RouteHashMethodEnum { + + /** + * TreeMap + */ + TREE(1,"com.lld.im.common.route.algorithm.consistenthash" + + ".TreeMapConsistentHash"), + + /** + * 自定义map + */ + CUSTOMER(2,"com.lld.im.common.route.algorithm.consistenthash.xxxx"), + + ; + + + private int code; + private String clazz; + + /** + * 不能用 默认的 enumType b= enumType.values()[i]; 因为本枚举是类形式封装 + * @param ordinal + * @return + */ + public static RouteHashMethodEnum getHandler(int ordinal) { + for (int i = 0; i < RouteHashMethodEnum.values().length; i++) { + if (RouteHashMethodEnum.values()[i].getCode() == ordinal) { + return RouteHashMethodEnum.values()[i]; + } + } + return null; + } + + RouteHashMethodEnum(int code, String clazz){ + this.code=code; + this.clazz=clazz; + } + + public String getClazz() { + return clazz; + } + + public int getCode() { + return code; + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/Command.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/Command.java new file mode 100644 index 0000000..63e37a2 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/Command.java @@ -0,0 +1,10 @@ +package com.lld.im.common.enums.command; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +public interface Command { + public int getCommand(); +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/CommandType.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/CommandType.java new file mode 100644 index 0000000..d348a91 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/CommandType.java @@ -0,0 +1,35 @@ +package com.lld.im.common.enums.command; + + +public enum CommandType { + + USER("4"), + + FRIEND("3"), + + GROUP("2"), + + MESSAGE("1"), + + ; + + private String commandType; + + public String getCommandType() { + return commandType; + } + + CommandType(String commandType) { + this.commandType = commandType; + } + + public static CommandType getCommandType(String ordinal) { + for (int i = 0; i < CommandType.values().length; i++) { + if (CommandType.values()[i].getCommandType().equals(ordinal)) { + return CommandType.values()[i]; + } + } + return null; + } + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/ConversationEventCommand.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/ConversationEventCommand.java new file mode 100644 index 0000000..ea70137 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/ConversationEventCommand.java @@ -0,0 +1,24 @@ +package com.lld.im.common.enums.command; + +public enum ConversationEventCommand implements Command { + + //删除会话 + CONVERSATION_DELETE(5000), + + //删除会话 + CONVERSATION_UPDATE(5001), + + ; + + private int command; + + ConversationEventCommand(int command){ + this.command=command; + } + + + @Override + public int getCommand() { + return command; + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/FriendshipEventCommand.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/FriendshipEventCommand.java new file mode 100644 index 0000000..92c7360 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/FriendshipEventCommand.java @@ -0,0 +1,58 @@ +package com.lld.im.common.enums.command; + +public enum FriendshipEventCommand implements Command { + + //添加好友 + FRIEND_ADD(3000), + + //更新好友 + FRIEND_UPDATE(3001), + + //删除好友 + FRIEND_DELETE(3002), + + //好友申请 + FRIEND_REQUEST(3003), + + //好友申请已读 + FRIEND_REQUEST_READ(3004), + + //好友申请审批 + FRIEND_REQUEST_APPROVER(3005), + + //添加黑名单 + FRIEND_BLACK_ADD(3010), + + //移除黑名单 + FRIEND_BLACK_DELETE(3011), + + //新建好友分组 + FRIEND_GROUP_ADD(3012), + + //删除好友分组 + FRIEND_GROUP_DELETE(3013), + + //好友分组添加成员 + FRIEND_GROUP_MEMBER_ADD(3014), + + //好友分组移除成员 + FRIEND_GROUP_MEMBER_DELETE(3015), + + //删除所有好友 + FRIEND_ALL_DELETE(3016), + + + ; + + private int command; + + FriendshipEventCommand(int command){ + this.command=command; + } + + + @Override + public int getCommand() { + return command; + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/GroupEventCommand.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/GroupEventCommand.java new file mode 100644 index 0000000..993045e --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/GroupEventCommand.java @@ -0,0 +1,91 @@ +package com.lld.im.common.enums.command; + +//2 +public enum GroupEventCommand implements Command { + + + /** + * 推送申请入群通知 2023 + */ + JOIN_GROUP(2000), + + /** + * 推送添加群成员 2001,通知给所有管理员和本人 + */ + ADDED_MEMBER(2001), + + /** + * 推送创建群组通知 2002,通知给所有人 + */ + CREATED_GROUP(2002), + + /** + * 推送更新群组通知 2003,通知给所有人 + */ + UPDATED_GROUP(2003), + + /** + * 推送退出群组通知 2004,通知给管理员和操作人 + */ + EXIT_GROUP(2004), + + /** + * 推送修改群成员通知 2005,通知给管理员和被操作人 + */ + UPDATED_MEMBER(2005), + + /** + * 推送删除群成员通知 2006,通知给所有群成员和被踢人 + */ + DELETED_MEMBER(2006), + + /** + * 推送解散群通知 2007,通知所有人 + */ + DESTROY_GROUP(2007), + + /** + * 推送转让群主 2008,通知所有人 + */ + TRANSFER_GROUP(2008), + + /** + * 禁言群 2009,通知所有人 + */ + MUTE_GROUP(2009), + + + /** + * 禁言/解禁 群成员 2010,通知管理员和被操作人 + */ + SPEAK_GOUP_MEMBER(2010), + + //群聊消息收发 2104 + MSG_GROUP(0x838), + + //发送消息已读 2106 + MSG_GROUP_READED(0x83a), + + //消息已读通知给同步端 2053 + MSG_GROUP_READED_NOTIFY(0x805), + + //消息已读回执,给原消息发送方 2054 + MSG_GROUP_READED_RECEIPT(0x806), + + //群聊消息ack 2047 + GROUP_MSG_ACK(0x7ff), + + + ; + + private Integer command; + + GroupEventCommand(int command) { + this.command = command; + } + + + public int getCommand() { + return command; + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/MediaEventCommand.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/MediaEventCommand.java new file mode 100644 index 0000000..4b277b7 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/MediaEventCommand.java @@ -0,0 +1,46 @@ +package com.lld.im.common.enums.command; + +public enum MediaEventCommand implements Command { + + //6000 向对方拨打语音 notify ack + CALL_VOICE(6000), + + //6001 向对方拨打视频 notify ack + CALL_VIDEO(6001), + + //6002 同意请求 notify ack + ACCEPT_CALL(6002), + + //6003 同步ice +// TRANSMIT_ICE(6003), + +// //6004 发送offer +// TRANSMIT_OFFER(6004), + +// //6005 发送ANSWER +// TRANSMIT_ANSWER(6005), + + //6006 hangup 挂断 notify ack + HANG_UP(6006), + + //6007 拒绝 notify ack + REJECT_CALL(6007), + + //6008 取消呼叫 notify ack + CANCEL_CALL(6008), + + + ; + + private Integer command; + + MediaEventCommand(int command) { + this.command = command; + } + + + @Override + public int getCommand() { + return command; + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/MessageCommand.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/MessageCommand.java new file mode 100644 index 0000000..b8343ce --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/MessageCommand.java @@ -0,0 +1,45 @@ +package com.lld.im.common.enums.command; + +public enum MessageCommand implements Command { + + //单聊消息 1103 + MSG_P2P(0x44F), + + //单聊消息ACK 1046 + MSG_ACK(0x416), + + //消息收到ack 1107 + MSG_RECIVE_ACK(1107), + + //发送消息已读 1106 + MSG_READED(0x452), + + //消息已读通知给同步端 1053 + MSG_READED_NOTIFY(0x41D), + + //消息已读回执,给原消息发送方 1054 + MSG_READED_RECEIPT(0x41E), + + //消息撤回 1050 + MSG_RECALL(0x41A), + + //消息撤回通知 1052 + MSG_RECALL_NOTIFY(0x41C), + + //消息撤回回报 1051 + MSG_RECALL_ACK(0x41B), + + ; + + private int command; + + MessageCommand(int command){ + this.command=command; + } + + + @Override + public int getCommand() { + return command; + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/SystemCommand.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/SystemCommand.java new file mode 100644 index 0000000..00400ba --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/SystemCommand.java @@ -0,0 +1,35 @@ +package com.lld.im.common.enums.command; + +public enum SystemCommand implements Command { + + //心跳 9999 + PING(0x270f), + + /** + * 登录 9000 + */ + LOGIN(0x2328), + + //登录ack 9001 + LOGINACK(0x2329), + + //登出 9003 + LOGOUT(0x232b), + + //下线通知 用于多端互斥 9002 + MUTUALLOGIN(0x232a), + + ; + + private int command; + + SystemCommand(int command){ + this.command=command; + } + + + @Override + public int getCommand() { + return command; + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/UserEventCommand.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/UserEventCommand.java new file mode 100644 index 0000000..a2b304c --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/enums/command/UserEventCommand.java @@ -0,0 +1,32 @@ +package com.lld.im.common.enums.command; + +public enum UserEventCommand implements Command { + + //用户修改command 4000 + USER_MODIFY(4000), + + //4001 + USER_ONLINE_STATUS_CHANGE(4001), + + + //4004 用户在线状态通知报文 + USER_ONLINE_STATUS_CHANGE_NOTIFY(4004), + + //4005 用户在线状态通知同步报文 + USER_ONLINE_STATUS_CHANGE_NOTIFY_SYNC(4005), + + + ; + + private int command; + + UserEventCommand(int command){ + this.command=command; + } + + + @Override + public int getCommand() { + return command; + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/model/SyncReq.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/SyncReq.java new file mode 100644 index 0000000..45990a4 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/SyncReq.java @@ -0,0 +1,17 @@ +package com.lld.im.common.model; + +import lombok.Data; + +/** + * @author: Chackylee + * @description: + **/ +@Data +public class SyncReq extends RequestBase { + + //客户端最大seq + private Long lastSequence; + //一次拉取多少 + private Integer maxLimit; + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/model/SyncResp.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/SyncResp.java new file mode 100644 index 0000000..8337d8c --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/SyncResp.java @@ -0,0 +1,20 @@ +package com.lld.im.common.model; + +import lombok.Data; + +import java.util.List; + +/** + * @author: Chackylee + * @description: + **/ +@Data +public class SyncResp { + + private Long maxSequence; + + private boolean isCompleted; + + private List dataList; + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/CheckSendMessageReq.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/CheckSendMessageReq.java new file mode 100644 index 0000000..3227424 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/CheckSendMessageReq.java @@ -0,0 +1,22 @@ +package com.lld.im.common.model.message; + +import lombok.Data; +import sun.dc.pr.PRError; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class CheckSendMessageReq { + + private String fromId; + + private String toId; + + private Integer appId; + + private Integer command; + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/DoStoreGroupMessageDto.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/DoStoreGroupMessageDto.java new file mode 100644 index 0000000..ca12468 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/DoStoreGroupMessageDto.java @@ -0,0 +1,17 @@ +package com.lld.im.common.model.message; + +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class DoStoreGroupMessageDto { + + private GroupChatMessageContent groupChatMessageContent; + + private ImMessageBody messageBody; + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/DoStoreP2PMessageDto.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/DoStoreP2PMessageDto.java new file mode 100644 index 0000000..1786178 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/DoStoreP2PMessageDto.java @@ -0,0 +1,17 @@ +package com.lld.im.common.model.message; + +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class DoStoreP2PMessageDto { + + private MessageContent messageContent; + + private ImMessageBody messageBody; + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/GroupChatMessageContent.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/GroupChatMessageContent.java new file mode 100644 index 0000000..5959df1 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/GroupChatMessageContent.java @@ -0,0 +1,19 @@ +package com.lld.im.common.model.message; + +import lombok.Data; + +import java.util.List; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class GroupChatMessageContent extends MessageContent { + + private String groupId; + + private List memberId; + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/ImMessageBody.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/ImMessageBody.java new file mode 100644 index 0000000..c39caf4 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/ImMessageBody.java @@ -0,0 +1,29 @@ +package com.lld.im.common.model.message; + +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class ImMessageBody { + private Integer appId; + + /** messageBodyId*/ + private Long messageKey; + + /** messageBody*/ + private String messageBody; + + private String securityKey; + + private Long messageTime; + + private Long createTime; + + private String extra; + + private Integer delFlag; +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/MessageContent.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/MessageContent.java new file mode 100644 index 0000000..4f90cd5 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/MessageContent.java @@ -0,0 +1,30 @@ +package com.lld.im.common.model.message; + +import com.lld.im.common.model.ClientInfo; +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class MessageContent extends ClientInfo { + + private String messageId; + + private String fromId; + + private String toId; + + private String messageBody; + + private Long messageTime; + + private String extra; + + private Long messageKey; + + private long messageSequence; + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/MessageReadedContent.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/MessageReadedContent.java new file mode 100644 index 0000000..25e3590 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/MessageReadedContent.java @@ -0,0 +1,24 @@ +package com.lld.im.common.model.message; + +import com.lld.im.common.model.ClientInfo; +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class MessageReadedContent extends ClientInfo { + + private long messageSequence; + + private String fromId; + + private String groupId; + + private String toId; + + private Integer conversationType; + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/MessageReciveAckContent.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/MessageReciveAckContent.java new file mode 100644 index 0000000..8ade994 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/MessageReciveAckContent.java @@ -0,0 +1,24 @@ +package com.lld.im.common.model.message; + +import com.lld.im.common.model.ClientInfo; +import lombok.Data; +import sun.dc.pr.PRError; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class MessageReciveAckContent extends ClientInfo { + + private Long messageKey; + + private String fromId; + + private String toId; + + private Long messageSequence; + + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/OfflineMessageContent.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/OfflineMessageContent.java new file mode 100644 index 0000000..ef3c3e2 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/OfflineMessageContent.java @@ -0,0 +1,40 @@ +package com.lld.im.common.model.message; + +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class OfflineMessageContent { + + private Integer appId; + + /** messageBodyId*/ + private Long messageKey; + + /** messageBody*/ + private String messageBody; + + private Long messageTime; + + private String extra; + + private Integer delFlag; + + private String fromId; + + private String toId; + + /** 序列号*/ + private Long messageSequence; + + private String messageRandom; + + private Integer conversationType; + + private String conversationId; + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/RecallMessageContent.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/RecallMessageContent.java new file mode 100644 index 0000000..5ea0300 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/model/message/RecallMessageContent.java @@ -0,0 +1,37 @@ +package com.lld.im.common.model.message; + +import com.lld.im.common.model.ClientInfo; +import lombok.Data; + +/** + * @author: Chackylee + * @description: + **/ +@Data +public class RecallMessageContent extends ClientInfo { + + private Long messageKey; + + private String fromId; + + private String toId; + + private Long messageTime; + + private Long messageSequence; + + private Integer conversationType; + + +// { +// "messageKey":419455774914383872, +// "fromId":"lld", +// "toId":"lld4", +// "messageTime":"1665026849851", +// "messageSequence":2, +// "appId": 10000, +// "clientType": 1, +// "imei": "web", +// "conversationType":0 +// } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/route/RouteHandle.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/route/RouteHandle.java new file mode 100644 index 0000000..bc97344 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/route/RouteHandle.java @@ -0,0 +1,14 @@ +package com.lld.im.common.route; + +import java.util.List; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +public interface RouteHandle { + + public String routeServer(List values,String key); + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/route/RouteInfo.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/route/RouteInfo.java new file mode 100644 index 0000000..e59e2b3 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/route/RouteInfo.java @@ -0,0 +1,19 @@ +package com.lld.im.common.route; + +import lombok.Data; + +/** + * @since JDK 1.8 + */ + +@Data +public final class RouteInfo { + + private String ip; + private Integer port; + + public RouteInfo(String ip, Integer port) { + this.ip = ip; + this.port = port; + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/route/algorithm/consistenthash/AbstractConsistentHash.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/route/algorithm/consistenthash/AbstractConsistentHash.java new file mode 100644 index 0000000..3b4bafc --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/route/algorithm/consistenthash/AbstractConsistentHash.java @@ -0,0 +1,79 @@ +package com.lld.im.common.route.algorithm.consistenthash; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +/** + * @description: 一致性hash 抽象类 + * @author: lld + * @version: 1.0 + */ +public abstract class AbstractConsistentHash { + + //add + protected abstract void add(long key,String value); + + //sort + protected void sort(){} + + //获取节点 get + protected abstract String getFirstNodeValue(String value); + + /** + * 处理之前事件 + */ + protected abstract void processBefore(); + + /** + * 传入节点列表以及客户端信息获取一个服务节点 + * @param values + * @param key + * @return + */ + public synchronized String process(List values, String key){ + processBefore(); + for (String value : values) { + add(hash(value), value); + } + sort(); + return getFirstNodeValue(key) ; + } + + + //hash + /** + * hash 运算 + * @param value + * @return + */ + public Long hash(String value){ + MessageDigest md5; + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 not supported", e); + } + md5.reset(); + byte[] keyBytes = null; + try { + keyBytes = value.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Unknown string :" + value, e); + } + + md5.update(keyBytes); + byte[] digest = md5.digest(); + + // hash code, Truncate to 32-bits + long hashCode = ((long) (digest[3] & 0xFF) << 24) + | ((long) (digest[2] & 0xFF) << 16) + | ((long) (digest[1] & 0xFF) << 8) + | (digest[0] & 0xFF); + + long truncateHashCode = hashCode & 0xffffffffL; + return truncateHashCode; + } + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/route/algorithm/consistenthash/ConsistentHashHandle.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/route/algorithm/consistenthash/ConsistentHashHandle.java new file mode 100644 index 0000000..c5f3f6e --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/route/algorithm/consistenthash/ConsistentHashHandle.java @@ -0,0 +1,25 @@ +package com.lld.im.common.route.algorithm.consistenthash; + +import com.lld.im.common.route.RouteHandle; + +import java.util.List; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +public class ConsistentHashHandle implements RouteHandle { + + //TreeMap + private AbstractConsistentHash hash; + + public void setHash(AbstractConsistentHash hash) { + this.hash = hash; + } + + @Override + public String routeServer(List values, String key) { + return hash.process(values,key); + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/route/algorithm/consistenthash/TreeMapConsistentHash.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/route/algorithm/consistenthash/TreeMapConsistentHash.java new file mode 100644 index 0000000..e9851c6 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/route/algorithm/consistenthash/TreeMapConsistentHash.java @@ -0,0 +1,49 @@ +package com.lld.im.common.route.algorithm.consistenthash; + +import com.lld.im.common.enums.UserErrorCode; +import com.lld.im.common.exception.ApplicationException; + +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +public class TreeMapConsistentHash extends AbstractConsistentHash { + + private TreeMap treeMap = new TreeMap<>(); + + private static final int NODE_SIZE = 2; + + @Override + protected void add(long key, String value) { + for (int i = 0; i < NODE_SIZE; i++) { + treeMap.put(super.hash("node" + key +i),value); + } + treeMap.put(key,value); + } + + + @Override + protected String getFirstNodeValue(String value) { + + Long hash = super.hash(value); + SortedMap last = treeMap.tailMap(hash); + if(!last.isEmpty()){ + return last.get(last.firstKey()); + } + + if (treeMap.size() == 0){ + throw new ApplicationException(UserErrorCode.SERVER_NOT_AVAILABLE) ; + } + + return treeMap.firstEntry().getValue(); + } + + @Override + protected void processBefore() { + treeMap.clear(); + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/route/algorithm/loop/LoopHandle.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/route/algorithm/loop/LoopHandle.java new file mode 100644 index 0000000..e09d1ac --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/route/algorithm/loop/LoopHandle.java @@ -0,0 +1,31 @@ +package com.lld.im.common.route.algorithm.loop; + +import com.lld.im.common.enums.UserErrorCode; +import com.lld.im.common.exception.ApplicationException; +import com.lld.im.common.route.RouteHandle; + +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +public class LoopHandle implements RouteHandle { + + private AtomicLong index = new AtomicLong(); + + @Override + public String routeServer(List values, String key) { + int size = values.size(); + if(size == 0){ + throw new ApplicationException(UserErrorCode.SERVER_NOT_AVAILABLE); + } + Long l = index.incrementAndGet() % size; + if(l < 0){ + l = 0L; + } + return values.get(l.intValue()); + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/route/algorithm/random/RandomHandle.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/route/algorithm/random/RandomHandle.java new file mode 100644 index 0000000..100a1be --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/route/algorithm/random/RandomHandle.java @@ -0,0 +1,25 @@ +package com.lld.im.common.route.algorithm.random; + +import com.lld.im.common.enums.UserErrorCode; +import com.lld.im.common.exception.ApplicationException; +import com.lld.im.common.route.RouteHandle; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +public class RandomHandle implements RouteHandle { + @Override + public String routeServer(List values, String key) { + int size = values.size(); + if(size == 0){ + throw new ApplicationException(UserErrorCode.SERVER_NOT_AVAILABLE); + } + int i = ThreadLocalRandom.current().nextInt(size); + return values.get(i); + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/utils/Base64URL.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/utils/Base64URL.java new file mode 100644 index 0000000..181ff1d --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/utils/Base64URL.java @@ -0,0 +1,88 @@ +package com.lld.im.common.utils; +import sun.misc.BASE64Decoder; +import sun.misc.BASE64Encoder; + +import java.io.IOException; +import java.nio.charset.Charset; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +public class Base64URL { + public static byte[] base64EncodeUrl(byte[] input) { + byte[] base64 = new BASE64Encoder().encode(input).getBytes(); + for (int i = 0; i < base64.length; ++i) + switch (base64[i]) { + case '+': + base64[i] = '*'; + break; + case '/': + base64[i] = '-'; + break; + case '=': + base64[i] = '_'; + break; + default: + break; + } + return base64; + } + + public static byte[] base64EncodeUrlNotReplace(byte[] input) { + byte[] base64 = new BASE64Encoder().encode(input).getBytes(Charset.forName("UTF-8")); + for (int i = 0; i < base64.length; ++i) + switch (base64[i]) { + case '+': + base64[i] = '*'; + break; + case '/': + base64[i] = '-'; + break; + case '=': + base64[i] = '_'; + break; + default: + break; + } + return base64; + } + + public static byte[] base64DecodeUrlNotReplace(byte[] input) throws IOException { + for (int i = 0; i < input.length; ++i) + switch (input[i]) { + case '*': + input[i] = '+'; + break; + case '-': + input[i] = '/'; + break; + case '_': + input[i] = '='; + break; + default: + break; + } + return new BASE64Decoder().decodeBuffer(new String(input,"UTF-8")); + } + + public static byte[] base64DecodeUrl(byte[] input) throws IOException { + byte[] base64 = input.clone(); + for (int i = 0; i < base64.length; ++i) + switch (base64[i]) { + case '*': + base64[i] = '+'; + break; + case '-': + base64[i] = '/'; + break; + case '_': + base64[i] = '='; + break; + default: + break; + } + return new BASE64Decoder().decodeBuffer(base64.toString()); + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/utils/HttpRequestUtils.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/utils/HttpRequestUtils.java new file mode 100644 index 0000000..8e70861 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/utils/HttpRequestUtils.java @@ -0,0 +1,278 @@ +package com.lld.im.common.utils; + +import com.alibaba.fastjson.JSON; +import com.lld.im.common.config.GlobalHttpClientConfig; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.Map; + +/** + * @author: Chackylee + * @description: + **/ +@Component +public class HttpRequestUtils { + + @Autowired + private CloseableHttpClient httpClient; + + @Autowired + private RequestConfig requestConfig; + + @Autowired + GlobalHttpClientConfig httpClientConfig; + + public String doGet(String url, Map params, String charset) throws Exception { + return doGet(url,params,null,charset); + } + + /** + * 通过给的url地址,获取服务器数据 + * + * @param url 服务器地址 + * @param params 封装用户参数 + * @param charset 设定字符编码 + * @return + */ + public String doGet(String url, Map params, Map header, String charset) throws Exception { + + if (StringUtils.isEmpty(charset)) { + charset = "utf-8"; + } + URIBuilder uriBuilder = new URIBuilder(url); + // 判断是否有参数 + if (params != null) { + // 遍历map,拼接请求参数 + for (Map.Entry entry : params.entrySet()) { + uriBuilder.setParameter(entry.getKey(), entry.getValue().toString()); + } + } + // 声明 http get 请求 + HttpGet httpGet = new HttpGet(uriBuilder.build()); + httpGet.setConfig(requestConfig); + + if (header != null) { + // 遍历map,拼接header参数 + for (Map.Entry entry : header.entrySet()) { + httpGet.addHeader(entry.getKey(),entry.getValue().toString()); + } + } + + String result = ""; + try { + // 发起请求 + CloseableHttpResponse response = httpClient.execute(httpGet); + // 判断状态码是否为200 + if (response.getStatusLine().getStatusCode() == 200) { + // 返回响应体的内容 + result = EntityUtils.toString(response.getEntity(), charset); + } + + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + return result; + } + + /** + * GET请求, 含URL 参数 + * + * @param url + * @param params + * @return 如果状态码为200,则返回body,如果不为200,则返回null + * @throws Exception + */ + public String doGet(String url, Map params) throws Exception { + return doGet(url, params, null); + } + + /** + * GET 请求,不含URL参数 + * + * @param url + * @return + * @throws Exception + */ + public String doGet(String url) throws Exception { + return doGet(url, null, null); + } + + public String doPost(String url, Map params, String jsonBody, String charset) throws Exception { + return doPost(url,params,null,jsonBody,charset); + } + + /** + * 带参数的post请求 + * + * @param url + * @return + * @throws Exception + */ + public String doPost(String url, Map params, Map header, String jsonBody, String charset) throws Exception { + + if (StringUtils.isEmpty(charset)) { + charset = "utf-8"; + } + URIBuilder uriBuilder = new URIBuilder(url); + // 判断是否有参数 + if (params != null) { + // 遍历map,拼接请求参数 + for (Map.Entry entry : params.entrySet()) { + uriBuilder.setParameter(entry.getKey(), entry.getValue().toString()); + } + } + + // 声明httpPost请求 + HttpPost httpPost = new HttpPost(uriBuilder.build()); + // 加入配置信息 + httpPost.setConfig(requestConfig); + + // 判断map是否为空,不为空则进行遍历,封装from表单对象 + if (StringUtils.isNotEmpty(jsonBody)) { + StringEntity s = new StringEntity(jsonBody, charset); + s.setContentEncoding(charset); + s.setContentType("application/json"); + + // 把json body放到post里 + httpPost.setEntity(s); + } + + if (header != null) { + // 遍历map,拼接header参数 + for (Map.Entry entry : header.entrySet()) { + httpPost.addHeader(entry.getKey(),entry.getValue().toString()); + } + } + + String result = ""; +// CloseableHttpClient httpClient = HttpClients.createDefault(); // 单个 + CloseableHttpResponse response = null; + try { + // 发起请求 + response = httpClient.execute(httpPost); + // 判断状态码是否为200 + if (response.getStatusLine().getStatusCode() == 200) { + // 返回响应体的内容 + result = EntityUtils.toString(response.getEntity(), charset); + } + + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + return result; + } + + /** + * 不带参数post请求 + * @param url + * @return + * @throws Exception + */ + public String doPost(String url) throws Exception { + return doPost(url, null,null,null); + } + + /** + * get 方法调用的通用方式 + * @param url + * @param tClass + * @param map + * @param charSet + * @return + * @throws Exception + */ + public T doGet(String url, Class tClass, Map map, String charSet) throws Exception { + + String result = doGet(url, map, charSet); + if (StringUtils.isNotEmpty(result)) + return JSON.parseObject(result, tClass); + return null; + + } + + /** + * get 方法调用的通用方式 + * @param url + * @param tClass + * @param map + * @param charSet + * @return + * @throws Exception + */ + public T doGet(String url, Class tClass, Map map, Map header, String charSet) throws Exception { + + String result = doGet(url, map, header, charSet); + if (StringUtils.isNotEmpty(result)) + return JSON.parseObject(result, tClass); + return null; + + } + + /** + * post 方法调用的通用方式 + * @param url + * @param tClass + * @param map + * @param jsonBody + * @param charSet + * @return + * @throws Exception + */ + public T doPost(String url, Class tClass, Map map, String jsonBody, String charSet) throws Exception { + + String result = doPost(url, map,jsonBody,charSet); + if (StringUtils.isNotEmpty(result)) + return JSON.parseObject(result, tClass); + return null; + + } + + public T doPost(String url, Class tClass, Map map, Map header, String jsonBody, String charSet) throws Exception { + + String result = doPost(url, map, header,jsonBody,charSet); + if (StringUtils.isNotEmpty(result)) + return JSON.parseObject(result, tClass); + return null; + + } + + /** + * post 方法调用的通用方式 + * @param url + * @param map + * @param jsonBody + * @param charSet + * @return + * @throws Exception + */ + public String doPostString(String url, Map map, String jsonBody, String charSet) throws Exception { + return doPost(url, map,jsonBody,charSet); + } + + /** + * post 方法调用的通用方式 + * @param url + * @param map + * @param jsonBody + * @param charSet + * @return + * @throws Exception + */ + public String doPostString(String url, Map map, Map header, String jsonBody, String charSet) throws Exception { + return doPost(url, map, header, jsonBody,charSet); + } + +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/utils/RouteInfoParseUtil.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/utils/RouteInfoParseUtil.java new file mode 100644 index 0000000..e99a937 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/utils/RouteInfoParseUtil.java @@ -0,0 +1,23 @@ +package com.lld.im.common.utils; + + +import com.lld.im.common.BaseErrorCode; +import com.lld.im.common.exception.ApplicationException; +import com.lld.im.common.route.RouteInfo; + +/** + * + * @since JDK 1.8 + */ +public class RouteInfoParseUtil { + + public static RouteInfo parse(String info){ + try { + String[] serverInfo = info.split(":"); + RouteInfo routeInfo = new RouteInfo(serverInfo[0], Integer.parseInt(serverInfo[1])) ; + return routeInfo ; + }catch (Exception e){ + throw new ApplicationException(BaseErrorCode.PARAMETER_ERROR) ; + } + } +} diff --git a/hs-im-server/im-common/src/main/java/com/lld/im/common/utils/SigAPI.java b/hs-im-server/im-common/src/main/java/com/lld/im/common/utils/SigAPI.java new file mode 100644 index 0000000..74dad32 --- /dev/null +++ b/hs-im-server/im-common/src/main/java/com/lld/im/common/utils/SigAPI.java @@ -0,0 +1,194 @@ +package com.lld.im.common.utils; + +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.lang3.StringUtils; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Base64; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + + +/** + * @description: app用户 后台管理员 + * + * 10000 xxx 10001 bbbb + * @author: lld + * @version: 1.0 + */ +public class SigAPI { + final private long appId; + final private String key; + + public SigAPI(long appId, String key) { + this.appId = appId; + this.key = key; + } + + public static void main(String[] args) throws InterruptedException { + SigAPI asd = new SigAPI(10000, "123456"); + String sign = asd.genUserSig("lld", 100000000); +// Thread.sleep(2000L); + JSONObject jsonObject = decodeUserSig(sign); + System.out.println("sign:" + sign); + System.out.println("decoder:" + jsonObject.toString()); + } + + /** + * @description: 解密方法 + * @param + * @return com.alibaba.fastjson.JSONObject + * @author lld + */ + public static JSONObject decodeUserSig(String userSig) { + JSONObject sigDoc = new JSONObject(true); + try { + byte[] decodeUrlByte = Base64URL.base64DecodeUrlNotReplace(userSig.getBytes()); + byte[] decompressByte = decompress(decodeUrlByte); + String decodeText = new String(decompressByte, "UTF-8"); + + if (StringUtils.isNotBlank(decodeText)) { + sigDoc = JSONObject.parseObject(decodeText); + + } + + } catch (Exception ex) { + ex.printStackTrace(); + } + + return sigDoc; + } + + /** + * 解压缩 + * + * @param data 待压缩的数据 + * @return byte[] 解压缩后的数据 + */ + public static byte[] decompress(byte[] data) { + byte[] output = new byte[0]; + + Inflater decompresser = new Inflater(); + decompresser.reset(); + decompresser.setInput(data); + + ByteArrayOutputStream o = new ByteArrayOutputStream(data.length); + try { + byte[] buf = new byte[1024]; + while (!decompresser.finished()) { + int i = decompresser.inflate(buf); + o.write(buf, 0, i); + } + output = o.toByteArray(); + } catch (Exception e) { + output = data; + e.printStackTrace(); + } finally { + try { + o.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + decompresser.end(); + return output; + } + + + /** + * 【功能说明】用于签发 IM 服务中必须要使用的 UserSig 鉴权票据 + *

+ * 【参数说明】 + */ + public String genUserSig(String userid, long expire) { + return genUserSig(userid, expire, null); + } + + + private String hmacsha256(String identifier, long currTime, long expire, String base64Userbuf) { + String contentToBeSigned = "TLS.identifier:" + identifier + "\n" + + "TLS.appId:" + appId + "\n" + + "TLS.expireTime:" + currTime + "\n" + + "TLS.expire:" + expire + "\n"; + if (null != base64Userbuf) { + contentToBeSigned += "TLS.userbuf:" + base64Userbuf + "\n"; + } + try { + byte[] byteKey = key.getBytes(StandardCharsets.UTF_8); + Mac hmac = Mac.getInstance("HmacSHA256"); + SecretKeySpec keySpec = new SecretKeySpec(byteKey, "HmacSHA256"); + hmac.init(keySpec); + byte[] byteSig = hmac.doFinal(contentToBeSigned.getBytes(StandardCharsets.UTF_8)); + return (Base64.getEncoder().encodeToString(byteSig)).replaceAll("\\s*", ""); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + return ""; + } + } + + private String genUserSig(String userid, long expire, byte[] userbuf) { + + long currTime = System.currentTimeMillis() / 1000; + + JSONObject sigDoc = new JSONObject(); + sigDoc.put("TLS.identifier", userid); + sigDoc.put("TLS.appId", appId); + sigDoc.put("TLS.expire", expire); + sigDoc.put("TLS.expireTime", currTime); + + String base64UserBuf = null; + if (null != userbuf) { + base64UserBuf = Base64.getEncoder().encodeToString(userbuf).replaceAll("\\s*", ""); + sigDoc.put("TLS.userbuf", base64UserBuf); + } + String sig = hmacsha256(userid, currTime, expire, base64UserBuf); + if (sig.length() == 0) { + return ""; + } + sigDoc.put("TLS.sig", sig); + Deflater compressor = new Deflater(); + compressor.setInput(sigDoc.toString().getBytes(StandardCharsets.UTF_8)); + compressor.finish(); + byte[] compressedBytes = new byte[2048]; + int compressedBytesLength = compressor.deflate(compressedBytes); + compressor.end(); + return (new String(Base64URL.base64EncodeUrl(Arrays.copyOfRange(compressedBytes, + 0, compressedBytesLength)))).replaceAll("\\s*", ""); + } + + public String genUserSig(String userid, long expire, long time,byte [] userbuf) { + + JSONObject sigDoc = new JSONObject(); + sigDoc.put("TLS.identifier", userid); + sigDoc.put("TLS.appId", appId); + sigDoc.put("TLS.expire", expire); + sigDoc.put("TLS.expireTime", time); + + String base64UserBuf = null; + if (null != userbuf) { + base64UserBuf = Base64.getEncoder().encodeToString(userbuf).replaceAll("\\s*", ""); + sigDoc.put("TLS.userbuf", base64UserBuf); + } + String sig = hmacsha256(userid, time, expire, base64UserBuf); + if (sig.length() == 0) { + return ""; + } + sigDoc.put("TLS.sig", sig); + Deflater compressor = new Deflater(); + compressor.setInput(sigDoc.toString().getBytes(StandardCharsets.UTF_8)); + compressor.finish(); + byte[] compressedBytes = new byte[2048]; + int compressedBytesLength = compressor.deflate(compressedBytes); + compressor.end(); + return (new String(Base64URL.base64EncodeUrl(Arrays.copyOfRange(compressedBytes, + 0, compressedBytesLength)))).replaceAll("\\s*", ""); + } + +} \ No newline at end of file diff --git a/hs-im-server/im-message-store/pom.xml b/hs-im-server/im-message-store/pom.xml new file mode 100644 index 0000000..0dc2a56 --- /dev/null +++ b/hs-im-server/im-message-store/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + + com.lld + im-system + 1.0.0-SNAPSHOT + + + im-message-store + + + 8 + 8 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-amqp + + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + + mysql + mysql-connector-java + runtime + + + + + com.baomidou + mybatis-plus-boot-starter + + + com.github.jeffreyning + mybatisplus-plus + 1.5.1-RELEASE + + + + cn.hutool + hutool-all + + + + + com.lld + common + 1.0.0-SNAPSHOT + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/hs-im-server/im-message-store/src/main/java/com/lld/message/Application.java b/hs-im-server/im-message-store/src/main/java/com/lld/message/Application.java new file mode 100644 index 0000000..607c1df --- /dev/null +++ b/hs-im-server/im-message-store/src/main/java/com/lld/message/Application.java @@ -0,0 +1,18 @@ +package com.lld.message; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@MapperScan("com.lld.message.dao.mapper") +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + +} + + diff --git a/hs-im-server/im-message-store/src/main/java/com/lld/message/config/BeanConfig.java b/hs-im-server/im-message-store/src/main/java/com/lld/message/config/BeanConfig.java new file mode 100644 index 0000000..7fb8692 --- /dev/null +++ b/hs-im-server/im-message-store/src/main/java/com/lld/message/config/BeanConfig.java @@ -0,0 +1,27 @@ +package com.lld.message.config; + +import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author: Chackylee + * @description: + **/ +@Configuration +public class BeanConfig { + + /** + * 分页插件 + */ + @Bean + public PaginationInterceptor paginationInterceptor() { + return new PaginationInterceptor(); + } + + @Bean + public EasySqlInjector easySqlInjector () { + return new EasySqlInjector(); + } + +} diff --git a/hs-im-server/im-message-store/src/main/java/com/lld/message/config/EasySqlInjector.java b/hs-im-server/im-message-store/src/main/java/com/lld/message/config/EasySqlInjector.java new file mode 100644 index 0000000..e6fad30 --- /dev/null +++ b/hs-im-server/im-message-store/src/main/java/com/lld/message/config/EasySqlInjector.java @@ -0,0 +1,17 @@ +package com.lld.message.config; + +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector; +import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn; + +import java.util.List; + +public class EasySqlInjector extends DefaultSqlInjector { + @Override + public List getMethodList(Class mapperClass) { + List methodList = super.getMethodList(mapperClass); + methodList.add(new InsertBatchSomeColumn()); // 添加InsertBatchSomeColumn方法 + return methodList; + } + +} diff --git a/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/ImGroupMessageHistoryEntity.java b/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/ImGroupMessageHistoryEntity.java new file mode 100644 index 0000000..945c5fd --- /dev/null +++ b/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/ImGroupMessageHistoryEntity.java @@ -0,0 +1,32 @@ +package com.lld.message.dao; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * @author: Chackylee + * @description: + **/ +@Data +@TableName("im_group_message_history") +public class ImGroupMessageHistoryEntity { + + private Integer appId; + + private String fromId; + + private String groupId; + + /** messageBodyId*/ + private Long messageKey; + /** 序列号*/ + private Long sequence; + + private String messageRandom; + + private Long messageTime; + + private Long createTime; + + +} diff --git a/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/ImMessageBodyEntity.java b/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/ImMessageBodyEntity.java new file mode 100644 index 0000000..1a231ee --- /dev/null +++ b/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/ImMessageBodyEntity.java @@ -0,0 +1,32 @@ +package com.lld.message.dao; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * @author: Chackylee + * @description: + **/ +@Data +@TableName("im_message_body") +public class ImMessageBodyEntity { + + private Integer appId; + + /** messageBodyId*/ + private Long messageKey; + + /** messageBody*/ + private String messageBody; + + private String securityKey; + + private Long messageTime; + + private Long createTime; + + private String extra; + + private Integer delFlag; + +} diff --git a/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/ImMessageHistoryEntity.java b/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/ImMessageHistoryEntity.java new file mode 100644 index 0000000..2516e06 --- /dev/null +++ b/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/ImMessageHistoryEntity.java @@ -0,0 +1,33 @@ +package com.lld.message.dao; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * @author: Chackylee + * @description: + **/ +@Data +@TableName("im_message_history") +public class ImMessageHistoryEntity { + + private Integer appId; + + private String fromId; + + private String toId; + + private String ownerId; + + /** messageBodyId*/ + private Long messageKey; + /** 序列号*/ + private Long sequence; + + private String messageRandom; + + private Long messageTime; + + private Long createTime; + +} diff --git a/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/mapper/ImGroupMessageHistoryMapper.java b/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/mapper/ImGroupMessageHistoryMapper.java new file mode 100644 index 0000000..33c7ff2 --- /dev/null +++ b/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/mapper/ImGroupMessageHistoryMapper.java @@ -0,0 +1,11 @@ +package com.lld.message.dao.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.lld.message.dao.ImGroupMessageHistoryEntity; +import org.springframework.stereotype.Repository; + +@Repository +public interface ImGroupMessageHistoryMapper extends BaseMapper { + + +} diff --git a/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/mapper/ImMessageBodyMapper.java b/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/mapper/ImMessageBodyMapper.java new file mode 100644 index 0000000..26d28e4 --- /dev/null +++ b/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/mapper/ImMessageBodyMapper.java @@ -0,0 +1,8 @@ +package com.lld.message.dao.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.lld.message.dao.ImMessageBodyEntity; +import org.springframework.stereotype.Repository; +@Repository +public interface ImMessageBodyMapper extends BaseMapper { +} diff --git a/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/mapper/ImMessageHistoryMapper.java b/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/mapper/ImMessageHistoryMapper.java new file mode 100644 index 0000000..82a978a --- /dev/null +++ b/hs-im-server/im-message-store/src/main/java/com/lld/message/dao/mapper/ImMessageHistoryMapper.java @@ -0,0 +1,17 @@ +package com.lld.message.dao.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.lld.message.dao.ImMessageHistoryEntity; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +@Repository +public interface ImMessageHistoryMapper extends BaseMapper { + + /** + * 批量插入(mysql) + * @param entityList + * @return + */ + Integer insertBatchSomeColumn(Collection entityList); +} diff --git a/hs-im-server/im-message-store/src/main/java/com/lld/message/model/DoStoreGroupMessageDto.java b/hs-im-server/im-message-store/src/main/java/com/lld/message/model/DoStoreGroupMessageDto.java new file mode 100644 index 0000000..12b0bb0 --- /dev/null +++ b/hs-im-server/im-message-store/src/main/java/com/lld/message/model/DoStoreGroupMessageDto.java @@ -0,0 +1,19 @@ +package com.lld.message.model; + +import com.lld.im.common.model.message.GroupChatMessageContent; +import com.lld.im.common.model.message.MessageContent; +import com.lld.message.dao.ImMessageBodyEntity; +import lombok.Data; + +/** + * @author: Chackylee + * @description: + **/ +@Data +public class DoStoreGroupMessageDto { + + private GroupChatMessageContent groupChatMessageContent; + + private ImMessageBodyEntity imMessageBodyEntity; + +} diff --git a/hs-im-server/im-message-store/src/main/java/com/lld/message/model/DoStoreP2PMessageDto.java b/hs-im-server/im-message-store/src/main/java/com/lld/message/model/DoStoreP2PMessageDto.java new file mode 100644 index 0000000..af8ac0b --- /dev/null +++ b/hs-im-server/im-message-store/src/main/java/com/lld/message/model/DoStoreP2PMessageDto.java @@ -0,0 +1,18 @@ +package com.lld.message.model; + +import com.lld.im.common.model.message.MessageContent; +import com.lld.message.dao.ImMessageBodyEntity; +import lombok.Data; + +/** + * @author: Chackylee + * @description: + **/ +@Data +public class DoStoreP2PMessageDto { + + private MessageContent messageContent; + + private ImMessageBodyEntity imMessageBodyEntity; + +} diff --git a/hs-im-server/im-message-store/src/main/java/com/lld/message/mq/StroeGroupMessageReceiver.java b/hs-im-server/im-message-store/src/main/java/com/lld/message/mq/StroeGroupMessageReceiver.java new file mode 100644 index 0000000..2404d6e --- /dev/null +++ b/hs-im-server/im-message-store/src/main/java/com/lld/message/mq/StroeGroupMessageReceiver.java @@ -0,0 +1,66 @@ +package com.lld.message.mq; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.lld.im.common.constant.Constants; +import com.lld.message.dao.ImMessageBodyEntity; +import com.lld.message.model.DoStoreGroupMessageDto; +import com.lld.message.model.DoStoreP2PMessageDto; +import com.lld.message.service.StoreMessageService; +import com.rabbitmq.client.Channel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.annotation.Exchange; +import org.springframework.amqp.rabbit.annotation.Queue; +import org.springframework.amqp.rabbit.annotation.QueueBinding; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.handler.annotation.Headers; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.stereotype.Service; + +import java.util.Map; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Service +public class StroeGroupMessageReceiver { + private static Logger logger = LoggerFactory.getLogger(StroeGroupMessageReceiver.class); + + @Autowired + StoreMessageService storeMessageService; + + @RabbitListener( + bindings = @QueueBinding( + value = @Queue(value = Constants.RabbitConstants.StoreGroupMessage,durable = "true"), + exchange = @Exchange(value = Constants.RabbitConstants.StoreGroupMessage,durable = "true") + ),concurrency = "1" + ) + public void onChatMessage(@Payload Message message, + @Headers Map headers, + Channel channel) throws Exception { + String msg = new String(message.getBody(),"utf-8"); + logger.info("CHAT MSG FORM QUEUE ::: {}", msg); + Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG); + try { + JSONObject jsonObject = JSON.parseObject(msg); + DoStoreGroupMessageDto doStoreGroupMessageDto = jsonObject.toJavaObject(DoStoreGroupMessageDto.class); + ImMessageBodyEntity messageBody = jsonObject.getObject("messageBody", ImMessageBodyEntity.class); + doStoreGroupMessageDto.setImMessageBodyEntity(messageBody); + storeMessageService.doStoreGroupMessage(doStoreGroupMessageDto); + channel.basicAck(deliveryTag, false); + }catch (Exception e){ + logger.error("处理消息出现异常:{}", e.getMessage()); + logger.error("RMQ_CHAT_TRAN_ERROR", e); + logger.error("NACK_MSG:{}", msg); + //第一个false 表示不批量拒绝,第二个false表示不重回队列 + channel.basicNack(deliveryTag, false, false); + } + + } +} diff --git a/hs-im-server/im-message-store/src/main/java/com/lld/message/mq/StroeP2PMessageReceiver.java b/hs-im-server/im-message-store/src/main/java/com/lld/message/mq/StroeP2PMessageReceiver.java new file mode 100644 index 0000000..0d0b358 --- /dev/null +++ b/hs-im-server/im-message-store/src/main/java/com/lld/message/mq/StroeP2PMessageReceiver.java @@ -0,0 +1,65 @@ +package com.lld.message.mq; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.lld.im.common.constant.Constants; +import com.lld.message.dao.ImMessageBodyEntity; +import com.lld.message.model.DoStoreP2PMessageDto; +import com.lld.message.service.StoreMessageService; +import com.rabbitmq.client.Channel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.annotation.Exchange; +import org.springframework.amqp.rabbit.annotation.Queue; +import org.springframework.amqp.rabbit.annotation.QueueBinding; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.handler.annotation.Headers; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.stereotype.Service; + +import java.util.Map; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Service +public class StroeP2PMessageReceiver { + private static Logger logger = LoggerFactory.getLogger(StroeP2PMessageReceiver.class); + + @Autowired + StoreMessageService storeMessageService; + + @RabbitListener( + bindings = @QueueBinding( + value = @Queue(value = Constants.RabbitConstants.StoreP2PMessage,durable = "true"), + exchange = @Exchange(value = Constants.RabbitConstants.StoreP2PMessage,durable = "true") + ),concurrency = "1" + ) + public void onChatMessage(@Payload Message message, + @Headers Map headers, + Channel channel) throws Exception { + String msg = new String(message.getBody(),"utf-8"); + logger.info("CHAT MSG FORM QUEUE ::: {}", msg); + Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG); + try { + JSONObject jsonObject = JSON.parseObject(msg); + DoStoreP2PMessageDto doStoreP2PMessageDto = jsonObject.toJavaObject(DoStoreP2PMessageDto.class); + ImMessageBodyEntity messageBody = jsonObject.getObject("messageBody", ImMessageBodyEntity.class); + doStoreP2PMessageDto.setImMessageBodyEntity(messageBody); + storeMessageService.doStoreP2PMessage(doStoreP2PMessageDto); + channel.basicAck(deliveryTag, false); + }catch (Exception e){ + logger.error("处理消息出现异常:{}", e.getMessage()); + logger.error("RMQ_CHAT_TRAN_ERROR", e); + logger.error("NACK_MSG:{}", msg); + //第一个false 表示不批量拒绝,第二个false表示不重回队列 + channel.basicNack(deliveryTag, false, false); + } + + } +} diff --git a/hs-im-server/im-message-store/src/main/java/com/lld/message/service/StoreMessageService.java b/hs-im-server/im-message-store/src/main/java/com/lld/message/service/StoreMessageService.java new file mode 100644 index 0000000..58e68d8 --- /dev/null +++ b/hs-im-server/im-message-store/src/main/java/com/lld/message/service/StoreMessageService.java @@ -0,0 +1,86 @@ +package com.lld.message.service; + +import com.lld.im.common.model.message.GroupChatMessageContent; +import com.lld.im.common.model.message.MessageContent; +import com.lld.message.dao.ImGroupMessageHistoryEntity; +import com.lld.message.dao.ImMessageBodyEntity; +import com.lld.message.dao.ImMessageHistoryEntity; +import com.lld.message.dao.mapper.ImGroupMessageHistoryMapper; +import com.lld.message.dao.mapper.ImMessageBodyMapper; +import com.lld.message.dao.mapper.ImMessageHistoryMapper; +import com.lld.message.model.DoStoreGroupMessageDto; +import com.lld.message.model.DoStoreP2PMessageDto; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Service +public class StoreMessageService { + + @Autowired + ImMessageHistoryMapper imMessageHistoryMapper; + + @Autowired + ImMessageBodyMapper imMessageBodyMapper; + + @Autowired + ImGroupMessageHistoryMapper imGroupMessageHistoryMapper; + + + @Transactional + public void doStoreP2PMessage(DoStoreP2PMessageDto doStoreP2PMessageDto){ + imMessageBodyMapper.insert(doStoreP2PMessageDto.getImMessageBodyEntity()); + List imMessageHistoryEntities = extractToP2PMessageHistory(doStoreP2PMessageDto.getMessageContent(), doStoreP2PMessageDto.getImMessageBodyEntity()); + imMessageHistoryMapper.insertBatchSomeColumn(imMessageHistoryEntities); + } + + + public List extractToP2PMessageHistory(MessageContent messageContent, + ImMessageBodyEntity imMessageBodyEntity){ + List list = new ArrayList<>(); + ImMessageHistoryEntity fromHistory = new ImMessageHistoryEntity(); + BeanUtils.copyProperties(messageContent,fromHistory); + fromHistory.setOwnerId(messageContent.getFromId()); + fromHistory.setMessageKey(imMessageBodyEntity.getMessageKey()); + fromHistory.setCreateTime(System.currentTimeMillis()); + fromHistory.setSequence(messageContent.getMessageSequence()); + + ImMessageHistoryEntity toHistory = new ImMessageHistoryEntity(); + BeanUtils.copyProperties(messageContent,toHistory); + toHistory.setOwnerId(messageContent.getToId()); + toHistory.setMessageKey(imMessageBodyEntity.getMessageKey()); + toHistory.setCreateTime(System.currentTimeMillis()); + toHistory.setSequence(messageContent.getMessageSequence()); + + list.add(fromHistory); + list.add(toHistory); + return list; + } + + @Transactional + public void doStoreGroupMessage(DoStoreGroupMessageDto doStoreGroupMessageDto) { + imMessageBodyMapper.insert(doStoreGroupMessageDto.getImMessageBodyEntity()); + ImGroupMessageHistoryEntity imGroupMessageHistoryEntity = extractToGroupMessageHistory(doStoreGroupMessageDto.getGroupChatMessageContent(),doStoreGroupMessageDto.getImMessageBodyEntity()); + imGroupMessageHistoryMapper.insert(imGroupMessageHistoryEntity); + + } + + private ImGroupMessageHistoryEntity extractToGroupMessageHistory(GroupChatMessageContent + messageContent , ImMessageBodyEntity messageBodyEntity){ + ImGroupMessageHistoryEntity result = new ImGroupMessageHistoryEntity(); + BeanUtils.copyProperties(messageContent,result); + result.setGroupId(messageContent.getGroupId()); + result.setMessageKey(messageBodyEntity.getMessageKey()); + result.setCreateTime(System.currentTimeMillis()); + return result; + } +} diff --git a/hs-im-server/im-message-store/src/main/resources/application.yml b/hs-im-server/im-message-store/src/main/resources/application.yml new file mode 100644 index 0000000..e6f9b0f --- /dev/null +++ b/hs-im-server/im-message-store/src/main/resources/application.yml @@ -0,0 +1,63 @@ +spring: + profiles: + active: dev + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + password: beAs0LHX2GyTxMw4 + url: jdbc:mysql://192.168.2.201:3306/im-core?serverTimezone=UTC&useSSL=false&characterEncoding=UTF8 + username: root + + redis: + host: 43.139.191.204 + port: 6379 + database: 8 + jedis: + pool: + max-active: 100 + max-idle: 100 + max-wait: 1000 + min-idle: 10 + password: dSMIXBQrCBXiHHjk123 + rabbitmq: + host: 192.168.2.180 + port: 5672 + addresses: 192.168.2.180 + username: guest + password: guest + # virtual-host: + listener: + simple: + concurrency: 5 + max-concurrency: 10 + acknowledge-mode: MANUAL + prefetch: 1 + publisher-confirms: true + publisher-returns: true + template: + mandatory: true + cache: + connection: + mode: channel + channel: + size: 36 + checkout-timeout: 0 + + + +# logger 配置 +logging: + config: classpath:logback-spring.xml + + +mybatis-plus: + + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + mapper-locations: classpath*:mapper/*.xml + global-config: + db-config: + update-strategy: NOT_EMPTY + +#mybatis: +# configuration: +# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl diff --git a/hs-im-server/im-message-store/src/main/resources/logback-spring.xml b/hs-im-server/im-message-store/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..b9cd630 --- /dev/null +++ b/hs-im-server/im-message-store/src/main/resources/logback-spring.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DEBUG + + + ${CONSOLE_LOG_PATTERN} + utf8 + + + + + + + + + + + + + true + + + ${logFile}.%d{yyyy-MM-dd}.log + + + + + %d{yyyy-MM-dd HH:mm:ss} -%msg%n + + + + + + + + + + + + true + + + ${logFile}.%d{yyyy-MM-dd}.log + + + + + %d{yyyy-MM-dd HH:mm:ss} -%msg%n + + + + + + + + + + \ No newline at end of file diff --git a/hs-im-server/im-service/pom.xml b/hs-im-server/im-service/pom.xml index 637de92..805ad60 100644 --- a/hs-im-server/im-service/pom.xml +++ b/hs-im-server/im-service/pom.xml @@ -18,26 +18,26 @@ - - - - + + org.springframework.boot + spring-boot-starter-amqp + org.springframework.boot spring-boot-starter-validation - - - - + + com.github.sgroschupf + zkclient + - - - - + + org.springframework.boot + spring-boot-starter-data-redis + @@ -45,11 +45,11 @@ spring-boot-starter-web - - - - - + + com.lld + im-codec + 1.0.0-SNAPSHOT + @@ -87,6 +87,12 @@ common 1.0.0-SNAPSHOT + + com.lld + im-system + 1.0.0-SNAPSHOT + compile + @@ -98,4 +104,4 @@ - \ No newline at end of file + diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/Application.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/Application.java index 272d021..30df89a 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/Application.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/Application.java @@ -4,8 +4,10 @@ import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -@SpringBootApplication +@SpringBootApplication(scanBasePackages = {"com.lld.im.service", + "com.lld.im.common"}) @MapperScan("com.lld.im.service.*.dao.mapper") +//导入用户资料,删除用户资料,修改用户资料,查询用户资料 public class Application { public static void main(String[] args) { @@ -17,4 +19,3 @@ public class Application { - diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/config/BeanConfig.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/config/BeanConfig.java index c07d827..2e671a5 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/config/BeanConfig.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/config/BeanConfig.java @@ -1,7 +1,22 @@ package com.lld.im.service.config; +import com.lld.im.common.config.AppConfig; +import com.lld.im.common.enums.ImUrlRouteWayEnum; +import com.lld.im.common.enums.RouteHashMethodEnum; +import com.lld.im.common.route.RouteHandle; +import com.lld.im.common.route.algorithm.consistenthash.AbstractConsistentHash; +import com.lld.im.common.route.algorithm.consistenthash.ConsistentHashHandle; +import com.lld.im.common.route.algorithm.consistenthash.TreeMapConsistentHash; +import com.lld.im.common.route.algorithm.loop.LoopHandle; +import com.lld.im.common.route.algorithm.random.RandomHandle; +import com.lld.im.service.utils.SnowflakeIdWorker; +import org.I0Itec.zkclient.ZkClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.lang.reflect.Method; + /** * @description: * @author: lld @@ -10,4 +25,50 @@ import org.springframework.context.annotation.Configuration; @Configuration public class BeanConfig { + @Autowired + AppConfig appConfig; + + @Bean + public ZkClient buildZKClient() { + return new ZkClient(appConfig.getZkAddr(), + appConfig.getZkConnectTimeOut()); + } + + @Bean + public RouteHandle routeHandle() throws Exception { + + Integer imRouteWay = appConfig.getImRouteWay(); + String routWay = ""; + + ImUrlRouteWayEnum handler = ImUrlRouteWayEnum.getHandler(imRouteWay); + routWay = handler.getClazz(); + + RouteHandle routeHandle = (RouteHandle) Class.forName(routWay).newInstance(); + if(handler == ImUrlRouteWayEnum.HASH){ + + Method setHash = Class.forName(routWay).getMethod("setHash", AbstractConsistentHash.class); + Integer consistentHashWay = appConfig.getConsistentHashWay(); + String hashWay = ""; + + RouteHashMethodEnum hashHandler = RouteHashMethodEnum.getHandler(consistentHashWay); + hashWay = hashHandler.getClazz(); + AbstractConsistentHash consistentHash + = (AbstractConsistentHash) Class.forName(hashWay).newInstance(); + setHash.invoke(routeHandle,consistentHash); + } + + return routeHandle; + } + + @Bean + public EasySqlInjector easySqlInjector () { + return new EasySqlInjector(); + } + + @Bean + public SnowflakeIdWorker buildSnowflakeSeq() throws Exception { + return new SnowflakeIdWorker(0); + } + + } diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/config/EasySqlInjector.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/config/EasySqlInjector.java new file mode 100644 index 0000000..b512cab --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/config/EasySqlInjector.java @@ -0,0 +1,17 @@ +package com.lld.im.service.config; + +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector; +import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn; + +import java.util.List; + +public class EasySqlInjector extends DefaultSqlInjector { + @Override + public List getMethodList(Class mapperClass) { + List methodList = super.getMethodList(mapperClass); + methodList.add(new InsertBatchSomeColumn()); // 添加InsertBatchSomeColumn方法 + return methodList; + } + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/config/RedisConfig.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/config/RedisConfig.java new file mode 100644 index 0000000..8426754 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/config/RedisConfig.java @@ -0,0 +1,48 @@ +package com.lld.im.service.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * @author: Chackylee + * @description: + **/ +@Configuration +public class RedisConfig { + + @Autowired + RedisConnectionFactory redisConnectionFactory; + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + + //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值 + //Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class); + //使用Fastjson2JsonRedisSerializer来序列化和反序列化redis的value值 by zhengkai + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class); + + ObjectMapper mapper = new ObjectMapper(); + mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); + serializer.setObjectMapper(mapper); + + template.setValueSerializer(new StringRedisSerializer()); + //使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + template.afterPropertiesSet(); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(serializer); + + return template; + } +} \ No newline at end of file diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/config/WebConfig.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/config/WebConfig.java index b6bd571..528fcce 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/config/WebConfig.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/config/WebConfig.java @@ -1,7 +1,10 @@ package com.lld.im.service.config; +import com.lld.im.service.interceptor.GateWayInterceptor; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** @@ -12,6 +15,16 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { + @Autowired + GateWayInterceptor gateWayInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(gateWayInterceptor) + .addPathPatterns("/**") + .excludePathPatterns("/v1/user/login") + .excludePathPatterns("/v1/message/checkSend"); + } @Override public void addCorsMappings(CorsRegistry registry) { diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/controller/ConversationController.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/controller/ConversationController.java new file mode 100644 index 0000000..f43f73f --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/controller/ConversationController.java @@ -0,0 +1,49 @@ +package com.lld.im.service.conversation.controller; + +import com.lld.im.common.ResponseVO; +import com.lld.im.common.model.SyncReq; +import com.lld.im.service.conversation.model.DeleteConversationReq; +import com.lld.im.service.conversation.model.UpdateConversationReq; +import com.lld.im.service.conversation.service.ConversationService; +import com.lld.im.service.group.model.req.ImportGroupReq; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@RestController +@RequestMapping("v1/conversation") +public class ConversationController { + + @Autowired + ConversationService conversationService; + + @RequestMapping("/deleteConversation") + public ResponseVO deleteConversation(@RequestBody @Validated DeleteConversationReq + req, Integer appId, String identifier) { + req.setAppId(appId); +// req.setOperater(identifier); + return conversationService.deleteConversation(req); + } + + @RequestMapping("/updateConversation") + public ResponseVO updateConversation(@RequestBody @Validated UpdateConversationReq + req, Integer appId, String identifier) { + req.setAppId(appId); +// req.setOperater(identifier); + return conversationService.updateConversation(req); + } + + @RequestMapping("/syncConversationList") + public ResponseVO syncFriendShipList(@RequestBody @Validated SyncReq req, Integer appId) { + req.setAppId(appId); + return conversationService.syncConversationSet(req); + } + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/dao/ImConversationSetEntity.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/dao/ImConversationSetEntity.java new file mode 100644 index 0000000..54e2315 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/dao/ImConversationSetEntity.java @@ -0,0 +1,33 @@ +package com.lld.im.service.conversation.dao; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * @author: Chackylee + * @description: + **/ +@Data +@TableName("im_conversation_set") +public class ImConversationSetEntity { + + //会话id 0_fromId_toId + private String conversationId; + + //会话类型 + private Integer conversationType; + + private String fromId; + + private String toId; + + private int isMute; + + private int isTop; + + private Long sequence; + + private Long readedSequence; + + private Integer appId; +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/dao/mapper/ImConversationSetMapper.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/dao/mapper/ImConversationSetMapper.java new file mode 100644 index 0000000..636e1f4 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/dao/mapper/ImConversationSetMapper.java @@ -0,0 +1,23 @@ +package com.lld.im.service.conversation.dao.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.lld.im.service.conversation.dao.ImConversationSetEntity; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; +import org.springframework.stereotype.Repository; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Repository +public interface ImConversationSetMapper extends BaseMapper { + + @Update(" update im_conversation_set set readed_sequence = #{readedSequence},sequence = #{sequence} " + + " where conversation_id = #{conversationId} and app_id = #{appId} AND readed_sequence < #{readedSequence}") + public void readMark(ImConversationSetEntity imConversationSetEntity); + + @Select(" select max(sequence) from im_conversation_set where app_id = #{appId} AND from_id = #{userId} ") + Long geConversationSetMaxSeq(Integer appId, String userId); +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/model/DeleteConversationReq.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/model/DeleteConversationReq.java new file mode 100644 index 0000000..2a15b66 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/model/DeleteConversationReq.java @@ -0,0 +1,22 @@ +package com.lld.im.service.conversation.model; + +import com.lld.im.common.model.RequestBase; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class DeleteConversationReq extends RequestBase { + + @NotBlank(message = "会话id不能为空") + private String conversationId; + + @NotBlank(message = "fromId不能为空") + private String fromId; + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/model/UpdateConversationReq.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/model/UpdateConversationReq.java new file mode 100644 index 0000000..4846962 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/model/UpdateConversationReq.java @@ -0,0 +1,23 @@ +package com.lld.im.service.conversation.model; + +import com.lld.im.common.model.RequestBase; +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class UpdateConversationReq extends RequestBase { + + private String conversationId; + + private Integer isMute; + + private Integer isTop; + + private String fromId; + + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/service/ConversationService.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/service/ConversationService.java new file mode 100644 index 0000000..24e7391 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/conversation/service/ConversationService.java @@ -0,0 +1,199 @@ +package com.lld.im.service.conversation.service; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.lld.im.codec.pack.conversation.DeleteConversationPack; +import com.lld.im.codec.pack.conversation.UpdateConversationPack; +import com.lld.im.common.ResponseVO; +import com.lld.im.common.config.AppConfig; +import com.lld.im.common.constant.Constants; +import com.lld.im.common.enums.ConversationErrorCode; +import com.lld.im.common.enums.ConversationTypeEnum; +import com.lld.im.common.enums.command.ConversationEventCommand; +import com.lld.im.common.model.ClientInfo; +import com.lld.im.common.model.SyncReq; +import com.lld.im.common.model.SyncResp; +import com.lld.im.common.model.message.MessageReadedContent; +import com.lld.im.service.conversation.dao.ImConversationSetEntity; +import com.lld.im.service.conversation.dao.mapper.ImConversationSetMapper; +import com.lld.im.service.conversation.model.DeleteConversationReq; +import com.lld.im.service.conversation.model.UpdateConversationReq; +import com.lld.im.service.friendship.dao.ImFriendShipEntity; +import com.lld.im.service.seq.RedisSeq; +import com.lld.im.service.utils.MessageProducer; +import com.lld.im.service.utils.WriteUserSeq; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Service +public class ConversationService { + + @Autowired + ImConversationSetMapper imConversationSetMapper; + + @Autowired + MessageProducer messageProducer; + + @Autowired + AppConfig appConfig; + + @Autowired + RedisSeq redisSeq; + + @Autowired + WriteUserSeq writeUserSeq; + + public String convertConversationId(Integer type,String fromId,String toId){ + return type + "_" + fromId + "_" + toId; + } + + public void messageMarkRead(MessageReadedContent messageReadedContent){ + + String toId = messageReadedContent.getToId(); + if(messageReadedContent.getConversationType() == ConversationTypeEnum.GROUP.getCode()){ + toId = messageReadedContent.getGroupId(); + } + String conversationId = convertConversationId(messageReadedContent.getConversationType(), + messageReadedContent.getFromId(), toId); + QueryWrapper query = new QueryWrapper<>(); + query.eq("conversation_id",conversationId); + query.eq("app_id",messageReadedContent.getAppId()); + ImConversationSetEntity imConversationSetEntity = imConversationSetMapper.selectOne(query); + if(imConversationSetEntity == null){ + imConversationSetEntity = new ImConversationSetEntity(); + long seq = redisSeq.doGetSeq(messageReadedContent.getAppId() + ":" + Constants.SeqConstants.Conversation); + imConversationSetEntity.setConversationId(conversationId); + BeanUtils.copyProperties(messageReadedContent,imConversationSetEntity); + imConversationSetEntity.setReadedSequence(messageReadedContent.getMessageSequence()); + imConversationSetEntity.setToId(toId); + imConversationSetEntity.setSequence(seq); + imConversationSetMapper.insert(imConversationSetEntity); + writeUserSeq.writeUserSeq(messageReadedContent.getAppId(), + messageReadedContent.getFromId(),Constants.SeqConstants.Conversation,seq); + }else{ + long seq = redisSeq.doGetSeq(messageReadedContent.getAppId() + ":" + Constants.SeqConstants.Conversation); + imConversationSetEntity.setSequence(seq); + imConversationSetEntity.setReadedSequence(messageReadedContent.getMessageSequence()); + imConversationSetMapper.readMark(imConversationSetEntity); + writeUserSeq.writeUserSeq(messageReadedContent.getAppId(), + messageReadedContent.getFromId(),Constants.SeqConstants.Conversation,seq); + } + } + + /** + * @description: 删除会话 + * @param + * @return com.lld.im.common.ResponseVO + * @author lld + */ + public ResponseVO deleteConversation(DeleteConversationReq req){ + + //置顶 有免打扰 +// QueryWrapper queryWrapper = new QueryWrapper<>(); +// queryWrapper.eq("conversation_id",req.getConversationId()); +// queryWrapper.eq("app_id",req.getAppId()); +// ImConversationSetEntity imConversationSetEntity = imConversationSetMapper.selectOne(queryWrapper); +// if(imConversationSetEntity != null){ +// imConversationSetEntity.setIsMute(0); +// imConversationSetEntity.setIsTop(0); +// imConversationSetMapper.update(imConversationSetEntity,queryWrapper); +// } + + if(appConfig.getDeleteConversationSyncMode() == 1){ + DeleteConversationPack pack = new DeleteConversationPack(); + pack.setConversationId(req.getConversationId()); + messageProducer.sendToUserExceptClient(req.getFromId(), + ConversationEventCommand.CONVERSATION_DELETE, + pack,new ClientInfo(req.getAppId(),req.getClientType(), + req.getImei())); + } + return ResponseVO.successResponse(); + } + + /** + * @description: 更新会话 置顶or免打扰 + * @param + * @return com.lld.im.common.ResponseVO + * @author lld + */ + public ResponseVO updateConversation(UpdateConversationReq req){ + + + + + if(req.getIsTop() == null && req.getIsMute() == null){ + return ResponseVO.errorResponse(ConversationErrorCode.CONVERSATION_UPDATE_PARAM_ERROR); + } + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("conversation_id",req.getConversationId()); + queryWrapper.eq("app_id",req.getAppId()); + ImConversationSetEntity imConversationSetEntity = imConversationSetMapper.selectOne(queryWrapper); + if(imConversationSetEntity != null){ + long seq = redisSeq.doGetSeq(req.getAppId() + ":" + Constants.SeqConstants.Conversation); + + if(req.getIsMute() != null){ + imConversationSetEntity.setIsTop(req.getIsTop()); + } + if(req.getIsMute() != null){ + imConversationSetEntity.setIsMute(req.getIsMute()); + } + imConversationSetEntity.setSequence(seq); + imConversationSetMapper.update(imConversationSetEntity,queryWrapper); + writeUserSeq.writeUserSeq(req.getAppId(), req.getFromId(), + Constants.SeqConstants.Conversation, seq); + + UpdateConversationPack pack = new UpdateConversationPack(); + pack.setConversationId(req.getConversationId()); + pack.setIsMute(imConversationSetEntity.getIsMute()); + pack.setIsTop(imConversationSetEntity.getIsTop()); + pack.setSequence(seq); + pack.setConversationType(imConversationSetEntity.getConversationType()); + messageProducer.sendToUserExceptClient(req.getFromId(), + ConversationEventCommand.CONVERSATION_UPDATE, + pack,new ClientInfo(req.getAppId(),req.getClientType(), + req.getImei())); + } + return ResponseVO.successResponse(); + } + + public ResponseVO syncConversationSet(SyncReq req) { + if(req.getMaxLimit() > 100){ + req.setMaxLimit(100); + } + + SyncResp resp = new SyncResp<>(); + //seq > req.getseq limit maxLimit + QueryWrapper queryWrapper = + new QueryWrapper<>(); + queryWrapper.eq("from_id",req.getOperater()); + queryWrapper.gt("sequence",req.getLastSequence()); + queryWrapper.eq("app_id",req.getAppId()); + queryWrapper.last(" limit " + req.getMaxLimit()); + queryWrapper.orderByAsc("sequence"); + List list = imConversationSetMapper + .selectList(queryWrapper); + + if(!CollectionUtils.isEmpty(list)){ + ImConversationSetEntity maxSeqEntity = list.get(list.size() - 1); + resp.setDataList(list); + //设置最大seq + Long friendShipMaxSeq = imConversationSetMapper.geConversationSetMaxSeq(req.getAppId(), req.getOperater()); + resp.setMaxSequence(friendShipMaxSeq); + //设置是否拉取完毕 + resp.setCompleted(maxSeqEntity.getSequence() >= friendShipMaxSeq); + return ResponseVO.successResponse(resp); + } + + resp.setCompleted(true); + return ResponseVO.successResponse(resp); + + } +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/dao/mapper/ImFriendShipMapper.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/dao/mapper/ImFriendShipMapper.java index 87e0430..6070330 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/dao/mapper/ImFriendShipMapper.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/dao/mapper/ImFriendShipMapper.java @@ -20,6 +20,7 @@ public interface ImFriendShipMapper extends BaseMapper { " " + "#{id}" + "" + + " and app_id = #{appId} " + "") public List checkFriendShip(CheckFriendShipReq req); @@ -88,4 +89,11 @@ public interface ImFriendShipMapper extends BaseMapper { ) List checkFriendShipBlackBoth(CheckFriendShipReq toId); + @Select(" select max(friend_sequence) from im_friendship where app_id = #{appId} AND from_id = #{userId} ") + Long getFriendShipMaxSeq(Integer appId,String userId); + + @Select( + " select to_id from im_friendship where from_id = #{userId} AND app_id = #{appId} and status = 1 and black = 1 " + ) + List getAllFriendId(String userId,Integer appId); } diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/model/callback/AddFriendAfterCallbackDto.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/model/callback/AddFriendAfterCallbackDto.java new file mode 100644 index 0000000..803725e --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/model/callback/AddFriendAfterCallbackDto.java @@ -0,0 +1,17 @@ +package com.lld.im.service.friendship.model.callback; + +import com.lld.im.service.friendship.model.req.FriendDto; +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class AddFriendAfterCallbackDto { + + private String fromId; + + private FriendDto toItem; +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/model/callback/AddFriendBlackAfterCallbackDto.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/model/callback/AddFriendBlackAfterCallbackDto.java new file mode 100644 index 0000000..a86fd78 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/model/callback/AddFriendBlackAfterCallbackDto.java @@ -0,0 +1,16 @@ +package com.lld.im.service.friendship.model.callback; + +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class AddFriendBlackAfterCallbackDto { + + private String fromId; + + private String toId; +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/model/callback/DeleteFriendAfterCallbackDto.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/model/callback/DeleteFriendAfterCallbackDto.java new file mode 100644 index 0000000..f87dac8 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/model/callback/DeleteFriendAfterCallbackDto.java @@ -0,0 +1,17 @@ +package com.lld.im.service.friendship.model.callback; + +import com.lld.im.service.friendship.model.req.FriendDto; +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class DeleteFriendAfterCallbackDto { + + private String fromId; + + private String toId; +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/ImFriendService.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/ImFriendService.java index 885cfd5..893a4e8 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/ImFriendService.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/ImFriendService.java @@ -2,6 +2,7 @@ package com.lld.im.service.friendship.service; import com.lld.im.common.ResponseVO; import com.lld.im.common.model.RequestBase; +import com.lld.im.common.model.SyncReq; import com.lld.im.service.friendship.model.req.*; import java.util.List; @@ -37,4 +38,7 @@ public interface ImFriendService { public ResponseVO checkBlck(CheckFriendShipReq req); + public ResponseVO syncFriendshipList(SyncReq req); + + public List getAllFriendId(String userId, Integer appId); } diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/ImFriendShipGroupService.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/ImFriendShipGroupService.java index 2a1c268..77120e5 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/ImFriendShipGroupService.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/ImFriendShipGroupService.java @@ -17,5 +17,5 @@ public interface ImFriendShipGroupService { public ResponseVO getGroup(String fromId, String groupName, Integer appId); - + public Long updateSeq(String fromId, String groupName, Integer appId); } diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/impl/ImFriendServiceImpl.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/impl/ImFriendServiceImpl.java index 88c4142..a49d28c 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/impl/ImFriendServiceImpl.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/impl/ImFriendServiceImpl.java @@ -4,22 +4,36 @@ import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.lld.im.codec.pack.friendship.*; +import com.lld.im.codec.proto.Message; import com.lld.im.common.ResponseVO; +import com.lld.im.common.config.AppConfig; +import com.lld.im.common.constant.Constants; import com.lld.im.common.enums.AllowFriendTypeEnum; import com.lld.im.common.enums.CheckFriendShipTypeEnum; import com.lld.im.common.enums.FriendShipErrorCode; import com.lld.im.common.enums.FriendShipStatusEnum; +import com.lld.im.common.enums.command.FriendshipEventCommand; import com.lld.im.common.exception.ApplicationException; import com.lld.im.common.model.RequestBase; +import com.lld.im.common.model.SyncReq; +import com.lld.im.common.model.SyncResp; import com.lld.im.service.friendship.dao.ImFriendShipEntity; import com.lld.im.service.friendship.dao.mapper.ImFriendShipMapper; +import com.lld.im.service.friendship.model.callback.AddFriendAfterCallbackDto; +import com.lld.im.service.friendship.model.callback.AddFriendBlackAfterCallbackDto; +import com.lld.im.service.friendship.model.callback.DeleteFriendAfterCallbackDto; import com.lld.im.service.friendship.model.req.*; import com.lld.im.service.friendship.model.resp.CheckFriendShipResp; import com.lld.im.service.friendship.model.resp.ImportFriendShipResp; import com.lld.im.service.friendship.service.ImFriendService; import com.lld.im.service.friendship.service.ImFriendShipRequestService; +import com.lld.im.service.seq.RedisSeq; import com.lld.im.service.user.dao.ImUserDataEntity; import com.lld.im.service.user.service.ImUserService; +import com.lld.im.service.utils.CallbackService; +import com.lld.im.service.utils.MessageProducer; +import com.lld.im.service.utils.WriteUserSeq; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -46,12 +60,27 @@ public class ImFriendServiceImpl implements ImFriendService { @Autowired ImUserService imUserService; + @Autowired + AppConfig appConfig; + + @Autowired + CallbackService callbackService; + + @Autowired + MessageProducer messageProducer; + @Autowired ImFriendService imFriendService; @Autowired ImFriendShipRequestService imFriendShipRequestService; + @Autowired + RedisSeq redisSeq; + + @Autowired + WriteUserSeq writeUserSeq; + @Override public ResponseVO importFriendShip(ImporFriendShipReq req) { @@ -101,6 +130,16 @@ public class ImFriendServiceImpl implements ImFriendService { return toInfo; } + if(appConfig.isAddFriendBeforeCallback()){ + ResponseVO callbackResp = callbackService + .beforeCallback(req.getAppId(), + Constants.CallbackCommand.AddFriendBefore + ,JSONObject.toJSONString(req)); + if(!callbackResp.isOk()){ + return callbackResp; + } + } + ImUserDataEntity data = toInfo.getData(); if(data.getFriendAllowType() != null && data.getFriendAllowType() == AllowFriendTypeEnum.NOT_NEED.getCode()){ @@ -141,6 +180,23 @@ public class ImFriendServiceImpl implements ImFriendService { } ResponseVO responseVO = this.doUpdate(req.getFromId(), req.getToItem(), req.getAppId()); + if(responseVO.isOk()){ + UpdateFriendPack updateFriendPack = new UpdateFriendPack(); + updateFriendPack.setRemark(req.getToItem().getRemark()); + updateFriendPack.setToId(req.getToItem().getToId()); + messageProducer.sendToUser(req.getFromId(), + req.getClientType(),req.getImei(),FriendshipEventCommand + .FRIEND_UPDATE,updateFriendPack,req.getAppId()); + + if (appConfig.isModifyFriendAfterCallback()) { + AddFriendAfterCallbackDto callbackDto = new AddFriendAfterCallbackDto(); + callbackDto.setFromId(req.getFromId()); + callbackDto.setToItem(req.getToItem()); + callbackService.beforeCallback(req.getAppId(), + Constants.CallbackCommand.UpdateFriendAfter, JSONObject + .toJSONString(callbackDto)); + } + } return responseVO; } @@ -148,9 +204,11 @@ public class ImFriendServiceImpl implements ImFriendService { public ResponseVO doUpdate(String fromId, FriendDto dto,Integer appId){ + long seq = redisSeq.doGetSeq(appId + ":" + Constants.SeqConstants.Friendship); UpdateWrapper updateWrapper = new UpdateWrapper<>(); updateWrapper.lambda().set(ImFriendShipEntity::getAddSource,dto.getAddSource()) .set(ImFriendShipEntity::getExtra,dto.getExtra()) + .set(ImFriendShipEntity::getFriendSequence,seq) .set(ImFriendShipEntity::getRemark,dto.getRemark()) .eq(ImFriendShipEntity::getAppId,appId) .eq(ImFriendShipEntity::getToId,dto.getToId()) @@ -158,13 +216,22 @@ public class ImFriendServiceImpl implements ImFriendService { int update = imFriendShipMapper.update(null, updateWrapper); if(update == 1){ + //之后回调 +// if (appConfig.isModifyFriendAfterCallback()){ +// AddFriendAfterCallbackDto callbackDto = new AddFriendAfterCallbackDto(); +// callbackDto.setFromId(fromId); +// callbackDto.setToItem(dto); +// callbackService.beforeCallback(appId, +// Constants.CallbackCommand.UpdateFriendAfter, JSONObject +// .toJSONString(callbackDto)); +// } + writeUserSeq.writeUserSeq(appId,fromId,Constants.SeqConstants.Friendship,seq); return ResponseVO.successResponse(); } return ResponseVO.errorResponse(); } - @Override @Transactional public ResponseVO doAddFriend(RequestBase requestBase,String fromId, FriendDto dto, Integer appId){ @@ -177,10 +244,13 @@ public class ImFriendServiceImpl implements ImFriendService { query.eq("from_id",fromId); query.eq("to_id",dto.getToId()); ImFriendShipEntity fromItem = imFriendShipMapper.selectOne(query); + long seq = 0L; if(fromItem == null){ //走添加逻辑。 fromItem = new ImFriendShipEntity(); + seq = redisSeq.doGetSeq(appId+":"+Constants.SeqConstants.Friendship); fromItem.setAppId(appId); + fromItem.setFriendSequence(seq); fromItem.setFromId(fromId); // entity.setToId(to); BeanUtils.copyProperties(dto,fromItem); @@ -190,8 +260,10 @@ public class ImFriendServiceImpl implements ImFriendService { if(insert != 1){ return ResponseVO.errorResponse(FriendShipErrorCode.ADD_FRIEND_ERROR); } + writeUserSeq.writeUserSeq(appId,fromId,Constants.SeqConstants.Friendship,seq); } else{ //如果存在则判断状态,如果是已添加,则提示已添加,如果是未添加,则修改状态 + if(fromItem.getStatus() == FriendShipStatusEnum.FRIEND_STATUS_NORMAL.getCode()){ return ResponseVO.errorResponse(FriendShipErrorCode.TO_IS_YOUR_FRIEND); } else{ @@ -208,12 +280,15 @@ public class ImFriendServiceImpl implements ImFriendService { if(StringUtils.isNotBlank(dto.getExtra())){ update.setExtra(dto.getExtra()); } + seq = redisSeq.doGetSeq(appId+":"+Constants.SeqConstants.Friendship); + update.setFriendSequence(seq); update.setStatus(FriendShipStatusEnum.FRIEND_STATUS_NORMAL.getCode()); int result = imFriendShipMapper.update(update, query); if(result != 1){ return ResponseVO.errorResponse(FriendShipErrorCode.ADD_FRIEND_ERROR); } + writeUserSeq.writeUserSeq(appId,fromId,Constants.SeqConstants.Friendship,seq); } } @@ -229,18 +304,53 @@ public class ImFriendServiceImpl implements ImFriendService { toItem.setFromId(dto.getToId()); BeanUtils.copyProperties(dto,toItem); toItem.setToId(fromId); + toItem.setFriendSequence(seq); toItem.setStatus(FriendShipStatusEnum.FRIEND_STATUS_NORMAL.getCode()); toItem.setCreateTime(System.currentTimeMillis()); // toItem.setBlack(FriendShipStatusEnum.BLACK_STATUS_NORMAL.getCode()); int insert = imFriendShipMapper.insert(toItem); + writeUserSeq.writeUserSeq(appId,dto.getToId(),Constants.SeqConstants.Friendship,seq); }else{ if(FriendShipStatusEnum.FRIEND_STATUS_NORMAL.getCode() != toItem.getStatus()){ ImFriendShipEntity update = new ImFriendShipEntity(); + update.setFriendSequence(seq); update.setStatus(FriendShipStatusEnum.FRIEND_STATUS_NORMAL.getCode()); imFriendShipMapper.update(update,toQuery); + writeUserSeq.writeUserSeq(appId,dto.getToId(),Constants.SeqConstants.Friendship,seq); } } + + //发送给from + AddFriendPack addFriendPack = new AddFriendPack(); + BeanUtils.copyProperties(fromItem,addFriendPack); + addFriendPack.setSequence(seq); + if(requestBase != null){ + messageProducer.sendToUser(fromId,requestBase.getClientType(), + requestBase.getImei(), FriendshipEventCommand.FRIEND_ADD,addFriendPack + ,requestBase.getAppId()); + }else { + messageProducer.sendToUser(fromId, + FriendshipEventCommand.FRIEND_ADD,addFriendPack + ,requestBase.getAppId()); + } + + AddFriendPack addFriendToPack = new AddFriendPack(); + BeanUtils.copyProperties(toItem,addFriendPack); + messageProducer.sendToUser(toItem.getFromId(), + FriendshipEventCommand.FRIEND_ADD,addFriendToPack + ,requestBase.getAppId()); + + //之后回调 + if (appConfig.isAddFriendAfterCallback()){ + AddFriendAfterCallbackDto callbackDto = new AddFriendAfterCallbackDto(); + callbackDto.setFromId(fromId); + callbackDto.setToItem(dto); + callbackService.beforeCallback(appId, + Constants.CallbackCommand.AddFriendAfter, JSONObject + .toJSONString(callbackDto)); + } + return ResponseVO.successResponse(); } @@ -258,8 +368,29 @@ public class ImFriendServiceImpl implements ImFriendService { }else{ if(fromItem.getStatus() != null && fromItem.getStatus() == FriendShipStatusEnum.FRIEND_STATUS_NORMAL.getCode()){ ImFriendShipEntity update = new ImFriendShipEntity(); + long seq = redisSeq.doGetSeq(req.getAppId() + ":" + Constants.SeqConstants.Friendship); + update.setFriendSequence(seq); update.setStatus(FriendShipStatusEnum.FRIEND_STATUS_DELETE.getCode()); imFriendShipMapper.update(update,query); + writeUserSeq.writeUserSeq(req.getAppId(),req.getFromId(),Constants.SeqConstants.Friendship,seq); + DeleteFriendPack deleteFriendPack = new DeleteFriendPack(); + deleteFriendPack.setFromId(req.getFromId()); + deleteFriendPack.setSequence(seq); + deleteFriendPack.setToId(req.getToId()); + messageProducer.sendToUser(req.getFromId(), + req.getClientType(), req.getImei(), + FriendshipEventCommand.FRIEND_DELETE, + deleteFriendPack, req.getAppId()); + + //之后回调 + if (appConfig.isAddFriendAfterCallback()){ + DeleteFriendAfterCallbackDto callbackDto = new DeleteFriendAfterCallbackDto(); + callbackDto.setFromId(req.getFromId()); + callbackDto.setToId(req.getToId()); + callbackService.beforeCallback(req.getAppId(), + Constants.CallbackCommand.DeleteFriendAfter, JSONObject + .toJSONString(callbackDto)); + } }else{ return ResponseVO.errorResponse(FriendShipErrorCode.FRIEND_IS_DELETED); @@ -278,6 +409,12 @@ public class ImFriendServiceImpl implements ImFriendService { ImFriendShipEntity update = new ImFriendShipEntity(); update.setStatus(FriendShipStatusEnum.FRIEND_STATUS_DELETE.getCode()); imFriendShipMapper.update(update,query); + + DeleteAllFriendPack deleteFriendPack = new DeleteAllFriendPack(); + deleteFriendPack.setFromId(req.getFromId()); + messageProducer.sendToUser(req.getFromId(), req.getClientType(), req.getImei(), FriendshipEventCommand.FRIEND_ALL_DELETE, + deleteFriendPack, req.getAppId()); + return ResponseVO.successResponse(); } @@ -335,6 +472,42 @@ public class ImFriendServiceImpl implements ImFriendService { return ResponseVO.successResponse(result); } + @Override + public ResponseVO syncFriendshipList(SyncReq req) { + + if(req.getMaxLimit() > 100){ + req.setMaxLimit(100); + } + + SyncResp resp = new SyncResp<>(); + //seq > req.getseq limit maxLimit + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("from_id",req.getOperater()); + queryWrapper.gt("friend_sequence",req.getLastSequence()); + queryWrapper.eq("app_id",req.getAppId()); + queryWrapper.last(" limit " + req.getMaxLimit()); + queryWrapper.orderByAsc("friend_sequence"); + List list = imFriendShipMapper.selectList(queryWrapper); + + if(!CollectionUtils.isEmpty(list)){ + ImFriendShipEntity maxSeqEntity = list.get(list.size() - 1); + resp.setDataList(list); + //设置最大seq + Long friendShipMaxSeq = imFriendShipMapper.getFriendShipMaxSeq(req.getAppId(), req.getOperater()); + resp.setMaxSequence(friendShipMaxSeq); + //设置是否拉取完毕 + resp.setCompleted(maxSeqEntity.getFriendSequence() >= friendShipMaxSeq); + return ResponseVO.successResponse(resp); + } + + resp.setCompleted(true); + return ResponseVO.successResponse(resp); + } + + @Override + public List getAllFriendId(String userId, Integer appId) { + return imFriendShipMapper.getAllFriendId(userId,appId); + } @Override public ResponseVO addBlack(AddFriendShipBlackReq req) { @@ -354,11 +527,15 @@ public class ImFriendServiceImpl implements ImFriendService { query.eq("to_id",req.getToId()); ImFriendShipEntity fromItem = imFriendShipMapper.selectOne(query); + Long seq = 0L; if(fromItem == null){ //走添加逻辑。 + seq = redisSeq.doGetSeq(req.getAppId() + ":" + Constants.SeqConstants.Friendship); + fromItem = new ImFriendShipEntity(); fromItem.setFromId(req.getFromId()); fromItem.setToId(req.getToId()); + fromItem.setFriendSequence(seq); fromItem.setAppId(req.getAppId()); fromItem.setBlack(FriendShipStatusEnum.BLACK_STATUS_BLACKED.getCode()); fromItem.setCreateTime(System.currentTimeMillis()); @@ -366,21 +543,47 @@ public class ImFriendServiceImpl implements ImFriendService { if(insert != 1){ return ResponseVO.errorResponse(FriendShipErrorCode.ADD_FRIEND_ERROR); } + writeUserSeq.writeUserSeq(req.getAppId(),req.getFromId(),Constants.SeqConstants.Friendship,seq); } else{ //如果存在则判断状态,如果是拉黑,则提示已拉黑,如果是未拉黑,则修改状态 if(fromItem.getBlack() != null && fromItem.getBlack() == FriendShipStatusEnum.BLACK_STATUS_BLACKED.getCode()){ return ResponseVO.errorResponse(FriendShipErrorCode.FRIEND_IS_BLACK); - } else { + } + + else { + seq = redisSeq.doGetSeq(req.getAppId() + ":" + Constants.SeqConstants.Friendship); + ImFriendShipEntity update = new ImFriendShipEntity(); + update.setFriendSequence(seq); update.setBlack(FriendShipStatusEnum.BLACK_STATUS_BLACKED.getCode()); int result = imFriendShipMapper.update(update, query); if(result != 1){ return ResponseVO.errorResponse(FriendShipErrorCode.ADD_BLACK_ERROR); } + writeUserSeq.writeUserSeq(req.getAppId(),req.getFromId(),Constants.SeqConstants.Friendship,seq); + } } + AddFriendBlackPack addFriendBlackPack = new AddFriendBlackPack(); + addFriendBlackPack.setFromId(req.getFromId()); + addFriendBlackPack.setSequence(seq); + addFriendBlackPack.setToId(req.getToId()); + //发送tcp通知 + messageProducer.sendToUser(req.getFromId(), req.getClientType(), req.getImei(), + FriendshipEventCommand.FRIEND_BLACK_ADD, addFriendBlackPack, req.getAppId()); + + //之后回调 + if (appConfig.isAddFriendShipBlackAfterCallback()){ + AddFriendBlackAfterCallbackDto callbackDto = new AddFriendBlackAfterCallbackDto(); + callbackDto.setFromId(req.getFromId()); + callbackDto.setToId(req.getToId()); + callbackService.beforeCallback(req.getAppId(), + Constants.CallbackCommand.AddBlackAfter, JSONObject + .toJSONString(callbackDto)); + } + return ResponseVO.successResponse(); } @@ -395,13 +598,32 @@ public class ImFriendServiceImpl implements ImFriendService { throw new ApplicationException(FriendShipErrorCode.FRIEND_IS_NOT_YOUR_BLACK); } + long seq = redisSeq.doGetSeq(req.getAppId() + ":" + Constants.SeqConstants.Friendship); + ImFriendShipEntity update = new ImFriendShipEntity(); + update.setFriendSequence(seq); update.setBlack(FriendShipStatusEnum.BLACK_STATUS_NORMAL.getCode()); int update1 = imFriendShipMapper.update(update, queryFrom); if(update1 == 1){ - return ResponseVO.successResponse(); + writeUserSeq.writeUserSeq(req.getAppId(),req.getFromId(),Constants.SeqConstants.Friendship,seq); + DeleteBlackPack deleteFriendPack = new DeleteBlackPack(); + deleteFriendPack.setFromId(req.getFromId()); + deleteFriendPack.setSequence(seq); + deleteFriendPack.setToId(req.getToId()); + messageProducer.sendToUser(req.getFromId(), req.getClientType(), req.getImei(), FriendshipEventCommand.FRIEND_BLACK_DELETE, + deleteFriendPack, req.getAppId()); + + //之后回调 + if (appConfig.isAddFriendShipBlackAfterCallback()){ + AddFriendBlackAfterCallbackDto callbackDto = new AddFriendBlackAfterCallbackDto(); + callbackDto.setFromId(req.getFromId()); + callbackDto.setToId(req.getToId()); + callbackService.beforeCallback(req.getAppId(), + Constants.CallbackCommand.DeleteBlack, JSONObject + .toJSONString(callbackDto)); + } } - return ResponseVO.errorResponse(); + return ResponseVO.successResponse(); } @Override @@ -436,4 +658,7 @@ public class ImFriendServiceImpl implements ImFriendService { return ResponseVO.successResponse(resp); } + + + } diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/impl/ImFriendShipGroupMemberServiceImpl.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/impl/ImFriendShipGroupMemberServiceImpl.java index 0431113..7191367 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/impl/ImFriendShipGroupMemberServiceImpl.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/impl/ImFriendShipGroupMemberServiceImpl.java @@ -1,7 +1,12 @@ package com.lld.im.service.friendship.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.lld.im.codec.pack.friendship.AddFriendGroupMemberPack; +import com.lld.im.codec.pack.friendship.DeleteFriendGroupMemberPack; +import com.lld.im.codec.proto.Message; import com.lld.im.common.ResponseVO; +import com.lld.im.common.enums.command.FriendshipEventCommand; +import com.lld.im.common.model.ClientInfo; import com.lld.im.service.friendship.dao.ImFriendShipGroupEntity; import com.lld.im.service.friendship.dao.ImFriendShipGroupMemberEntity; import com.lld.im.service.friendship.dao.mapper.ImFriendShipGroupMemberMapper; @@ -11,6 +16,7 @@ import com.lld.im.service.friendship.service.ImFriendShipGroupMemberService; import com.lld.im.service.friendship.service.ImFriendShipGroupService; import com.lld.im.service.user.dao.ImUserDataEntity; import com.lld.im.service.user.service.ImUserService; +import com.lld.im.service.utils.MessageProducer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -38,6 +44,9 @@ public class ImFriendShipGroupMemberServiceImpl @Autowired ImFriendShipGroupMemberService thisService; + @Autowired + MessageProducer messageProducer; + @Override @Transactional public ResponseVO addGroupMember(AddFriendShipGroupMemberReq req) { @@ -59,6 +68,15 @@ public class ImFriendShipGroupMemberServiceImpl } } + Long seq = imFriendShipGroupService.updateSeq(req.getFromId(), req.getGroupName(), req.getAppId()); + AddFriendGroupMemberPack pack = new AddFriendGroupMemberPack(); + pack.setFromId(req.getFromId()); + pack.setGroupName(req.getGroupName()); + pack.setToIds(successId); + pack.setSequence(seq); + messageProducer.sendToUserExceptClient(req.getFromId(), FriendshipEventCommand.FRIEND_GROUP_MEMBER_ADD, + pack,new ClientInfo(req.getAppId(),req.getClientType(),req.getImei())); + return ResponseVO.successResponse(successId); } @@ -70,17 +88,26 @@ public class ImFriendShipGroupMemberServiceImpl return group; } - ArrayList list = new ArrayList(); + List successId = new ArrayList<>(); for (String toId : req.getToIds()) { ResponseVO singleUserInfo = imUserService.getSingleUserInfo(toId, req.getAppId()); if(singleUserInfo.isOk()){ int i = deleteGroupMember(group.getData().getGroupId(), toId); if(i == 1){ - list.add(toId); + successId.add(toId); } } } - return ResponseVO.successResponse(list); + + Long seq = imFriendShipGroupService.updateSeq(req.getFromId(), req.getGroupName(), req.getAppId()); + DeleteFriendGroupMemberPack pack = new DeleteFriendGroupMemberPack(); + pack.setFromId(req.getFromId()); + pack.setGroupName(req.getGroupName()); + pack.setToIds(successId); + pack.setSequence(seq); + messageProducer.sendToUserExceptClient(req.getFromId(), FriendshipEventCommand.FRIEND_GROUP_MEMBER_DELETE, + pack,new ClientInfo(req.getAppId(),req.getClientType(),req.getImei())); + return ResponseVO.successResponse(successId); } @Override @@ -88,6 +115,7 @@ public class ImFriendShipGroupMemberServiceImpl ImFriendShipGroupMemberEntity imFriendShipGroupMemberEntity = new ImFriendShipGroupMemberEntity(); imFriendShipGroupMemberEntity.setGroupId(groupId); imFriendShipGroupMemberEntity.setToId(toId); + try { int insert = imFriendShipGroupMemberMapper.insert(imFriendShipGroupMemberEntity); return insert; diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/impl/ImFriendShipGroupServiceImpl.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/impl/ImFriendShipGroupServiceImpl.java index 350e226..5f7b26b 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/impl/ImFriendShipGroupServiceImpl.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/impl/ImFriendShipGroupServiceImpl.java @@ -2,9 +2,14 @@ package com.lld.im.service.friendship.service.impl; import cn.hutool.core.collection.CollectionUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.lld.im.codec.pack.friendship.AddFriendGroupPack; +import com.lld.im.codec.pack.friendship.DeleteFriendGroupPack; import com.lld.im.common.ResponseVO; +import com.lld.im.common.constant.Constants; import com.lld.im.common.enums.DelFlagEnum; import com.lld.im.common.enums.FriendShipErrorCode; +import com.lld.im.common.enums.command.FriendshipEventCommand; +import com.lld.im.common.model.ClientInfo; import com.lld.im.service.friendship.dao.ImFriendShipGroupEntity; import com.lld.im.service.friendship.dao.mapper.ImFriendShipGroupMapper; import com.lld.im.service.friendship.model.req.AddFriendShipGroupMemberReq; @@ -12,7 +17,10 @@ import com.lld.im.service.friendship.model.req.AddFriendShipGroupReq; import com.lld.im.service.friendship.model.req.DeleteFriendShipGroupReq; import com.lld.im.service.friendship.service.ImFriendShipGroupMemberService; import com.lld.im.service.friendship.service.ImFriendShipGroupService; +import com.lld.im.service.seq.RedisSeq; import com.lld.im.service.user.service.ImUserService; +import com.lld.im.service.utils.MessageProducer; +import com.lld.im.service.utils.WriteUserSeq; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; @@ -30,6 +38,15 @@ public class ImFriendShipGroupServiceImpl implements ImFriendShipGroupService { @Autowired ImUserService imUserService; + @Autowired + RedisSeq redisSeq; + + @Autowired + MessageProducer messageProducer; + + @Autowired + WriteUserSeq writeUserSeq; + @Override @Transactional public ResponseVO addGroup(AddFriendShipGroupReq req) { @@ -52,6 +69,8 @@ public class ImFriendShipGroupServiceImpl implements ImFriendShipGroupService { insert.setCreateTime(System.currentTimeMillis()); insert.setDelFlag(DelFlagEnum.NORMAL.getCode()); insert.setGroupName(req.getGroupName()); + long seq = redisSeq.doGetSeq(req.getAppId() + ":" + Constants.SeqConstants.FriendshipGroup); + insert.setSequence(seq); insert.setFromId(req.getFromId()); try { int insert1 = imFriendShipGroupMapper.insert(insert); @@ -59,7 +78,6 @@ public class ImFriendShipGroupServiceImpl implements ImFriendShipGroupService { if (insert1 != 1) { return ResponseVO.errorResponse(FriendShipErrorCode.FRIEND_SHIP_GROUP_CREATE_ERROR); } - if (insert1 == 1 && CollectionUtil.isNotEmpty(req.getToIds())) { AddFriendShipGroupMemberReq addFriendShipGroupMemberReq = new AddFriendShipGroupMemberReq(); addFriendShipGroupMemberReq.setFromId(req.getFromId()); @@ -73,6 +91,16 @@ public class ImFriendShipGroupServiceImpl implements ImFriendShipGroupService { e.getStackTrace(); return ResponseVO.errorResponse(FriendShipErrorCode.FRIEND_SHIP_GROUP_IS_EXIST); } + + AddFriendGroupPack addFriendGropPack = new AddFriendGroupPack(); + addFriendGropPack.setFromId(req.getFromId()); + addFriendGropPack.setGroupName(req.getGroupName()); + addFriendGropPack.setSequence(seq); + messageProducer.sendToUserExceptClient(req.getFromId(), FriendshipEventCommand.FRIEND_GROUP_ADD, + addFriendGropPack,new ClientInfo(req.getAppId(),req.getClientType(),req.getImei())); + //写入seq + writeUserSeq.writeUserSeq(req.getAppId(), req.getFromId(), Constants.SeqConstants.FriendshipGroup, seq); + return ResponseVO.successResponse(); } @@ -90,12 +118,22 @@ public class ImFriendShipGroupServiceImpl implements ImFriendShipGroupService { ImFriendShipGroupEntity entity = imFriendShipGroupMapper.selectOne(query); if (entity != null) { + long seq = redisSeq.doGetSeq(req.getAppId() + ":" + Constants.SeqConstants.FriendshipGroup); ImFriendShipGroupEntity update = new ImFriendShipGroupEntity(); + update.setSequence(seq); update.setGroupId(entity.getGroupId()); update.setDelFlag(DelFlagEnum.DELETE.getCode()); imFriendShipGroupMapper.updateById(update); imFriendShipGroupMemberService.clearGroupMember(entity.getGroupId()); - + DeleteFriendGroupPack deleteFriendGroupPack = new DeleteFriendGroupPack(); + deleteFriendGroupPack.setFromId(req.getFromId()); + deleteFriendGroupPack.setGroupName(groupName); + deleteFriendGroupPack.setSequence(seq); + //TCP通知 + messageProducer.sendToUserExceptClient(req.getFromId(), FriendshipEventCommand.FRIEND_GROUP_DELETE, + deleteFriendGroupPack,new ClientInfo(req.getAppId(),req.getClientType(),req.getImei())); + //写入seq + writeUserSeq.writeUserSeq(req.getAppId(), req.getFromId(), Constants.SeqConstants.FriendshipGroup, seq); } } return ResponseVO.successResponse(); @@ -116,4 +154,22 @@ public class ImFriendShipGroupServiceImpl implements ImFriendShipGroupService { return ResponseVO.successResponse(entity); } + @Override + public Long updateSeq(String fromId, String groupName, Integer appId) { + QueryWrapper query = new QueryWrapper<>(); + query.eq("group_name", groupName); + query.eq("app_id", appId); + query.eq("from_id", fromId); + + ImFriendShipGroupEntity entity = imFriendShipGroupMapper.selectOne(query); + + long seq = redisSeq.doGetSeq(appId + ":" + Constants.SeqConstants.FriendshipGroup); + + ImFriendShipGroupEntity group = new ImFriendShipGroupEntity(); + group.setGroupId(entity.getGroupId()); + group.setSequence(seq); + imFriendShipGroupMapper.updateById(group); + return seq; + } + } diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/impl/ImFriendShipRequestServiceImpl.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/impl/ImFriendShipRequestServiceImpl.java index d806731..952006f 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/impl/ImFriendShipRequestServiceImpl.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/friendship/service/impl/ImFriendShipRequestServiceImpl.java @@ -1,9 +1,14 @@ package com.lld.im.service.friendship.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.lld.im.codec.pack.friendship.ApproverFriendRequestPack; +import com.lld.im.codec.pack.friendship.ReadAllFriendRequestPack; +import com.lld.im.codec.proto.Message; import com.lld.im.common.ResponseVO; +import com.lld.im.common.constant.Constants; import com.lld.im.common.enums.ApproverFriendRequestStatusEnum; import com.lld.im.common.enums.FriendShipErrorCode; +import com.lld.im.common.enums.command.FriendshipEventCommand; import com.lld.im.common.exception.ApplicationException; import com.lld.im.service.friendship.dao.ImFriendShipRequestEntity; import com.lld.im.service.friendship.dao.mapper.ImFriendShipRequestMapper; @@ -12,6 +17,9 @@ import com.lld.im.service.friendship.model.req.FriendDto; import com.lld.im.service.friendship.model.req.ReadFriendShipRequestReq; import com.lld.im.service.friendship.service.ImFriendService; import com.lld.im.service.friendship.service.ImFriendShipRequestService; +import com.lld.im.service.seq.RedisSeq; +import com.lld.im.service.utils.MessageProducer; +import com.lld.im.service.utils.WriteUserSeq; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -29,6 +37,15 @@ public class ImFriendShipRequestServiceImpl implements ImFriendShipRequestServic @Autowired ImFriendService imFriendShipService; + @Autowired + MessageProducer messageProducer; + + @Autowired + RedisSeq redisSeq; + + @Autowired + WriteUserSeq writeUserSeq; + @Override public ResponseVO getFriendRequest(String fromId, Integer appId) { @@ -52,10 +69,14 @@ public class ImFriendShipRequestServiceImpl implements ImFriendShipRequestServic queryWrapper.eq("to_id",dto.getToId()); ImFriendShipRequestEntity request = imFriendShipRequestMapper.selectOne(queryWrapper); + long seq = redisSeq.doGetSeq(appId+":"+ + Constants.SeqConstants.FriendshipRequest); + if(request == null){ request = new ImFriendShipRequestEntity(); request.setAddSource(dto.getAddSource()); request.setAddWording(dto.getAddWording()); + request.setSequence(seq); request.setAppId(appId); request.setFromId(fromId); request.setToId(dto.getToId()); @@ -76,11 +97,19 @@ public class ImFriendShipRequestServiceImpl implements ImFriendShipRequestServic if(StringUtils.isNotBlank(dto.getAddWording())){ request.setAddWording(dto.getAddWording()); } + request.setSequence(seq); request.setApproveStatus(0); request.setReadStatus(0); imFriendShipRequestMapper.updateById(request); } + writeUserSeq.writeUserSeq(appId,dto.getToId(), + Constants.SeqConstants.FriendshipRequest,seq); + + //发送好友申请的tcp给接收方 + messageProducer.sendToUser(dto.getToId(), + null, "", FriendshipEventCommand.FRIEND_REQUEST, + request, appId); return ResponseVO.successResponse(); } @@ -98,12 +127,19 @@ public class ImFriendShipRequestServiceImpl implements ImFriendShipRequestServic throw new ApplicationException(FriendShipErrorCode.NOT_APPROVER_OTHER_MAN_REQUEST); } + long seq = redisSeq.doGetSeq(req.getAppId()+":"+ + Constants.SeqConstants.FriendshipRequest); + ImFriendShipRequestEntity update = new ImFriendShipRequestEntity(); update.setApproveStatus(req.getStatus()); update.setUpdateTime(System.currentTimeMillis()); + update.setSequence(seq); update.setId(req.getId()); imFriendShipRequestMapper.updateById(update); + writeUserSeq.writeUserSeq(req.getAppId(),req.getOperater(), + Constants.SeqConstants.FriendshipRequest,seq); + if(ApproverFriendRequestStatusEnum.AGREE.getCode() == req.getStatus()){ //同意 ===> 去执行添加好友逻辑 FriendDto dto = new FriendDto(); @@ -121,6 +157,12 @@ public class ImFriendShipRequestServiceImpl implements ImFriendShipRequestServic } } + ApproverFriendRequestPack approverFriendRequestPack = new ApproverFriendRequestPack(); + approverFriendRequestPack.setId(req.getId()); + approverFriendRequestPack.setSequence(seq); + approverFriendRequestPack.setStatus(req.getStatus()); + messageProducer.sendToUser(imFriendShipRequestEntity.getToId(),req.getClientType(),req.getImei(), FriendshipEventCommand + .FRIEND_REQUEST_APPROVER,approverFriendRequestPack,req.getAppId()); return ResponseVO.successResponse(); } @@ -130,9 +172,20 @@ public class ImFriendShipRequestServiceImpl implements ImFriendShipRequestServic query.eq("app_id", req.getAppId()); query.eq("to_id", req.getFromId()); + long seq = redisSeq.doGetSeq(req.getAppId()+":"+ + Constants.SeqConstants.FriendshipRequest); ImFriendShipRequestEntity update = new ImFriendShipRequestEntity(); update.setReadStatus(1); + update.setSequence(seq); imFriendShipRequestMapper.update(update, query); + writeUserSeq.writeUserSeq(req.getAppId(),req.getOperater(), + Constants.SeqConstants.FriendshipRequest,seq); + //TCP通知 + ReadAllFriendRequestPack readAllFriendRequestPack = new ReadAllFriendRequestPack(); + readAllFriendRequestPack.setFromId(req.getFromId()); + readAllFriendRequestPack.setSequence(seq); + messageProducer.sendToUser(req.getFromId(),req.getClientType(),req.getImei(),FriendshipEventCommand + .FRIEND_REQUEST_READ,readAllFriendRequestPack,req.getAppId()); return ResponseVO.successResponse(); } diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/controller/ImGroupController.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/controller/ImGroupController.java index f1bd328..a3cc8f7 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/controller/ImGroupController.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/controller/ImGroupController.java @@ -1,7 +1,9 @@ package com.lld.im.service.group.controller; import com.lld.im.common.ResponseVO; +import com.lld.im.common.model.SyncReq; import com.lld.im.service.group.model.req.*; +import com.lld.im.service.group.service.GroupMessageService; import com.lld.im.service.group.service.ImGroupService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; @@ -21,6 +23,9 @@ public class ImGroupController { @Autowired ImGroupService groupService; + @Autowired + GroupMessageService groupMessageService; + @RequestMapping("/importGroup") public ResponseVO importGroup(@RequestBody @Validated ImportGroupReq req, Integer appId, String identifier) { req.setAppId(appId); @@ -77,4 +82,19 @@ public class ImGroupController { return groupService.muteGroup(req); } + @RequestMapping("/sendMessage") + public ResponseVO sendMessage(@RequestBody @Validated SendGroupMessageReq + req, Integer appId, + String identifier) { + req.setAppId(appId); + req.setOperater(identifier); + return ResponseVO.successResponse(groupMessageService.send(req)); + } + + @RequestMapping("/syncJoinedGroup") + public ResponseVO syncJoinedGroup(@RequestBody @Validated SyncReq req, Integer appId, String identifier) { + req.setAppId(appId); + return groupService.syncJoinedGroupList(req); + } + } diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/dao/mapper/ImGroupMapper.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/dao/mapper/ImGroupMapper.java index c361fcc..3d1a88b 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/dao/mapper/ImGroupMapper.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/dao/mapper/ImGroupMapper.java @@ -10,4 +10,17 @@ import java.util.Collection; @Repository public interface ImGroupMapper extends BaseMapper { + /** + * @description 获取加入的群的最大seq + * @author chackylee + * @param [] + * @return java.lang.Long + */ + @Select(" ") + Long getGroupMaxSeq(Collection groupId, Integer appId); } diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/dao/mapper/ImGroupMemberMapper.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/dao/mapper/ImGroupMemberMapper.java index e7a9311..819d095 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/dao/mapper/ImGroupMemberMapper.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/dao/mapper/ImGroupMemberMapper.java @@ -16,6 +16,8 @@ public interface ImGroupMemberMapper extends BaseMapper { @Select("select group_id from im_group_member where app_id = #{appId} AND member_id = #{memberId} ") public List getJoinedGroupId(Integer appId, String memberId); + @Select("select group_id from im_group_member where app_id = #{appId} AND member_id = #{memberId} and role != #{role}" ) + public List syncJoinedGroupId(Integer appId, String memberId, int role); @Results({ diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/model/callback/AddMemberAfterCallback.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/model/callback/AddMemberAfterCallback.java new file mode 100644 index 0000000..0e186c4 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/model/callback/AddMemberAfterCallback.java @@ -0,0 +1,19 @@ +package com.lld.im.service.group.model.callback; + +import com.lld.im.service.group.model.resp.AddMemberResp; +import lombok.Data; + +import java.util.List; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class AddMemberAfterCallback { + private String groupId; + private Integer groupType; + private String operater; + private List memberId; +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/model/callback/DestroyGroupCallbackDto.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/model/callback/DestroyGroupCallbackDto.java new file mode 100644 index 0000000..ef99d47 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/model/callback/DestroyGroupCallbackDto.java @@ -0,0 +1,14 @@ +package com.lld.im.service.group.model.callback; + +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class DestroyGroupCallbackDto { + + private String groupId; +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/mq/GroupChatOperateReceiver.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/mq/GroupChatOperateReceiver.java new file mode 100644 index 0000000..159dd7b --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/mq/GroupChatOperateReceiver.java @@ -0,0 +1,86 @@ +package com.lld.im.service.group.mq; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.TypeReference; +import com.lld.im.common.constant.Constants; +import com.lld.im.common.enums.command.GroupEventCommand; +import com.lld.im.common.enums.command.MessageCommand; +import com.lld.im.common.model.message.GroupChatMessageContent; +import com.lld.im.common.model.message.MessageReadedContent; +import com.lld.im.service.group.service.GroupMessageService; +import com.lld.im.service.message.service.MessageSyncService; +import com.lld.im.service.message.service.P2PMessageService; +import com.rabbitmq.client.Channel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.annotation.Exchange; +import org.springframework.amqp.rabbit.annotation.Queue; +import org.springframework.amqp.rabbit.annotation.QueueBinding; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.handler.annotation.Headers; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Component +public class GroupChatOperateReceiver { + + private static Logger logger = LoggerFactory.getLogger(GroupChatOperateReceiver.class); + +// @Autowired +// P2PMessageService p2PMessageService; + @Autowired + GroupMessageService groupMessageService; + + @Autowired + MessageSyncService messageSyncService; + + @RabbitListener( + bindings = @QueueBinding( + value = @Queue(value = Constants.RabbitConstants.Im2GroupService,durable = "true"), + exchange = @Exchange(value = Constants.RabbitConstants.Im2GroupService,durable = "true") + ),concurrency = "1" + ) + public void onChatMessage(@Payload Message message, + @Headers Map headers, + Channel channel) throws Exception { + String msg = new String(message.getBody(),"utf-8"); + logger.info("CHAT MSG FORM QUEUE ::: {}", msg); + Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG); + try { + JSONObject jsonObject = JSON.parseObject(msg); + Integer command = jsonObject.getInteger("command"); + if(command.equals(GroupEventCommand.MSG_GROUP.getCommand())){ + //处理消息 + GroupChatMessageContent messageContent + = jsonObject.toJavaObject(GroupChatMessageContent.class); +// p2PMessageService.process(messageContent); + groupMessageService.process(messageContent); + }else if (command.equals(GroupEventCommand.MSG_GROUP_READED.getCommand())) { + MessageReadedContent messageReaded = JSON.parseObject(msg, new TypeReference() { + }.getType()); + messageSyncService.groupReadMark(messageReaded); + } + channel.basicAck(deliveryTag, false); + }catch (Exception e){ + logger.error("处理消息出现异常:{}", e.getMessage()); + logger.error("RMQ_CHAT_TRAN_ERROR", e); + logger.error("NACK_MSG:{}", msg); + //第一个false 表示不批量拒绝,第二个false表示不重回队列 + channel.basicNack(deliveryTag, false, false); + } + + } + + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/GroupMessageService.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/GroupMessageService.java new file mode 100644 index 0000000..961d1e1 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/GroupMessageService.java @@ -0,0 +1,164 @@ +package com.lld.im.service.group.service; + +import com.lld.im.codec.pack.message.ChatMessageAck; +import com.lld.im.common.ResponseVO; +import com.lld.im.common.constant.Constants; +import com.lld.im.common.enums.command.GroupEventCommand; +import com.lld.im.common.enums.command.MessageCommand; +import com.lld.im.common.model.ClientInfo; +import com.lld.im.common.model.message.GroupChatMessageContent; +import com.lld.im.common.model.message.MessageContent; +import com.lld.im.common.model.message.OfflineMessageContent; +import com.lld.im.service.group.model.req.SendGroupMessageReq; +import com.lld.im.service.message.model.resp.SendMessageResp; +import com.lld.im.service.message.service.CheckSendMessageService; +import com.lld.im.service.message.service.MessageStoreService; +import com.lld.im.service.seq.RedisSeq; +import com.lld.im.service.utils.MessageProducer; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Service +public class GroupMessageService { + + @Autowired + CheckSendMessageService checkSendMessageService; + + @Autowired + MessageProducer messageProducer; + + @Autowired + ImGroupMemberService imGroupMemberService; + + @Autowired + MessageStoreService messageStoreService; + + @Autowired + RedisSeq redisSeq; + + private final ThreadPoolExecutor threadPoolExecutor; + + { + AtomicInteger num = new AtomicInteger(0); + threadPoolExecutor = new ThreadPoolExecutor(8, 8, 60, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(1000), new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r); + thread.setDaemon(true); + thread.setName("message-group-thread-" + num.getAndIncrement()); + return thread; + } + }); + } + + public void process(GroupChatMessageContent messageContent){ + String fromId = messageContent.getFromId(); + String groupId = messageContent.getGroupId(); + Integer appId = messageContent.getAppId(); + //前置校验 + //这个用户是否被禁言 是否被禁用 + //发送方和接收方是否是好友 + GroupChatMessageContent messageFromMessageIdCache = messageStoreService.getMessageFromMessageIdCache(messageContent.getAppId(), + messageContent.getMessageId(), GroupChatMessageContent.class); + if(messageFromMessageIdCache != null){ + threadPoolExecutor.execute(() ->{ + //1.回ack成功给自己 + ack(messageContent,ResponseVO.successResponse()); + //2.发消息给同步在线端 + syncToSender(messageContent,messageContent); + //3.发消息给对方在线端 + dispatchMessage(messageContent); + }); + } + long seq = redisSeq.doGetSeq(messageContent.getAppId() + ":" + Constants.SeqConstants.GroupMessage + + messageContent.getGroupId()); + messageContent.setMessageSequence(seq); + threadPoolExecutor.execute(() ->{ + messageStoreService.storeGroupMessage(messageContent); + + List groupMemberId = imGroupMemberService.getGroupMemberId(messageContent.getGroupId(), + messageContent.getAppId()); + messageContent.setMemberId(groupMemberId); + + OfflineMessageContent offlineMessageContent = new OfflineMessageContent(); + BeanUtils.copyProperties(messageContent,offlineMessageContent); + offlineMessageContent.setToId(messageContent.getGroupId()); + messageStoreService.storeGroupOfflineMessage(offlineMessageContent,groupMemberId); + + //1.回ack成功给自己 + ack(messageContent,ResponseVO.successResponse()); + //2.发消息给同步在线端 + syncToSender(messageContent,messageContent); + //3.发消息给对方在线端 + dispatchMessage(messageContent); + + messageStoreService.setMessageFromMessageIdCache(messageContent.getAppId(), + messageContent.getMessageId(),messageContent); + }); + } + + private void dispatchMessage(GroupChatMessageContent messageContent){ + for (String memberId : messageContent.getMemberId()) { + if(!memberId.equals(messageContent.getFromId())){ + messageProducer.sendToUser(memberId, + GroupEventCommand.MSG_GROUP, + messageContent,messageContent.getAppId()); + } + } + } + + private void ack(MessageContent messageContent,ResponseVO responseVO){ + + ChatMessageAck chatMessageAck = new ChatMessageAck(messageContent.getMessageId()); + responseVO.setData(chatMessageAck); + //發消息 + messageProducer.sendToUser(messageContent.getFromId(), + GroupEventCommand.GROUP_MSG_ACK, + responseVO,messageContent + ); + } + + private void syncToSender(GroupChatMessageContent messageContent, ClientInfo clientInfo){ + messageProducer.sendToUserExceptClient(messageContent.getFromId(), + GroupEventCommand.MSG_GROUP,messageContent,messageContent); + } + + private ResponseVO imServerPermissionCheck(String fromId, String toId,Integer appId){ + ResponseVO responseVO = checkSendMessageService + .checkGroupMessage(fromId, toId,appId); + return responseVO; + } + + public SendMessageResp send(SendGroupMessageReq req) { + + SendMessageResp sendMessageResp = new SendMessageResp(); + GroupChatMessageContent message = new GroupChatMessageContent(); + BeanUtils.copyProperties(req,message); + + messageStoreService.storeGroupMessage(message); + + sendMessageResp.setMessageKey(message.getMessageKey()); + sendMessageResp.setMessageTime(System.currentTimeMillis()); + //2.发消息给同步在线端 + syncToSender(message,message); + //3.发消息给对方在线端 + dispatchMessage(message); + + return sendMessageResp; + + } +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/ImGroupMemberService.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/ImGroupMemberService.java index 6154733..5a24a0b 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/ImGroupMemberService.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/ImGroupMemberService.java @@ -40,4 +40,5 @@ public interface ImGroupMemberService { public ResponseVO speak(SpeaMemberReq req); + ResponseVO> syncMemberJoinedGroup(String operater, Integer appId); } diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/ImGroupService.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/ImGroupService.java index eeb140c..b813ce0 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/ImGroupService.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/ImGroupService.java @@ -1,6 +1,7 @@ package com.lld.im.service.group.service; import com.lld.im.common.ResponseVO; +import com.lld.im.common.model.SyncReq; import com.lld.im.service.group.dao.ImGroupEntity; import com.lld.im.service.group.model.req.*; @@ -29,4 +30,7 @@ public interface ImGroupService { public ResponseVO muteGroup(MuteGroupReq req); + ResponseVO syncJoinedGroupList(SyncReq req); + + Long getUserGroupMaxSeq(String userId, Integer appId); } diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/impl/ImGroupMemberServiceImpl.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/impl/ImGroupMemberServiceImpl.java index df91cd8..6364911 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/impl/ImGroupMemberServiceImpl.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/impl/ImGroupMemberServiceImpl.java @@ -1,18 +1,30 @@ package com.lld.im.service.group.service.impl; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.lld.im.codec.pack.group.AddGroupMemberPack; +import com.lld.im.codec.pack.group.GroupMemberSpeakPack; +import com.lld.im.codec.pack.group.RemoveGroupMemberPack; +import com.lld.im.codec.pack.group.UpdateGroupMemberPack; import com.lld.im.common.ResponseVO; +import com.lld.im.common.config.AppConfig; +import com.lld.im.common.constant.Constants; import com.lld.im.common.enums.GroupErrorCode; import com.lld.im.common.enums.GroupMemberRoleEnum; import com.lld.im.common.enums.GroupStatusEnum; import com.lld.im.common.enums.GroupTypeEnum; +import com.lld.im.common.enums.command.GroupEventCommand; import com.lld.im.common.exception.ApplicationException; +import com.lld.im.common.model.ClientInfo; import com.lld.im.service.group.dao.ImGroupEntity; import com.lld.im.service.group.dao.ImGroupMemberEntity; import com.lld.im.service.group.dao.mapper.ImGroupMemberMapper; +import com.lld.im.service.group.model.callback.AddMemberAfterCallback; +import com.lld.im.service.group.model.callback.DestroyGroupCallbackDto; import com.lld.im.service.group.model.req.*; import com.lld.im.service.group.model.resp.AddMemberResp; import com.lld.im.service.group.model.resp.GetRoleInGroupResp; @@ -20,6 +32,8 @@ import com.lld.im.service.group.service.ImGroupMemberService; import com.lld.im.service.group.service.ImGroupService; import com.lld.im.service.user.dao.ImUserDataEntity; import com.lld.im.service.user.service.ImUserService; +import com.lld.im.service.utils.CallbackService; +import com.lld.im.service.utils.GroupMessageProducer; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; @@ -47,9 +61,18 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { @Autowired ImGroupMemberService groupMemberService; + @Autowired + AppConfig appConfig; + + @Autowired + CallbackService callbackService; + @Autowired ImUserService imUserService; + @Autowired + GroupMessageProducer groupMessageProducer; + @Override public ResponseVO importGroupMember(ImportGroupMemberReq req) { @@ -95,7 +118,7 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { public ResponseVO addGroupMember(String groupId, Integer appId, GroupMemberDto dto) { ResponseVO singleUserInfo = imUserService.getSingleUserInfo(dto.getMemberId(), appId); - if (!singleUserInfo.isOk()) { + if(!singleUserInfo.isOk()){ return singleUserInfo; } @@ -155,7 +178,7 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { public ResponseVO removeGroupMember(String groupId, Integer appId, String memberId) { ResponseVO singleUserInfo = imUserService.getSingleUserInfo(memberId, appId); - if (!singleUserInfo.isOk()) { + if(!singleUserInfo.isOk()){ return singleUserInfo; } @@ -242,14 +265,33 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { } List memberDtos = req.getMembers(); + if(appConfig.isAddGroupMemberBeforeCallback()){ + + ResponseVO responseVO = callbackService.beforeCallback(req.getAppId(), Constants.CallbackCommand.GroupMemberAddBefore + , JSONObject.toJSONString(req)); + if(!responseVO.isOk()){ + return responseVO; + } + + try { + memberDtos + = JSONArray.parseArray(JSONObject.toJSONString(responseVO.getData()), GroupMemberDto.class); + }catch (Exception e){ + e.printStackTrace(); + log.error("GroupMemberAddBefore 回调失败:{}",req.getAppId()); + } + } ImGroupEntity group = groupResp.getData(); + /** * 私有群(private) 类似普通微信群,创建后仅支持已在群内的好友邀请加群,且无需被邀请方同意或群主审批 * 公开群(Public) 类似 QQ 群,创建后群主可以指定群管理员,需要群主或管理员审批通过才能入群 * 群类型 1私有群(类似微信) 2公开群(类似qq) + * */ + if (!isAdmin && GroupTypeEnum.PUBLIC.getCode() == group.getGroupType()) { throw new ApplicationException(GroupErrorCode.THIS_OPERATE_NEED_APPMANAGER_ROLE); } @@ -279,12 +321,30 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { resp.add(addMemberResp); } + AddGroupMemberPack addGroupMemberPack = new AddGroupMemberPack(); + addGroupMemberPack.setGroupId(req.getGroupId()); + addGroupMemberPack.setMembers(successId); + groupMessageProducer.producer(req.getOperater(), GroupEventCommand.ADDED_MEMBER, addGroupMemberPack + , new ClientInfo(req.getAppId(), req.getClientType(), req.getImei())); + + if(appConfig.isAddGroupMemberAfterCallback()){ + AddMemberAfterCallback dto = new AddMemberAfterCallback(); + dto.setGroupId(req.getGroupId()); + dto.setGroupType(group.getGroupType()); + dto.setMemberId(resp); + dto.setOperater(req.getOperater()); + callbackService.callback(req.getAppId() + ,Constants.CallbackCommand.GroupMemberAddAfter, + JSONObject.toJSONString(dto)); + } + return ResponseVO.successResponse(resp); } @Override public ResponseVO removeMember(RemoveGroupMemberReq req) { + List resp = new ArrayList<>(); boolean isAdmin = false; ResponseVO groupResp = groupService.getGroup(req.getGroupId(), req.getAppId()); if (!groupResp.isOk()) { @@ -337,6 +397,20 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { } } ResponseVO responseVO = groupMemberService.removeGroupMember(req.getGroupId(), req.getAppId(), req.getMemberId()); + if(responseVO.isOk()){ + + RemoveGroupMemberPack removeGroupMemberPack = new RemoveGroupMemberPack(); + removeGroupMemberPack.setGroupId(req.getGroupId()); + removeGroupMemberPack.setMember(req.getMemberId()); + groupMessageProducer.producer(req.getMemberId(), GroupEventCommand.DELETED_MEMBER, removeGroupMemberPack + , new ClientInfo(req.getAppId(), req.getClientType(), req.getImei())); + if(appConfig.isDeleteGroupMemberAfterCallback()){ + callbackService.callback(req.getAppId(), + Constants.CallbackCommand.GroupMemberDeleteAfter, + JSONObject.toJSONString(req)); + } + } + return responseVO; } @@ -379,25 +453,24 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { if (StringUtils.isBlank(req.getAlias()) && !isMeOperate) { return ResponseVO.errorResponse(GroupErrorCode.THIS_OPERATE_NEED_ONESELF); } + //私有群不能设置管理员 + if (groupData.getGroupType() == GroupTypeEnum.PRIVATE.getCode() && + req.getRole() != null && (req.getRole() == GroupMemberRoleEnum.MAMAGER.getCode() || + req.getRole() == GroupMemberRoleEnum.OWNER.getCode())) { + return ResponseVO.errorResponse(GroupErrorCode.THIS_OPERATE_NEED_MANAGER_ROLE); + } //如果要修改权限相关的则走下面的逻辑 - if (req.getRole() != null) { - //私有群不能设置管理员 - if (groupData.getGroupType() == GroupTypeEnum.PRIVATE.getCode() && - req.getRole() != null && (req.getRole() == GroupMemberRoleEnum.MAMAGER.getCode() || - req.getRole() == GroupMemberRoleEnum.OWNER.getCode())) { - return ResponseVO.errorResponse(GroupErrorCode.THIS_OPERATE_NEED_APPMANAGER_ROLE); - } - + if(req.getRole() != null){ //获取被操作人的是否在群内 ResponseVO roleInGroupOne = this.getRoleInGroupOne(req.getGroupId(), req.getMemberId(), req.getAppId()); - if (!roleInGroupOne.isOk()) { + if(!roleInGroupOne.isOk()){ return roleInGroupOne; } //获取操作人权限 ResponseVO operateRoleInGroupOne = this.getRoleInGroupOne(req.getGroupId(), req.getOperater(), req.getAppId()); - if (!operateRoleInGroupOne.isOk()) { + if(!operateRoleInGroupOne.isOk()){ return operateRoleInGroupOne; } @@ -407,12 +480,12 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { boolean isManager = roleInfo == GroupMemberRoleEnum.MAMAGER.getCode(); //不是管理员不能修改权限 - if (req.getRole() != null && !isOwner && !isManager) { + if(req.getRole() != null && !isOwner && !isManager){ return ResponseVO.errorResponse(GroupErrorCode.THIS_OPERATE_NEED_MANAGER_ROLE); } //管理员只有群主能够设置 - if (req.getRole() != null && req.getRole() == GroupMemberRoleEnum.MAMAGER.getCode() && !isOwner) { + if(req.getRole() != null && req.getRole() == GroupMemberRoleEnum.MAMAGER.getCode() && !isOwner){ return ResponseVO.errorResponse(GroupErrorCode.THIS_OPERATE_NEED_OWNER_ROLE); } @@ -436,6 +509,11 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { objectUpdateWrapper.eq("group_id", req.getGroupId()); imGroupMemberMapper.update(update, objectUpdateWrapper); + UpdateGroupMemberPack pack = new UpdateGroupMemberPack(); + BeanUtils.copyProperties(req, pack); + groupMessageProducer.producer(req.getOperater(), GroupEventCommand.UPDATED_MEMBER, pack, new ClientInfo(req.getAppId(), req.getClientType(), req.getImei())); + + return ResponseVO.successResponse(); } @@ -513,7 +591,7 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { } ImGroupMemberEntity imGroupMemberEntity = new ImGroupMemberEntity(); - if (memberRole == null) { + if(memberRole == null){ //获取被操作的权限 ResponseVO roleInGroupOne = this.getRoleInGroupOne(req.getGroupId(), req.getMemberId(), req.getAppId()); if (!roleInGroupOne.isOk()) { @@ -523,15 +601,26 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { } imGroupMemberEntity.setGroupMemberId(memberRole.getGroupMemberId()); - if (req.getSpeakDate() > 0) { + if(req.getSpeakDate() > 0){ imGroupMemberEntity.setSpeakDate(System.currentTimeMillis() + req.getSpeakDate()); - } else { + }else{ imGroupMemberEntity.setSpeakDate(req.getSpeakDate()); } int i = imGroupMemberMapper.updateById(imGroupMemberEntity); + if(i == 1){ + GroupMemberSpeakPack pack = new GroupMemberSpeakPack(); + BeanUtils.copyProperties(req,pack); + groupMessageProducer.producer(req.getOperater(),GroupEventCommand.SPEAK_GOUP_MEMBER,pack, + new ClientInfo(req.getAppId(),req.getClientType(),req.getImei())); + } return ResponseVO.successResponse(); } + @Override + public ResponseVO> syncMemberJoinedGroup(String operater, Integer appId) { + return ResponseVO.successResponse(imGroupMemberMapper.syncJoinedGroupId(appId,operater,GroupMemberRoleEnum.LEAVE.getCode())); + } + } diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/impl/ImGroupServiceImpl.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/impl/ImGroupServiceImpl.java index d8760e9..e3997ae 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/impl/ImGroupServiceImpl.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/group/service/impl/ImGroupServiceImpl.java @@ -1,23 +1,37 @@ package com.lld.im.service.group.service.impl; +import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.lld.im.codec.pack.group.CreateGroupPack; +import com.lld.im.codec.pack.group.DestroyGroupPack; +import com.lld.im.codec.pack.group.UpdateGroupInfoPack; import com.lld.im.common.ResponseVO; +import com.lld.im.common.config.AppConfig; +import com.lld.im.common.constant.Constants; import com.lld.im.common.enums.GroupErrorCode; import com.lld.im.common.enums.GroupMemberRoleEnum; import com.lld.im.common.enums.GroupStatusEnum; import com.lld.im.common.enums.GroupTypeEnum; +import com.lld.im.common.enums.command.GroupEventCommand; import com.lld.im.common.exception.ApplicationException; import com.lld.im.common.model.ClientInfo; +import com.lld.im.common.model.SyncReq; +import com.lld.im.common.model.SyncResp; +import com.lld.im.service.conversation.dao.ImConversationSetEntity; import com.lld.im.service.group.dao.ImGroupEntity; import com.lld.im.service.group.dao.mapper.ImGroupMapper; +import com.lld.im.service.group.model.callback.DestroyGroupCallbackDto; import com.lld.im.service.group.model.req.*; import com.lld.im.service.group.model.resp.GetGroupResp; import com.lld.im.service.group.model.resp.GetJoinedGroupResp; import com.lld.im.service.group.model.resp.GetRoleInGroupResp; import com.lld.im.service.group.service.ImGroupMemberService; import com.lld.im.service.group.service.ImGroupService; +import com.lld.im.service.seq.RedisSeq; +import com.lld.im.service.utils.CallbackService; +import com.lld.im.service.utils.GroupMessageProducer; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -43,6 +57,17 @@ public class ImGroupServiceImpl implements ImGroupService { @Autowired ImGroupMemberService groupMemberService; + @Autowired + AppConfig appConfig; + + @Autowired + CallbackService callbackService; + + @Autowired + GroupMessageProducer groupMessageProducer; + + @Autowired + RedisSeq redisSeq; @Override public ResponseVO importGroup(ImportGroupReq req) { @@ -110,6 +135,8 @@ public class ImGroupServiceImpl implements ImGroupService { } ImGroupEntity imGroupEntity = new ImGroupEntity(); + long seq = redisSeq.doGetSeq(req.getAppId() + ":" + Constants.SeqConstants.Group); + imGroupEntity.setSequence(seq); imGroupEntity.setCreateTime(System.currentTimeMillis()); imGroupEntity.setStatus(GroupStatusEnum.NORMAL.getCode()); BeanUtils.copyProperties(req, imGroupEntity); @@ -126,6 +153,15 @@ public class ImGroupServiceImpl implements ImGroupService { groupMemberService.addGroupMember(req.getGroupId(), req.getAppId(), dto); } + if(appConfig.isCreateGroupAfterCallback()){ + callbackService.callback(req.getAppId(), Constants.CallbackCommand.CreateGroupAfter, + JSONObject.toJSONString(imGroupEntity)); + } + + CreateGroupPack createGroupPack = new CreateGroupPack(); + BeanUtils.copyProperties(imGroupEntity, createGroupPack); + groupMessageProducer.producer(req.getOperater(), GroupEventCommand.CREATED_GROUP, createGroupPack + , new ClientInfo(req.getAppId(), req.getClientType(), req.getImei())); return ResponseVO.successResponse(); } @@ -176,13 +212,26 @@ public class ImGroupServiceImpl implements ImGroupService { } ImGroupEntity update = new ImGroupEntity(); + long seq = redisSeq.doGetSeq(req.getAppId() + ":" + Constants.SeqConstants.Group); BeanUtils.copyProperties(req, update); update.setUpdateTime(System.currentTimeMillis()); + update.setSequence(seq); int row = imGroupDataMapper.update(update, query); if (row != 1) { throw new ApplicationException(GroupErrorCode.THIS_OPERATE_NEED_MANAGER_ROLE); } + if(appConfig.isModifyGroupAfterCallback()){ + callbackService.callback(req.getAppId(),Constants.CallbackCommand.UpdateGroupAfter, + JSONObject.toJSONString(imGroupDataMapper.selectOne(query))); + } + + UpdateGroupInfoPack pack = new UpdateGroupInfoPack(); + BeanUtils.copyProperties(req, pack); + groupMessageProducer.producer(req.getOperater(), GroupEventCommand.UPDATED_GROUP, + pack, new ClientInfo(req.getAppId(), req.getClientType(), req.getImei())); + + return ResponseVO.successResponse(); } @@ -264,12 +313,29 @@ public class ImGroupServiceImpl implements ImGroupService { } ImGroupEntity update = new ImGroupEntity(); + long seq = redisSeq.doGetSeq(req.getAppId() + ":" + Constants.SeqConstants.Group); update.setStatus(GroupStatusEnum.DESTROY.getCode()); + update.setSequence(seq); int update1 = imGroupDataMapper.update(update, objectQueryWrapper); if (update1 != 1) { throw new ApplicationException(GroupErrorCode.UPDATE_GROUP_BASE_INFO_ERROR); } + + if(appConfig.isModifyGroupAfterCallback()){ + DestroyGroupCallbackDto dto = new DestroyGroupCallbackDto(); + dto.setGroupId(req.getGroupId()); + callbackService.callback(req.getAppId() + ,Constants.CallbackCommand.DestoryGroupAfter, + JSONObject.toJSONString(dto)); + } + + DestroyGroupPack pack = new DestroyGroupPack(); + pack.setSequence(seq); + pack.setGroupId(req.getGroupId()); + groupMessageProducer.producer(req.getOperater(), + GroupEventCommand.DESTROY_GROUP, pack, new ClientInfo(req.getAppId(), req.getClientType(), req.getImei())); + return ResponseVO.successResponse(); } @@ -390,4 +456,56 @@ public class ImGroupServiceImpl implements ImGroupService { return ResponseVO.successResponse(); } + @Override + public ResponseVO syncJoinedGroupList(SyncReq req) { + if(req.getMaxLimit() > 100){ + req.setMaxLimit(100); + } + + SyncResp resp = new SyncResp<>(); + + ResponseVO> memberJoinedGroup = groupMemberService.syncMemberJoinedGroup(req.getOperater(), req.getAppId()); + if(memberJoinedGroup.isOk()){ + + Collection data = memberJoinedGroup.getData(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("app_id",req.getAppId()); + queryWrapper.in("group_id",data); + queryWrapper.gt("sequence",req.getLastSequence()); + queryWrapper.last(" limit " + req.getMaxLimit()); + queryWrapper.orderByAsc("sequence"); + + List list = imGroupDataMapper.selectList(queryWrapper); + + if(!CollectionUtils.isEmpty(list)){ + ImGroupEntity maxSeqEntity + = list.get(list.size() - 1); + resp.setDataList(list); + //设置最大seq + Long maxSeq = + imGroupDataMapper.getGroupMaxSeq(data, req.getAppId()); + resp.setMaxSequence(maxSeq); + //设置是否拉取完毕 + resp.setCompleted(maxSeqEntity.getSequence() >= maxSeq); + return ResponseVO.successResponse(resp); + } + + } + resp.setCompleted(true); + return ResponseVO.successResponse(resp); + } + + @Override + public Long getUserGroupMaxSeq(String userId, Integer appId) { + + ResponseVO> memberJoinedGroup = groupMemberService.syncMemberJoinedGroup(userId, appId); + if(!memberJoinedGroup.isOk()){ + throw new ApplicationException(500,""); + } + Long maxSeq = + imGroupDataMapper.getGroupMaxSeq(memberJoinedGroup.getData(), + appId); + return maxSeq; + } + } diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/interceptor/GateWayInterceptor.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/interceptor/GateWayInterceptor.java new file mode 100644 index 0000000..4968852 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/interceptor/GateWayInterceptor.java @@ -0,0 +1,99 @@ +package com.lld.im.service.interceptor; + +import com.alibaba.fastjson.JSONObject; +import com.lld.im.common.BaseErrorCode; +import com.lld.im.common.ResponseVO; +import com.lld.im.common.enums.GateWayErrorCode; +import com.lld.im.common.exception.ApplicationExceptionEnum; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.PrintWriter; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Component +public class GateWayInterceptor implements HandlerInterceptor { + + @Autowired + IdentityCheck identityCheck; + + + //appService -》im接口 -》 userSign + //appService(gen userSig) + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + +// if (1 == 1){ +// return true; +// } + + //获取appId 操作人 userSign + String appIdStr = request.getParameter("appId"); + if(StringUtils.isBlank(appIdStr)){ + resp(ResponseVO.errorResponse(GateWayErrorCode + .APPID_NOT_EXIST),response); + return false; + } + + String identifier = request.getParameter("identifier"); + if(StringUtils.isBlank(identifier)){ + resp(ResponseVO.errorResponse(GateWayErrorCode + .OPERATER_NOT_EXIST),response); + return false; + } + + String userSign = request.getParameter("userSign"); + if(StringUtils.isBlank(userSign)){ + resp(ResponseVO.errorResponse(GateWayErrorCode + .USERSIGN_NOT_EXIST),response); + return false; + } + + //签名和操作人和appid是否匹配 + ApplicationExceptionEnum applicationExceptionEnum = identityCheck.checkUserSig(identifier, appIdStr, userSign); + if(applicationExceptionEnum != BaseErrorCode.SUCCESS){ + resp(ResponseVO.errorResponse(applicationExceptionEnum),response); + return false; + } + + return true; + } + + + private void resp(ResponseVO respVo ,HttpServletResponse response){ + + PrintWriter writer = null; + response.setCharacterEncoding("UTF-8"); + response.setContentType("text/html; charset=utf-8"); + try { + String resp = JSONObject.toJSONString(respVo); + + response.setCharacterEncoding("UTF-8"); + response.setHeader("Content-type", "application/json;charset=UTF-8"); + response.setHeader("Access-Control-Allow-Origin","*"); + response.setHeader("Access-Control-Allow-Credentials","true"); + response.setHeader("Access-Control-Allow-Methods","*"); + response.setHeader("Access-Control-Allow-Headers","*"); + response.setHeader("Access-Control-Max-Age","3600"); + + writer = response.getWriter(); + writer.write(resp); + } catch (Exception e){ + e.printStackTrace(); + } finally { + if(writer != null){ + writer.checkError(); + } + } + + } +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/interceptor/IdentityCheck.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/interceptor/IdentityCheck.java new file mode 100644 index 0000000..36d065d --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/interceptor/IdentityCheck.java @@ -0,0 +1,135 @@ +package com.lld.im.service.interceptor; + +import com.alibaba.fastjson.JSONObject; +import com.lld.im.common.BaseErrorCode; +import com.lld.im.common.ResponseVO; +import com.lld.im.common.config.AppConfig; +import com.lld.im.common.constant.Constants; +import com.lld.im.common.enums.GateWayErrorCode; +import com.lld.im.common.enums.ImUserTypeEnum; +import com.lld.im.common.exception.ApplicationExceptionEnum; +import com.lld.im.common.utils.SigAPI; +import com.lld.im.service.user.dao.ImUserDataEntity; +import com.lld.im.service.user.service.ImUserService; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; +import sun.rmi.runtime.Log; + +import java.util.concurrent.TimeUnit; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Component +public class IdentityCheck { + + private static Logger logger = LoggerFactory.getLogger(IdentityCheck.class); + + @Autowired + ImUserService imUserService; + + //10000 123456 10001 123456789 + @Autowired + AppConfig appConfig; + + @Autowired + StringRedisTemplate stringRedisTemplate; + + public ApplicationExceptionEnum checkUserSig(String identifier, + String appId,String userSig){ + + String cacheUserSig = stringRedisTemplate.opsForValue() + .get(appId + ":" + Constants.RedisConstants.userSign + ":" + + identifier + userSig); + if(!StringUtils.isBlank(cacheUserSig) && Long.valueOf(cacheUserSig) + > System.currentTimeMillis() / 1000){ + this.setIsAdmin(identifier,Integer.valueOf(appId)); + return BaseErrorCode.SUCCESS; + } + + //获取秘钥 + String privateKey = appConfig.getPrivateKey(); + + //根据appid + 秘钥创建sigApi + SigAPI sigAPI = new SigAPI(Long.valueOf(appId), privateKey); + + //调用sigApi对userSig解密 + JSONObject jsonObject = sigAPI.decodeUserSig(userSig); + + //取出解密后的appid 和 操作人 和 过期时间做匹配,不通过则提示错误 + Long expireTime = 0L; + Long expireSec = 0L; + Long time = 0L; + String decoerAppId = ""; + String decoderidentifier = ""; + + try { + decoerAppId = jsonObject.getString("TLS.appId"); + decoderidentifier = jsonObject.getString("TLS.identifier"); + String expireStr = jsonObject.get("TLS.expire").toString(); + String expireTimeStr = jsonObject.get("TLS.expireTime").toString(); + time = Long.valueOf(expireTimeStr); + expireSec = Long.valueOf(expireStr); + expireTime = Long.valueOf(expireTimeStr) + expireSec; + }catch (Exception e){ + e.printStackTrace(); + logger.error("checkUserSig-error:{}",e.getMessage()); + } + + if(!decoderidentifier.equals(identifier)){ + return GateWayErrorCode.USERSIGN_OPERATE_NOT_MATE; + } + + if(!decoerAppId.equals(appId)){ + return GateWayErrorCode.USERSIGN_IS_ERROR; + } + + if(expireSec == 0L){ + return GateWayErrorCode.USERSIGN_IS_EXPIRED; + } + + if(expireTime < System.currentTimeMillis() / 1000){ + return GateWayErrorCode.USERSIGN_IS_EXPIRED; + } + + //appid + "xxx" + userId + sign + String genSig = sigAPI.genUserSig(identifier, expireSec,time,null); + if (genSig.toLowerCase().equals(userSig.toLowerCase())) + { + String key = appId + ":" + Constants.RedisConstants.userSign + ":" + +identifier + userSig; + + Long etime = expireTime - System.currentTimeMillis() / 1000; + stringRedisTemplate.opsForValue().set( + key,expireTime.toString(),etime, TimeUnit.SECONDS + ); + this.setIsAdmin(identifier,Integer.valueOf(appId)); + return BaseErrorCode.SUCCESS; + } + + return GateWayErrorCode.USERSIGN_IS_ERROR; + } + + + /** + * 根据appid,identifier判断是否App管理员,并设置到RequestHolder + * @param identifier + * @param appId + * @return + */ + public void setIsAdmin(String identifier, Integer appId) { + //去DB或Redis中查找, 后面写 + ResponseVO singleUserInfo = imUserService.getSingleUserInfo(identifier, appId); + if(singleUserInfo.isOk()){ + RequestHolder.set(singleUserInfo.getData().getUserType() == ImUserTypeEnum.APP_ADMIN.getCode()); + }else{ + RequestHolder.set(false); + } + } +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/interceptor/RequestHolder.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/interceptor/RequestHolder.java new file mode 100644 index 0000000..9e9d5c2 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/interceptor/RequestHolder.java @@ -0,0 +1,22 @@ +package com.lld.im.service.interceptor; + +/** + * @author: Chackylee + * @description: + **/ +public class RequestHolder { + + private final static ThreadLocal requestHolder = new ThreadLocal<>(); + + public static void set(Boolean isadmin) { + requestHolder.set(isadmin); + } + + public static Boolean get() { + return requestHolder.get(); + } + + public static void remove() { + requestHolder.remove(); + } +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/message/controller/MessageController.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/controller/MessageController.java new file mode 100644 index 0000000..ba3b6b4 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/controller/MessageController.java @@ -0,0 +1,49 @@ +package com.lld.im.service.message.controller; + +import com.lld.im.common.ResponseVO; +import com.lld.im.common.model.SyncReq; +import com.lld.im.common.model.message.CheckSendMessageReq; +import com.lld.im.service.message.model.req.SendMessageReq; +import com.lld.im.service.message.service.MessageSyncService; +import com.lld.im.service.message.service.P2PMessageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@RestController +@RequestMapping("v1/message") +public class MessageController { + + @Autowired + P2PMessageService p2PMessageService; + + @Autowired + MessageSyncService messageSyncService; + + @RequestMapping("/send") + public ResponseVO send(@RequestBody @Validated SendMessageReq req, Integer appId) { + req.setAppId(appId); + return ResponseVO.successResponse(p2PMessageService.send(req)); + } + + @RequestMapping("/checkSend") + public ResponseVO checkSend(@RequestBody @Validated CheckSendMessageReq req) { + return p2PMessageService.imServerPermissionCheck(req.getFromId(),req.getToId() + ,req.getAppId()); + } + + @RequestMapping("/syncOfflineMessage") + public ResponseVO syncOfflineMessage(@RequestBody + @Validated SyncReq req, Integer appId) { + req.setAppId(appId); + return messageSyncService.syncOfflineMessage(req); + } + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/message/dao/ImMessageBodyEntity.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/dao/ImMessageBodyEntity.java new file mode 100644 index 0000000..c7e1043 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/dao/ImMessageBodyEntity.java @@ -0,0 +1,32 @@ +package com.lld.im.service.message.dao; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * @author: Chackylee + * @description: + **/ +@Data +@TableName("im_message_body") +public class ImMessageBodyEntity { + + private Integer appId; + + /** messageBodyId*/ + private Long messageKey; + + /** messageBody*/ + private String messageBody; + + private String securityKey; + + private Long messageTime; + + private Long createTime; + + private String extra; + + private Integer delFlag; + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/message/dao/ImMessageHistoryEntity.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/dao/ImMessageHistoryEntity.java new file mode 100644 index 0000000..b7dd9b0 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/dao/ImMessageHistoryEntity.java @@ -0,0 +1,33 @@ +package com.lld.im.service.message.dao; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * @author: Chackylee + * @description: + **/ +@Data +@TableName("im_message_history") +public class ImMessageHistoryEntity { + + private Integer appId; + + private String fromId; + + private String toId; + + private String ownerId; + + /** messageBodyId*/ + private Long messageKey; + /** 序列号*/ + private Long sequence; + + private String messageRandom; + + private Long messageTime; + + private Long createTime; + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/message/dao/mapper/ImMessageBodyMapper.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/dao/mapper/ImMessageBodyMapper.java new file mode 100644 index 0000000..4b06ecd --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/dao/mapper/ImMessageBodyMapper.java @@ -0,0 +1,9 @@ +package com.lld.im.service.message.dao.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.lld.im.service.message.dao.ImMessageBodyEntity; +import org.springframework.stereotype.Repository; + +@Repository +public interface ImMessageBodyMapper extends BaseMapper { +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/message/dao/mapper/ImMessageHistoryMapper.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/dao/mapper/ImMessageHistoryMapper.java new file mode 100644 index 0000000..155122b --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/dao/mapper/ImMessageHistoryMapper.java @@ -0,0 +1,18 @@ +package com.lld.im.service.message.dao.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.lld.im.service.message.dao.ImMessageHistoryEntity; +import org.springframework.stereotype.Repository; + +import java.util.Collection; + +@Repository +public interface ImMessageHistoryMapper extends BaseMapper { + + /** + * 批量插入(mysql) + * @param entityList + * @return + */ + Integer insertBatchSomeColumn(Collection entityList); +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/message/model/req/SendMessageReq.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/model/req/SendMessageReq.java new file mode 100644 index 0000000..d6d6cb0 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/model/req/SendMessageReq.java @@ -0,0 +1,34 @@ +package com.lld.im.service.message.model.req; + +import com.lld.im.common.model.RequestBase; +import lombok.Data; + +/** + * @author: Chackylee + * @description: + **/ +@Data +public class SendMessageReq extends RequestBase { + + //客户端传的messageId + private String messageId; + + private String fromId; + + private String toId; + + private int messageRandom; + + private long messageTime; + + private String messageBody; + /** + * 这个字段缺省或者为 0 表示需要计数,为 1 表示本条消息不需要计数,即右上角图标数字不增加 + */ + private int badgeMode; + + private Long messageLifeTime; + + private Integer appId; + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/message/model/resp/SendMessageResp.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/model/resp/SendMessageResp.java new file mode 100644 index 0000000..4961bd7 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/model/resp/SendMessageResp.java @@ -0,0 +1,17 @@ +package com.lld.im.service.message.model.resp; + +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class SendMessageResp { + + private Long messageKey; + + private Long messageTime; + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/message/mq/ChatOperateReceiver.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/mq/ChatOperateReceiver.java new file mode 100644 index 0000000..5ca1f8b --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/mq/ChatOperateReceiver.java @@ -0,0 +1,97 @@ +package com.lld.im.service.message.mq; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.TypeReference; +import com.lld.im.common.constant.Constants; +import com.lld.im.common.enums.command.MessageCommand; +import com.lld.im.common.model.message.MessageContent; +import com.lld.im.common.model.message.MessageReadedContent; +import com.lld.im.common.model.message.MessageReciveAckContent; +import com.lld.im.common.model.message.RecallMessageContent; +import com.lld.im.service.message.service.MessageSyncService; +import com.lld.im.service.message.service.P2PMessageService; +import com.rabbitmq.client.Channel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.annotation.Exchange; +import org.springframework.amqp.rabbit.annotation.Queue; +import org.springframework.amqp.rabbit.annotation.QueueBinding; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.handler.annotation.Headers; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.stereotype.Component; +import sun.nio.cs.ext.MS874; + +import java.io.UnsupportedEncodingException; +import java.util.Map; +import java.util.Objects; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Component +public class ChatOperateReceiver { + + private static Logger logger = LoggerFactory.getLogger(ChatOperateReceiver.class); + + @Autowired + P2PMessageService p2PMessageService; + + @Autowired + MessageSyncService messageSyncService; + + @RabbitListener( + bindings = @QueueBinding( + value = @Queue(value = Constants.RabbitConstants.Im2MessageService,durable = "true"), + exchange = @Exchange(value = Constants.RabbitConstants.Im2MessageService,durable = "true") + ),concurrency = "1" + ) + public void onChatMessage(@Payload Message message, + @Headers Map headers, + Channel channel) throws Exception { + String msg = new String(message.getBody(),"utf-8"); + logger.info("CHAT MSG FORM QUEUE ::: {}", msg); + Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG); + try { + JSONObject jsonObject = JSON.parseObject(msg); + Integer command = jsonObject.getInteger("command"); + if(command.equals(MessageCommand.MSG_P2P.getCommand())){ + //处理消息 + MessageContent messageContent + = jsonObject.toJavaObject(MessageContent.class); + p2PMessageService.process(messageContent); + }else if(command.equals(MessageCommand.MSG_RECIVE_ACK.getCommand())){ + //消息接收确认 + MessageReciveAckContent messageContent + = jsonObject.toJavaObject(MessageReciveAckContent.class); + messageSyncService.receiveMark(messageContent); + }else if(command.equals(MessageCommand.MSG_READED.getCommand())){ + //消息接收确认 + MessageReadedContent messageContent + = jsonObject.toJavaObject(MessageReadedContent.class); + messageSyncService.readMark(messageContent); + }else if (Objects.equals(command, MessageCommand.MSG_RECALL.getCommand())) { +// 撤回消息 + RecallMessageContent messageContent = JSON.parseObject(msg, new TypeReference() { + }.getType()); + messageSyncService.recallMessage(messageContent); + } + channel.basicAck(deliveryTag, false); + }catch (Exception e){ + logger.error("处理消息出现异常:{}", e.getMessage()); + logger.error("RMQ_CHAT_TRAN_ERROR", e); + logger.error("NACK_MSG:{}", msg); + //第一个false 表示不批量拒绝,第二个false表示不重回队列 + channel.basicNack(deliveryTag, false, false); + } + + } + + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/message/service/CheckSendMessageService.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/service/CheckSendMessageService.java new file mode 100644 index 0000000..dd65703 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/service/CheckSendMessageService.java @@ -0,0 +1,143 @@ +package com.lld.im.service.message.service; + +import com.lld.im.common.ResponseVO; +import com.lld.im.common.config.AppConfig; +import com.lld.im.common.enums.*; +import com.lld.im.service.friendship.dao.ImFriendShipEntity; +import com.lld.im.service.friendship.model.req.GetRelationReq; +import com.lld.im.service.friendship.service.ImFriendService; +import com.lld.im.service.group.dao.ImGroupEntity; +import com.lld.im.service.group.model.resp.GetRoleInGroupResp; +import com.lld.im.service.group.service.ImGroupMemberService; +import com.lld.im.service.group.service.ImGroupService; +import com.lld.im.service.user.dao.ImUserDataEntity; +import com.lld.im.service.user.service.ImUserService; +import com.sun.org.apache.regexp.internal.RE; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Service +public class CheckSendMessageService { + + @Autowired + ImUserService imUserService; + + @Autowired + ImFriendService imFriendService; + + @Autowired + ImGroupService imGroupService; + + @Autowired + ImGroupMemberService imGroupMemberService; + + @Autowired + AppConfig appConfig; + + + public ResponseVO checkSenderForvidAndMute(String fromId,Integer appId){ + + ResponseVO singleUserInfo + = imUserService.getSingleUserInfo(fromId, appId); + if(!singleUserInfo.isOk()){ + return singleUserInfo; + } + + ImUserDataEntity user = singleUserInfo.getData(); + if(user.getForbiddenFlag() == UserForbiddenFlagEnum.FORBIBBEN.getCode()){ + return ResponseVO.errorResponse(MessageErrorCode.FROMER_IS_FORBIBBEN); + }else if (user.getSilentFlag() == UserSilentFlagEnum.MUTE.getCode()){ + return ResponseVO.errorResponse(MessageErrorCode.FROMER_IS_MUTE); + } + + return ResponseVO.successResponse(); + } + + public ResponseVO checkFriendShip(String fromId,String toId,Integer appId){ + + if(appConfig.isSendMessageCheckFriend()){ + GetRelationReq fromReq = new GetRelationReq(); + fromReq.setFromId(fromId); + fromReq.setToId(toId); + fromReq.setAppId(appId); + ResponseVO fromRelation = imFriendService.getRelation(fromReq); + if(!fromRelation.isOk()){ + return fromRelation; + } + GetRelationReq toReq = new GetRelationReq(); + fromReq.setFromId(toId); + fromReq.setToId(fromId); + fromReq.setAppId(appId); + ResponseVO toRelation = imFriendService.getRelation(fromReq); + if(!toRelation.isOk()){ + return toRelation; + } + + if(FriendShipStatusEnum.FRIEND_STATUS_NORMAL.getCode() + != fromRelation.getData().getStatus()){ + return ResponseVO.errorResponse(FriendShipErrorCode.FRIEND_IS_DELETED); + } + + if(FriendShipStatusEnum.FRIEND_STATUS_NORMAL.getCode() + != toRelation.getData().getStatus()){ + return ResponseVO.errorResponse(FriendShipErrorCode.FRIEND_IS_DELETED); + } + + if(appConfig.isSendMessageCheckBlack()){ + if(FriendShipStatusEnum.BLACK_STATUS_NORMAL.getCode() + != fromRelation.getData().getBlack()){ + return ResponseVO.errorResponse(FriendShipErrorCode.FRIEND_IS_BLACK); + } + + if(FriendShipStatusEnum.BLACK_STATUS_NORMAL.getCode() + != toRelation.getData().getBlack()){ + return ResponseVO.errorResponse(FriendShipErrorCode.TARGET_IS_BLACK_YOU); + } + } + } + + return ResponseVO.successResponse(); + } + public ResponseVO checkGroupMessage(String fromId,String groupId,Integer appId){ + + ResponseVO responseVO = checkSenderForvidAndMute(fromId, appId); + if(!responseVO.isOk()){ + return responseVO; + } + + //判断群逻辑 + ResponseVO group = imGroupService.getGroup(groupId, appId); + if(!group.isOk()){ + return group; + } + + //判断群成员是否在群内 + ResponseVO roleInGroupOne = imGroupMemberService.getRoleInGroupOne(groupId, fromId, appId); + if(!roleInGroupOne.isOk()){ + return roleInGroupOne; + } + GetRoleInGroupResp data = roleInGroupOne.getData(); + + //判断群是否被禁言 + //如果禁言 只有裙管理和群主可以发言 + ImGroupEntity groupData = group.getData(); + if(groupData.getMute() == GroupMuteTypeEnum.MUTE.getCode() + && (data.getRole() == GroupMemberRoleEnum.MAMAGER.getCode() || + data.getRole() == GroupMemberRoleEnum.OWNER.getCode() )){ + return ResponseVO.errorResponse(GroupErrorCode.THIS_GROUP_IS_MUTE); + } + + if(data.getSpeakDate() != null && data.getSpeakDate() > System.currentTimeMillis()){ + return ResponseVO.errorResponse(GroupErrorCode.GROUP_MEMBER_IS_SPEAK); + } + + return ResponseVO.successResponse(); + } + + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/message/service/MessageStoreService.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/service/MessageStoreService.java new file mode 100644 index 0000000..ea602b4 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/service/MessageStoreService.java @@ -0,0 +1,226 @@ +package com.lld.im.service.message.service; + +import com.alibaba.fastjson.JSONObject; +import com.lld.im.common.config.AppConfig; +import com.lld.im.common.constant.Constants; +import com.lld.im.common.enums.ConversationTypeEnum; +import com.lld.im.common.enums.DelFlagEnum; +import com.lld.im.common.model.message.*; +import com.lld.im.service.conversation.service.ConversationService; +import com.lld.im.service.group.dao.ImGroupMessageHistoryEntity; +import com.lld.im.service.group.dao.mapper.ImGroupMessageHistoryMapper; +import com.lld.im.service.message.dao.ImMessageBodyEntity; +import com.lld.im.service.message.dao.ImMessageHistoryEntity; +import com.lld.im.service.message.dao.mapper.ImMessageBodyMapper; +import com.lld.im.service.message.dao.mapper.ImMessageHistoryMapper; +import com.lld.im.service.utils.SnowflakeIdWorker; +import org.apache.commons.lang3.StringUtils; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Service +public class MessageStoreService { + + @Autowired + ImMessageHistoryMapper imMessageHistoryMapper; + + @Autowired + ImMessageBodyMapper imMessageBodyMapper; + + @Autowired + SnowflakeIdWorker snowflakeIdWorker; + + @Autowired + ImGroupMessageHistoryMapper imGroupMessageHistoryMapper; + + @Autowired + RabbitTemplate rabbitTemplate; + + @Autowired + StringRedisTemplate stringRedisTemplate; + + @Autowired + ConversationService conversationService; + + @Autowired + AppConfig appConfig; + + @Transactional + public void storeP2PMessage(MessageContent messageContent){ + //messageContent 转化成 messageBody +// ImMessageBody imMessageBodyEntity = extractMessageBody(messageContent); + //插入messageBody +// imMessageBodyMapper.insert(imMessageBodyEntity); +// //转化成MessageHistory +// List imMessageHistoryEntities = extractToP2PMessageHistory(messageContent, imMessageBodyEntity); +// //批量插入 +// imMessageHistoryMapper.insertBatchSomeColumn(imMessageHistoryEntities); +// messageContent.setMessageKey(imMessageBodyEntity.getMessageKey()); + ImMessageBody imMessageBodyEntity = extractMessageBody(messageContent); + DoStoreP2PMessageDto dto = new DoStoreP2PMessageDto(); + dto.setMessageContent(messageContent); + dto.setMessageBody(imMessageBodyEntity); + messageContent.setMessageKey(imMessageBodyEntity.getMessageKey()); + rabbitTemplate.convertAndSend(Constants.RabbitConstants.StoreP2PMessage,"", + JSONObject.toJSONString(dto)); + } + + public ImMessageBody extractMessageBody(MessageContent messageContent){ + ImMessageBody messageBody = new ImMessageBody(); + messageBody.setAppId(messageContent.getAppId()); + messageBody.setMessageKey(snowflakeIdWorker.nextId()); + messageBody.setCreateTime(System.currentTimeMillis()); + messageBody.setSecurityKey(""); + messageBody.setExtra(messageContent.getExtra()); + messageBody.setDelFlag(DelFlagEnum.NORMAL.getCode()); + messageBody.setMessageTime(messageContent.getMessageTime()); + messageBody.setMessageBody(messageContent.getMessageBody()); + return messageBody; + } + + public List extractToP2PMessageHistory(MessageContent messageContent, + ImMessageBodyEntity imMessageBodyEntity){ + List list = new ArrayList<>(); + ImMessageHistoryEntity fromHistory = new ImMessageHistoryEntity(); + BeanUtils.copyProperties(messageContent,fromHistory); + fromHistory.setOwnerId(messageContent.getFromId()); + fromHistory.setMessageKey(imMessageBodyEntity.getMessageKey()); + fromHistory.setCreateTime(System.currentTimeMillis()); + + ImMessageHistoryEntity toHistory = new ImMessageHistoryEntity(); + BeanUtils.copyProperties(messageContent,toHistory); + toHistory.setOwnerId(messageContent.getToId()); + toHistory.setMessageKey(imMessageBodyEntity.getMessageKey()); + toHistory.setCreateTime(System.currentTimeMillis()); + + list.add(fromHistory); + list.add(toHistory); + return list; + } + + @Transactional + public void storeGroupMessage(GroupChatMessageContent messageContent){ + ImMessageBody imMessageBody = extractMessageBody(messageContent); + DoStoreGroupMessageDto dto = new DoStoreGroupMessageDto(); + dto.setMessageBody(imMessageBody); + dto.setGroupChatMessageContent(messageContent); + rabbitTemplate.convertAndSend(Constants.RabbitConstants.StoreGroupMessage, + "", + JSONObject.toJSONString(dto)); + messageContent.setMessageKey(imMessageBody.getMessageKey()); + } + + private ImGroupMessageHistoryEntity extractToGroupMessageHistory(GroupChatMessageContent + messageContent ,ImMessageBodyEntity messageBodyEntity){ + ImGroupMessageHistoryEntity result = new ImGroupMessageHistoryEntity(); + BeanUtils.copyProperties(messageContent,result); + result.setGroupId(messageContent.getGroupId()); + result.setMessageKey(messageBodyEntity.getMessageKey()); + result.setCreateTime(System.currentTimeMillis()); + return result; + } + + public void setMessageFromMessageIdCache(Integer appId,String messageId,Object messageContent){ + //appid : cache : messageId + String key =appId + ":" + Constants.RedisConstants.cacheMessage + ":" + messageId; + stringRedisTemplate.opsForValue().set(key,JSONObject.toJSONString(messageContent),300, TimeUnit.SECONDS); + } + + public T getMessageFromMessageIdCache(Integer appId, + String messageId,Class clazz){ + //appid : cache : messageId + String key = appId + ":" + Constants.RedisConstants.cacheMessage + ":" + messageId; + String msg = stringRedisTemplate.opsForValue().get(key); + if(StringUtils.isBlank(msg)){ + return null; + } + return JSONObject.parseObject(msg, clazz); + } + + /** + * @description: 存储单人离线消息 + * @param + * @return void + * @author lld + */ + public void storeOfflineMessage(OfflineMessageContent offlineMessage){ + + // 找到fromId的队列 + String fromKey = offlineMessage.getAppId() + ":" + Constants.RedisConstants.OfflineMessage + ":" + offlineMessage.getFromId(); + // 找到toId的队列 + String toKey = offlineMessage.getAppId() + ":" + Constants.RedisConstants.OfflineMessage + ":" + offlineMessage.getToId(); + + ZSetOperations operations = stringRedisTemplate.opsForZSet(); + //判断 队列中的数据是否超过设定值 + if(operations.zCard(fromKey) > appConfig.getOfflineMessageCount()){ + operations.removeRange(fromKey,0,0); + } + offlineMessage.setConversationId(conversationService.convertConversationId( + ConversationTypeEnum.P2P.getCode(),offlineMessage.getFromId(),offlineMessage.getToId() + )); + // 插入 数据 根据messageKey 作为分值 + operations.add(fromKey,JSONObject.toJSONString(offlineMessage), + offlineMessage.getMessageKey()); + + //判断 队列中的数据是否超过设定值 + if(operations.zCard(toKey) > appConfig.getOfflineMessageCount()){ + operations.removeRange(toKey,0,0); + } + + offlineMessage.setConversationId(conversationService.convertConversationId( + ConversationTypeEnum.P2P.getCode(),offlineMessage.getToId(),offlineMessage.getFromId() + )); + // 插入 数据 根据messageKey 作为分值 + operations.add(toKey,JSONObject.toJSONString(offlineMessage), + offlineMessage.getMessageKey()); + + } + + + /** + * @description: 存储单人离线消息 + * @param + * @return void + * @author lld + */ + public void storeGroupOfflineMessage(OfflineMessageContent offlineMessage + ,List memberIds){ + + ZSetOperations operations = stringRedisTemplate.opsForZSet(); + //判断 队列中的数据是否超过设定值 + offlineMessage.setConversationType(ConversationTypeEnum.GROUP.getCode()); + + for (String memberId : memberIds) { + // 找到toId的队列 + String toKey = offlineMessage.getAppId() + ":" + + Constants.RedisConstants.OfflineMessage + ":" + + memberId; + offlineMessage.setConversationId(conversationService.convertConversationId( + ConversationTypeEnum.GROUP.getCode(),memberId,offlineMessage.getToId() + )); + if(operations.zCard(toKey) > appConfig.getOfflineMessageCount()){ + operations.removeRange(toKey,0,0); + } + // 插入 数据 根据messageKey 作为分值 + operations.add(toKey,JSONObject.toJSONString(offlineMessage), + offlineMessage.getMessageKey()); + } + + + } + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/message/service/MessageSyncService.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/service/MessageSyncService.java new file mode 100644 index 0000000..2fadac8 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/service/MessageSyncService.java @@ -0,0 +1,255 @@ +package com.lld.im.service.message.service; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.lld.im.codec.pack.message.MessageReadedPack; +import com.lld.im.codec.pack.message.RecallMessageNotifyPack; +import com.lld.im.codec.proto.Message; +import com.lld.im.common.ResponseVO; +import com.lld.im.common.constant.Constants; +import com.lld.im.common.enums.ConversationTypeEnum; +import com.lld.im.common.enums.DelFlagEnum; +import com.lld.im.common.enums.MessageErrorCode; +import com.lld.im.common.enums.command.Command; +import com.lld.im.common.enums.command.GroupEventCommand; +import com.lld.im.common.enums.command.MessageCommand; +import com.lld.im.common.model.ClientInfo; +import com.lld.im.common.model.SyncReq; +import com.lld.im.common.model.SyncResp; +import com.lld.im.common.model.message.MessageReadedContent; +import com.lld.im.common.model.message.MessageReciveAckContent; +import com.lld.im.common.model.message.OfflineMessageContent; +import com.lld.im.common.model.message.RecallMessageContent; +import com.lld.im.service.conversation.service.ConversationService; +import com.lld.im.service.group.service.ImGroupMemberService; +import com.lld.im.service.message.dao.ImMessageBodyEntity; +import com.lld.im.service.message.dao.mapper.ImMessageBodyMapper; +import com.lld.im.service.seq.RedisSeq; +import com.lld.im.service.utils.ConversationIdGenerate; +import com.lld.im.service.utils.GroupMessageProducer; +import com.lld.im.service.utils.MessageProducer; +import com.lld.im.service.utils.SnowflakeIdWorker; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.DefaultTypedTuple; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Service +public class MessageSyncService { + + @Autowired + MessageProducer messageProducer; + + @Autowired + ConversationService conversationService; + + @Autowired + RedisTemplate redisTemplate; + + @Autowired + ImMessageBodyMapper imMessageBodyMapper; + + @Autowired + RedisSeq redisSeq; + + @Autowired + SnowflakeIdWorker snowflakeIdWorker; + + @Autowired + ImGroupMemberService imGroupMemberService; + + @Autowired + GroupMessageProducer groupMessageProducer; + + + public void receiveMark(MessageReciveAckContent messageReciveAckContent){ + messageProducer.sendToUser(messageReciveAckContent.getToId(), + MessageCommand.MSG_RECIVE_ACK,messageReciveAckContent,messageReciveAckContent.getAppId()); + } + + /** + * @description: 消息已读。更新会话的seq,通知在线的同步端发送指定command ,发送已读回执通知对方(消息发起方)我已读 + * @param + * @return void + * @author lld + */ + public void readMark(MessageReadedContent messageContent) { + conversationService.messageMarkRead(messageContent); + MessageReadedPack messageReadedPack = new MessageReadedPack(); + BeanUtils.copyProperties(messageContent,messageReadedPack); + syncToSender(messageReadedPack,messageContent,MessageCommand.MSG_READED_NOTIFY); + //发送给对方 + messageProducer.sendToUser(messageContent.getToId(), + MessageCommand.MSG_READED_RECEIPT,messageReadedPack,messageContent.getAppId()); + } + + private void syncToSender(MessageReadedPack pack, MessageReadedContent content, Command command){ + MessageReadedPack messageReadedPack = new MessageReadedPack(); +// BeanUtils.copyProperties(messageReadedContent,messageReadedPack); + //发送给自己的其他端 + messageProducer.sendToUserExceptClient(pack.getFromId(), + command,pack, + content); + } + + public void groupReadMark(MessageReadedContent messageReaded) { + conversationService.messageMarkRead(messageReaded); + MessageReadedPack messageReadedPack = new MessageReadedPack(); + BeanUtils.copyProperties(messageReaded,messageReadedPack); + syncToSender(messageReadedPack,messageReaded, GroupEventCommand.MSG_GROUP_READED_NOTIFY + ); + if(!messageReaded.getFromId().equals(messageReaded.getToId())){ + messageProducer.sendToUser(messageReadedPack.getToId(),GroupEventCommand.MSG_GROUP_READED_RECEIPT + ,messageReaded,messageReaded.getAppId()); + } + } + + public ResponseVO syncOfflineMessage(SyncReq req) { + + SyncResp resp = new SyncResp<>(); + + String key = req.getAppId() + ":" + Constants.RedisConstants.OfflineMessage + ":" + req.getOperater(); + //获取最大的seq + Long maxSeq = 0L; + ZSetOperations zSetOperations = redisTemplate.opsForZSet(); + Set set = zSetOperations.reverseRangeWithScores(key, 0, 0); + if(!CollectionUtils.isEmpty(set)){ + List list = new ArrayList(set); + DefaultTypedTuple o = (DefaultTypedTuple) list.get(0); + maxSeq = o.getScore().longValue(); + } + + List respList = new ArrayList<>(); + resp.setMaxSequence(maxSeq); + + Set querySet = zSetOperations.rangeByScoreWithScores(key, + req.getLastSequence(), maxSeq, 0, req.getMaxLimit()); + for (ZSetOperations.TypedTuple typedTuple : querySet) { + String value = typedTuple.getValue(); + OfflineMessageContent offlineMessageContent = JSONObject.parseObject(value, OfflineMessageContent.class); + respList.add(offlineMessageContent); + } + resp.setDataList(respList); + + if(!CollectionUtils.isEmpty(respList)){ + OfflineMessageContent offlineMessageContent = respList.get(respList.size() - 1); + resp.setCompleted(maxSeq <= offlineMessageContent.getMessageKey()); + } + + return ResponseVO.successResponse(resp); + } + + //修改历史消息的状态 + //修改离线消息的状态 + //ack给发送方 + //发送给同步端 + //分发给消息的接收方 + public void recallMessage(RecallMessageContent content) { + + Long messageTime = content.getMessageTime(); + Long now = System.currentTimeMillis(); + + RecallMessageNotifyPack pack = new RecallMessageNotifyPack(); + BeanUtils.copyProperties(content,pack); + + if(120000L < now - messageTime){ + recallAck(pack,ResponseVO.errorResponse(MessageErrorCode.MESSAGE_RECALL_TIME_OUT),content); + return; + } + + QueryWrapper query = new QueryWrapper<>(); + query.eq("app_id",content.getAppId()); + query.eq("message_key",content.getMessageKey()); + ImMessageBodyEntity body = imMessageBodyMapper.selectOne(query); + + if(body == null){ + //TODO ack失败 不存在的消息不能撤回 + recallAck(pack,ResponseVO.errorResponse(MessageErrorCode.MESSAGEBODY_IS_NOT_EXIST),content); + return; + } + + if(body.getDelFlag() == DelFlagEnum.DELETE.getCode()){ + recallAck(pack,ResponseVO.errorResponse(MessageErrorCode.MESSAGE_IS_RECALLED),content); + + return; + } + + body.setDelFlag(DelFlagEnum.DELETE.getCode()); + imMessageBodyMapper.update(body,query); + + if(content.getConversationType() == ConversationTypeEnum.P2P.getCode()){ + + // 找到fromId的队列 + String fromKey = content.getAppId() + ":" + Constants.RedisConstants.OfflineMessage + ":" + content.getFromId(); + // 找到toId的队列 + String toKey = content.getAppId() + ":" + Constants.RedisConstants.OfflineMessage + ":" + content.getToId(); + + OfflineMessageContent offlineMessageContent = new OfflineMessageContent(); + BeanUtils.copyProperties(content,offlineMessageContent); + offlineMessageContent.setDelFlag(DelFlagEnum.DELETE.getCode()); + offlineMessageContent.setMessageKey(content.getMessageKey()); + offlineMessageContent.setConversationType(ConversationTypeEnum.P2P.getCode()); + offlineMessageContent.setConversationId(conversationService.convertConversationId(offlineMessageContent.getConversationType() + ,content.getFromId(),content.getToId())); + offlineMessageContent.setMessageBody(body.getMessageBody()); + + long seq = redisSeq.doGetSeq(content.getAppId() + ":" + Constants.SeqConstants.Message + ":" + ConversationIdGenerate.generateP2PId(content.getFromId(),content.getToId())); + offlineMessageContent.setMessageSequence(seq); + + long messageKey = SnowflakeIdWorker.nextId(); + + redisTemplate.opsForZSet().add(fromKey,JSONObject.toJSONString(offlineMessageContent),messageKey); + redisTemplate.opsForZSet().add(toKey,JSONObject.toJSONString(offlineMessageContent),messageKey); + + //ack + recallAck(pack,ResponseVO.successResponse(),content); + //分发给同步端 + messageProducer.sendToUserExceptClient(content.getFromId(), + MessageCommand.MSG_RECALL_NOTIFY,pack,content); + //分发给对方 + messageProducer.sendToUser(content.getToId(),MessageCommand.MSG_RECALL_NOTIFY, + pack,content.getAppId()); + }else{ + List groupMemberId = imGroupMemberService.getGroupMemberId(content.getToId(), content.getAppId()); + long seq = redisSeq.doGetSeq(content.getAppId() + ":" + Constants.SeqConstants.Message + ":" + ConversationIdGenerate.generateP2PId(content.getFromId(),content.getToId())); + //ack + recallAck(pack,ResponseVO.successResponse(),content); + //发送给同步端 + messageProducer.sendToUserExceptClient(content.getFromId(), MessageCommand.MSG_RECALL_NOTIFY, pack + , content); + for (String memberId : groupMemberId) { + String toKey = content.getAppId() + ":" + Constants.SeqConstants.Message + ":" + memberId; + OfflineMessageContent offlineMessageContent = new OfflineMessageContent(); + offlineMessageContent.setDelFlag(DelFlagEnum.DELETE.getCode()); + BeanUtils.copyProperties(content,offlineMessageContent); + offlineMessageContent.setConversationType(ConversationTypeEnum.GROUP.getCode()); + offlineMessageContent.setConversationId(conversationService.convertConversationId(offlineMessageContent.getConversationType() + ,content.getFromId(),content.getToId())); + offlineMessageContent.setMessageBody(body.getMessageBody()); + offlineMessageContent.setMessageSequence(seq); + redisTemplate.opsForZSet().add(toKey,JSONObject.toJSONString(offlineMessageContent),seq); + + groupMessageProducer.producer(content.getFromId(), MessageCommand.MSG_RECALL_NOTIFY, pack,content); + } + } + + } + private void recallAck(RecallMessageNotifyPack recallPack, ResponseVO success, ClientInfo clientInfo) { + ResponseVO wrappedResp = success; + messageProducer.sendToUser(recallPack.getFromId(), + MessageCommand.MSG_RECALL_ACK, wrappedResp, clientInfo); + } + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/message/service/P2PMessageService.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/service/P2PMessageService.java new file mode 100644 index 0000000..7787d18 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/message/service/P2PMessageService.java @@ -0,0 +1,238 @@ +package com.lld.im.service.message.service; + +import com.alibaba.fastjson.JSONObject; +import com.lld.im.codec.pack.message.ChatMessageAck; +import com.lld.im.codec.pack.message.MessageReciveServerAckPack; +import com.lld.im.codec.proto.Message; +import com.lld.im.common.ResponseVO; +import com.lld.im.common.config.AppConfig; +import com.lld.im.common.constant.Constants; +import com.lld.im.common.enums.ConversationTypeEnum; +import com.lld.im.common.enums.command.MessageCommand; +import com.lld.im.common.model.ClientInfo; +import com.lld.im.common.model.message.MessageContent; +import com.lld.im.common.model.message.OfflineMessageContent; +import com.lld.im.service.message.model.req.SendMessageReq; +import com.lld.im.service.message.model.resp.SendMessageResp; +import com.lld.im.service.seq.RedisSeq; +import com.lld.im.service.utils.CallbackService; +import com.lld.im.service.utils.ConversationIdGenerate; +import com.lld.im.service.utils.MessageProducer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Service +public class P2PMessageService { + + private static Logger logger = LoggerFactory.getLogger(P2PMessageService.class); + + @Autowired + CheckSendMessageService checkSendMessageService; + + @Autowired + MessageProducer messageProducer; + + @Autowired + MessageStoreService messageStoreService; + + @Autowired + RedisSeq redisSeq; + + @Autowired + AppConfig appConfig; + + @Autowired + CallbackService callbackService; + + + private final ThreadPoolExecutor threadPoolExecutor; + + { + final AtomicInteger num = new AtomicInteger(0); + threadPoolExecutor = new ThreadPoolExecutor(8, 8, 60, TimeUnit.SECONDS, + new LinkedBlockingDeque<>(1000), new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r); + thread.setDaemon(true); + thread.setName("message-process-thread-" + num.getAndIncrement()); + return thread; + } + }); + } + + //离线 + //存储介质 + //1.mysql + //2.redis + //怎么存? + //list + + + //历史消息 + + //发送方客户端时间 + //messageKey + //redis 1 2 3 + public void process(MessageContent messageContent){ + + logger.info("消息开始处理:{}",messageContent.getMessageId()); + String fromId = messageContent.getFromId(); + String toId = messageContent.getToId(); + Integer appId = messageContent.getAppId(); + + MessageContent messageFromMessageIdCache = messageStoreService.getMessageFromMessageIdCache + (messageContent.getAppId(), messageContent.getMessageId(),MessageContent.class); + if (messageFromMessageIdCache != null){ + threadPoolExecutor.execute(() ->{ + ack(messageContent,ResponseVO.successResponse()); + //2.发消息给同步在线端 + syncToSender(messageFromMessageIdCache,messageFromMessageIdCache); + //3.发消息给对方在线端 + List clientInfos = dispatchMessage(messageFromMessageIdCache); + if(clientInfos.isEmpty()){ + //发送接收确认给发送方,要带上是服务端发送的标识 + reciverAck(messageFromMessageIdCache); + } + }); + return; + } + + //回调 + ResponseVO responseVO = ResponseVO.successResponse(); + if(appConfig.isSendMessageAfterCallback()){ + responseVO = callbackService.beforeCallback(messageContent.getAppId(), Constants.CallbackCommand.SendMessageBefore + , JSONObject.toJSONString(messageContent)); + } + + if(!responseVO.isOk()){ + ack(messageContent,responseVO); + return; + } + + long seq = redisSeq.doGetSeq(messageContent.getAppId() + ":" + + Constants.SeqConstants.Message+ ":" + ConversationIdGenerate.generateP2PId( + messageContent.getFromId(),messageContent.getToId() + )); + messageContent.setMessageSequence(seq); + + //前置校验 + //这个用户是否被禁言 是否被禁用 + //发送方和接收方是否是好友 +// ResponseVO responseVO = imServerPermissionCheck(fromId, toId, appId); +// if(responseVO.isOk()){ + threadPoolExecutor.execute(() ->{ + //appId + Seq + (from + to) groupId + messageStoreService.storeP2PMessage(messageContent); + + OfflineMessageContent offlineMessageContent = new OfflineMessageContent(); + BeanUtils.copyProperties(messageContent,offlineMessageContent); + offlineMessageContent.setConversationType(ConversationTypeEnum.P2P.getCode()); + messageStoreService.storeOfflineMessage(offlineMessageContent); + + //插入数据 + //1.回ack成功给自己 + ack(messageContent,ResponseVO.successResponse()); + //2.发消息给同步在线端 + syncToSender(messageContent,messageContent); + //3.发消息给对方在线端 + List clientInfos = dispatchMessage(messageContent); + + messageStoreService.setMessageFromMessageIdCache(messageContent.getAppId(), + messageContent.getMessageId(),messageContent); + if(clientInfos.isEmpty()){ + //发送接收确认给发送方,要带上是服务端发送的标识 + reciverAck(messageContent); + } + + if(appConfig.isSendMessageAfterCallback()){ + callbackService.callback(messageContent.getAppId(),Constants.CallbackCommand.SendMessageAfter, + JSONObject.toJSONString(messageContent)); + } + + logger.info("消息处理完成:{}",messageContent.getMessageId()); + }); +// }else{ +// //告诉客户端失败了 +// //ack +// ack(messageContent,responseVO); +// } + } + + private List dispatchMessage(MessageContent messageContent){ + List clientInfos = messageProducer.sendToUser(messageContent.getToId(), MessageCommand.MSG_P2P, + messageContent, messageContent.getAppId()); + return clientInfos; + } + + private void ack(MessageContent messageContent,ResponseVO responseVO){ + logger.info("msg ack,msgId={},checkResut{}",messageContent.getMessageId(),responseVO.getCode()); + + ChatMessageAck chatMessageAck = new + ChatMessageAck(messageContent.getMessageId(),messageContent.getMessageSequence()); + responseVO.setData(chatMessageAck); + //發消息 + messageProducer.sendToUser(messageContent.getFromId(), MessageCommand.MSG_ACK, + responseVO,messageContent + ); + } + + public void reciverAck(MessageContent messageContent){ + MessageReciveServerAckPack pack = new MessageReciveServerAckPack(); + pack.setFromId(messageContent.getToId()); + pack.setToId(messageContent.getFromId()); + pack.setMessageKey(messageContent.getMessageKey()); + pack.setMessageSequence(messageContent.getMessageSequence()); + pack.setServerSend(true); + messageProducer.sendToUser(messageContent.getFromId(),MessageCommand.MSG_RECIVE_ACK, + pack,new ClientInfo(messageContent.getAppId(),messageContent.getClientType() + ,messageContent.getImei())); + } + + private void syncToSender(MessageContent messageContent, ClientInfo clientInfo){ + messageProducer.sendToUserExceptClient(messageContent.getFromId(), + MessageCommand.MSG_P2P,messageContent,messageContent); + } + + public ResponseVO imServerPermissionCheck(String fromId,String toId, + Integer appId){ + ResponseVO responseVO = checkSendMessageService.checkSenderForvidAndMute(fromId, appId); + if(!responseVO.isOk()){ + return responseVO; + } + responseVO = checkSendMessageService.checkFriendShip(fromId, toId, appId); + return responseVO; + } + + public SendMessageResp send(SendMessageReq req) { + + SendMessageResp sendMessageResp = new SendMessageResp(); + MessageContent message = new MessageContent(); + BeanUtils.copyProperties(req,message); + //插入数据 + messageStoreService.storeP2PMessage(message); + sendMessageResp.setMessageKey(message.getMessageKey()); + sendMessageResp.setMessageTime(System.currentTimeMillis()); + + //2.发消息给同步在线端 + syncToSender(message,message); + //3.发消息给对方在线端 + dispatchMessage(message); + return sendMessageResp; + } +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/seq/RedisSeq.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/seq/RedisSeq.java new file mode 100644 index 0000000..f809b1e --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/seq/RedisSeq.java @@ -0,0 +1,23 @@ +package com.lld.im.service.seq; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Service +public class RedisSeq { + + @Autowired + StringRedisTemplate stringRedisTemplate; + + public long doGetSeq(String key){ + return stringRedisTemplate.opsForValue().increment(key); + } + + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/user/controller/ImUserController.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/controller/ImUserController.java index b91428f..f392dcc 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/user/controller/ImUserController.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/controller/ImUserController.java @@ -1,14 +1,22 @@ package com.lld.im.service.user.controller; +import com.lld.im.common.ClientType; import com.lld.im.common.ResponseVO; +import com.lld.im.common.route.RouteHandle; +import com.lld.im.common.route.RouteInfo; +import com.lld.im.common.utils.RouteInfoParseUtil; import com.lld.im.service.user.model.req.*; import com.lld.im.service.user.service.ImUserService; +import com.lld.im.service.user.service.ImUserStatusService; +import com.lld.im.service.utils.ZKit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.ArrayList; +import java.util.List; /** * @description: @@ -21,15 +29,96 @@ public class ImUserController { @Autowired ImUserService imUserService; + @Autowired + RouteHandle routeHandle; + + @Autowired + ImUserStatusService imUserStatusService; + + @Autowired + ZKit zKit; + @RequestMapping("importUser") public ResponseVO importUser(@RequestBody ImportUserReq req, Integer appId) { + req.setAppId(appId); return imUserService.importUser(req); } + @RequestMapping("/deleteUser") public ResponseVO deleteUser(@RequestBody @Validated DeleteUserReq req, Integer appId) { req.setAppId(appId); return imUserService.deleteUser(req); } + /** + * @param [req] + * @return com.lld.im.common.ResponseVO + * @description im的登录接口,返回im地址 + * @author chackylee + */ + @RequestMapping("/login") + public ResponseVO login(@RequestBody @Validated LoginReq req, Integer appId) { + req.setAppId(appId); + + ResponseVO login = imUserService.login(req); + if (login.isOk()) { + List allNode = new ArrayList<>(); + if (req.getClientType() == ClientType.WEB.getCode()) { + allNode = zKit.getAllWebNode(); + } else { + allNode = zKit.getAllTcpNode(); + } + String s = routeHandle.routeServer(allNode, req + .getUserId()); + RouteInfo parse = RouteInfoParseUtil.parse(s); + return ResponseVO.successResponse(parse); + } + + return ResponseVO.errorResponse(); + } + + @RequestMapping("/getUserSequence") + public ResponseVO getUserSequence(@RequestBody @Validated + GetUserSequenceReq req, Integer appId) { + req.setAppId(appId); + return imUserService.getUserSequence(req); + } + + @RequestMapping("/subscribeUserOnlineStatus") + public ResponseVO subscribeUserOnlineStatus(@RequestBody @Validated + SubscribeUserOnlineStatusReq req, Integer appId,String identifier) { + req.setAppId(appId); + req.setOperater(identifier); + imUserStatusService.subscribeUserOnlineStatus(req); + return ResponseVO.successResponse(); + } + + @RequestMapping("/setUserCustomerStatus") + public ResponseVO setUserCustomerStatus(@RequestBody @Validated + SetUserCustomerStatusReq req, Integer appId,String identifier) { + req.setAppId(appId); + req.setOperater(identifier); + imUserStatusService.setUserCustomerStatus(req); + return ResponseVO.successResponse(); + } + + @RequestMapping("/queryFriendOnlineStatus") + public ResponseVO queryFriendOnlineStatus(@RequestBody @Validated + PullFriendOnlineStatusReq req, Integer appId,String identifier) { + req.setAppId(appId); + req.setOperater(identifier); + return ResponseVO.successResponse(imUserStatusService.queryFriendOnlineStatus(req)); + } + + @RequestMapping("/queryUserOnlineStatus") + public ResponseVO queryUserOnlineStatus(@RequestBody @Validated + PullUserOnlineStatusReq req, Integer appId,String identifier) { + req.setAppId(appId); + req.setOperater(identifier); + return ResponseVO.successResponse(imUserStatusService.queryUserOnlineStatus(req)); + } + + + } diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/user/model/req/LoginReq.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/model/req/LoginReq.java new file mode 100644 index 0000000..16ccb19 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/model/req/LoginReq.java @@ -0,0 +1,23 @@ +package com.lld.im.service.user.model.req; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + + +/** + * @author: Chackylee + * @description: + **/ +@Data +public class LoginReq { + + @NotNull(message = "用户id不能位空") + private String userId; + + @NotNull(message = "appId不能为空") + private Integer appId; + + private Integer clientType; + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/user/model/req/PullFriendOnlineStatusReq.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/model/req/PullFriendOnlineStatusReq.java new file mode 100644 index 0000000..00d4445 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/model/req/PullFriendOnlineStatusReq.java @@ -0,0 +1,13 @@ +package com.lld.im.service.user.model.req; + +import com.lld.im.common.model.RequestBase; +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class PullFriendOnlineStatusReq extends RequestBase { +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/user/model/req/PullUserOnlineStatusReq.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/model/req/PullUserOnlineStatusReq.java new file mode 100644 index 0000000..ab29ed2 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/model/req/PullUserOnlineStatusReq.java @@ -0,0 +1,18 @@ +package com.lld.im.service.user.model.req; + +import com.lld.im.common.model.RequestBase; +import lombok.Data; + +import java.util.List; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class PullUserOnlineStatusReq extends RequestBase { + + private List userList; + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/user/model/req/SetUserCustomerStatusReq.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/model/req/SetUserCustomerStatusReq.java new file mode 100644 index 0000000..d35e973 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/model/req/SetUserCustomerStatusReq.java @@ -0,0 +1,20 @@ +package com.lld.im.service.user.model.req; + +import com.lld.im.common.model.RequestBase; +import lombok.Data; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class SetUserCustomerStatusReq extends RequestBase { + + private String userId; + + private String customText; + + private Integer customStatus; + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/user/model/req/SubscribeUserOnlineStatusReq.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/model/req/SubscribeUserOnlineStatusReq.java new file mode 100644 index 0000000..11e56de --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/model/req/SubscribeUserOnlineStatusReq.java @@ -0,0 +1,21 @@ +package com.lld.im.service.user.model.req; + +import com.lld.im.common.model.RequestBase; +import lombok.Data; + +import java.util.List; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Data +public class SubscribeUserOnlineStatusReq extends RequestBase { + + private List subUserId; + + private Long subTime; + + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/user/service/ImUserService.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/service/ImUserService.java index 81b61f9..49e0c56 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/user/service/ImUserService.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/service/ImUserService.java @@ -22,5 +22,8 @@ public interface ImUserService { public ResponseVO modifyUserInfo(ModifyUserInfoReq req); + public ResponseVO login(LoginReq req); + + ResponseVO getUserSequence(GetUserSequenceReq req); } diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/user/service/ImUserStatusService.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/service/ImUserStatusService.java new file mode 100644 index 0000000..bf39589 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/service/ImUserStatusService.java @@ -0,0 +1,29 @@ +package com.lld.im.service.user.service; + +import com.lld.im.common.ResponseVO; +import com.lld.im.service.user.model.UserStatusChangeNotifyContent; +import com.lld.im.service.user.model.req.PullFriendOnlineStatusReq; +import com.lld.im.service.user.model.req.PullUserOnlineStatusReq; +import com.lld.im.service.user.model.req.SetUserCustomerStatusReq; +import com.lld.im.service.user.model.req.SubscribeUserOnlineStatusReq; +import com.lld.im.service.user.model.resp.UserOnlineStatusResp; + +import java.util.Map; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +public interface ImUserStatusService { + + public void processUserOnlineStatusNotify(UserStatusChangeNotifyContent content); + + void subscribeUserOnlineStatus(SubscribeUserOnlineStatusReq req); + + void setUserCustomerStatus(SetUserCustomerStatusReq req); + + Map queryFriendOnlineStatus(PullFriendOnlineStatusReq req); + + Map queryUserOnlineStatus(PullUserOnlineStatusReq req); +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/user/service/impl/ImUserStatusServiceImpl.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/service/impl/ImUserStatusServiceImpl.java new file mode 100644 index 0000000..38160ad --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/service/impl/ImUserStatusServiceImpl.java @@ -0,0 +1,178 @@ +package com.lld.im.service.user.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.lld.im.codec.pack.user.UserCustomStatusChangeNotifyPack; +import com.lld.im.codec.pack.user.UserStatusChangeNotifyPack; +import com.lld.im.common.ResponseVO; +import com.lld.im.common.constant.Constants; +import com.lld.im.common.enums.command.UserEventCommand; +import com.lld.im.common.model.ClientInfo; +import com.lld.im.common.model.UserSession; +import com.lld.im.service.friendship.service.ImFriendService; +import com.lld.im.service.user.model.UserStatusChangeNotifyContent; +import com.lld.im.service.user.model.req.PullFriendOnlineStatusReq; +import com.lld.im.service.user.model.req.PullUserOnlineStatusReq; +import com.lld.im.service.user.model.req.SetUserCustomerStatusReq; +import com.lld.im.service.user.model.req.SubscribeUserOnlineStatusReq; +import com.lld.im.service.user.model.resp.UserOnlineStatusResp; +import com.lld.im.service.user.service.ImUserStatusService; +import com.lld.im.service.utils.MessageProducer; +import com.lld.im.service.utils.UserSessionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Service +public class ImUserStatusServiceImpl implements ImUserStatusService { + + @Autowired + UserSessionUtils userSessionUtils; + + @Autowired + MessageProducer messageProducer; + + @Autowired + ImFriendService imFriendService; + + @Autowired + StringRedisTemplate stringRedisTemplate; + + @Override + public void processUserOnlineStatusNotify(UserStatusChangeNotifyContent content) { + + List userSession = userSessionUtils.getUserSession(content.getAppId(), content.getUserId()); + UserStatusChangeNotifyPack userStatusChangeNotifyPack = new UserStatusChangeNotifyPack(); + BeanUtils.copyProperties(content,userStatusChangeNotifyPack); + userStatusChangeNotifyPack.setClient(userSession); + + syncSender(userStatusChangeNotifyPack,content.getUserId(), + content); + + dispatcher(userStatusChangeNotifyPack,content.getUserId(), + content.getAppId()); + } + + + + private void syncSender(Object pack, String userId, ClientInfo clientInfo){ + messageProducer.sendToUserExceptClient(userId, + UserEventCommand.USER_ONLINE_STATUS_CHANGE_NOTIFY_SYNC, + pack,clientInfo); + } + + private void dispatcher(Object pack,String userId,Integer appId){ + List allFriendId = imFriendService.getAllFriendId(userId, appId); + for (String fid : allFriendId) { + messageProducer.sendToUser(fid,UserEventCommand.USER_ONLINE_STATUS_CHANGE_NOTIFY, + pack,appId); + } + + String userKey = appId + ":" + Constants.RedisConstants.subscribe + userId; + Set keys = stringRedisTemplate.opsForHash().keys(userKey); + for (Object key : keys) { + String filed = (String) key; + Long expire = Long.valueOf((String) stringRedisTemplate.opsForHash().get(userKey, filed)); + if(expire > 0 && expire > System.currentTimeMillis()){ + messageProducer.sendToUser(filed,UserEventCommand.USER_ONLINE_STATUS_CHANGE_NOTIFY, + pack,appId); + }else{ + stringRedisTemplate.opsForHash().delete(userKey,filed); + } + } + } + + + /** + * @description: + * @param + * @return void + * @author lld + */ + @Override + public void subscribeUserOnlineStatus(SubscribeUserOnlineStatusReq req) { + // A + // Z + // A - B C D + // C:A Z F + //hash + // B - [A:xxxx,C:xxxx] + // C - [] + // D - [] + Long subExpireTime = 0L; + if(req != null && req.getSubTime() > 0){ + subExpireTime = System.currentTimeMillis() + req.getSubTime(); + } + + for (String beSubUserId : req.getSubUserId()) { + String userKey = req.getAppId() + ":" + Constants.RedisConstants.subscribe + ":" + beSubUserId; + stringRedisTemplate.opsForHash().put(userKey,req.getOperater(),subExpireTime.toString()); + } + } + + /** + * @description: 设置自定义状态 + * @param + * @return void + * @author lld + */ + @Override + public void setUserCustomerStatus(SetUserCustomerStatusReq req) { + UserCustomStatusChangeNotifyPack userCustomStatusChangeNotifyPack = new UserCustomStatusChangeNotifyPack(); + userCustomStatusChangeNotifyPack.setCustomStatus(req.getCustomStatus()); + userCustomStatusChangeNotifyPack.setCustomText(req.getCustomText()); + userCustomStatusChangeNotifyPack.setUserId(req.getUserId()); + stringRedisTemplate.opsForValue().set(req.getAppId() + +":"+ Constants.RedisConstants.userCustomerStatus + ":" + req.getUserId() + ,JSONObject.toJSONString(userCustomStatusChangeNotifyPack)); + + syncSender(userCustomStatusChangeNotifyPack, + req.getUserId(),new ClientInfo(req.getAppId(),req.getClientType(),req.getImei())); + dispatcher(userCustomStatusChangeNotifyPack,req.getUserId(),req.getAppId()); + } + + @Override + public Map queryFriendOnlineStatus(PullFriendOnlineStatusReq req) { + + List allFriendId = imFriendService.getAllFriendId(req.getOperater(), req.getAppId()); + return getUserOnlineStatus(allFriendId,req.getAppId()); + } + + @Override + public Map queryUserOnlineStatus(PullUserOnlineStatusReq req) { + return getUserOnlineStatus(req.getUserList(),req.getAppId()); + } + + private Map getUserOnlineStatus(List userId,Integer appId){ + + Map result = new HashMap<>(userId.size()); + for (String uid : userId) { + + UserOnlineStatusResp resp = new UserOnlineStatusResp(); + List userSession = userSessionUtils.getUserSession(appId, uid); + resp.setSession(userSession); + String userKey = appId + ":" + Constants.RedisConstants.userCustomerStatus + ":" + uid; + String s = stringRedisTemplate.opsForValue().get(userKey); + if(StringUtils.isNotBlank(s)){ + JSONObject parse = (JSONObject) JSON.parse(s); + resp.setCustomText(parse.getString("customText")); + resp.setCustomStatus(parse.getInteger("customStatus")); + } + result.put(uid,resp); + } + return result; + } + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/user/service/impl/ImUserviceImpl.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/service/impl/ImUserviceImpl.java index ef590bd..51bf591 100644 --- a/hs-im-server/im-service/src/main/java/com/lld/im/service/user/service/impl/ImUserviceImpl.java +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/user/service/impl/ImUserviceImpl.java @@ -2,9 +2,13 @@ package com.lld.im.service.user.service.impl; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.lld.im.codec.pack.user.UserModifyPack; import com.lld.im.common.ResponseVO; +import com.lld.im.common.config.AppConfig; +import com.lld.im.common.constant.Constants; import com.lld.im.common.enums.DelFlagEnum; import com.lld.im.common.enums.UserErrorCode; +import com.lld.im.common.enums.command.UserEventCommand; import com.lld.im.common.exception.ApplicationException; import com.lld.im.service.group.service.ImGroupService; import com.lld.im.service.user.dao.ImUserDataEntity; @@ -13,8 +17,11 @@ import com.lld.im.service.user.model.req.*; import com.lld.im.service.user.model.resp.GetUserInfoResp; import com.lld.im.service.user.model.resp.ImportUserResp; import com.lld.im.service.user.service.ImUserService; +import com.lld.im.service.utils.CallbackService; +import com.lld.im.service.utils.MessageProducer; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,6 +41,17 @@ public class ImUserviceImpl implements ImUserService { @Autowired ImUserDataMapper imUserDataMapper; + @Autowired + AppConfig appConfig; + + @Autowired + CallbackService callbackService; + + @Autowired + MessageProducer messageProducer; + + @Autowired + StringRedisTemplate stringRedisTemplate; @Autowired ImGroupService imGroupService; @@ -165,9 +183,31 @@ public class ImUserviceImpl implements ImUserService { update.setUserId(null); int update1 = imUserDataMapper.update(update, query); if(update1 == 1){ + UserModifyPack pack = new UserModifyPack(); + BeanUtils.copyProperties(req,pack); + messageProducer.sendToUser(req.getUserId(),req.getClientType(),req.getImei(), + UserEventCommand.USER_MODIFY,pack,req.getAppId()); + + if(appConfig.isModifyUserAfterCallback()){ + callbackService.callback(req.getAppId(), + Constants.CallbackCommand.ModifyUserAfter, + JSONObject.toJSONString(req)); + } return ResponseVO.successResponse(); } throw new ApplicationException(UserErrorCode.MODIFY_USER_ERROR); } + @Override + public ResponseVO login(LoginReq req) { + return ResponseVO.successResponse(); + } + + @Override + public ResponseVO getUserSequence(GetUserSequenceReq req) { + Map map = stringRedisTemplate.opsForHash().entries(req.getAppId() + ":" + Constants.RedisConstants.SeqPrefix + ":" + req.getUserId()); + Long groupSeq = imGroupService.getUserGroupMaxSeq(req.getUserId(),req.getAppId()); + map.put(Constants.SeqConstants.Group,groupSeq); + return ResponseVO.successResponse(map); + } } diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/CallbackService.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/CallbackService.java new file mode 100644 index 0000000..b50104a --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/CallbackService.java @@ -0,0 +1,64 @@ +package com.lld.im.service.utils; + +import com.lld.im.common.ResponseVO; +import com.lld.im.common.config.AppConfig; +import com.lld.im.common.utils.HttpRequestUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Component +public class CallbackService { + + private Logger logger = LoggerFactory.getLogger(CallbackService.class); + + @Autowired + HttpRequestUtils httpRequestUtils; + + @Autowired + AppConfig appConfig; + + @Autowired + ShareThreadPool shareThreadPool; + + + public void callback(Integer appId,String callbackCommand,String jsonBody){ + shareThreadPool.submit(() -> { + try { + httpRequestUtils.doPost(appConfig.getCallbackUrl(),Object.class,builderUrlParams(appId,callbackCommand), + jsonBody,null); + }catch (Exception e){ + logger.error("callback 回调{} : {}出现异常 : {} ",callbackCommand , appId, e.getMessage()); + } + }); + } + + public ResponseVO beforeCallback(Integer appId,String callbackCommand,String jsonBody){ + try { + ResponseVO responseVO = httpRequestUtils.doPost("", ResponseVO.class, builderUrlParams(appId, callbackCommand), + jsonBody, null); + return responseVO; + }catch (Exception e){ + logger.error("callback 之前 回调{} : {}出现异常 : {} ",callbackCommand , appId, e.getMessage()); + return ResponseVO.successResponse(); + } + } + + public Map builderUrlParams(Integer appId, String command) { + Map map = new HashMap(); + map.put("appId", appId); + map.put("command", command); + return map; + } + + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/ConversationIdGenerate.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/ConversationIdGenerate.java new file mode 100644 index 0000000..36a62ee --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/ConversationIdGenerate.java @@ -0,0 +1,21 @@ +package com.lld.im.service.utils; + + +/** + * @author: Chackylee + **/ +public class ConversationIdGenerate { + + //A|B + //B A + public static String generateP2PId(String fromId,String toId){ + int i = fromId.compareTo(toId); + if(i < 0){ + return toId+"|"+fromId; + }else if(i > 0){ + return fromId+"|"+toId; + } + + throw new RuntimeException(""); + } +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/GroupMessageProducer.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/GroupMessageProducer.java new file mode 100644 index 0000000..c182477 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/GroupMessageProducer.java @@ -0,0 +1,102 @@ +package com.lld.im.service.utils; + +import com.alibaba.fastjson.JSONObject; +import com.lld.im.codec.pack.group.AddGroupMemberPack; +import com.lld.im.codec.pack.group.RemoveGroupMemberPack; +import com.lld.im.codec.pack.group.UpdateGroupMemberPack; +import com.lld.im.common.ClientType; +import com.lld.im.common.enums.command.Command; +import com.lld.im.common.enums.command.GroupEventCommand; +import com.lld.im.common.model.ClientInfo; +import com.lld.im.service.group.model.req.GroupMemberDto; +import com.lld.im.service.group.service.ImGroupMemberService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Component +public class GroupMessageProducer { + + @Autowired + MessageProducer messageProducer; + + @Autowired + ImGroupMemberService imGroupMemberService; + + public void producer(String userId, Command command, Object data, + ClientInfo clientInfo){ + JSONObject o = (JSONObject) JSONObject.toJSON(data); + String groupId = o.getString("groupId"); + List groupMemberId = imGroupMemberService + .getGroupMemberId(groupId, clientInfo.getAppId()); + + if(command.equals(GroupEventCommand.ADDED_MEMBER)){ + //发送给管理员和被加入人本身 + List groupManager = imGroupMemberService.getGroupManager(groupId, clientInfo.getAppId()); + AddGroupMemberPack addGroupMemberPack + = o.toJavaObject(AddGroupMemberPack.class); + List members = addGroupMemberPack.getMembers(); + for (GroupMemberDto groupMemberDto : groupManager) { + if(clientInfo.getClientType() != ClientType.WEBAPI.getCode() && groupMemberDto.getMemberId().equals(userId)){ + messageProducer.sendToUserExceptClient(groupMemberDto.getMemberId(),command,data,clientInfo); + }else{ + messageProducer.sendToUser(groupMemberDto.getMemberId(),command,data,clientInfo.getAppId()); + } + } + for (String member : members) { + if(clientInfo.getClientType() != ClientType.WEBAPI.getCode() && member.equals(userId)){ + messageProducer.sendToUserExceptClient(member,command,data,clientInfo); + }else{ + messageProducer.sendToUser(member,command,data,clientInfo.getAppId()); + } + } + }else if(command.equals(GroupEventCommand.DELETED_MEMBER)){ + RemoveGroupMemberPack pack = o.toJavaObject(RemoveGroupMemberPack.class); + String member = pack.getMember(); + List members = imGroupMemberService.getGroupMemberId(groupId, clientInfo.getAppId()); + members.add(member); + for (String memberId : members) { + if(clientInfo.getClientType() != ClientType.WEBAPI.getCode() && member.equals(userId)){ + messageProducer.sendToUserExceptClient(memberId,command,data,clientInfo); + }else{ + messageProducer.sendToUser(memberId,command,data,clientInfo.getAppId()); + } + } + }else if(command.equals(GroupEventCommand.UPDATED_MEMBER)){ + UpdateGroupMemberPack pack = + o.toJavaObject(UpdateGroupMemberPack.class); + String memberId = pack.getMemberId(); + List groupManager = imGroupMemberService.getGroupManager(groupId, clientInfo.getAppId()); + GroupMemberDto groupMemberDto = new GroupMemberDto(); + groupMemberDto.setMemberId(memberId); + groupManager.add(groupMemberDto); + for (GroupMemberDto member : groupManager) { + if(clientInfo.getClientType() != ClientType.WEBAPI.getCode() && member.equals(userId)){ + messageProducer.sendToUserExceptClient(member.getMemberId(),command,data,clientInfo); + }else{ + messageProducer.sendToUser(member.getMemberId(),command,data,clientInfo.getAppId()); + } + } + }else { + for (String memberId : groupMemberId) { + if(clientInfo.getClientType() != null && clientInfo.getClientType() != + ClientType.WEBAPI.getCode() && memberId.equals(userId)){ + messageProducer.sendToUserExceptClient(memberId,command, + data,clientInfo); + }else{ + messageProducer.sendToUser(memberId,command,data,clientInfo.getAppId()); + } + } + } + + + + } + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/MessageKeyGenerate.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/MessageKeyGenerate.java new file mode 100644 index 0000000..b401655 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/MessageKeyGenerate.java @@ -0,0 +1,149 @@ +package com.lld.im.service.utils; + + + +import java.util.Calendar; +import java.util.Date; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author: Chackylee + * @description: 消息key生成 + **/ +public class MessageKeyGenerate { + + //标识从2020.1.1开始 + private static final long T202001010000 = 1577808000000L; + +// private Lock lock = new ReentrantLock(); + AtomicReference owner = new AtomicReference<>(); + + private static volatile int rotateId = 0; + private static int rotateIdWidth = 15; + + private static int rotateIdMask = 32767; + private static volatile long timeId = 0; + + private int nodeId = 0; + private static int nodeIdWidth = 6; + private static int nodeIdMask = 63; + + + public void setNodeId(int nodeId) { + this.nodeId = nodeId; + } + + /** + * ID = timestamp(43) + nodeId(6) + rotateId(15) + * + */ + public synchronized long generateId() throws Exception { + +// lock.lock(); + + this.lock(); + + rotateId = rotateId + 1; + + long id = System.currentTimeMillis() - T202001010000; + + //不同毫秒数生成的id要重置timeId和自选次数 + if (id > timeId) { + timeId = id; + rotateId = 1; + } else if (id == timeId) { + //表示是同一毫秒的请求 + if (rotateId == rotateIdMask) { + //一毫秒只能发送32768到这里表示当前毫秒数已经超过了 + while (id <= timeId) { + //重新给id赋值 + id = System.currentTimeMillis() - T202001010000; + } + this.unLock(); + return generateId(); + } + } + + id <<= nodeIdWidth; + id += (nodeId & nodeIdMask); + + + id <<= rotateIdWidth; + id += rotateId; + +// lock.unlock(); + this.unLock(); + return id; + } + + public static int getSharding(long mid) { + + Calendar calendar = Calendar.getInstance(); + + mid >>= nodeIdWidth; + mid >>= rotateIdWidth; + + calendar.setTime(new Date(T202001010000 + mid)); + + int month = calendar.get(Calendar.MONTH); + int year = calendar.get(Calendar.YEAR); + year %= 3; + + return (year * 12 + month); + } + + public static long getMsgIdFromTimestamp(long timestamp) { + long id = timestamp - T202001010000; + + id <<= rotateIdWidth; + id <<= nodeIdWidth; + + return id; + } + + public void lock() { + Thread cur = Thread.currentThread(); + //lock函数将owner设置为当前线程,并且预测原来的值为空。 + // unlock函数将owner设置为null,并且预测值为当前线程。 + // 当有第二个线程调用lock操作时由于owner值不为空,导致循环 + //一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。 + while (!owner.compareAndSet(null, cur)){ + } + } + public void unLock() { + Thread cur = Thread.currentThread(); + owner.compareAndSet(cur, null); + } + + public static void main(String[] args) throws Exception { +// try { +// Calendar calendar = Calendar.getInstance(); +// MessageKeyGenerate messageKeyGenerate = new MessageKeyGenerate(); +// long msgIdFromTimestamp = getMsgIdFromTimestamp(1678459712000L); +// System.out.println(getSharding(msgIdFromTimestamp)); +// } catch (Exception e) { +// +// } + MessageKeyGenerate messageKeyGenerate = new MessageKeyGenerate(); + for (int i = 0; i < 10; i++) { + long l = messageKeyGenerate.generateId(); + System.out.println(l); + } +// MessageKeyGenerate messageKeyGenerate = new MessageKeyGenerate(); +// long l = messageKeyGenerate.generateId(); +// System.out.println("生成了一个id:" + l); +// int sharding = getSharding(l); +// System.out.println("解密id的时间戳:" + sharding); + + //im_message_history_12 + + + //10000 10001 + //0 1 + + long msgIdFromTimestamp = getMsgIdFromTimestamp(1734529845000L); + int sharding = getSharding(msgIdFromTimestamp); + System.out.println(sharding); + } + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/MessageProducer.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/MessageProducer.java new file mode 100644 index 0000000..8600dba --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/MessageProducer.java @@ -0,0 +1,117 @@ +package com.lld.im.service.utils; + +import com.alibaba.fastjson.JSONObject; +import com.lld.im.codec.proto.MessagePack; +import com.lld.im.common.ClientType; +import com.lld.im.common.constant.Constants; +import com.lld.im.common.enums.command.Command; +import com.lld.im.common.model.ClientInfo; +import com.lld.im.common.model.UserSession; +import jdk.nashorn.internal.scripts.JO; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Service +public class MessageProducer { + + private static Logger logger = LoggerFactory.getLogger(MessageProducer.class); + + @Autowired + RabbitTemplate rabbitTemplate; + + @Autowired + UserSessionUtils userSessionUtils; + + private String queueName = Constants.RabbitConstants.MessageService2Im; + + public boolean sendMessage(UserSession session,Object msg){ + try { + logger.info("send message == " + msg); + rabbitTemplate.convertAndSend(queueName,session.getBrokerId()+"",msg); + return true; + }catch (Exception e){ + logger.error("send error :" + e.getMessage()); + return false; + } + } + + //包装数据,调用sendMessage + public boolean sendPack(String toId, Command command,Object msg,UserSession session){ + MessagePack messagePack = new MessagePack(); + messagePack.setCommand(command.getCommand()); + messagePack.setToId(toId); + messagePack.setClientType(session.getClientType()); + messagePack.setAppId(session.getAppId()); + messagePack.setImei(session.getImei()); + JSONObject jsonObject = JSONObject.parseObject(JSONObject.toJSONString(msg)); + messagePack.setData(jsonObject); + + String body = JSONObject.toJSONString(messagePack); + return sendMessage(session, body); + } + + //发送给所有端的方法 + public List sendToUser(String toId,Command command,Object data,Integer appId){ + List userSession + = userSessionUtils.getUserSession(appId, toId); + List list = new ArrayList<>(); + for (UserSession session : userSession) { + boolean b = sendPack(toId, command, data, session); + if(b){ + list.add(new ClientInfo(session.getAppId(),session.getClientType(),session.getImei())); + } + } + return list; + } + + public void sendToUser(String toId, Integer clientType,String imei, Command command, + Object data, Integer appId){ + if(clientType != null && StringUtils.isNotBlank(imei)){ + ClientInfo clientInfo = new ClientInfo(appId, clientType, imei); + sendToUserExceptClient(toId,command,data,clientInfo); + }else{ + sendToUser(toId,command,data,appId); + } + } + + //发送给某个用户的指定客户端 + public void sendToUser(String toId, Command command + , Object data, ClientInfo clientInfo){ + UserSession userSession = userSessionUtils.getUserSession(clientInfo.getAppId(), toId, clientInfo.getClientType(), + clientInfo.getImei()); + sendPack(toId,command,data,userSession); + } + + private boolean isMatch(UserSession sessionDto, ClientInfo clientInfo) { + return Objects.equals(sessionDto.getAppId(), clientInfo.getAppId()) + && Objects.equals(sessionDto.getImei(), clientInfo.getImei()) + && Objects.equals(sessionDto.getClientType(), clientInfo.getClientType()); + } + + //发送给除了某一端的其他端 + public void sendToUserExceptClient(String toId, Command command + , Object data, ClientInfo clientInfo){ + List userSession = userSessionUtils + .getUserSession(clientInfo.getAppId(), + toId); + for (UserSession session : userSession) { + if(!isMatch(session,clientInfo)){ + sendPack(toId,command,data,session); + } + } + } + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/ShareThreadPool.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/ShareThreadPool.java new file mode 100644 index 0000000..0e46109 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/ShareThreadPool.java @@ -0,0 +1,73 @@ +package com.lld.im.service.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * @description 共享线程池 + * @author chackylee + * @param + * @return +*/ +@Service +public class ShareThreadPool { + + private Logger logger = LoggerFactory.getLogger(ShareThreadPool.class); + + private final ThreadPoolExecutor threadPoolExecutor; + + { + final AtomicInteger tNum = new AtomicInteger(0); + + threadPoolExecutor = new ThreadPoolExecutor(8, 8, 120, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2 << 20), new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setDaemon(true); + t.setName("SHARE-Processor-" + tNum.getAndIncrement()); + return t; + } + }); + + } + + + private AtomicLong ind = new AtomicLong(0); + + public void submit(Runnable r) { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + + ind.incrementAndGet(); + + threadPoolExecutor.submit(() -> { + long start = System.currentTimeMillis(); + try { + r.run(); + } catch (Exception e) { + logger.error("ShareThreadPool_ERROR", e); + } finally { + long end = System.currentTimeMillis(); + long dur = end - start; + long andDecrement = ind.decrementAndGet(); + if (dur > 1000) { + logger.warn("ShareThreadPool executed taskDone,remanent num = {},slow task fatal warning,costs time = {},stack: {}", andDecrement, dur, stackTrace); + } else if (dur > 300) { + logger.warn("ShareThreadPool executed taskDone,remanent num = {},slow task warning: {},costs time = {},", andDecrement,r, dur); + } else { + logger.debug("ShareThreadPool executed taskDone,remanent num = {}", andDecrement); + } + } + }); + + + } + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/SnowflakeIdWorker.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/SnowflakeIdWorker.java new file mode 100644 index 0000000..a697931 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/SnowflakeIdWorker.java @@ -0,0 +1,175 @@ +package com.lld.im.service.utils; + + +import cn.hutool.core.date.SystemClock; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * @author: Chackylee + * @description: + **/ +@Slf4j +public class SnowflakeIdWorker { + + /** + * 初始偏移时间戳 + */ + private static final long OFFSET = 1546300800L; + + /** + * 机器id (0~15 保留 16~31作为备份机器) + */ + private static long WORKER_ID; + + /** + * 机器id所占位数 (5bit, 支持最大机器数 2^5 = 32) + */ + private static final long WORKER_ID_BITS = 5L; + /** + * 自增序列所占位数 (16bit, 支持最大每秒生成 2^16 = ‭65536‬) + */ + private static final long SEQUENCE_ID_BITS = 16L; + /** + * 机器id偏移位数 + */ + private static final long WORKER_SHIFT_BITS = SEQUENCE_ID_BITS; + /** + * 自增序列偏移位数 + */ + private static final long OFFSET_SHIFT_BITS = SEQUENCE_ID_BITS + WORKER_ID_BITS; + /** + * 机器标识最大值 (2^5 / 2 - 1 = 15) + */ + private static final long WORKER_ID_MAX = ((1 << WORKER_ID_BITS) - 1) >> 1; + /** + * 备份机器ID开始位置 (2^5 / 2 = 16) + */ + private static final long BACK_WORKER_ID_BEGIN = (1 << WORKER_ID_BITS) >> 1; + /** + * 自增序列最大值 (2^16 - 1 = ‭65535) + */ + private static final long SEQUENCE_MAX = (1 << SEQUENCE_ID_BITS) - 1; + /** + * 发生时间回拨时容忍的最大回拨时间 (秒) + */ + private static final long BACK_TIME_MAX = 1L; + + /** + * 上次生成ID的时间戳 (秒) + */ + private static long lastTimestamp = 0L; + /** + * 当前秒内序列 (2^16) + */ + private static long sequence = 0L; + /** + * 备份机器上次生成ID的时间戳 (秒) + */ + private static long lastTimestampBak = 0L; + /** + * 备份机器当前秒内序列 (2^16) + */ + private static long sequenceBak = 0L; + + //==============================Constructors==================== + + /** + * 构造函数 + * + * @param workerId 工作ID (0~31) + */ + public SnowflakeIdWorker(long workerId) { + if (workerId < 0 || workerId > WORKER_ID_MAX) { + throw new IllegalArgumentException(String.format("cmallshop.workerId范围: 0 ~ %d 目前: %d", WORKER_ID_MAX, workerId)); + } + WORKER_ID = workerId; + } + + // ==============================Methods================================= + public static long nextId() { + return nextId(SystemClock.now() / 1000); + } + + /** + * 主机器自增序列 + * + * @param timestamp 当前Unix时间戳 + * @return long + */ + private static synchronized long nextId(long timestamp) { + // 时钟回拨检查 + if (timestamp < lastTimestamp) { + // 发生时钟回拨 + log.warn("时钟回拨, 启用备份机器ID: now: [{}] last: [{}]", timestamp, lastTimestamp); + return nextIdBackup(timestamp); + } + + // 开始下一秒 + if (timestamp != lastTimestamp) { + lastTimestamp = timestamp; + sequence = 0L; + } + if (0L == (++sequence & SEQUENCE_MAX)) { + // 秒内序列用尽 +// log.warn("秒内[{}]序列用尽, 启用备份机器ID序列", timestamp); + sequence--; + return nextIdBackup(timestamp); + } + + return ((timestamp - OFFSET) << OFFSET_SHIFT_BITS) | (WORKER_ID << WORKER_SHIFT_BITS) | sequence; + } + + /** + * 阻塞到下一个毫秒,直到获得新的时间戳 + * + * @param lastTimestamp 上次生成ID的时间截 + * @return 当前时间戳 + */ + protected long tilNextMillis(long lastTimestamp) { + long timestamp = timeGen(); + while (timestamp <= lastTimestamp) { + timestamp = timeGen(); + } + return timestamp; + } + + /** + * 备份机器自增序列 + * @param timestamp timestamp 当前Unix时间戳 + * @return long + */ + private static long nextIdBackup(long timestamp) { + if (timestamp < lastTimestampBak) { + if (lastTimestampBak - SystemClock.now() / 1000 <= BACK_TIME_MAX) { + timestamp = lastTimestampBak; + } else { + throw new RuntimeException(String.format("时钟回拨: now: [%d] last: [%d]", timestamp, lastTimestampBak)); + } + } + + if (timestamp != lastTimestampBak) { + lastTimestampBak = timestamp; + sequenceBak = 0L; + } + + if (0L == (++sequenceBak & SEQUENCE_MAX)) { + // 秒内序列用尽 +// logger.warn("秒内[{}]序列用尽, 备份机器ID借取下一秒序列", timestamp); + return nextIdBackup(timestamp + 1); + } + + return ((timestamp - OFFSET) << OFFSET_SHIFT_BITS) | ((WORKER_ID ^ BACK_WORKER_ID_BEGIN) << WORKER_SHIFT_BITS) | sequenceBak; + } + + + /** + * 返回以毫秒为单位的当前时间 + * + * @return 当前时间(毫秒) + */ + protected long timeGen() { + return System.currentTimeMillis(); + } + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/UserSessionUtils.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/UserSessionUtils.java new file mode 100644 index 0000000..ff695a1 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/UserSessionUtils.java @@ -0,0 +1,66 @@ +package com.lld.im.service.utils; + +import com.alibaba.fastjson.JSONObject; +import com.lld.im.common.constant.Constants; +import com.lld.im.common.enums.ImConnectStatusEnum; +import com.lld.im.common.model.UserSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Component +public class UserSessionUtils { + + public Object get; + @Autowired + StringRedisTemplate stringRedisTemplate; + + //1.获取用户所有的session + + public List getUserSession(Integer appId,String userId){ + + String userSessionKey = appId + Constants.RedisConstants.UserSessionConstants + + userId; + Map entries = + stringRedisTemplate.opsForHash().entries(userSessionKey); + List list = new ArrayList<>(); + Collection values = entries.values(); + for (Object o : values){ + String str = (String) o; + UserSession session = + JSONObject.parseObject(str, UserSession.class); + if(session.getConnectState() == ImConnectStatusEnum.ONLINE_STATUS.getCode()){ + list.add(session); + } + } + return list; + } + + //2.获取用户除了本端的session + + //1.获取用户所有的session + + public UserSession getUserSession(Integer appId,String userId + ,Integer clientType,String imei){ + + String userSessionKey = appId + Constants.RedisConstants.UserSessionConstants + + userId; + String hashKey = clientType + ":" + imei; + Object o = stringRedisTemplate.opsForHash().get(userSessionKey, hashKey); + UserSession session = + JSONObject.parseObject(o.toString(), UserSession.class); + return session; + } + + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/WriteUserSeq.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/WriteUserSeq.java new file mode 100644 index 0000000..a732b33 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/WriteUserSeq.java @@ -0,0 +1,28 @@ +package com.lld.im.service.utils; + +import com.lld.im.common.constant.Constants; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Service +public class WriteUserSeq { + + //redis + //uid friend 10 + // group 12 + // conversation 123 + @Autowired + RedisTemplate redisTemplate; + + public void writeUserSeq(Integer appId,String userId,String type,Long seq){ + String key = appId + ":" + Constants.RedisConstants.SeqPrefix + ":" + userId; + redisTemplate.opsForHash().put(key,type,seq); + } + +} diff --git a/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/ZKit.java b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/ZKit.java new file mode 100644 index 0000000..df17ec4 --- /dev/null +++ b/hs-im-server/im-service/src/main/java/com/lld/im/service/utils/ZKit.java @@ -0,0 +1,44 @@ +package com.lld.im.service.utils; + +import com.lld.im.common.constant.Constants; +import org.I0Itec.zkclient.ZkClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @author: Chackylee + * @description: Zookeeper 工具 + **/ +@Component +public class ZKit { + + private static Logger logger = LoggerFactory.getLogger(ZKit.class); + + @Autowired + private ZkClient zkClient; + /** + * get all TCP server node from zookeeper + * + * @return + */ + public List getAllTcpNode() { + List children = zkClient.getChildren(Constants.ImCoreZkRoot + Constants.ImCoreZkRootTcp); +// logger.info("Query all node =[{}] success.", JSON.toJSONString(children)); + return children; + } + + /** + * get all WEB server node from zookeeper + * + * @return + */ + public List getAllWebNode() { + List children = zkClient.getChildren(Constants.ImCoreZkRoot + Constants.ImCoreZkRootWeb); +// logger.info("Query all node =[{}] success.", JSON.toJSONString(children)); + return children; + } +} diff --git a/hs-im-server/im-service/src/main/resources/application.yml b/hs-im-server/im-service/src/main/resources/application.yml index f3230cb..5ba1357 100644 --- a/hs-im-server/im-service/src/main/resources/application.yml +++ b/hs-im-server/im-service/src/main/resources/application.yml @@ -7,17 +7,96 @@ spring: url: jdbc:mysql://192.168.2.201:3306/im-core?serverTimezone=UTC&useSSL=false&characterEncoding=UTF8 username: root + redis: + host: 43.139.191.204 + port: 6379 + database: 8 + jedis: + pool: + max-active: 100 + max-idle: 100 + max-wait: 1000 + min-idle: 10 + password: dSMIXBQrCBXiHHjk123 + rabbitmq: + host: 192.168.2.180 + port: 5672 + addresses: 192.168.2.180 + username: guest + password: guest + # virtual-host: + listener: + simple: + concurrency: 5 + max-concurrency: 10 + acknowledge-mode: MANUAL + prefetch: 1 + publisher-confirms: true + publisher-returns: true + template: + mandatory: true + cache: + connection: + mode: channel + channel: + size: 36 + checkout-timeout: 0 application: name: im-core + # logger 配置 logging: config: classpath:logback-spring.xml server: - port: 8000 + port: 28000 + +appConfig: + appId: 10000 + privateKey: 123456 + zkAddr: 192.168.2.180:2181 # zk连接地址 + zkConnectTimeOut: 50000 #zk超时时间 + imRouteWay: 3 # 路由策略1轮训 2随机 3hash + consistentHashWay: 1 # 如果选用一致性hash的话具体hash算法 1 TreeMap 2 自定义Map + tcpPort: 19000 # tcp端口 + webSocketPort: 29000 # webSocket端口 + needWebSocket: true #是否需要开启webSocket + loginModel: 1 + messageRecallTimeOut : 1200000000 #消息可撤回时间,单位毫秒 + # * 多端同步模式:1 只允许一端在线,手机/电脑/web 踢掉除了本client+imel的设备 + # * 2 允许手机/电脑的一台设备 + web在线 踢掉除了本client+imel的非web端设备 + # * 3 允许手机和电脑单设备 + web 同时在线 踢掉非本client+imel的同端设备 + # * 4 允许所有端多设备登录 不踢任何设备 + groupMaxMemberCount: 500 + sendMessageCheckFriend: false # 发送消息是否校验关系链 + sendMessageCheckBlack: false # 发送消息是否校验黑名单 + callbackUrl: http://127.0.0.1:8000/callback + modifyUserAfterCallback: false # 用户资料变更之后回调开关 + addFriendAfterCallback: false # 添加好友之后回调开关 + addFriendBeforeCallback: false # 添加好友之前回调开关 + modifyFriendAfterCallback: false # 修改好友之后回调开关 + deleteFriendAfterCallback: false # 删除好友之后回调开关 + addFriendShipBlackAfterCallback: false #添加黑名单之后回调开关 + deleteFriendShipBlackAfterCallback: false #删除黑名单之后回调开关 + createGroupAfterCallback: false # 创建群聊之后回调开关 + modifyGroupAfterCallback: false # 修改群聊之后回调开关 + destroyGroupAfterCallback: false # 解散群聊之后回调开关 + deleteGroupMemberAfterCallback: false # 删除群成员之后回调 + addGroupMemberAfterCallback: false # 拉人入群之后回调 + addGroupMemberBeforeCallback: false # 拉人入群之前回调 + sendMessageAfterCallback: false # 发送单聊消息之后 + sendMessageBeforeCallback: false # 发送单聊消息之前 + sendGroupMessageAfterCallback: false # 发送群聊消息之后 + sendGroupMessageBeforeCallback: false # 发送群聊消息之前 + offlineMessageCount: 1000 #离线消息存储条数 + deleteConversationSyncMode: 1 #1多段同步 + + +mqQueueName: 123 mybatis-plus: + configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath*:mapper/*.xml @@ -36,3 +115,6 @@ httpclient: connectionRequestTimeout: 2000 socketTimeout: 5000 staleConnectionCheckEnabled: true + +mpp: + entityBasePath: com.lld.im.service.friendship.dao diff --git a/hs-im-server/im-system.iml b/hs-im-server/im-system.iml new file mode 100644 index 0000000..f8173b1 --- /dev/null +++ b/hs-im-server/im-system.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/hs-im-server/im-tcp/pom.xml b/hs-im-server/im-tcp/pom.xml new file mode 100644 index 0000000..de32ecf --- /dev/null +++ b/hs-im-server/im-tcp/pom.xml @@ -0,0 +1,93 @@ + + + + im-system + com.lld + 1.0.0-SNAPSHOT + + 4.0.0 + + tcp + + 1.8 + 3.3.0 + 5.0.6 + + + + + + org.apache.commons + commons-lang3 + + + + + org.hibernate + hibernate-validator + + + + com.github.sgroschupf + zkclient + + + + com.lld + common + 1.0.0-SNAPSHOT + + + + + io.netty + netty-all + + + + + org.yaml + snakeyaml + + + + + org.redisson + redisson + + + + + com.rabbitmq + amqp-client + + + + + com.netflix.feign + feign-core + + + + com.netflix.feign + feign-jackson + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + + diff --git a/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/Starter.java b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/Starter.java new file mode 100644 index 0000000..6f42030 --- /dev/null +++ b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/Starter.java @@ -0,0 +1,61 @@ +package com.lld.im.tcp; + +import com.lld.im.codec.config.BootstrapConfig; +import com.lld.im.tcp.reciver.MessageReciver; +import com.lld.im.tcp.redis.RedisManager; +import com.lld.im.tcp.register.RegistryZK; +import com.lld.im.tcp.register.ZKit; +import com.lld.im.tcp.server.LimServer; +import com.lld.im.tcp.server.LimWebSocketServer; +import com.lld.im.tcp.utils.MqFactory; +import org.I0Itec.zkclient.ZkClient; +import org.yaml.snakeyaml.Yaml; + +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class Starter { + public static void main(String[] args) { + if(args.length > 0){ + start(args[0]); + } + } + + private static void start(String path){ + try { + Yaml yaml = new Yaml(); + InputStream inputStream = new FileInputStream(path); + BootstrapConfig bootstrapConfig = yaml.loadAs(inputStream, BootstrapConfig.class); + + + new LimServer(bootstrapConfig.getLim()).start(); + new LimWebSocketServer(bootstrapConfig.getLim()); + + + RedisManager.init(bootstrapConfig); + MqFactory.init(bootstrapConfig.getLim().getRabbitmq()); + MessageReciver.init(bootstrapConfig.getLim().getBrokerId()+""); + + registerZK(bootstrapConfig); + + }catch (Exception e){ + e.printStackTrace(); + System.exit(500); + } + } + + + + public static void registerZK(BootstrapConfig config) throws UnknownHostException { + String hostAddress = InetAddress.getLocalHost().getHostAddress(); + ZkClient zkClient = new ZkClient(config.getLim().getZkConfig().getZkAddr(), + config.getLim().getZkConfig().getZkConnectTimeOut()); + ZKit zKit = new ZKit(zkClient); + RegistryZK registryZK = new RegistryZK(zKit, hostAddress, config.getLim()); + Thread thread = new Thread(registryZK); + thread.start(); + + } +} diff --git a/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/feign/FeignMessageService.java b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/feign/FeignMessageService.java new file mode 100644 index 0000000..8dea44f --- /dev/null +++ b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/feign/FeignMessageService.java @@ -0,0 +1,19 @@ +package com.lld.im.tcp.feign; + +import com.lld.im.common.ResponseVO; +import com.lld.im.common.model.message.CheckSendMessageReq; +import feign.Headers; +import feign.RequestLine; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +public interface FeignMessageService { + + @Headers({"Content-Type: application/json","Accept: application/json"}) + @RequestLine("POST /message/checkSend") + public ResponseVO checkSendMessage(CheckSendMessageReq o); + +} diff --git a/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/handler/HeartBeatHandler.java b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/handler/HeartBeatHandler.java new file mode 100644 index 0000000..6d1e61a --- /dev/null +++ b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/handler/HeartBeatHandler.java @@ -0,0 +1,45 @@ +package com.lld.im.tcp.handler; + +import com.lld.im.common.constant.Constants; +import com.lld.im.tcp.utils.SessionSocketHolder; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.util.AttributeKey; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class HeartBeatHandler extends ChannelInboundHandlerAdapter { + + private Long heartBeatTime; + + public HeartBeatHandler(Long heartBeatTime) { + this.heartBeatTime = heartBeatTime; + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + + // 判断evt是否是IdleStateEvent(用于触发用户事件,包含 读空闲/写空闲/读写空闲 ) + if (evt instanceof IdleStateEvent) { + IdleStateEvent event = (IdleStateEvent)evt; // 强制类型转换 + if (event.state() == IdleState.READER_IDLE) { + log.info("读空闲"); + } else if (event.state() == IdleState.WRITER_IDLE) { + log.info("进入写空闲"); + } else if (event.state() == IdleState.ALL_IDLE) { + Long lastReadTime = (Long) ctx.channel() + .attr(AttributeKey.valueOf(Constants.ReadTime)).get(); + long now = System.currentTimeMillis(); + + if(lastReadTime != null && now - lastReadTime > heartBeatTime){ + //TODO 退后台逻辑 + SessionSocketHolder.offlineUserSession((NioSocketChannel) ctx.channel()); + } + + } + } + } +} diff --git a/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/handler/NettyServerHandler.java b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/handler/NettyServerHandler.java new file mode 100644 index 0000000..88959c2 --- /dev/null +++ b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/handler/NettyServerHandler.java @@ -0,0 +1,197 @@ +package com.lld.im.tcp.handler; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.TypeReference; +import com.lld.im.codec.pack.LoginPack; +import com.lld.im.codec.pack.message.ChatMessageAck; +import com.lld.im.codec.pack.user.LoginAckPack; +import com.lld.im.codec.pack.user.UserStatusChangeNotifyPack; +import com.lld.im.codec.proto.Message; +import com.lld.im.codec.proto.MessagePack; +import com.lld.im.common.ResponseVO; +import com.lld.im.common.constant.Constants; +import com.lld.im.common.enums.ImConnectStatusEnum; +import com.lld.im.common.enums.command.GroupEventCommand; +import com.lld.im.common.enums.command.MessageCommand; +import com.lld.im.common.enums.command.SystemCommand; +import com.lld.im.common.enums.command.UserEventCommand; +import com.lld.im.common.model.UserClientDto; +import com.lld.im.common.model.UserSession; +import com.lld.im.common.model.message.CheckSendMessageReq; +import com.lld.im.tcp.feign.FeignMessageService; +import com.lld.im.tcp.publish.MqMessageProducer; +import com.lld.im.tcp.redis.RedisManager; +import com.lld.im.tcp.utils.SessionSocketHolder; +import feign.jackson.JacksonDecoder; +import feign.jackson.JacksonEncoder; +import feign.Feign; +import feign.Request; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.AttributeKey; +import org.redisson.api.RMap; +import org.redisson.api.RTopic; +import org.redisson.api.RedissonClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetAddress; + +public class NettyServerHandler extends SimpleChannelInboundHandler { + private final static Logger logger = LoggerFactory.getLogger(NettyServerHandler.class); + + private Integer brokerId; + + + private FeignMessageService feignMessageService; + + + public NettyServerHandler(Integer brokerId, String logicUrl) { + this.brokerId = brokerId; + + feignMessageService = Feign.builder() + .encoder(new JacksonEncoder()) + .decoder(new JacksonDecoder()) + .options(new Request.Options(1000, 3500))//设置超时时间 + .target(FeignMessageService.class, logicUrl); + } + + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Message msg) throws Exception { + logger.info("***********************收到消息:{}", msg); + logger.info("***********************收到消息:{}", msg); + logger.info("***********************收到消息:{}", msg); + + Integer command = msg.getMessageHeader().getCommand(); + + //登录command + if (command == SystemCommand.LOGIN.getCommand()) { + + LoginPack loginPack = JSON.parseObject(JSONObject.toJSONString(msg.getMessagePack()), + new TypeReference() { + }.getType()); + /** 登陸事件 **/ + String userId = loginPack.getUserId(); + /** 为channel设置用户id **/ + ctx.channel().attr(AttributeKey.valueOf(Constants.UserId)).set(userId); + String clientImei = msg.getMessageHeader().getClientType() + ":" + msg.getMessageHeader().getImei(); + /** 为channel设置client和imel **/ + ctx.channel().attr(AttributeKey.valueOf(Constants.ClientImei)).set(clientImei); + /** 为channel设置appId **/ + ctx.channel().attr(AttributeKey.valueOf(Constants.AppId)).set(msg.getMessageHeader().getAppId()); + /** 为channel设置ClientType **/ + ctx.channel().attr(AttributeKey.valueOf(Constants.ClientType)) + .set(msg.getMessageHeader().getClientType()); + /** 为channel设置Imei **/ + ctx.channel().attr(AttributeKey.valueOf(Constants.Imei)) + .set(msg.getMessageHeader().getImei()); + + + UserSession userSession = new UserSession(); + userSession.setAppId(msg.getMessageHeader().getAppId()); + userSession.setClientType(msg.getMessageHeader().getClientType()); + userSession.setUserId(loginPack.getUserId()); + userSession.setConnectState(ImConnectStatusEnum.ONLINE_STATUS.getCode()); + userSession.setBrokerId(brokerId); + userSession.setImei(msg.getMessageHeader().getImei()); + + try { + InetAddress localHost = InetAddress.getLocalHost(); + userSession.setBrokerHost(localHost.getHostAddress()); + } catch (Exception e) { + e.printStackTrace(); + } + + + RedissonClient redissonClient = RedisManager.getRedissonClient(); + RMap map = redissonClient.getMap(msg.getMessageHeader().getAppId() + Constants.RedisConstants.UserSessionConstants + loginPack.getUserId()); + map.put(msg.getMessageHeader().getClientType() + ":" + msg.getMessageHeader().getImei() + , JSONObject.toJSONString(userSession)); + SessionSocketHolder + .put(msg.getMessageHeader().getAppId() + , loginPack.getUserId(), + msg.getMessageHeader().getClientType(), msg.getMessageHeader().getImei(), (NioSocketChannel) ctx.channel()); + + + UserClientDto dto = new UserClientDto(); + dto.setImei(msg.getMessageHeader().getImei()); + dto.setUserId(loginPack.getUserId()); + dto.setClientType(msg.getMessageHeader().getClientType()); + dto.setAppId(msg.getMessageHeader().getAppId()); + + RTopic topic = redissonClient.getTopic(Constants.RedisConstants.UserLoginChannel); + topic.publish(JSONObject.toJSONString(dto)); + + UserStatusChangeNotifyPack userStatusChangeNotifyPack = new UserStatusChangeNotifyPack(); + userStatusChangeNotifyPack.setAppId(msg.getMessageHeader().getAppId()); + userStatusChangeNotifyPack.setUserId(loginPack.getUserId()); + userStatusChangeNotifyPack.setStatus(ImConnectStatusEnum.ONLINE_STATUS.getCode()); + MqMessageProducer.sendMessage(userStatusChangeNotifyPack,msg.getMessageHeader(), UserEventCommand.USER_ONLINE_STATUS_CHANGE.getCommand()); + + MessagePack loginSuccess = new MessagePack<>(); + LoginAckPack loginAckPack = new LoginAckPack(); + loginAckPack.setUserId(loginPack.getUserId()); + loginSuccess.setCommand(SystemCommand.LOGINACK.getCommand()); + loginSuccess.setData(loginAckPack); + loginSuccess.setImei(msg.getMessageHeader().getImei()); + loginSuccess.setAppId(msg.getMessageHeader().getAppId()); + ctx.channel().writeAndFlush(loginSuccess); + logger.info("===============登录成功:{}",loginPack.getUserId()); + + }else if(command == SystemCommand.LOGOUT.getCommand()){ + //删除session + //redis 删除 + SessionSocketHolder.removeUserSession((NioSocketChannel) ctx.channel()); + }else if(command == SystemCommand.PING.getCommand()){ + ctx.channel().attr(AttributeKey.valueOf(Constants.ReadTime)).set(System.currentTimeMillis()); + }else if(command == MessageCommand.MSG_P2P.getCommand() + || command == GroupEventCommand.MSG_GROUP.getCommand()){ + try { + String toId = ""; + CheckSendMessageReq req = new CheckSendMessageReq(); + req.setAppId(msg.getMessageHeader().getAppId()); + req.setCommand(msg.getMessageHeader().getCommand()); + JSONObject jsonObject = JSON.parseObject(JSONObject.toJSONString(msg.getMessagePack())); + String fromId = jsonObject.getString("fromId"); + if(command == MessageCommand.MSG_P2P.getCommand()){ + toId = jsonObject.getString("toId"); + }else { + toId = jsonObject.getString("groupId"); + } + req.setToId(toId); + req.setFromId(fromId); + + ResponseVO responseVO = feignMessageService.checkSendMessage(req); + if(responseVO.isOk()){ + MqMessageProducer.sendMessage(msg,command); + }else{ + Integer ackCommand = 0; + if(command == MessageCommand.MSG_P2P.getCommand()){ + ackCommand = MessageCommand.MSG_ACK.getCommand(); + }else { + ackCommand = GroupEventCommand.GROUP_MSG_ACK.getCommand(); + } + + ChatMessageAck chatMessageAck = new ChatMessageAck(jsonObject.getString("messageId")); + responseVO.setData(chatMessageAck); + MessagePack ack = new MessagePack<>(); + ack.setData(responseVO); + ack.setCommand(ackCommand); + ctx.channel().writeAndFlush(ack); + } + }catch (Exception e){ + e.printStackTrace(); + } + }else { + MqMessageProducer.sendMessage(msg,command); + } + + + + + + } +} diff --git a/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/publish/MqMessageProducer.java b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/publish/MqMessageProducer.java new file mode 100644 index 0000000..081e808 --- /dev/null +++ b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/publish/MqMessageProducer.java @@ -0,0 +1,85 @@ + +package com.lld.im.tcp.publish; + + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.lld.im.codec.proto.Message; +import com.lld.im.codec.proto.MessageHeader; +import com.lld.im.common.constant.Constants; +import com.lld.im.common.enums.command.CommandType; +import com.lld.im.tcp.utils.MqFactory; +import com.rabbitmq.client.Channel; +import lombok.extern.slf4j.Slf4j; + +/** + * @description: + * @author: lld + * @version: 1.0 + */ +@Slf4j +public class MqMessageProducer { + + public static void sendMessage(Message message,Integer command){ + Channel channel = null; + String com = command.toString(); + String commandSub = com.substring(0, 1); + CommandType commandType = CommandType.getCommandType(commandSub); + String channelName = ""; + if(commandType == CommandType.MESSAGE){ + channelName = Constants.RabbitConstants.Im2MessageService; + }else if(commandType == CommandType.GROUP){ + channelName = Constants.RabbitConstants.Im2GroupService; + }else if(commandType == CommandType.FRIEND){ + channelName = Constants.RabbitConstants.Im2FriendshipService; + }else if(commandType == CommandType.USER){ + channelName = Constants.RabbitConstants.Im2UserService; + } + + try { + channel = MqFactory.getChannel(channelName); + + JSONObject o = (JSONObject) JSON.toJSON(message.getMessagePack()); + o.put("command",command); + o.put("clientType",message.getMessageHeader().getClientType()); + o.put("imei",message.getMessageHeader().getImei()); + o.put("appId",message.getMessageHeader().getAppId()); + channel.basicPublish(channelName,"", + null, o.toJSONString().getBytes()); + }catch (Exception e){ + log.error("发送消息出现异常:{}",e.getMessage()); + } + } + + public static void sendMessage(Object message, MessageHeader header, Integer command){ + Channel channel = null; + String com = command.toString(); + String commandSub = com.substring(0, 1); + CommandType commandType = CommandType.getCommandType(commandSub); + String channelName = ""; + if(commandType == CommandType.MESSAGE){ + channelName = Constants.RabbitConstants.Im2MessageService; + }else if(commandType == CommandType.GROUP){ + channelName = Constants.RabbitConstants.Im2GroupService; + }else if(commandType == CommandType.FRIEND){ + channelName = Constants.RabbitConstants.Im2FriendshipService; + }else if(commandType == CommandType.USER){ + channelName = Constants.RabbitConstants.Im2UserService; + } + + try { + channel = MqFactory.getChannel(channelName); + + JSONObject o = (JSONObject) JSON.toJSON(message); + o.put("command",command); + o.put("clientType",header.getClientType()); + o.put("imei",header.getImei()); + o.put("appId",header.getAppId()); + channel.basicPublish(channelName,"", + null, o.toJSONString().getBytes()); + }catch (Exception e){ + log.error("发送消息出现异常:{}",e.getMessage()); + } + } + +} diff --git a/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/reciver/MessageReciver.java b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/reciver/MessageReciver.java new file mode 100644 index 0000000..fd03ed9 --- /dev/null +++ b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/reciver/MessageReciver.java @@ -0,0 +1,83 @@ +package com.lld.im.tcp.reciver; + +import com.alibaba.fastjson.JSONObject; +import com.lld.im.codec.proto.MessagePack; +import com.lld.im.common.constant.Constants; +import com.lld.im.tcp.reciver.process.BaseProcess; +import com.lld.im.tcp.reciver.process.ProcessFactory; +import com.lld.im.tcp.utils.MqFactory; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; + +/** + * @description: + * @author: rowger + * @version: 1.0 + */ +@Slf4j +public class MessageReciver { + + private static String brokerId; + + private static void startReciverMessage() { + try { + String channelNameMessageService2Im=Constants.RabbitConstants.MessageService2Im + brokerId; + + log.info("==============channelNameMessageService2Im:"+channelNameMessageService2Im); + + Channel channel = MqFactory.getChannel(channelNameMessageService2Im); + channel.queueDeclare(channelNameMessageService2Im,true, false, false, null); + channel.queueBind(channelNameMessageService2Im,Constants.RabbitConstants.MessageService2Im, brokerId); + + channel.basicConsume(channelNameMessageService2Im, false,new DefaultConsumer(channel) { + @Override + public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { + try { + //处理消息服务发来的消息 + String msgStr = new String(body); + log.info("=================处理消息服务发来的消息:{msgStr}:"+msgStr); + MessagePack messagePack = + JSONObject.parseObject(msgStr, MessagePack.class); + BaseProcess messageProcess = ProcessFactory + .getMessageProcess(messagePack.getCommand()); + messageProcess.process(messagePack); + + channel.basicAck(envelope.getDeliveryTag(),false); + + }catch (Exception e){ + e.printStackTrace(); + channel.basicNack(envelope.getDeliveryTag(),false,false); + } + } + } + ); + } catch (Exception e) { + e.printStackTrace(); + + } + } + + public static void init() { + startReciverMessage(); + log.info("===========startReciverMessage no brokerId"); + + } + + public static void init(String brokerId) { + + if (StringUtils.isBlank(MessageReciver.brokerId)) { + MessageReciver.brokerId = brokerId; + } + startReciverMessage(); + log.info("===========startReciverMessage has brokerId:"+brokerId); + + } + + +} diff --git a/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/reciver/UserLoginMessageListener.java b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/reciver/UserLoginMessageListener.java new file mode 100644 index 0000000..c87c61f --- /dev/null +++ b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/reciver/UserLoginMessageListener.java @@ -0,0 +1,119 @@ +package com.lld.im.tcp.reciver; + +import com.alibaba.fastjson.JSONObject; +import com.lld.im.codec.proto.MessagePack; +import com.lld.im.common.ClientType; +import com.lld.im.common.constant.Constants; +import com.lld.im.common.enums.DeviceMultiLoginEnum; +import com.lld.im.common.enums.command.SystemCommand; +import com.lld.im.common.model.UserClientDto; +import com.lld.im.tcp.handler.NettyServerHandler; +import com.lld.im.tcp.redis.RedisManager; +import com.lld.im.tcp.utils.SessionSocketHolder; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.AttributeKey; +import org.redisson.api.RTopic; +import org.redisson.api.listener.MessageListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * @description: + * 多端同步:1单端登录:一端在线:踢掉除了本clinetType + imel 的设备 + * 2双端登录:允许pc/mobile 其中一端登录 + web端 踢掉除了本clinetType + imel 以外的web端设备 + * 3 三端登录:允许手机+pc+web,踢掉同端的其他imei 除了web + * 4 不做任何处理 + * + * @author: rowger + * @version: 1.0 + */ +public class UserLoginMessageListener { + + private final static Logger logger = LoggerFactory.getLogger(UserLoginMessageListener.class); + + private Integer loginModel; + + public UserLoginMessageListener(Integer loginModel) { + this.loginModel = loginModel; + } + + public void listenerUserLogin(){ + RTopic topic = RedisManager.getRedissonClient().getTopic(Constants.RedisConstants.UserLoginChannel); + topic.addListener(String.class, new MessageListener() { + @Override + public void onMessage(CharSequence charSequence, String msg) { + logger.info("收到用户上线通知:" + msg); + UserClientDto dto = JSONObject.parseObject(msg, UserClientDto.class); + List nioSocketChannels = SessionSocketHolder.get(dto.getAppId(), dto.getUserId()); + + for (NioSocketChannel nioSocketChannel : nioSocketChannels) { + if(loginModel == DeviceMultiLoginEnum.ONE.getLoginMode()){ + Integer clientType = (Integer) nioSocketChannel.attr(AttributeKey.valueOf(Constants.ClientType)).get(); + String imei = (String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.Imei)).get(); + + if(!(clientType + ":" + imei).equals(dto.getClientType()+":"+dto.getImei())){ + MessagePack pack = new MessagePack<>(); + pack.setToId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get()); + pack.setUserId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get()); + pack.setCommand(SystemCommand.MUTUALLOGIN.getCommand()); + nioSocketChannel.writeAndFlush(pack); + } + + }else if(loginModel == DeviceMultiLoginEnum.TWO.getLoginMode()){ + if(dto.getClientType() == ClientType.WEB.getCode()){ + continue; + } + Integer clientType = (Integer) nioSocketChannel.attr(AttributeKey.valueOf(Constants.ClientType)).get(); + + if (clientType == ClientType.WEB.getCode()){ + continue; + } + String imei = (String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.Imei)).get(); + if(!(clientType + ":" + imei).equals(dto.getClientType()+":"+dto.getImei())){ + MessagePack pack = new MessagePack<>(); + pack.setToId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get()); + pack.setUserId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get()); + pack.setCommand(SystemCommand.MUTUALLOGIN.getCommand()); + nioSocketChannel.writeAndFlush(pack); + } + + }else if(loginModel == DeviceMultiLoginEnum.THREE.getLoginMode()){ + + Integer clientType = (Integer) nioSocketChannel.attr(AttributeKey.valueOf(Constants.ClientType)).get(); + String imei = (String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.Imei)).get(); + if(dto.getClientType() == ClientType.WEB.getCode()){ + continue; + } + + Boolean isSameClient = false; + if((clientType == ClientType.IOS.getCode() || + clientType == ClientType.ANDROID.getCode()) && + (dto.getClientType() == ClientType.IOS.getCode() || + dto.getClientType() == ClientType.ANDROID.getCode())){ + isSameClient = true; + } + + if((clientType == ClientType.MAC.getCode() || + clientType == ClientType.WINDOWS.getCode()) && + (dto.getClientType() == ClientType.MAC.getCode() || + dto.getClientType() == ClientType.WINDOWS.getCode())){ + isSameClient = true; + } + + if(isSameClient && !(clientType + ":" + imei).equals(dto.getClientType()+":"+dto.getImei())){ + MessagePack pack = new MessagePack<>(); + pack.setToId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get()); + pack.setUserId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get()); + pack.setCommand(SystemCommand.MUTUALLOGIN.getCommand()); + nioSocketChannel.writeAndFlush(pack); + } + } + } + + + } + }); + } +} diff --git a/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/reciver/process/BaseProcess.java b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/reciver/process/BaseProcess.java new file mode 100644 index 0000000..f727eca --- /dev/null +++ b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/reciver/process/BaseProcess.java @@ -0,0 +1,29 @@ +package com.lld.im.tcp.reciver.process; + +import com.lld.im.codec.proto.MessagePack; +import com.lld.im.tcp.utils.SessionSocketHolder; +import io.netty.channel.socket.nio.NioSocketChannel; + +/** + * @description: + * @author: rowger + * @version: 1.0 + */ +public abstract class BaseProcess { + + public abstract void processBefore(); + + public void process(MessagePack messagePack){ + processBefore(); + NioSocketChannel channel = SessionSocketHolder.get(messagePack.getAppId(), + messagePack.getToId(), messagePack.getClientType(), + messagePack.getImei()); + if(channel != null){ + channel.writeAndFlush(messagePack); + } + processAfter(); + } + + public abstract void processAfter(); + +} diff --git a/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/reciver/process/ProcessFactory.java b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/reciver/process/ProcessFactory.java new file mode 100644 index 0000000..95d6623 --- /dev/null +++ b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/reciver/process/ProcessFactory.java @@ -0,0 +1,30 @@ +package com.lld.im.tcp.reciver.process; + +/** + * @description: + * @author: rowger + * @version: 1.0 + */ +public class ProcessFactory { + + private static BaseProcess defaultProcess; + + static { + defaultProcess = new BaseProcess() { + @Override + public void processBefore() { + + } + + @Override + public void processAfter() { + + } + }; + } + + public static BaseProcess getMessageProcess(Integer command) { + return defaultProcess; + } + +} diff --git a/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/redis/RedisManager.java b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/redis/RedisManager.java new file mode 100644 index 0000000..ca3a54f --- /dev/null +++ b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/redis/RedisManager.java @@ -0,0 +1,30 @@ +package com.lld.im.tcp.redis; + +import com.lld.im.codec.config.BootstrapConfig; +import com.sun.org.apache.regexp.internal.RE; +import org.redisson.api.RedissonClient; + +/** + * @description: + * @author: rowger + * @version: 1.0 + */ +public class RedisManager { + + private static RedissonClient redissonClient; + + private static Integer loginModel; + + public static void init(BootstrapConfig config){ + loginModel = config.getLim().getLoginModel(); + SingleClientStrategy singleClientStrategy = new SingleClientStrategy(); + redissonClient = singleClientStrategy.getRedissonClient(config.getLim().getRedis()); +// UserLoginMessageListener userLoginMessageListener = new UserLoginMessageListener(loginModel); +// userLoginMessageListener.listenerUserLogin(); + } + + public static RedissonClient getRedissonClient(){ + return redissonClient; + } + +} diff --git a/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/redis/SingleClientStrategy.java b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/redis/SingleClientStrategy.java new file mode 100644 index 0000000..2581878 --- /dev/null +++ b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/redis/SingleClientStrategy.java @@ -0,0 +1,38 @@ +package com.lld.im.tcp.redis; + + +import com.lld.im.codec.config.BootstrapConfig; +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.client.codec.StringCodec; +import org.redisson.config.Config; +import org.redisson.config.SingleServerConfig; + +/** + * @description: + * @author: rowger + * @version: 1.0 + */ +public class SingleClientStrategy { + + public RedissonClient getRedissonClient(BootstrapConfig.RedisConfig redisConfig) { + Config config = new Config(); + String node = redisConfig.getSingle().getAddress(); + node = node.startsWith("redis://") ? node : "redis://" + node; + SingleServerConfig serverConfig = config.useSingleServer() + .setAddress(node) + .setDatabase(redisConfig.getDatabase()) + .setTimeout(redisConfig.getTimeout()) + .setConnectionMinimumIdleSize(redisConfig.getPoolMinIdle()) + .setConnectTimeout(redisConfig.getPoolConnTimeout()) + .setConnectionPoolSize(redisConfig.getPoolSize()); + if (StringUtils.isNotBlank(redisConfig.getPassword())) { + serverConfig.setPassword(redisConfig.getPassword()); + } + StringCodec stringCodec = new StringCodec(); + config.setCodec(stringCodec); + return Redisson.create(config); + } + +} diff --git a/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/register/RegistryZK.java b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/register/RegistryZK.java new file mode 100644 index 0000000..6dbc716 --- /dev/null +++ b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/register/RegistryZK.java @@ -0,0 +1,42 @@ +package com.lld.im.tcp.register; + +import com.lld.im.codec.config.BootstrapConfig; +import com.lld.im.common.constant.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @description: + * @author: rowger + * @version: 1.0 + */ +public class RegistryZK implements Runnable { + + private static Logger logger = LoggerFactory.getLogger(RegistryZK.class); + + private ZKit zKit; + + private String ip; + + private BootstrapConfig.TcpConfig tcpConfig; + + public RegistryZK(ZKit zKit, String ip, BootstrapConfig.TcpConfig tcpConfig) { + this.zKit = zKit; + this.ip = ip; + this.tcpConfig = tcpConfig; + } + + @Override + public void run() { + zKit.createRootNode(); + String tcpPath = Constants.ImCoreZkRoot + Constants.ImCoreZkRootTcp + "/" + ip + ":" + tcpConfig.getTcpPort(); + zKit.createNode(tcpPath); + logger.info("Registry zookeeper tcpPath success, msg=[{}]", tcpPath); + + String webPath = + Constants.ImCoreZkRoot + Constants.ImCoreZkRootWeb + "/" + ip + ":" + tcpConfig.getWebSocketPort(); + zKit.createNode(webPath); + logger.info("Registry zookeeper webPath success, msg=[{}]", tcpPath); + + } +} diff --git a/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/register/ZKit.java b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/register/ZKit.java new file mode 100644 index 0000000..6d613bf --- /dev/null +++ b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/register/ZKit.java @@ -0,0 +1,46 @@ +package com.lld.im.tcp.register; + +import com.lld.im.common.constant.Constants; +import org.I0Itec.zkclient.ZkClient; + +/** + * @description: + * @author: rowger + * @version: 1.0 + */ +public class ZKit { + + private ZkClient zkClient; + + public ZKit(ZkClient zkClient) { + this.zkClient = zkClient; + } + + //im-coreRoot/tcp/ip:port + public void createRootNode(){ + boolean exists = zkClient.exists(Constants.ImCoreZkRoot); + if(!exists){ + zkClient.createPersistent(Constants.ImCoreZkRoot); + } + boolean tcpExists = zkClient.exists(Constants.ImCoreZkRoot + + Constants.ImCoreZkRootTcp); + if(!tcpExists){ + zkClient.createPersistent(Constants.ImCoreZkRoot + + Constants.ImCoreZkRootTcp); + } + + boolean webExists = zkClient.exists(Constants.ImCoreZkRoot + + Constants.ImCoreZkRootWeb); + if(!tcpExists){ + zkClient.createPersistent(Constants.ImCoreZkRoot + + Constants.ImCoreZkRootWeb); + } + } + + //ip+port + public void createNode(String path){ + if(!zkClient.exists(path)){ + zkClient.createPersistent(path); + } + } +} diff --git a/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/server/LimServer.java b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/server/LimServer.java new file mode 100644 index 0000000..d9f0b09 --- /dev/null +++ b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/server/LimServer.java @@ -0,0 +1,59 @@ +package com.lld.im.tcp.server; + +import com.lld.im.codec.MessageDecoder; +import com.lld.im.codec.config.BootstrapConfig; +import com.lld.im.tcp.handler.HeartBeatHandler; +import com.lld.im.tcp.handler.NettyServerHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; + +public class LimServer { + private final static Logger logger = LoggerFactory.getLogger(LimServer.class); + + BootstrapConfig.TcpConfig config; + EventLoopGroup mainGroup; + EventLoopGroup subGroup; + ServerBootstrap server; + + public LimServer(BootstrapConfig.TcpConfig config) { + this.config = config; + + mainGroup = new NioEventLoopGroup(); + subGroup = new NioEventLoopGroup(); + + server = new ServerBootstrap(); + + server.group(mainGroup, subGroup) + .channel(NioServerSocketChannel.class) + .option(ChannelOption.SO_BACKLOG, 10240) // 服务端可连接队列大小 + .option(ChannelOption.SO_REUSEADDR, true) // 参数表示允许重复使用本地地址和端口 + .childOption(ChannelOption.TCP_NODELAY, true) // 是否禁用Nagle算法 简单点说是否批量发送数据 true关闭 false开启。 开启的话可以减少一定的网络开销,但影响消息实时性 + .childOption(ChannelOption.SO_KEEPALIVE, true) // 保活开关2h没有数据服务端会发送心跳包 + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + System.out.println("============================"); + ch.pipeline().addLast(new MessageDecoder()); + ch.pipeline().addLast(new HeartBeatHandler(config.getHeartBeatTime())); + ch.pipeline().addLast(new NettyServerHandler(config.getBrokerId(),config.getLogicUrl())); + + } + }); + +// server.bind(config.getTcpPort()); + } + + public void start(){ + this.server.bind(this.config.getTcpPort()); + logger.info("==================tcp server start on {}",this.config.getTcpPort()); + + } +} diff --git a/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/server/LimWebSocketServer.java b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/server/LimWebSocketServer.java new file mode 100644 index 0000000..86d266f --- /dev/null +++ b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/server/LimWebSocketServer.java @@ -0,0 +1,63 @@ +package com.lld.im.tcp.server; + +import com.lld.im.codec.WebSocketMessageDecoder; +import com.lld.im.codec.WebSocketMessageEncoder; +import com.lld.im.codec.config.BootstrapConfig; +import com.lld.im.tcp.handler.NettyServerHandler; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.stream.ChunkedWriteHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LimWebSocketServer { + private final static Logger logger = LoggerFactory.getLogger(LimWebSocketServer.class); + + public LimWebSocketServer(BootstrapConfig.TcpConfig config) { + EventLoopGroup mainGroup = new NioEventLoopGroup(); + EventLoopGroup subGroup = new NioEventLoopGroup(); + ServerBootstrap serverBootstrap = new ServerBootstrap(); + serverBootstrap.group(mainGroup, subGroup) + .channel(NioServerSocketChannel.class) + .option(ChannelOption.SO_BACKLOG, 10240) // 服务端可连接队列大小 + .option(ChannelOption.SO_REUSEADDR, true) // 参数表示允许重复使用本地地址和端口 + .childOption(ChannelOption.TCP_NODELAY, true) // 是否禁用Nagle算法 简单点说是否批量发送数据 true关闭 false开启。 开启的话可以减少一定的网络开销,但影响消息实时性 + .childOption(ChannelOption.SO_KEEPALIVE, true) // 保活开关2h没有数据服务端会发送心跳包 + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + // websocket 基于http协议,所以要有http编解码器 + pipeline.addLast("http-codec", new HttpServerCodec()); + // 对写大数据流的支持 + pipeline.addLast("http-chunked", new ChunkedWriteHandler()); + // 几乎在netty中的编程,都会使用到此hanler + pipeline.addLast("aggregator", new HttpObjectAggregator(65535)); + /** + * websocket 服务器处理的协议,用于指定给客户端连接访问的路由 : /ws + * 本handler会帮你处理一些繁重的复杂的事 + * 会帮你处理握手动作: handshaking(close, ping, pong) ping + pong = 心跳 + * 对于websocket来讲,都是以frames进行传输的,不同的数据类型对应的frames也不同 + */ + pipeline.addLast(new WebSocketServerProtocolHandler("/ws")); + pipeline.addLast(new WebSocketMessageDecoder()); + pipeline.addLast(new WebSocketMessageEncoder()); + pipeline.addLast(new NettyServerHandler(config.getBrokerId(),config.getLogicUrl())); + + } + }); + + + serverBootstrap.bind(config.getWebSocketPort()); + logger.info("websocket start====={}",config.getWebSocketPort()); + } +} diff --git a/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/utils/MqFactory.java b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/utils/MqFactory.java new file mode 100644 index 0000000..4243a8e --- /dev/null +++ b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/utils/MqFactory.java @@ -0,0 +1,50 @@ +package com.lld.im.tcp.utils; + +import com.lld.im.codec.config.BootstrapConfig; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeoutException; + +/** + * @description: + * @author: rowger + * @version: 1.0 + */ +public class MqFactory { + + private static ConnectionFactory factory = null; + + private static Channel defaultChannel; + + private static ConcurrentHashMap channelMap = new ConcurrentHashMap<>(); + + private static Connection getConnection() throws IOException, TimeoutException { + Connection connection = factory.newConnection(); + return connection; + } + + public static Channel getChannel(String channelName) throws IOException, TimeoutException { + Channel channel = channelMap.get(channelName); + if(channel == null){ + channel = getConnection().createChannel(); + channelMap.put(channelName,channel); + } + return channel; + } + + public static void init(BootstrapConfig.Rabbitmq rabbitmq){ + if(factory == null){ + factory = new ConnectionFactory(); + factory.setHost(rabbitmq.getHost()); + factory.setPort(rabbitmq.getPort()); + factory.setUsername(rabbitmq.getUserName()); + factory.setPassword(rabbitmq.getPassword()); + factory.setVirtualHost(rabbitmq.getVirtualHost()); + } + } + +} diff --git a/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/utils/SessionSocketHolder.java b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/utils/SessionSocketHolder.java new file mode 100644 index 0000000..12594dc --- /dev/null +++ b/hs-im-server/im-tcp/src/main/java/com/lld/im/tcp/utils/SessionSocketHolder.java @@ -0,0 +1,143 @@ +package com.lld.im.tcp.utils; + +import com.lld.im.codec.proto.MessageHeader; +import com.alibaba.fastjson.JSONObject; +import com.lld.im.codec.pack.user.UserStatusChangeNotifyPack; +import com.lld.im.common.constant.Constants; +import com.lld.im.common.enums.ImConnectStatusEnum; +import com.lld.im.common.enums.command.UserEventCommand; +import com.lld.im.common.model.UserClientDto; +import com.lld.im.common.model.UserSession; +import com.lld.im.tcp.publish.MqMessageProducer; +import com.lld.im.tcp.redis.RedisManager; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.AttributeKey; +import org.apache.commons.lang3.StringUtils; +import org.redisson.api.RMap; +import org.redisson.api.RedissonClient; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class SessionSocketHolder { + + private static final Map CHANNELS = new ConcurrentHashMap<>(); + + + public static void put(Integer appId,String userId,Integer clientType, + String imei + ,NioSocketChannel channel){ + UserClientDto dto = new UserClientDto(); + dto.setImei(imei); + dto.setAppId(appId); + dto.setClientType(clientType); + dto.setUserId(userId); + CHANNELS.put(dto,channel); + } + + public static NioSocketChannel get(Integer appId,String userId, + Integer clientType,String imei){ + UserClientDto dto = new UserClientDto(); + dto.setImei(imei); + dto.setAppId(appId); + dto.setClientType(clientType); + dto.setUserId(userId); + return CHANNELS.get(dto); + } + + public static List get(Integer appId , String id) { + + Set channelInfos = CHANNELS.keySet(); + List channels = new ArrayList<>(); + + channelInfos.forEach(channel ->{ + if(channel.getAppId().equals(appId) && id.equals(channel.getUserId())){ + channels.add(CHANNELS.get(channel)); + } + }); + + return channels; + } + + public static void remove(Integer appId,String userId,Integer clientType,String imei){ + UserClientDto dto = new UserClientDto(); + dto.setAppId(appId); + dto.setImei(imei); + dto.setClientType(clientType); + dto.setUserId(userId); + CHANNELS.remove(dto); + } + + public static void remove(NioSocketChannel channel){ + CHANNELS.entrySet().stream().filter(entity -> entity.getValue() == channel) + .forEach(entry -> CHANNELS.remove(entry.getKey())); + } + + + + public static void removeUserSession(NioSocketChannel nioSocketChannel){ + String userId = (String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get(); + Integer appId = (Integer) nioSocketChannel.attr(AttributeKey.valueOf(Constants.AppId)).get(); + Integer clientType = (Integer) nioSocketChannel.attr(AttributeKey.valueOf(Constants.ClientType)).get(); + String imei = (String) nioSocketChannel + .attr(AttributeKey.valueOf(Constants.Imei)).get(); + + SessionSocketHolder.remove(appId,userId,clientType,imei); + RedissonClient redissonClient = RedisManager.getRedissonClient(); + RMap map = redissonClient.getMap(appId + + Constants.RedisConstants.UserSessionConstants + userId); + map.remove(clientType+":"+imei); + + MessageHeader messageHeader = new MessageHeader(); + messageHeader.setAppId(appId); + messageHeader.setImei(imei); + messageHeader.setClientType(clientType); + + UserStatusChangeNotifyPack userStatusChangeNotifyPack = new UserStatusChangeNotifyPack(); + userStatusChangeNotifyPack.setAppId(appId); + userStatusChangeNotifyPack.setUserId(userId); + userStatusChangeNotifyPack.setStatus(ImConnectStatusEnum.OFFLINE_STATUS.getCode()); + + MqMessageProducer.sendMessage(userStatusChangeNotifyPack,messageHeader, UserEventCommand.USER_ONLINE_STATUS_CHANGE.getCommand()); + + nioSocketChannel.close(); + } + + public static void offlineUserSession(NioSocketChannel nioSocketChannel){ + String userId = (String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get(); + Integer appId = (Integer) nioSocketChannel.attr(AttributeKey.valueOf(Constants.AppId)).get(); + Integer clientType = (Integer) nioSocketChannel.attr(AttributeKey.valueOf(Constants.ClientType)).get(); + String imei = (String) nioSocketChannel + .attr(AttributeKey.valueOf(Constants.Imei)).get(); + SessionSocketHolder.remove(appId,userId,clientType,imei); + RedissonClient redissonClient = RedisManager.getRedissonClient(); + RMap map = redissonClient.getMap(appId + + Constants.RedisConstants.UserSessionConstants + userId); + String sessionStr = map.get(clientType.toString()+":" + imei); + + if(!StringUtils.isBlank(sessionStr)){ + UserSession userSession = JSONObject.parseObject(sessionStr, UserSession.class); + userSession.setConnectState(ImConnectStatusEnum.OFFLINE_STATUS.getCode()); + map.put(clientType.toString()+":"+imei,JSONObject.toJSONString(userSession)); + } + + MessageHeader messageHeader = new MessageHeader(); + messageHeader.setAppId(appId); + messageHeader.setImei(imei); + messageHeader.setClientType(clientType); + + UserStatusChangeNotifyPack userStatusChangeNotifyPack = new UserStatusChangeNotifyPack(); + userStatusChangeNotifyPack.setAppId(appId); + userStatusChangeNotifyPack.setUserId(userId); + userStatusChangeNotifyPack.setStatus(ImConnectStatusEnum.OFFLINE_STATUS.getCode()); + + MqMessageProducer.sendMessage(userStatusChangeNotifyPack,messageHeader, UserEventCommand.USER_ONLINE_STATUS_CHANGE.getCommand()); + + nioSocketChannel.close(); + } + + +} diff --git a/hs-im-server/im-tcp/src/main/resources/config.yml b/hs-im-server/im-tcp/src/main/resources/config.yml new file mode 100644 index 0000000..fe650ec --- /dev/null +++ b/hs-im-server/im-tcp/src/main/resources/config.yml @@ -0,0 +1,34 @@ +lim: + tcpPort: 29000 + webSocketPort: 19000 + bossThreadSize: 1 + workThreadSize: 8 + heartBeatTime: 20000 #心跳超时时间 单位毫秒 + brokerId: 1000 + loginModel: 3 + logicUrl: http://127.0.0.1:28000/v1 + # * 多端同步模式:1 只允许一端在线,手机/电脑/web 踢掉除了本client+imel的设备 + # * 2 允许手机/电脑的一台设备 + web在线 踢掉除了本client+imel的非web端设备 + # * 3 允许手机和电脑单设备 + web 同时在线 踢掉非本client+imel的同端设备 + # * 4 允许所有端多设备登录 不踢任何设备 + + redis: + mode: single # 单机模式:single 哨兵模式:sentinel 集群模式:cluster + database: 8 + password: dSMIXBQrCBXiHHjk123 + timeout: 3000 # 超时时间 + poolMinIdle: 8 #最小空闲数 + poolConnTimeout: 3000 # 连接超时时间(毫秒) + poolSize: 10 # 连接池大小 + single: #redis单机配置 + address: 43.139.191.204:6379 + rabbitmq: + host: 192.168.2.180 + port: 5672 + virtualHost: / + userName: guest + password: guest + + zkConfig: + zkAddr: 192.168.2.180:2181 + zkConnectTimeOut: 5000 diff --git a/hs-im-server/im-tcp/src/main/resources/config2.yml b/hs-im-server/im-tcp/src/main/resources/config2.yml new file mode 100644 index 0000000..c13e4e2 --- /dev/null +++ b/hs-im-server/im-tcp/src/main/resources/config2.yml @@ -0,0 +1,34 @@ +lim: + tcpPort: 29002 + webSocketPort: 19002 + bossThreadSize: 1 + workThreadSize: 8 + heartBeatTime: 20000 #心跳超时时间 单位毫秒 + brokerId: 1001 + loginModel: 3 + logicUrl: http://127.0.0.1:28000/v1 + # * 多端同步模式:1 只允许一端在线,手机/电脑/web 踢掉除了本client+imel的设备 + # * 2 允许手机/电脑的一台设备 + web在线 踢掉除了本client+imel的非web端设备 + # * 3 允许手机和电脑单设备 + web 同时在线 踢掉非本client+imel的同端设备 + # * 4 允许所有端多设备登录 不踢任何设备 + + redis: + mode: single # 单机模式:single 哨兵模式:sentinel 集群模式:cluster + database: 8 + password: dSMIXBQrCBXiHHjk123 + timeout: 3000 # 超时时间 + poolMinIdle: 8 #最小空闲数 + poolConnTimeout: 3000 # 连接超时时间(毫秒) + poolSize: 10 # 连接池大小 + single: #redis单机配置 + address: 43.139.191.204:6379 + rabbitmq: + host: 192.168.2.180 + port: 5672 + virtualHost: / + userName: guest + password: guest + + zkConfig: + zkAddr: 192.168.2.180:2181 + zkConnectTimeOut: 5000 diff --git a/hs-im-server/im-tcp/src/main/resources/py/heartBeat.py b/hs-im-server/im-tcp/src/main/resources/py/heartBeat.py new file mode 100644 index 0000000..b9457af --- /dev/null +++ b/hs-im-server/im-tcp/src/main/resources/py/heartBeat.py @@ -0,0 +1,51 @@ +import socket +import json +import struct +import threading +import time +import uuid + +def doPing(scoket): +## 基础数据 + command = 0x270f + print(command) + version = 1 + clientType = 4 + messageType = 0x0 + appId = 10000 + userId = 'lld' + imei = str(uuid.uuid1()) + + ## 数据转换为bytes + commandByte = command.to_bytes(4,'big') + versionByte = version.to_bytes(4,'big') + messageTypeByte = messageType.to_bytes(4,'big') + clientTypeByte = clientType.to_bytes(4,'big') + appIdByte = appId.to_bytes(4,'big') + clientTypeByte = clientType.to_bytes(4,'big') + imeiBytes = bytes(imei,"utf-8"); + imeiLength = len(imeiBytes) + imeiLengthByte = imeiLength.to_bytes(4,'big') + data = {} + jsonData = json.dumps(data) + body = bytes(jsonData, 'utf-8') + body_len = len(body) + bodyLenBytes = body_len.to_bytes(4,'big') + + s.sendall(commandByte + versionByte + clientTypeByte + messageTypeByte + appIdByte + imeiLengthByte + bodyLenBytes + imeiBytes + body) + +def ping(scoket): + while True: + time.sleep(10) + doPing(scoket) + +s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) +s.connect(("127.0.0.1",9000)) + +# 创建一个线程专门接收服务端数据并且打印 +t1 = threading.Thread(target=ping,args=(s,)) +t1.start() + + + + diff --git a/hs-im-server/im-tcp/src/main/resources/py/login.py b/hs-im-server/im-tcp/src/main/resources/py/login.py new file mode 100644 index 0000000..64c08fb --- /dev/null +++ b/hs-im-server/im-tcp/src/main/resources/py/login.py @@ -0,0 +1,49 @@ + + +import socket +import json +import struct +import threading +import time +import uuid + + +imei = str(uuid.uuid1()) + +s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) +s.connect(("127.0.0.1",9000)) + + +## 基础数据 +command = 0x2328 +print(command) +version = 1 +clientType = 4 +messageType = 0x0 +appId = 10000 +userId = 'lld' + +## 数据转换为bytes +commandByte = command.to_bytes(4,'big') +versionByte = version.to_bytes(4,'big') +messageTypeByte = messageType.to_bytes(4,'big') +clientTypeByte = clientType.to_bytes(4,'big') +appIdByte = appId.to_bytes(4,'big') +clientTypeByte = clientType.to_bytes(4,'big') +imeiBytes = bytes(imei,"utf-8"); +imeiLength = len(imeiBytes) +imeiLengthByte = imeiLength.to_bytes(4,'big') +data = {"userId": userId} +jsonData = json.dumps(data) +body = bytes(jsonData, 'utf-8') +body_len = len(body) +bodyLenBytes = body_len.to_bytes(4,'big') + +s.sendall(commandByte + versionByte + clientTypeByte + messageTypeByte + appIdByte + imeiLengthByte + bodyLenBytes + imeiBytes + body) +# for x in range(100): +# s.sendall(commandByte + versionByte + clientTypeByte + messageTypeByte + appIdByte + imeiLengthByte + bodyLenBytes + imeiBytes + body) + + + + + diff --git a/hs-im-server/im-tcp/src/main/resources/py/loginLongTime.py b/hs-im-server/im-tcp/src/main/resources/py/loginLongTime.py new file mode 100644 index 0000000..a7bc9f1 --- /dev/null +++ b/hs-im-server/im-tcp/src/main/resources/py/loginLongTime.py @@ -0,0 +1,71 @@ + + +import socket +import json +import struct +import threading +import time +import uuid + + +def task(s): + print("task开始") + while True: + # datab = scoket.recv(4) + command = struct.unpack('>I', s.recv(4))[0] # 接受command并且解析 + num = struct.unpack('>I', s.recv(4))[0] # 接受包大小并且解析 + print(command) + if command == 0x232a : + print("收到下线通知,退出登录") + s.close() + # exit; + # break + + +imei = str(uuid.uuid1()) + +s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) +s.connect(("127.0.0.1",9000)) + +t1 = threading.Thread(target=task,args=(s,)) +t1.start() + +## 基础数据 +command = 0x2328 + +version = 2 +clientType = 5 +print(clientType) +messageType = 0x0 +appId = 10000 +userId = 'lld' + +## 数据转换为bytes +commandByte = command.to_bytes(4,'big') +versionByte = version.to_bytes(4,'big') +messageTypeByte = messageType.to_bytes(4,'big') +clientTypeByte = clientType.to_bytes(4,'big') +appIdByte = appId.to_bytes(4,'big') +clientTypeByte = clientType.to_bytes(4,'big') +imeiBytes = bytes(imei,"utf-8"); +imeiLength = len(imeiBytes) +imeiLengthByte = imeiLength.to_bytes(4,'big') +data = {"userId": userId} +jsonData = json.dumps(data) +body = bytes(jsonData, 'utf-8') +body_len = len(body) +bodyLenBytes = body_len.to_bytes(4,'big') + +s.sendall(commandByte + versionByte + clientTypeByte + messageTypeByte + appIdByte + imeiLengthByte + bodyLenBytes + imeiBytes + body) + + + # WEB(1,"web"), + # IOS(2,"ios"), + # ANDROID(3,"android"), + # WINDOWS(4,"windows"), + # MAC(5,"mac"), + +while(True): + i = 1+1 + + diff --git a/hs-im-server/im-tcp/src/main/resources/py/privateSend.py b/hs-im-server/im-tcp/src/main/resources/py/privateSend.py new file mode 100644 index 0000000..9d36b98 --- /dev/null +++ b/hs-im-server/im-tcp/src/main/resources/py/privateSend.py @@ -0,0 +1,48 @@ + + +import socket +import json +import struct +import threading +import time +import uuid + + +imei = str(uuid.uuid1()) + +s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) +s.connect(("127.0.0.1",9000)) + + +## 基础数据 +command = 9888 +version = 1 +clientType = 4 +messageType = 0x0 +appId = 10000 +name = 'lld' + +## 数据转换为bytes +commandByte = command.to_bytes(4,'big') +versionByte = version.to_bytes(4,'big') +messageTypeByte = messageType.to_bytes(4,'big') +clientTypeByte = clientType.to_bytes(4,'big') +appIdByte = appId.to_bytes(4,'big') +clientTypeByte = clientType.to_bytes(4,'big') +imeiBytes = bytes(imei,"utf-8"); +imeiLength = len(imeiBytes) +imeiLengthByte = imeiLength.to_bytes(4,'big') +data = {"name": name, "appId": appId, "clientType": clientType, "imei": imei} +jsonData = json.dumps(data) +body = bytes(jsonData, 'utf-8') +body_len = len(body) +bodyLenBytes = body_len.to_bytes(4,'big') + +# s.sendall(commandByte + versionByte + clientTypeByte + messageTypeByte + appIdByte + imeiLengthByte + bodyLenBytes + imeiBytes + body) +for x in range(100): + s.sendall(commandByte + versionByte + clientTypeByte + messageTypeByte + appIdByte + imeiLengthByte + bodyLenBytes + imeiBytes + body) + + + + + diff --git a/hs-im-server/im-tcp/src/main/resources/web.html b/hs-im-server/im-tcp/src/main/resources/web.html new file mode 100644 index 0000000..941521a --- /dev/null +++ b/hs-im-server/im-tcp/src/main/resources/web.html @@ -0,0 +1,751 @@ + + + + + WebSocket客户端 + + + +
+ + + + + + + + + + + + + + +

服务器输出:

+ + + + +
+ + diff --git a/hs-im-server/pom.xml b/hs-im-server/pom.xml index 136f53c..86ec431 100644 --- a/hs-im-server/pom.xml +++ b/hs-im-server/pom.xml @@ -6,7 +6,7 @@ im-system pom 1.0.0-SNAPSHOT - lld-im + im-system l-im project @@ -19,6 +19,9 @@ im-service im-common + im-tcp + im-codec + im-message-store @@ -64,6 +67,41 @@ org.projectlombok lombok + + io.netty + netty-all + + + org.yaml + snakeyaml + + + + org.redisson + redisson + + + com.rabbitmq + amqp-client + + + com.github.sgroschupf + zkclient + + + com.lld + common + 1.0.0-SNAPSHOT + compile + + + com.netflix.feign + feign-core + + + com.netflix.feign + feign-jackson + @@ -213,4 +251,4 @@ - \ No newline at end of file + diff --git a/l-im-master/l-im/im-tcp/src/main/resources/py/heartBeat.py b/l-im-master/l-im/im-tcp/src/main/resources/py/heartBeat.py index b9457af..53fddce 100644 --- a/l-im-master/l-im/im-tcp/src/main/resources/py/heartBeat.py +++ b/l-im-master/l-im/im-tcp/src/main/resources/py/heartBeat.py @@ -40,7 +40,7 @@ def ping(scoket): doPing(scoket) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) -s.connect(("127.0.0.1",9000)) +s.connect(("127.0.0.1",29000)) # 创建一个线程专门接收服务端数据并且打印 t1 = threading.Thread(target=ping,args=(s,)) diff --git a/l-im-master/l-im/im-tcp/src/main/resources/py/privateSend.py b/l-im-master/l-im/im-tcp/src/main/resources/py/privateSend.py index 9d36b98..017dc0d 100644 --- a/l-im-master/l-im/im-tcp/src/main/resources/py/privateSend.py +++ b/l-im-master/l-im/im-tcp/src/main/resources/py/privateSend.py @@ -11,16 +11,16 @@ import uuid imei = str(uuid.uuid1()) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) -s.connect(("127.0.0.1",9000)) +s.connect(("127.0.0.1",29002)) ## 基础数据 -command = 9888 +command = 9000 version = 1 clientType = 4 messageType = 0x0 appId = 10000 -name = 'lld' +name = 'lld1123' ## 数据转换为bytes commandByte = command.to_bytes(4,'big') @@ -29,17 +29,19 @@ messageTypeByte = messageType.to_bytes(4,'big') clientTypeByte = clientType.to_bytes(4,'big') appIdByte = appId.to_bytes(4,'big') clientTypeByte = clientType.to_bytes(4,'big') -imeiBytes = bytes(imei,"utf-8"); +imeiBytes = bytes(imei,"utf-8") imeiLength = len(imeiBytes) +print(imeiLength) imeiLengthByte = imeiLength.to_bytes(4,'big') -data = {"name": name, "appId": appId, "clientType": clientType, "imei": imei} +data = {"name": name, "appId": appId, "clientType": clientType, "imei": imei,"userId":name} jsonData = json.dumps(data) body = bytes(jsonData, 'utf-8') body_len = len(body) bodyLenBytes = body_len.to_bytes(4,'big') # s.sendall(commandByte + versionByte + clientTypeByte + messageTypeByte + appIdByte + imeiLengthByte + bodyLenBytes + imeiBytes + body) -for x in range(100): +for x in range(10): + print(imeiLengthByte) s.sendall(commandByte + versionByte + clientTypeByte + messageTypeByte + appIdByte + imeiLengthByte + bodyLenBytes + imeiBytes + body) diff --git a/uni-im示例/.hbuilderx/launch.json b/uni-im示例/.hbuilderx/launch.json new file mode 100644 index 0000000..c9abd4b --- /dev/null +++ b/uni-im示例/.hbuilderx/launch.json @@ -0,0 +1,11 @@ +{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/ + // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数 + "version": "0.0", + "configurations": [{ + "type": "uniCloud", + "default": { + "launchtype": "local" + } + } + ] +} diff --git a/uni-im示例/App.vue b/uni-im示例/App.vue new file mode 100644 index 0000000..8816b78 --- /dev/null +++ b/uni-im示例/App.vue @@ -0,0 +1,42 @@ + + + diff --git a/uni-im示例/README.md b/uni-im示例/README.md new file mode 100644 index 0000000..47bc701 --- /dev/null +++ b/uni-im示例/README.md @@ -0,0 +1,707 @@ +> 本文为uni-im v2.x 的文档,如果旧项目需要继续使用老版本的uni-im v1.x,另见:[https://gitcode.net/dcloud/hello-uni-im/-/blob/main/README.md](https://gitcode.net/dcloud/hello-uni-im/-/blob/main/README.md) + +# 简介 +uni-im是云端一体的、全平台的、免费的、开源即时通讯系统。 +- 基于uni-app,App、小程序、web全端兼容 +- 基于uniCloud,前后端都使用js开发 +- 基于[uni-push2](https://uniapp.dcloud.net.cn/unipush-v2.html),专业稳定的全端推送系统 +- 基于[uni-id](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html),完善的账户体系 +- 支持服务端为非uniCloud(比如:应用服务端的开发语言是php、java、go、.net、python、c#等)或 不基于uni-id-pages 开发的项目接入 + +案例: + + + +如图:在插件市场任意插件详情页面,点击咨询作者按钮,即可看到基于uni-im搭建的客服系统。 + +下载地址:[https://ext.dcloud.net.cn/plugin?name=uni-im](https://ext.dcloud.net.cn/plugin?name=uni-im) + +## 特点优势 +- 性价比高;前后端代码均免费开源,相比竞品使用uni-im仅需花费极少的托管在uniCloud(serverless服务器)产生的费用[详情查看](#cost) +- 全端可用 +- App端支持nvue,更好的长列表性能。list组件性能优势[详情参考](https://uniapp.dcloud.net.cn/component/list.html) +- 智能本地缓存(app端sqlite,web端indexDB,小程序端storage),更快的历史消息加载速度,更小的网络请求压力 +- 中心化响应式数据管理,切换会话无需重新加载数据,更流畅的体验 +- App端聚合多个手机厂商推送通道,app不在线也可以收到消息 + +## 版本计划 +### 已上线 +- 应用内嵌入uni-im,使用户方便、实时的与App运营者互动,咨询问题、反馈意见、进行投诉。 +- 可发送文字、图片、音频、视频、代码、任意文件 +- im交友场景:群聊、好友关系 +- 会话细节:消息删除、撤回、消息回复 + +### 后续计划 +1. 通信方式扩展:音频通话、视频通话 +2. 细节完善:聊天记录识别电话邮件、消息转发和批转、勿扰设置、会话置顶、留言转文字、图片提取文字 +3. 客服场景:管理端支持座席(暂时可先用通过:给每个用户创建一个群,隐藏群信息查看入口,成员进出群实现转坐席) + +优先开发哪些,取决于开发者的反馈。同时也欢迎开发者共建这个开源项目。 + +> uni-im相关功能建议或问题,可以加入由uni-im(本插件)搭建的交流群,[点此加入](https://im.dcloud.net.cn/#/?joinUniImGroup=1),备用QQ群(当系统处于维护中使用)群号:[854520009](https://qm.qq.com/cgi-bin/qm/qr?k=DJNSajXAYHnYcr9pouOfxF9Rwwl1AJHc&jump_from=webapi&authKey=HZ1fG58Eudp3o0GCoyx1/UPMY9Fv1sGT5jdqYqPJlTGT0XVUip3Bk8E+UyToQOMo) + +## 使用uniCloud产生的费用说明@cost + +uni-im本身并不收费,实际使用中需要依赖uniCloud云服务,会产生费用;而uniCloud的价格很实惠: +- 调用10000次云函数仅需0.0133元 +- 调用10000次数据库查询仅需0.015元 +> 更多计费参考:[阿里云版uniCloud按量计费文档](https://uniapp.dcloud.net.cn/uniCloud/price.html#aliyun-postpay) + +### 举例说明: + +- 单聊场景,向用户发送一条消息的过程: +1. 调用uni-im-co云对象的sendMsg方法(产生1次云函数请求) +2. 查询当前对话的会话记录(产生1次云数据库读操作) +3. 根据步骤2的查询结果,如果已经有会话记录,就更新会话,否则就创建一条会话记录(产生1次云数据库写操作) +4. 查询发送消息的用户信息,用于接收消息时在通知栏显示发送者昵称和头像(产生1次云数据库读操作) +5. 记录发送的消息内容到数据库,用于保存消息历史记录(产生1次云数据库写操作) +6. 以`user_id`为标识通过`uni-push2`向用户发送消息会产生0.00000283元uniCloud使用费用[详情查看](https://uniapp.dcloud.net.cn/unipush-v2.html#cost) + +合计:1次云函数请求、2次数据库读操作、2次数据库写操作、1次uni-push2推送操作,即 (1 * 0.0133 + 2 * 0.015 + 2 * 0.05 + 1 * 0.0283)/10000 ≈ 0.000017元 + +- 群聊场景,向用户发送一条消息的过程: +1. 调用uni-im-co云对象的sendMsg方法(产生1次云函数请求) +2. 查询当前用户是否为群成员,防止非群成员发送消息(产生1次云数据库读操作) +3. 查询当前对话的会话记录(产生1次云数据库读操作) +4. 根据步骤3的查询结果,如果已经有会话记录,就更新会话,否则就创建一条会话记录(产生1次云数据库写操作) +5. 查询发送消息的用户信息,用于接收消息时在通知栏显示发送者昵称和头像(产生1次云数据库读操作) +6. 记录发送的消息内容到数据库,用于保存消息历史记录(产生1次云数据库写操作) +7. 以群id为参数,调用uni-im-co云对象的sendMsgToGroup方法,这是一个递归方法每次向500名群成员推送消息(如果群成员数量为0-500只需执行1次,500-1000需执行2次,以此类推),(会产生最少1次数据库读操作,和1次以`user_id`为标识通过`uni-push2`向用户发送消息会产生0.00000283元uniCloud使用费用[详情查看](https://uniapp.dcloud.net.cn/unipush-v2.html#cost)) + +合计:向500人群发送消息,会产生:1次云函数请求、4次数据库读操作、2次数据库写操作、1次uni-push2推送操作,即 (1 * 0.0133 + 4 * 0.015 + 2 * 0.05 + 1 * 0.0283)/10000 ≈ 0.000020元 + +相比市面上同类型产品,使用uni-im仅需花费如此便宜的uniCloud(serverless服务器)费用;在价格这块uni-im性价比极高。 + +# 快速部署体验 +## 前提条件 +1. 开通uniCloud并创建服务空间 [控制面板](https://unicloud.dcloud.net.cn/) + 传统的IM产品服务端代码托管在服务商名下的服务器内,你只拥有代码和产生的数据的使用权,并非所有权;而uni-im的前后端代码都是开源的,是将代码托管在你名下的unicloud([serverless](https://uniapp.dcloud.net.cn/uniCloud/#%E4%BB%80%E4%B9%88%E6%98%AFserverless)服务器)内。 +2. 开通`uni-push2.0`(注意:**无论是APP、小程序、web端都需要开通,否则消息将无法实时更新**)[点此前往开通](https://uniapp.dcloud.net.cn/unipush-v2.html#%E7%AC%AC%E4%B8%80%E6%AD%A5-%E5%BC%80%E9%80%9A) + +## 体验步骤 + +1. 打开`uni-im`插件下载地址:[https://ext.dcloud.net.cn/plugin?name=uni-im](https://ext.dcloud.net.cn/plugin?name=uni-im) +2. 点击`使用HBuilderX导入示例项目` +3. 对项目根目录uniCloud点右键选择“云服务空间初始化向导”界面按提示部署项目(注意:选择绑定的服务空间,须在uni-push2.0的[web控制台](https://dev.dcloud.net.cn/pages/app/push2/info)关联) +4. 在HBuilderX控制台,更改`连接本地云函数`为`连接云端云函数` + +5. `运行项目`到2个不同的浏览器,因为在同一个浏览器打开相同网络地址(ip或者域名)的uni-im项目,socket会相互占线。 + 所以需要使用两个浏览器(或者使用浏览器`打开新的无痕式窗口`功能充当第二个浏览器)分别`注册账号并登录`, + 到此部署已经结束 +6. 向对应的用户发起会话,通过访问路径:`/uni_modules/uni-im/pages/chat/chat?user_id=` + `对应的用户id` 即可 + +## 部署到自己的项目 +1. 打开`uni-im`插件下载地址:[https://ext.dcloud.net.cn/plugin?name=uni-im](https://ext.dcloud.net.cn/plugin?name=uni-im) +2. 点击`使用HBuilderX导入插件`,选择你的项目,点击确定(同时会自动导入依赖的uni_modules`uni-id-pages`)按提示操作自动配置`pages.json` +3. 打开项目根目录的App.vue文件,初始化uni-id-pages和uniIm模块 +示例如下: + +```html + +``` + +4. 部署到uniCloud +对项目根目录uniCloud点右键,选择“云服务空间初始化向导” 按提示部署项目(注意:选择绑定的服务空间,须在uni-push2.0的[web控制台](https://dev.dcloud.net.cn/pages/app/push2/info)关联) + +5. 登录uni-im + + uni-im的服务端代码托管在uniCloud下,账户体系是[uni-id 4.0+](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html)的; + uni-app生态下绝大部分项目的架构与uni-im相同,所以不需要考虑账号打通问题,用户登录项目后,不需要额外登录uni-im。 + + 而有些传统项目,服务端的开发语言是php、java、go、.net、python、c#等,是自己设计的账号体系;用户登录所获得的token,与uni-im所需的token不是同一个账号体系;需要在传统服务器端,通过[uni-id的外部系统联登](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external)同步你项目的账号数据到uni-im用户体系并获得uni-id的token,客户端再调用uniImUtils的login方法登录uni-im;示例代码如下: + + ```js + import uniImUtils from '@/uni_modules/uni-im/common/utils.js'; + + uni.request({ + url: 'https://www.example.com/login', //仅为示例,并非真实接口地址。 + data: { + username: 'test', + password: '123456' + }, + success:async (res) => { + console.log(res.data); + // 得到你自己项目的token和uni-id的token + let {token,uniIdToken} = res.data + uni.setStorageSync('token',token) + // 【请注意】这里的`uniIdToken` 是一个`对象`:包含:`token`和`tokenExpired` + await uniImUtils.login(uniIdToken) + } + }); + + ``` + + 其他情况: + + - 客户端如果不是uni-app的,如果是网页,可iframe内嵌。如果是原生app,可嵌入[uni小程序sdk](https://nativesupport.dcloud.net.cn/README) + + - 不基于`uni-id-pages`的客户端代码,仅基于`uni-id-co`的项目,需要在登录成功和用户信息更新时,同步更新uniId store内的当前用户信息(uni-im显示当前用户头像、昵称时会用到)示例代码: + + ```js + //导入uniCloud客户端账户体系,用户信息状态管理模块 + import {mutations as uniIdMutations} from '@/uni_modules/uni-id-pages/common/store.js'; + await uniIdMutations.updateUserInfo() + ``` + - 基于老版uni-id(版本号:3.x) 开发的项目,需要如下改造: + 1. 在登录成功和token续期后,绑定当前账号与设备推送标识的关联关系。示例代码: + + ```js + const uniIdCo = uniCloud.importObject("uni-id-co", {customUI: true}) + uni.getPushClientId({ + success: async function(e) { + console.log(e) + let pushClientId = e.cid + let res = await uniIdCo.setPushCid({ + pushClientId + }) + console.log('getPushClientId', res); + }, + fail(e) { + console.error(e) + } + }) + ``` + 2. 在登录成功和用户信息更新时,同步更新uniId store内的当前用户信息(uni-im显示当前用户头像、昵称时会用到)示例代码: + ```js + //导入uniCloud客户端账户体系,用户信息状态管理模块 + import {mutations as uniIdMutations} from '@/uni_modules/uni-id-pages/common/store.js'; + await uniIdMutations.updateUserInfo() + ``` + +6. 确保账户对接成功后,打开“用户列表页”,路径:`/uni_modules/uni-im/pages/userList/userList`可以看到所有的注册用户 +7. 点击某个用户,会自动创建与该用户的会话,并打开“聊天对话页”(路径:`/uni_modules/uni-im/pages/chat/chat`),然后就可以开始聊天了。 +8. 还可以导入uni-im的示例项目作为管理员端与用户聊天。 +9. 如果你是2个不同appId的应用相互通讯(比如:淘宝的买家端和卖家端通讯)的场景,请打开聊天对话文件(路径:`/uni_modules/uni-im/pages/chat/chat`)搜索`data.appId = this.systemInfo.appId`修改`this.systemInfo.appId`为相对的appId + +不基于uni-id-pages开发的项目还要注意以下两个问题: +1. 退出登录;需要在执行退出登录/切换账号时,调用uni-id的退出登录接口。否则会出现退出登录后的设备仍然能收到im消息,或导致此设备再登录其他账号不能正常收到消息的问题;示例代码如下: +```js +import {mutations as uniIdMutations} from '@/uni_modules/uni-id-pages/common/store.js' +uniIdMutations.logout() +``` +2. token有效期问题,保证你的项目token有效期和uni-id的token有效期保持一致。这涉及两个操作: +- 配置uni-id的token过期时间与你的项目token有效期一致。配置路径:`/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json`,关于配置说明[详情查看](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#config) +- 如果你的项目有token续期逻辑,需要在续期后调用uni-id的token续期接口,示例代码: +```js +const uniIdCo = uniCloud.importObject("uni-id-co", {customUI: true}) +await uniIdCo.refreshToken() +``` + +常见问题: +1. 为什么不能实时接收到推送的消息,需要刷新或者关闭重新打开才能收到? +答: uni-im通过`uni-push2`实现消息实时送达,请检查是否已正确配合并开通,且在配置正常后重新登录 + +2. 怎么样快速上手 +答:先下载示例项目,部署并正确配置push后,体验没问题了再部署到自己的项目。 + + +## 限制普通用户向其他用户发起会话 +客服场景下,我们希望管理员客服可以向任意用户发起会话。而普通用户的会话对象只能是客服。 +- 客户端限制 +删除或隐藏“用户列表页”和“会话列表页”,仅保留“聊天对话页”。并绘制按钮,如:“联系客服”,点击后打开“聊天对话页” +逻辑代码如下: +```js +uni.navigateTo({ + url:'/uni_modules/uni-im/pages/chat/chat?user_id=' + 对应的用户id +}) +``` + +- 服务端限制 + +1. 添加`uni-im`配置文件,打开:`/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/`;新建`uni-im`文件夹和`config.json`文件,示例如下: +```json +{ + "customer_service_uids":["user-id-01","user-id-02"] +} +``` + +2. 配置`customer_service_uids`的值为管理员客服的user_id(支持多个以数组的形式指定),如果会话双方均不属于此域则无法通讯。不配置或为false则表示不限制。 + +# 开发文档 +## 目录结构 +
+
+├─uni_modules
+│    ├─其他module
+│    └─uni-im
+│        ├─uniCloud
+│        │    ├─cloudfunctions                         云函数目录
+│        │    │    └─uni-im-co                         集成调用uni-im方法的云对象
+│        │    └─database
+│        │      ├─uni-id-users.schema.ext.js           用户表触发器
+│        │      ├─uni-im-conversation.schema.ext.js    聊天会话表触发器
+│        │      ├─uni-im-conversation.schema.json      聊天会话表的表结构
+│        │      ├─uni-im-friend-invite.schema.ext.js   邀请加为好友表触发器
+│        │      ├─uni-im-friend-invite.schema.json     邀请加为好友表表结构
+│        │      ├─uni-im-friend.schema.ext.js          好友关系表触发器
+│        │      ├─uni-im-friend.schema.json            好友关系表表结构
+│        │      ├─uni-im-group-join.schema.ext.js      申请加入群聊表触发器
+│        │      ├─uni-im-group-join.schema.json        申请加入群聊表表结构
+│        │      ├─uni-im-group-member.schema.ext.js    群成员表触发器
+│        │      ├─uni-im-group-member.schema.json      群成员表表结构
+│        │      ├─uni-im-group.schema.ext.js           群信息表触发器
+│        │      ├─uni-im-group.schema.json             群信息表表结构
+│        │      ├─uni-im-msg.schema.ext.js             聊天消息表触发器
+│        │      ├─uni-im-msg.schema.json               聊天消息表表结构
+│        │      └─uni-im-notification.schema.json      推送消息记录表(仅记录系统消息)
+│        ├─common
+│        │    ├─appEvent.js             生命周期事件api库
+│        │    ├─emojiCodes.js           emoji表情列表
+│        │    ├─initIndexDB.js          indexDB本地数据库初始化文件(仅Web端使用)
+│        │    ├─md5.js                  md5哈希加密算法(用于本地直接生成会话id)
+│        │    ├─toFriendlyTime.js       时间戳转友好时间提示字符库文件(如:x年x月x日,昨天,下午,周二,1小时前等)
+│        │    ├─sqlite.js               sqlite本地数据库初始化文件(仅App端使用)
+│        │    └─utils.js                工具类库
+│        ├─components
+│        │    └─uni-im-msg              显示聊天消息气泡组件
+│        ├─ lib
+│        │    ├─createObservable.js     创建响应式对象文件
+│        │    ├─main.js                 核心库入口文件
+│        │    └─msgManager.js           消息管理类库
+│        ├─pages
+│        │    ├─chat
+│        │    │    ├─info.nvue           对话详情(显示好友信息,可在此页面操作删除好友。后续支持:备注好友、打标签、拉黑、屏蔽等功能)
+│        │    │    └─chat.nvue           聊天对话页
+│        │    ├─common                   公共页面
+│        │    │    ├─uni-im-code-pages   代码类型消息浏览专用页面
+│        │    │    └─video               视频播放专用页面
+│        │    ├─contacts
+│        │    │    ├─addPeopleGroups     查找并添加用户或群
+│        │    │    ├─createGroup         创建群聊
+│        │    │    ├─groupList           我的群列表
+│        │    │    ├─notification        im系统通知页面
+│        │    │    └─contacts.nvue       联系人页面
+│        │    ├─group                    
+│        │    │    ├─groupQRCode         群二维码页面(未完成)
+│        │    │    └─info                群信息页面(管理群)
+│        │    ├─index                    首页(展示会话列表)
+│        │    └─userList                 所有用户列表页
+│        ├─static                        静态资源目录
+│        ├─changelog.md                  更新日志
+│        ├─package.json                  包管理文件
+│        └─readme.md                     插件自述文件
+
+
+名词解释 +- 聊天会话ID +根据通讯双方用户id,或群聊id,生成的唯一索引值;用于更加方便查找聊天记录等。 +- 聊天会话 +以会话ID为索引的一组数据,记录:未读消息数量、会话更新时间、会话类型、会话所属用户的id、对话的用户id、对话的群id、群信息、最后一条消息概述(文本消息的前15个字,消息为多媒体时只描述类型) + +## uni-im-co 云函数(云对象) +API列表 + +|API |描述 | +|-- |-- | +|getConversationList |获取会话列表[见下方](#coGetConversationList) | +|sendMsg |发送聊天消息[见下方](#coSendMsg) | +|sendPushMsg |触发器专用消息推送方法 | +|sendMsgToGroup |向群用户递归推送消息[见下方](#coSendMsgToGroup) | +|addFriendInvite |向用户发起加好友邀请[见下方](#coAddFriendInvite) | +|chooseUserIntoGroup |选择用户加入群聊(不传群id时为创建群)[见下方](#coSendMsgToGroup) | +|revokeMsg |撤回已经发送的消息[见下方](#coRevokeMsg) | + + + +### 获取会话列表 getConversationList@coGetConversationList +**参数说明** + +|参数名 |类型 |必填 |说明 | +|-- |-- |-- |-- | +|limit |number |否 |数量,默认值:500 | +|maxUpdateTime |number |否 |最大更新时间(实现高性能分页) | +|page |number |是 |页码 | + +**返回值** + +|参数名 |类型 |说明 | +|-- |-- |-- | +|errCode|string|number |错误码,0表示成功 | +|errMsg |string |错误信息 | +|data |array |会话数据 | + +### 发送聊天消息 sendMsg@coSendMsg + +|参数名 |类型 |必填 |说明 | +|-- |-- |-- |-- | +|appId |string |是 |接收消息的appId;如果你是2个不同appId的应用相互发,请修改此值为相对的appId | +|to_uid |string |否 |接收消息的用户id | +|group_id |string |否 |接收消息的群id| +|body |string |是 |消息内容,`type = text`时为文本内容.`type = image`时为图片网络地址| +|type |string |是 |消息类型,暂时仅支持:text(表示文本类型)、image(表示图片类型)| +|isRetries |Boolean|否 |是否为重发| + +**返回值** + +|参数名 |类型 |说明 | +|-- |-- |-- | +|errCode |string|number |错误码,0表示成功 | +|errMsg |string |错误信息 | +|data |object | | +| |- create_time |无 |创建时间 | + +**接口形式** + +```js +const uniImCo = uniCloud.importObject('uni-im-co', { + customUI: true +}); +await uniImCo.sendMsg({ + to_uid:"630cacf46293d20001f3c368", + type:"text", + body:"您好!" +}) +``` + +### 向群用户递归推送消息 sendMsgToGroup@coSendMsgToGroup +注意:这是一个递归云对象,500个设备为一组批量向用户推送消息(该方法仅支持云对象的方法,或者触发器调用) + +|参数名 |类型 |必填 |说明 | +|-- |-- |-- |-- | +|appId |string |是 |接收消息的应用appId| +|pushParam |object |是 |参数同uni-push2.0的sendMessage方法,详情参考[https://uniapp.dcloud.net.cn/uniCloud/uni-cloud-push/api.html#sendmessage](https://uniapp.dcloud.net.cn/uniCloud/uni-cloud-push/api.html#sendmessage)| +|before_id |string |否 |从哪个用户id开始(用于实现高性能分页)| +|push_clientids |array |否 |个推设备id列表| + +**返回值** + +|参数名 |类型 |说明 | +|-- |-- |-- | +|errCode |string|number |错误码,0表示成功 | +|errMsg |string |错误信息 | + +### 撤回已发送的消息 revokeMsg@coRevokeMsg +|参数名 |类型 |必填 |说明 | +|-- |-- |-- |-- | +|msgId |string |是 |消息id | +**返回值** + +|参数名 |类型 |说明 | +|-- |-- |-- | +|errCode |string|number |错误码,0表示成功 | +|errMsg |string |错误信息 | + + +### 向用户发起加好友邀请 addFriendInvite@coAddFriendInvite + +|参数名 |类型 |必填 |说明 | +|-- |-- |-- |-- | +|to_uid |string |是 |被邀请的用户id| +|message |string |否 |请求信息| + +**返回值** + +|参数名 |类型 |说明 | +|-- |-- |-- | +|errCode |string|number |错误码,0表示成功 | +|errMsg |string |错误信息 | + +### 选择用户加入群聊 chooseUserIntoGroup@coSendMsgToGroup + +|参数名 |类型 |必填 |说明 | +|-- |-- |-- |-- | +|group_id |string |否 |群id(为空则创建群) | +|user_ids |string |是 |用户id数组 | + +**返回值** + +|参数名 |类型 |说明 | +|-- |-- |-- | +|errCode |string|number |错误码,0表示成功 | +|errMsg |string |错误信息 | +|data |object |返回信息 | +| |- group_id |string |群id | + + +## 服务端配置@uni-im-cloud-config +路径:`/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/uni-im/config.json` + +|字段名 |数据类型 |说明 | +|-- |-- |-- | +|customer_service_uids |string/boolean |客服用户id,不限制则填`false`即可;仅conversation_grade的值为100时有效 | +|conversation_grade |int |控制发起会话的条件,详情[会话控制](#conversation_grade) | + +### 会话控制@conversation_grade + +|值 |说明 | +|-- |-- | +|0 |不限制 | +|100|仅限当前用户向:客服、好友、群成员发起会话 | +|200|仅限当前用户向:好友或群成员发起会话 | + + +## 客户端sdk@clientSkd + +入口文件路径:`@/uni_modules/uni-im/lib/main.js` + +uni-im2.0 废弃了1.0通过Vuex的状态管理方式,不再需要关心vuex的用法,直接当做一个全局的响应式js变量即可。 + +### state + +|名称 |类型 |说明 | +|-- |-- |-- | +|conversation |object |会话对象 | +| |- dataList|array |会话数据列表 | +| |- hasMore |boolean |是否还有更多会话数据 | +|currentConversationId |string |正在对话的会话id | +|heartbeat |timestamp |心跳(精确到秒)详情:[心跳概念说明](#heartbeatExplain) | +|friend |object |好友对象 | +| |- dataList|array |好友数据列表 | +| |- hasMore |boolean |是否还有更多好友数据 | +|group |object |聊天群对象 | +| |- dataList|array |聊天群数据列表 | +| |- hasMore |boolean |是否还有更多群聊数据 | +|notification |object |系统通知对象 | +| |- dataList|array |系统通知数据列表 | +| |- hasMore |boolean |是否还有更多系统通知数据 | +|usersInfo |object |存储所有出现过的用户信息,包括群好友信息 | +|isWidescreen |boolean |是否为pc宽屏 | +|systemInfo |object |系统信息详情参考:[https://uniapp.dcloud.net.cn/api/system/info.html#系统信息的概念](https://uniapp.dcloud.net.cn/api/system/info.html#%E7%B3%BB%E7%BB%9F%E4%BF%A1%E6%81%AF%E7%9A%84%E6%A6%82%E5%BF%B5) | +|indexDB |object/boolean |indexDB对象(仅web端有效) | +|audioContext |object/boolean |audio对象 | +|dataBaseIsOpen |boolean |判断本地sqlite数据库是否已经打开(仅app端有用) | +|socketOpenIndex |number |记录socket打开次数(用于处理:从云端同步,socket意外断开期间丢失的数据使用) | + +心跳概念说明 heartbeat @heartbeatExplain +uni-im的会话列表和消息列表,需要显示实时的发生时间。而一个应用开启太多的定时器,会消耗大量的系统性能。 +所以uni-im提供了一个每秒钟更新一次的响应式数据`heartbeat`,由uniImInit方法:启用一个定时器刷新,挂载在全局,所有应用场景引用这一个变量即可 + +#### methods + +|名称 |说明 | +|-- |-- | +|conversation |会话对象 | +| |- get |获取会话数据 | +| |- loadMore |加载更多会话数据 | +| |- unreadCount |统计所有消息的未读数 | +| |- remove |删除会话 | +|notification |系统消息 | +| |- get |获取系统消息 | +| |- loadMore |加载更多系统消息 | +| |- unreadCount |统计未读数 | +|friend |好友列表 | +| |- get |获取好友数据 | +| |- loadMore |加载更多系统消息 | +|group |群列表 | +| |- get |获取群聊数据 | +| |- loadMore |加载更多群聊数据 | +|mergeUsersInfo |添加用户信息到本地用户信息库 | +|clearUnreadCount |设置某个会话的未读消息数为已读 | + + +使用示例: +```js +//引入uniImMethods +import uniIm from '@/uni_modules/uni-im/lib/main.js'; +``` + +- 获取会话数据 + +1. 获取全部会话数据 +```js +let param = null +let conversationList = await uniIm.conversation.get(param) +``` +2. 获取指定会话的id会话数据 +```js +//xxx表示会话id +let param = "xxx" +let conversationList = await uniIm.conversation.get(param) +``` +3. 获取指定好友id的会话数据(如果本地不存在则从云端拉取,仍然不存在则本地自动创建) +```js +//xxx表示好友id +let param = {"friend_uid":"xxx"}, +let conversationList = await uniIm.conversation.get(param) +``` +4. 获取指定群聊id的会话数据(如果本地不存在则从云端拉取,仍然不存在则本地自动创建) +```js +//xxx表示群聊id +let param = {"group_id":"xxx"} +let conversationList = await uniIm.conversation.get(param) +``` + +- 加载会话数据 + +1. 加载更多会话数据(分页加载,新数据的会话更新时间,小于列表中最小的会话更新时间) +```js +let param = null +let conversationList = await uniIm.conversation.loadMore(param) +``` + +2. 加载指定会话id的会话数据 +```js +// xxx表示会话id +let param = 'xxx' +let conversationData = await uniIm.conversation.loadMore(param) +``` + +**返回值** + +|属性名 |类型 |说明 | +|-- |-- |-- | +|id |string |当前会话id | +|title |string |普通会话为对方的用户名或昵称、群会话为群昵称 | +|avatar_file |uniCloud file |普通会话为对方的用户头像、群会话为群头像 | +|unread_count |number |未读消息数 | +|user_id |string |对话的用户id(群聊会话时为空) | +|group_id |string |对话的群聊id(普通会话时为空) | +|update_time |timestamp |更新时间(每次会话会更新) | +|msgList |array |当前会话聊天数据列表 | +|chatText |string |当前会话的文本框文字内容 | + + +- 统计所有消息的未读数 +```js +let unreadCount = await uniIm.conversation.unreadCount() +``` + +- 删除指定id的会话数据 +```js +// xxx表示会话id +let param = 'xxxx' +await uniIm.conversation.remove(param) +``` + +- 获取系统消息数据 + +1. 不限类型 +```js +let param = null +await uniIm.notification.get(param) +``` + +2. 指定类型(单个) +```js +// uni-im-group-join-request 表示加群通知 +let param = {type:"uni-im-friend-invite"} +await uniIm.notification.get(param) +``` + +3. 指定类型(多个) +```js +// uni-im-group-join-request uni-im-friend-invite 表示加群通知、好友加请求通知 +let param = {type:["uni-im-friend-invite","uni-im-friend-invite"]} +await uniIm.notification.get(param) +``` + +4. 排除类型(单个) +```js +// uni-im-group-join-request 表示加群通知 +let param = {excludeType:"uni-im-friend-invite"} +await uniIm.notification.get(param) +``` + +5. 排除类型(多个) +```js +// uni-im-group-join-request uni-im-friend-invite 表示加群通知、好友加请求通知 +let param = {excludeType:["uni-im-friend-invite","uni-im-friend-invite"]} +await uniIm.notification.get(param) +``` + +- 加载系统消息数据 +参数与`获取系统消息数据`一致 + +- 获取好友数据 +```js +await uniIm.friend.get() +``` + +- 加载更多好友数据 +1. 分页加载 +```js +await uniIm.friend.loadMore() +``` +2. 加载指定好友数据 +```js +let param = {"friend_uid":"xxx"} +await uniIm.friend.loadMore(param) +``` +- 删除好友数据 +```js +let param = {"friend_uid":"xxx"} +await uniIm.friend.remove(param) +``` + +- 获取群聊数据 +```js +await uniIm.group.get() +``` + +- 加载更多群聊数据 +1. 分页加载 +```js +await uniIm.group.loadMore() +``` +2. 加载指定群聊数据 +```js +let param = {"group_id":"xxx"} +await uniIm.group.loadMore(param) +``` +- 删除群聊数据 +```js +let param = {"group_id":"xxx"} +await uniIm.group.remove(param) +``` + +- 添加用户信息到本地用户信息库 +```js +// xxx表示用户数据 +let usersInfo = {xxx} +await uniIm.mergeUsersInfo(usersInfo) +``` + +- 设置某个会话的未读消息数为已读 +```js +// xxx表示会话id +let conversation_id = "xxx" +await uniIm.clearUnreadCount(conversation_id) +``` + +#### 工具类库@utils +utils封装了uni-im常用方法的模块,路径:`/uni_modules/uni-im/common/utils.js` + +|名称 |类型 |说明 |入参 |返回值 | +|-- |-- |-- |-- |-- | +|init |function |初始化uni-im(监听聊天消息,定时每秒更新心跳值为当前时间戳) |无 |无 | +|getConversationId |function |获取会话id |对话的用户id或群id 详见[详见](#getConversationId) |无 | +|toFriendlyTime |function |用于将时间戳转友好时间提示(距离当前2小时内的时间戳,每隔一秒钟会刷新一次) |时间戳:timestamp |格式化后的时间字符串。如:x年x月x日,昨天,下午,1小时前等 | +|clearPushNotify |function |清空push消息栏通知 |无 |无 | +|login |function |非uni-id体系系统登录到uni-im方法 |时间戳:timestamp |参数为对象,含token和token过期时间,例如:`{"token":"xxx","tokenExpired":1679403132582}` | + +- 获取会话id @getConversationId +1. 获取单聊会话id +```js +let friend_uid = "xxx" +uniIm.getConversationId(friend_uid,'single') +``` +2. 获取群聊会话id +```js +let group_id = "xxx" +uniIm.getConversationId(group_id,'group') +``` + +## 项目升级 +uni-im遵循uni-app的插件模块化规范,即:[uni_modules](https://uniapp.dcloud.io/uni_modules)。 + +在项目根目录下的`uni_modules`目录下,以插件ID即`uni-im`为插件文件夹命名,在该目录右键也会看到“从插件市场更新”选项,点击即可更新该插件。也可以用插件市场web界面下载覆盖。 \ No newline at end of file diff --git a/uni-im示例/index.html b/uni-im示例/index.html new file mode 100644 index 0000000..c3ff205 --- /dev/null +++ b/uni-im示例/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + +
+ + + diff --git a/uni-im示例/license.md b/uni-im示例/license.md new file mode 100644 index 0000000..52be38a --- /dev/null +++ b/uni-im示例/license.md @@ -0,0 +1,36 @@ +uni-im源码使用许可协议 + +2022年10月 + +本许可协议,是数字天堂(北京)网络技术有限公司(以下简称DCloud)对其所拥有著作权的“DCloud uni-im”(以下简称软件),提供的使用许可协议。 + +您对“软件”的复制、使用、修改及分发受本许可协议的条款的约束,如您不接受本协议,则不能使用、复制、修改本软件。 + +授权许可范围 + +a) 授予您永久性的、全球性的、免费的、非独占的、不可撤销的本软件的源码使用许可,您可以使用这些源码制作自己的应用。 + +b) 您只能在DCloud产品体系内使用本软件及其源码。您不能将源码修改后运行在DCloud产品体系之外的环境,比如客户端脱离uni-app,或服务端脱离uniCloud。 + +c) DCloud未向您授权商标使用许可。您在根据本软件源码制作自己的应用时,需以自己的名义发布软件,而不是以DCloud名义发布。 + +d) 本协议不构成代理关系。 + +DCloud的责任限制 +“软件”在提供时不带任何明示或默示的担保。在任何情况下,DCloud不对任何人因使用“软件”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。 + +您的责任限制 + +a) 您需要在授权许可范围内使用软件。 + +b) 您在分发自己的应用时,不得侵犯DCloud商标和名誉权利。 + +c) 您不得进行破解、反编译、套壳等侵害DCloud知识产权的行为。您不得利用DCloud系统漏洞谋利或侵害DCloud利益,如您发现DCloud系统漏洞应第一时间通知DCloud。您不得进行攻击DCloud的服务器、网络等妨碍DCloud运营的行为。您不得利用DCloud的产品进行与DCloud争夺开发者的行为。 + +d) 如您违反本许可协议,需承担因此给DCloud造成的损失。 + +本协议签订地点为中华人民共和国北京市海淀区。 + +根据发展,DCloud可能会对本协议进行修改。修改时,DCloud会在产品或者网页中显著的位置发布相关信息以便及时通知到用户。如果您选择继续使用本框架,即表示您同意接受这些修改。 + +条款结束 \ No newline at end of file diff --git a/uni-im示例/main.js b/uni-im示例/main.js new file mode 100644 index 0000000..661a2fb --- /dev/null +++ b/uni-im示例/main.js @@ -0,0 +1,22 @@ +import App from './App' + +// #ifndef VUE3 +import Vue from 'vue' +Vue.config.productionTip = false +App.mpType = 'app' +const app = new Vue({ + ...App +}) +app.$mount() +// #endif + + +// #ifdef VUE3 +import {createSSRApp} from 'vue' +export function createApp() { + const app = createSSRApp(App) + return { + app, + } +} +// #endif diff --git a/uni-im示例/manifest.json b/uni-im示例/manifest.json new file mode 100644 index 0000000..6ee3ebc --- /dev/null +++ b/uni-im示例/manifest.json @@ -0,0 +1,171 @@ +{ + "name" : "hs-im", + "appid" : "__UNI__8513892", + "description" : "hs音视频IM", + "versionName" : "1.0.0", + "versionCode" : "100", + "transformPx" : false, + "app-plus" : { + "nvueStyleCompiler" : "uni-app", + "usingComponents" : true, + "compilerVersion" : 3, + "splashscreen" : { + "alwaysShowBeforeRender" : true, + "waiting" : true, + "autoclose" : true, + "delay" : 0 + }, + "modules" : { + "OAuth" : {}, + "Record" : {}, + "Camera" : {}, + "Push" : {}, + "SQLite" : {}, + "VideoPlayer" : {} + }, + "distribute" : { + "android" : { + "permissions" : [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + "ios" : { + "dSYMs" : false, + "capabilities" : { + "entitlements" : { + "com.apple.developer.associated-domains" : [ "applinks:static-9f641af8-e860-44e5-b18f-f68dd8fe3fe4.bspapp.com" ] + } + } + }, + "sdkConfigs" : { + "push" : { + "unipush" : { + "version" : "2", + "offline" : false, + "hms" : {}, + "mi" : {} + } + }, + "oauth" : { + "weixin" : { + "appid" : "xxx", + "UniversalLinks" : "https://static-9f641af8-e860-44e5-b18f-f68dd8fe3fe4.bspapp.com/uni-universallinks/__UNI__ECAF623/" + } + }, + "ad" : {}, + "maps" : {}, + "speech" : {}, + "statics" : {} + }, + "icons" : {} + }, + "nvueCompiler" : "uni-app", + "uniStatistics" : { + "enable" : false + } + }, + "quickapp" : {}, + "mp-weixin" : { + "appid" : "", + "setting" : { + "urlCheck" : false + }, + "usingComponents" : true, + "unipush" : { + "enable" : true + }, + "uniStatistics" : { + "enable" : false + } + }, + "mp-alipay" : { + "usingComponents" : true, + "uniStatistics" : { + "enable" : false + } + }, + "mp-baidu" : { + "usingComponents" : true, + "uniStatistics" : { + "enable" : false + } + }, + "mp-toutiao" : { + "usingComponents" : true, + "uniStatistics" : { + "enable" : false + } + }, + "uniStatistics" : { + "enable" : false, + "version" : "2" + }, + "vueVersion" : "2", + "h5" : { + "unipush" : { + "enable" : true + }, + "devServer" : { + "port" : 8080, + "disableHostCheck" : true, + "https" : false + }, + "optimization" : { + "treeShaking" : { + "enable" : false + } + }, + "uniStatistics" : { + "enable" : false + } + }, + "_spaceID" : "9f641af8-e860-44e5-b18f-f68dd8fe3fe4", + "fallbackLocale" : "zh-Hans", + "mp-jd" : { + "uniStatistics" : { + "enable" : false + } + }, + "mp-kuaishou" : { + "uniStatistics" : { + "enable" : false + } + }, + "mp-lark" : { + "uniStatistics" : { + "enable" : false + } + }, + "mp-qq" : { + "uniStatistics" : { + "enable" : false + } + }, + "quickapp-webview-huawei" : { + "uniStatistics" : { + "enable" : false + } + }, + "quickapp-webview-union" : { + "uniStatistics" : { + "enable" : false + } + } +} diff --git a/uni-im示例/pages.json b/uni-im示例/pages.json new file mode 100644 index 0000000..550cadd --- /dev/null +++ b/uni-im示例/pages.json @@ -0,0 +1,306 @@ +{ + "pages": [ + { + "path": "pages/index/index", + "style": { + "navigationBarTitleText": "", + "enablePullDownRefresh": false + } + } + // #ifndef MP-WEIXIN + ,{ + "path": "uni_modules/uni-im/pages/chat/chat", + "style": { + "navigationBarTitleText": "", + "enablePullDownRefresh": false, + "app-plus": { + "titleNView": { + "buttons": [{ + "color": "#333", + "colorPressed": "#111111", + "float": "right", + "text": "...", + "fontSize": 26, + "onclick": "more" + }] + } + } + } + } + // #endif + ,{ + "path": "uni_modules/uni-id-pages/pages/userinfo/realname-verify/realname-verify", + "style": { + "enablePullDownRefresh": false, + "navigationBarTitleText": "实名认证" + } +} +], + "subPackages": [ + { + "root": "uni_modules/uni-im/pages", + "pages": [{ + "path": "index/index", + "style": { + "navigationBarTitleText": "会话列表", + "enablePullDownRefresh": false, + "app-plus": { + "titleNView": { + "buttons": [{ + "color": "#999999", + "colorPressed": "#111111", + "float": "right", + "text": "我的", + "fontSize": 14, + "onclick": "toLogin" + }] + } + } + } + }, + { + "path": "common/uni-im-code-pages/uni-im-code-pages", + "style": { + "navigationBarTitleText": "代码浏览", + "enablePullDownRefresh": false + } + }, + { + "path": "userList/userList", + "style": { + "navigationBarTitleText": "用户列表", + "enablePullDownRefresh": true + } + }, + // #ifdef MP-WEIXIN + { + "path": "chat/chat", + "style": { + "navigationBarTitleText": "", + "enablePullDownRefresh": false, + "app-plus": { + "titleNView": { + "buttons": [{ + "color": "#333", + "colorPressed": "#111111", + "float": "right", + "text": "...", + "fontSize": 26, + "onclick": "more" + }] + } + } + } + }, + // #endif + { + "path": "common/video/video", + "style": { + "navigationBarTitleText": "", + "enablePullDownRefresh": false, + "navigationStyle": "custom" + } + + }, + { + "path": "group/info", + "style": { + "navigationBarTitleText": "群信息", + "enablePullDownRefresh": false, + "app-plus": { + "titleNView": { + "buttons": [{ + "color": "#333", + "colorPressed": "#111111", + "float": "right", + "text": "管理", + "fontSize": 16, + "onclick": "more" + }] + } + } + } + + }, { + "path": "contacts/notification/notification", + "style": { + "navigationBarTitleText": "", + "enablePullDownRefresh": false + } + + }, { + "path": "contacts/contacts", + "style": { + "navigationBarTitleText": "通讯录", + "enablePullDownRefresh": false + } + + }, { + "path": "contacts/addPeopleGroups/addPeopleGroups", + "style": { + "navigationStyle": "custom", + "enablePullDownRefresh": false + } + + }, { + "path": "contacts/createGroup/createGroup", + "style": { + "navigationBarTitleText": "创建群聊", + "enablePullDownRefresh": false, + "maxWidth": 950 + } + + }, { + "path": "group/groupQRCode", + "style": { + "navigationBarTitleText": "群聊二维码", + "enablePullDownRefresh": false + } + + }, { + "path": "contacts/groupList/groupList", + "style": { + "navigationBarTitleText": "我的群聊", + "enablePullDownRefresh": false + } + + }, { + "path": "chat/info", + "style": { + "navigationBarTitleText": "聊天设置", + "enablePullDownRefresh": false + } + } + ] + }, + { + "root": "uni_modules/uni-id-pages/pages", + "pages": [{ + "path": "userinfo/userinfo", + "style": { + "navigationBarTitleText": "个人资料" + } + }, + { + "path": "login/login-withoutpwd" + }, + { + "path": "login/login-withpwd" + }, + { + "path": "userinfo/deactivate/deactivate", + "style": { + "navigationBarTitleText": "注销账号" + } + }, + { + "path": "userinfo/bind-mobile/bind-mobile", + "style": { + "navigationBarTitleText": "绑定手机号码" + } + }, + { + "path": "login/login-smscode", + "style": { + "navigationBarTitleText": "手机验证码登录" + } + }, + { + "path": "register/register", + "style": { + "navigationBarTitleText": "注册" + } + }, + { + "path": "retrieve/retrieve", + "style": { + "navigationBarTitleText": "重置密码" + } + }, { + "path": "common/webview/webview", + "style": { + "enablePullDownRefresh": false, + "navigationBarTitleText": "" + } + }, { + "path": "userinfo/change_pwd/change_pwd", + "style": { + "enablePullDownRefresh": false, + "navigationBarTitleText": "修改密码" + } + }, { + "path": "register/register-by-email", + "style": { + "navigationBarTitleText": "邮箱验证码注册" + } + }, { + "path": "retrieve/retrieve-by-email", + "style": { + "navigationBarTitleText": "通过邮箱重置密码" + } + }, + { + "path": "userinfo/set-pwd/set-pwd", + "style": { + "enablePullDownRefresh": false, + "navigationBarTitleText": "设置密码" + } + } + // #ifdef H5 + , + { + "path": "userinfo/cropImage/cropImage" + }, + { + "path": "register/register-admin", + "style": { + "enablePullDownRefresh": false, + "navigationBarTitleText": "注册管理员账号" + } + } + // #endif + ] + } + ], + // #ifndef MP-WEIXIN + "tabBar": { + "color": "#999999", + "selectedColor": "#38BC48", + "borderStyle": "black", + "backgroundColor": "#FFFFFF", + "list": [{ + "pagePath": "uni_modules/uni-im/pages/index/index", + "text": "会话", + "iconPath": "uni_modules/uni-im/static/tabbarIcon/chat.png", + "selectedIconPath": "uni_modules/uni-im/static/tabbarIcon/chatex.png" + }, + { + "pagePath": "uni_modules/uni-im/pages/userList/userList", + "text": "用户列表", + "iconPath": "uni_modules/uni-im/static/tabbarIcon/contacts.png", + "selectedIconPath": "uni_modules/uni-im/static/tabbarIcon/contactsex.png" + }, + { + "pagePath": "uni_modules/uni-im/pages/contacts/contacts", + "text": "通讯录", + "iconPath": "uni_modules/uni-im/static/tabbarIcon/contacts.png", + "selectedIconPath": "uni_modules/uni-im/static/tabbarIcon/contactsex.png" + } + ] + }, + // #endif + "uniIdRouter": { + "loginPage": "uni_modules/uni-id-pages/pages/login/login-withpwd", + "needLogin": [ + "uni_modules/uni-im/pages/userList/userList", + "uni_modules/uni-im/pages/contacts/contacts", + "pages/index/index" + ] + }, + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "uni-app", + "navigationBarBackgroundColor": "#F8F8F8", + "backgroundColor": "#F8F8F8" + } +} diff --git a/uni-im示例/pages/index/index.vue b/uni-im示例/pages/index/index.vue new file mode 100644 index 0000000..e18dcb1 --- /dev/null +++ b/uni-im示例/pages/index/index.vue @@ -0,0 +1,77 @@ + + + + + \ No newline at end of file diff --git a/uni-im示例/static/app-plus/mp-html/js/uni.webview.min.js b/uni-im示例/static/app-plus/mp-html/js/uni.webview.min.js new file mode 100644 index 0000000..328b91e --- /dev/null +++ b/uni-im示例/static/app-plus/mp-html/js/uni.webview.min.js @@ -0,0 +1 @@ +!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e=e||self).uni=n()}(this,(function(){"use strict";try{var e={};Object.defineProperty(e,"passive",{get:function(){!0}}),window.addEventListener("test-passive",null,e)}catch(e){}var n=Object.prototype.hasOwnProperty;function i(e,i){return n.call(e,i)}var t=[];function r(){return window.__dcloud_weex_postMessage||window.__dcloud_weex_}var o=function(e,n){var i={options:{timestamp:+new Date},name:e,arg:n};if(r()){if("postMessage"===e){var o={data:[n]};return window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessage(o):window.__dcloud_weex_.postMessage(JSON.stringify(o))}var a={type:"WEB_INVOKE_APPSERVICE",args:{data:i,webviewIds:t}};window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessageToService(a):window.__dcloud_weex_.postMessageToService(JSON.stringify(a))}if(!window.plus)return window.parent.postMessage({type:"WEB_INVOKE_APPSERVICE",data:i,pageId:""},"*");if(0===t.length){var d=plus.webview.currentWebview();if(!d)throw new Error("plus.webview.currentWebview() is undefined");var s=d.parent(),w="";w=s?s.id:d.id,t.push(w)}if(plus.webview.getWebviewById("__uniapp__service"))plus.webview.postMessageToUniNView({type:"WEB_INVOKE_APPSERVICE",args:{data:i,webviewIds:t}},"__uniapp__service");else{var u=JSON.stringify(i);plus.webview.getLaunchWebview().evalJS('UniPlusBridge.subscribeHandler("'.concat("WEB_INVOKE_APPSERVICE",'",').concat(u,",").concat(JSON.stringify(t),");"))}},a={navigateTo:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;o("navigateTo",{url:encodeURI(n)})},navigateBack:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.delta;o("navigateBack",{delta:parseInt(n)||1})},switchTab:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;o("switchTab",{url:encodeURI(n)})},reLaunch:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;o("reLaunch",{url:encodeURI(n)})},redirectTo:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;o("redirectTo",{url:encodeURI(n)})},getEnv:function(e){r()?e({nvue:!0}):window.plus?e({plus:!0}):e({h5:!0})},postMessage:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};o("postMessage",e.data||{})}},d=/uni-app/i.test(navigator.userAgent),s=/Html5Plus/i.test(navigator.userAgent),w=/complete|loaded|interactive/;var u=window.my&&navigator.userAgent.indexOf(["t","n","e","i","l","C","y","a","p","i","l","A"].reverse().join(""))>-1;var g=window.swan&&window.swan.webView&&/swan/i.test(navigator.userAgent);var v=window.qq&&window.qq.miniProgram&&/QQ/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var c=window.tt&&window.tt.miniProgram&&/toutiaomicroapp/i.test(navigator.userAgent);var m=window.wx&&window.wx.miniProgram&&/micromessenger/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var p=window.qa&&/quickapp/i.test(navigator.userAgent);var f=window.ks&&window.ks.miniProgram&&/micromessenger/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var l=window.tt&&window.tt.miniProgram&&/Lark|Feishu/i.test(navigator.userAgent);var _=window.jd&&window.jd.miniProgram&&/micromessenger/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var E=window.xhs&&window.xhs.miniProgram&&/xhsminiapp/i.test(navigator.userAgent);for(var h,P=function(){window.UniAppJSBridge=!0,document.dispatchEvent(new CustomEvent("UniAppJSBridgeReady",{bubbles:!0,cancelable:!0}))},b=[function(e){if(d||s)return window.__dcloud_weex_postMessage||window.__dcloud_weex_?document.addEventListener("DOMContentLoaded",e):window.plus&&w.test(document.readyState)?setTimeout(e,0):document.addEventListener("plusready",e),a},function(e){if(m)return window.WeixinJSBridge&&window.WeixinJSBridge.invoke?setTimeout(e,0):document.addEventListener("WeixinJSBridgeReady",e),window.wx.miniProgram},function(e){if(v)return window.QQJSBridge&&window.QQJSBridge.invoke?setTimeout(e,0):document.addEventListener("QQJSBridgeReady",e),window.qq.miniProgram},function(e){if(u){document.addEventListener("DOMContentLoaded",e);var n=window.my;return{navigateTo:n.navigateTo,navigateBack:n.navigateBack,switchTab:n.switchTab,reLaunch:n.reLaunch,redirectTo:n.redirectTo,postMessage:n.postMessage,getEnv:n.getEnv}}},function(e){if(g)return document.addEventListener("DOMContentLoaded",e),window.swan.webView},function(e){if(c)return document.addEventListener("DOMContentLoaded",e),window.tt.miniProgram},function(e){if(p){window.QaJSBridge&&window.QaJSBridge.invoke?setTimeout(e,0):document.addEventListener("QaJSBridgeReady",e);var n=window.qa;return{navigateTo:n.navigateTo,navigateBack:n.navigateBack,switchTab:n.switchTab,reLaunch:n.reLaunch,redirectTo:n.redirectTo,postMessage:n.postMessage,getEnv:n.getEnv}}},function(e){if(f)return window.WeixinJSBridge&&window.WeixinJSBridge.invoke?setTimeout(e,0):document.addEventListener("WeixinJSBridgeReady",e),window.ks.miniProgram},function(e){if(l)return document.addEventListener("DOMContentLoaded",e),window.tt.miniProgram},function(e){if(_)return window.JDJSBridgeReady&&window.JDJSBridgeReady.invoke?setTimeout(e,0):document.addEventListener("JDJSBridgeReady",e),window.jd.miniProgram},function(e){if(E)return window.xhs.miniProgram},function(e){return document.addEventListener("DOMContentLoaded",e),a}],y=0;y + + + + + + +
web-view content
+ + + \ No newline at end of file diff --git a/uni-im示例/static/logo.png b/uni-im示例/static/logo.png new file mode 100644 index 0000000..b5771e2 Binary files /dev/null and b/uni-im示例/static/logo.png differ diff --git a/uni-im示例/uni.scss b/uni-im示例/uni.scss new file mode 100644 index 0000000..a05adb4 --- /dev/null +++ b/uni-im示例/uni.scss @@ -0,0 +1,76 @@ +/** + * 这里是uni-app内置的常用样式变量 + * + * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 + * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App + * + */ + +/** + * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 + * + * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 + */ + +/* 颜色变量 */ + +/* 行为相关颜色 */ +$uni-color-primary: #007aff; +$uni-color-success: #4cd964; +$uni-color-warning: #f0ad4e; +$uni-color-error: #dd524d; + +/* 文字基本颜色 */ +$uni-text-color:#333;//基本色 +$uni-text-color-inverse:#fff;//反色 +$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息 +$uni-text-color-placeholder: #808080; +$uni-text-color-disable:#c0c0c0; + +/* 背景颜色 */ +$uni-bg-color:#ffffff; +$uni-bg-color-grey:#f8f8f8; +$uni-bg-color-hover:#f1f1f1;//点击状态颜色 +$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色 + +/* 边框颜色 */ +$uni-border-color:#c8c7cc; + +/* 尺寸变量 */ + +/* 文字尺寸 */ +$uni-font-size-sm:12px; +$uni-font-size-base:14px; +$uni-font-size-lg:16; + +/* 图片尺寸 */ +$uni-img-size-sm:20px; +$uni-img-size-base:26px; +$uni-img-size-lg:40px; + +/* Border Radius */ +$uni-border-radius-sm: 2px; +$uni-border-radius-base: 3px; +$uni-border-radius-lg: 6px; +$uni-border-radius-circle: 50%; + +/* 水平间距 */ +$uni-spacing-row-sm: 5px; +$uni-spacing-row-base: 10px; +$uni-spacing-row-lg: 15px; + +/* 垂直间距 */ +$uni-spacing-col-sm: 4px; +$uni-spacing-col-base: 8px; +$uni-spacing-col-lg: 12px; + +/* 透明度 */ +$uni-opacity-disabled: 0.3; // 组件禁用态的透明度 + +/* 文章场景相关 */ +$uni-color-title: #2C405A; // 文章标题颜色 +$uni-font-size-title:20px; +$uni-color-subtitle: #555555; // 二级标题颜色 +$uni-font-size-subtitle:26px; +$uni-color-paragraph: #3F536E; // 文章段落颜色 +$uni-font-size-paragraph:15px; diff --git a/uni-im示例/uniCloud-aliyun/database/JQL查询.jql b/uni-im示例/uniCloud-aliyun/database/JQL查询.jql new file mode 100644 index 0000000..81826ef --- /dev/null +++ b/uni-im示例/uniCloud-aliyun/database/JQL查询.jql @@ -0,0 +1,14 @@ +// 本文件用于,使用JQL语法操作项目关联的uniCloud空间的数据库,方便开发调试和远程数据库管理 +// 编写clientDB的js API(也支持常规js语法,比如var),可以对云数据库进行增删改查操作。不支持uniCloud-db组件写法 +// 可以全部运行,也可以选中部分代码运行。点击工具栏上的运行按钮或者按下【F5】键运行代码 +// 如果文档中存在多条JQL语句,只有最后一条语句生效 +// 如果混写了普通js,最后一条语句需是数据库操作语句 +// 此处代码运行不受DB Schema的权限控制,移植代码到实际业务中注意在schema中配好permission +// 不支持clientDB的action +// 数据库查询有最大返回条数限制,详见:https://uniapp.dcloud.net.cn/uniCloud/cf-database.html#limit +// 详细JQL语法,请参考:https://uniapp.dcloud.net.cn/uniCloud/jql.html + +// 下面示例查询uni-id-users表的所有数据 +db.collection('uni-im-msg').update({ + is_read:true +}); diff --git a/uni-im示例/uniCloud-aliyun/database/db_init.json b/uni-im示例/uniCloud-aliyun/database/db_init.json new file mode 100644 index 0000000..795b221 --- /dev/null +++ b/uni-im示例/uniCloud-aliyun/database/db_init.json @@ -0,0 +1,9 @@ +// 在本文件中可配置云数据库初始化,数据格式见:https://uniapp.dcloud.io/uniCloud/hellodb?id=db-init +// 编写完毕后对本文件点右键,可按配置规则创建表和添加数据 +{ + "opendb-tempdata": {}, + "opendb-department": {}, + "uni-id-users": { + "data": [] + } +} diff --git a/uni-im示例/uniCloud-aliyun/database/default.jql b/uni-im示例/uniCloud-aliyun/database/default.jql new file mode 100644 index 0000000..35d21de --- /dev/null +++ b/uni-im示例/uniCloud-aliyun/database/default.jql @@ -0,0 +1,12 @@ +// 本文件用于,使用JQL语法操作项目关联的uniCloud空间的数据库,方便开发调试和远程数据库管理 +// 编写clientDB的js API(也支持常规js语法,比如var),可以对云数据库进行增删改查操作。不支持uniCloud-db组件写法 +// 可以全部运行,也可以选中部分代码运行。点击工具栏上的运行按钮或者按下【F5】键运行代码 +// 如果文档中存在多条JQL语句,只有最后一条语句生效 +// 如果混写了普通js,最后一条语句需是数据库操作语句 +// 此处代码运行不受DB Schema的权限控制,移植代码到实际业务中注意在schema中配好permission +// 不支持clientDB的action +// 数据库查询有最大返回条数限制,详见:https://uniapp.dcloud.net.cn/uniCloud/cf-database.html#limit +// 详细JQL语法,请参考:https://uniapp.dcloud.net.cn/uniCloud/jql.html + +// 下面示例查询uni-id-users表的所有数据 +db.collection('uni-id-users').get(); diff --git a/uni-im示例/uniCloud-aliyun/database/opendb-department.schema.json b/uni-im示例/uniCloud-aliyun/database/opendb-department.schema.json new file mode 100644 index 0000000..1d16d76 --- /dev/null +++ b/uni-im示例/uniCloud-aliyun/database/opendb-department.schema.json @@ -0,0 +1,50 @@ +{ + "bsonType": "object", + "required": [ + "name" + ], + "permission": { + "read": true, + "create": false, + "update": false, + "delete": false + }, + "properties": { + "_id": { + "description": "ID,系统自动生成" + }, + "parent_id": { + "bsonType": "string", + "description": "父级部门ID", + "parentKey": "_id" + }, + "name": { + "bsonType": "string", + "description": "部门名称", + "title": "部门名称", + "trim": "both" + }, + "level": { + "bsonType": "int", + "description": "部门层级,为提升检索效率而作的冗余设计" + }, + "sort": { + "bsonType": "int", + "description": "部门在当前层级下的顺序,由小到大", + "title": "显示顺序" + }, + "manager_uid": { + "bsonType": "string", + "description": "部门主管的userid, 参考`uni-id-users` 表", + "foreignKey": "uni-id-users._id" + }, + "create_date": { + "bsonType": "timestamp", + "description": "部门创建时间", + "forceDefaultValue": { + "$env": "now" + } + } + }, + "version": "0.1.0" +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/LICENSE.md b/uni-im示例/uni_modules/Sansnn-uQRCode/LICENSE.md new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/LICENSE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/README.md b/uni-im示例/uni_modules/Sansnn-uQRCode/README.md new file mode 100644 index 0000000..77d7925 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/README.md @@ -0,0 +1,392 @@ +# 介绍 + +`uQRCode`是一款基于`Javascript`环境开发的二维码生成插件,适用所有`Javascript`运行环境的前端应用和`Node.js`应用。 + +`uQRCode`可扩展性高,它支持自定义渲染二维码,可通过`uQRCode API`得到二维码绘制关键信息后,使用`canvas`、`svg`或`js`操作`dom`的方式绘制二维码图案。还可自定义二维码样式,如随机颜色、圆点、方块、块与块之间的间距等。 + +欢迎加入群聊【uQRCode交流群】:[695070434](https://jq.qq.com/?_wv=1027&k=JRjzDqiw)。 + +# 设计器 + +uQRCode发布了配套的可视化设计器,可根据自己喜好在设计器中设计二维码样式,一键生成配置代码复制到项目中,详情请在微信小程序搜索“柚子二维码”,或扫描下方小程序码体验。 + +![uQRCode设计器](https://uqrcode.cn/mp_weixin_code.jpg) + +## 设计器模板示例 + +![uQRCode设计器](https://uqrcode.cn/yz_1.png) +![uQRCode设计器](https://uqrcode.cn/yz_2.png) +![uQRCode设计器](https://uqrcode.cn/yz_3.png) +![uQRCode设计器](https://uqrcode.cn/yz_4.png) +![uQRCode设计器](https://uqrcode.cn/yz_5.png) +![uQRCode设计器](https://uqrcode.cn/yz_6.png) +![uQRCode设计器](https://uqrcode.cn/yz_7.png) +![uQRCode设计器](https://uqrcode.cn/yz_8.png) +![uQRCode设计器](https://uqrcode.cn/yz_9.png) + +# 快速上手 + +> 在`uni-app`中,我们更推荐使用组件方式来生成二维码,组件方式大大提高了页面的可读性以及避开了一些平台容易出问题的地方,当组件无法满足需求的时候,再考虑切换成原生方式。 + +官方文档:[https://uqrcode.cn/doc](https://uqrcode.cn/doc)。 + +github地址:[https://github.com/Sansnn/uQRCode](https://github.com/Sansnn/uQRCode)。 + +npm地址:[https://www.npmjs.com/package/uqrcodejs](https://www.npmjs.com/package/uqrcodejs)。 + +uni-app插件市场地址:[https://ext.dcloud.net.cn/plugin?id=1287](https://ext.dcloud.net.cn/plugin?id=1287)。 + +## 原生方式 + +原生方式仅需要获取`uqrcode.js`文件便可使用。详细配置请移步到:文档 > [原生](https://uqrcode.cn/doc/document/native.html)。 + +### 安装 + +1. 通过`npm`安装,成功后即可使用`import`或`require`进行引用。 +``` bash +# npm安装 +npm install uqrcodejs +# 或者 +npm install @uqrcode/js +``` + +2. 通过项目开源地址获取`uqrcode.js`,下载`uqrcode.js`后,将其复制到您项目指定目录,在页面中引入`uqrcode.js`文件即可开始使用。 + +### 引入 + +- 通过`import`引入。 +``` javascript +// npm安装 +import UQRCode from 'uqrcodejs'; // npm install uqrcodejs +// 或者 +import UQRCode from '@uqrcode/js'; // npm install @uqrcode/js +``` + +- `Node.js`通过`require`引入。 +``` javascript +// npm安装 +const UQRCode = require('uqrcodejs'); // npm install uqrcodejs +// 或者 +const UQRCode = require('@uqrcode/js'); // npm install @uqrcode/js +``` + +- 原生浏览器环境,在js脚本加载时添加到`window`。 +``` html + + +``` + +### 简单用法 + +`uQRCode`基于`Canvas API`封装了一套方法,建议开发者使用`canvas`生成,一键调用,非常方便。以下是示例: + +- HTML示例 + - DOM部分 + ``` html + + ``` + + - JS部分 + ``` javascript + // 获取uQRCode实例 + var qr = new UQRCode(); + // 设置二维码内容 + qr.data = "https://uqrcode.cn/doc"; + // 设置二维码大小,必须与canvas设置的宽高一致 + qr.size = 200; + // 调用制作二维码方法 + qr.make(); + // 获取canvas元素 + var canvas = document.getElementById("qrcode"); + // 获取canvas上下文 + var canvasContext = canvas.getContext("2d"); + // 设置uQRCode实例的canvas上下文 + qr.canvasContext = canvasContext; + // 调用绘制方法将二维码图案绘制到canvas上 + qr.drawCanvas(); + ``` + +- uni-app示例 + - Template部分 + ``` html + + ``` + + - JS部分 + ``` javascript + onReady() { + // 获取uQRCode实例 + var qr = new UQRCode(); + // 设置二维码内容 + qr.data = "https://uqrcode.cn/doc"; + // 设置二维码大小,必须与canvas设置的宽高一致 + qr.size = 200; + // 调用制作二维码方法 + qr.make(); + // 获取canvas上下文 + var canvasContext = uni.createCanvasContext('qrcode', this); // 如果是组件,this必须传入 + // 设置uQRCode实例的canvas上下文 + qr.canvasContext = canvasContext; + // 调用绘制方法将二维码图案绘制到canvas上 + qr.drawCanvas(); + } + ``` + +- 微信小程序,推荐使用Canvas 2D,关于Canvas 2D的使用请参考微信开放文档。 + +### 高级用法 + +考虑到部分平台可能不支持`canvas`,所以`uQRCode`并没有强制要求和`canvas`一起使用,您还可以选择其他方式来生成二维码,例如使用`js`操作`dom`进行绘制或是使用`svg`绘制等。以下是示例: + +- uni-app v-for+view + +```html + + + +``` + +- js操作dom + +``` html + + + + + uQRCode二维码生成 + + +
+ + + + +``` + +- svg +``` html + + + + + uQRCode二维码生成 + + + + + + + +``` + +> 更多用法大家自行探索咯,期待分享哟~ + +### 导出临时文件路径 + +原生方式基于`Canvas`的,请自行参阅各平台`Canvas`的导出方式。以下是部分示例: + +- uni-app +```javascript +// 通过uni.createCanvasContext方式创建绘制上下文的,对应导出API为uni.canvasToTempFilePath +// 调用完ctx.draw()方法后不能第一时间导出,否则会异常,需要有一定的延时 +setTimeout(() => { + uni.canvasToTempFilePath( + { + canvasId: this.canvasId, + fileType: this.fileType, + width: this.canvasWidth, + height: this.canvasHeight, + success: res => { + console.log(res); + }, + fail: err => { + console.log(err); + } + }, + // this // 组件内使用必传当前实例 + ); +}, 300); +``` + +- Canvas2D +```javascript +// 得到base64 +console.log(canvas.toDataURL()); +// 得到buffer +console.log(canvas.toBuffer()); +``` + +### 保存二维码到本地相册 + +必须在导出临时文件路径成功后再执行保存。uni-app通用保存方式(H5除外): +```javascript +uni.saveImageToPhotosAlbum({ + filePath: tempFilePath, + success: res => { + console.log(res); + }, + fail: err => { + console.log(err); + } +}); +``` + +H5可以通过设置``标签`href`属性的方式进行保存: +```javascript +const aEle = document.createElement('a'); +aEle.download = 'uQRCode'; // 设置下载的文件名,默认是'下载' +aEle.href = tempFilePath; +document.body.appendChild(aEle); +aEle.click(); +aEle.remove(); // 下载之后把创建的元素删除 +``` +经过测试,PC端浏览器可以下载,部分安卓自带或第三方浏览器可以下载,安卓微信浏览器不适用,移动端iOS所有浏览器均不适用,差异较大,还是推荐各位导出文件给图片组件显示,然后提示用户通过长按图片进行保存这种方式。 + +## uni-app组件方式 + +### 安装 + +通过uni-app插件市场地址安装:[https://ext.dcloud.net.cn/plugin?id=1287](https://ext.dcloud.net.cn/plugin?id=1287)。详细配置请移步到:文档 > [uni-app组件](https://uqrcode.cn/doc/document/uni-app.html)。 + +### 引入 + +uni-app默认为easycom模式,可直接键入``标签。 + +### 简单用法 + +安装`uqrcode`组件后,在`template`中键入``。设置`ref`属性可使用组件内部方法,`canvas-id`属性为组件内部的canvas组件标识,`value`属性为二维码生成对应内容,`options`为配置选项,可配置二维码样式,绘制Logo等,详见:[options](https://uqrcode.cn/doc/document/uni-app.html#options) 。 + +``` html + +``` + +### 导出临时文件路径 + +为了保证方法调用成功,请在 [complete](https://uqrcode.cn/doc/document/uni-app.html#complete) 事件返回`success=true`后调用。 + +```javascript +// uqrcode为组件的ref名称 +this.$refs.uqrcode.toTempFilePath({ + success: res => { + console.log(res); + } +}); +``` + +### 保存二维码到本地相册 + +为了保证方法调用成功,请在 [complete](https://uqrcode.cn/doc/document/uni-app.html#complete) 事件返回`success=true`后调用。 + +```javascript +// uqrcode为组件的ref名称 +this.$refs.uqrcode.save({ + success: () => { + uni.showToast({ + icon: 'success', + title: '保存成功' + }); + } +}); +``` + +## 更多使用说明请前往官方文档查看:[https://uqrcode.cn/doc](https://uqrcode.cn/doc)。 \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/changelog.md b/uni-im示例/uni_modules/Sansnn-uQRCode/changelog.md new file mode 100644 index 0000000..74a6e4c --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/changelog.md @@ -0,0 +1,12 @@ +## 4.0.6(2022-12-12) +修复`getDrawModules`,第一次获取结果正常,后续获取`tile`模块不存在的问题; +修复安卓type:normal因Canvas API使用了小数或为0的参数导致生成异常的问题(注:安卓非2d Canvas部分API参数不支持携带小数,部分API参数必须大于0)。 +## 4.0.1(2022-11-28) +优化组件loading属性的表现; +新增组件type选项normal,以便于在某些条件编译初始为type=2d时还可以选择使用非2d组件类型; +修复组件条件编译在其他编辑器语法提示报错; +修复原生对es5的支持。 +## 4.0.0(2022-11-21) +v4版本源代码全面开放,开源地址:[https://github.com/Sansnn/uQRCode](https://github.com/Sansnn/uQRCode); + +升级说明:v4为大版本更新,虽然已尽可能兼容上一代版本,但不可避免的还是存在一些细节差异,若更新后出现问题,请参考对照[v3 文档](https://uqrcode.cn/doc/v3),[v4 文档](https://uqrcode.cn/doc)进行修改。 diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/common/cache.js b/uni-im示例/uni_modules/Sansnn-uQRCode/common/cache.js new file mode 100644 index 0000000..d897d26 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/common/cache.js @@ -0,0 +1 @@ +export const cacheImageList = []; \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/common/queue.js b/uni-im示例/uni_modules/Sansnn-uQRCode/common/queue.js new file mode 100644 index 0000000..be6b1d2 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/common/queue.js @@ -0,0 +1,41 @@ +function Queue() { + let waitingQueue = this.waitingQueue = []; + let isRunning = this.isRunning = false; // 记录是否有未完成的任务 + + function execute(task, resolve, reject) { + task() + .then((data) => { + resolve(data); + }) + .catch((e) => { + reject(e); + }) + .finally(() => { + // 等待任务队列中如果有任务,则触发它;否则设置isRunning = false,表示无任务状态 + if (waitingQueue.length) { + const next = waitingQueue.shift(); + execute(next.task, next.resolve, next.reject); + } else { + isRunning = false; + } + }); + } + this.exec = function(task) { + return new Promise((resolve, reject) => { + if (isRunning) { + waitingQueue.push({ + task, + resolve, + reject + }); + } else { + isRunning = true; + execute(task, resolve, reject); + } + }); + } +} + +/* 队列实例,某些平台一起使用多个组件时需要通过队列逐一绘制,否则部分绘制方法异常,nvue端的iOS gcanvas尤其明显,在不通过队列绘制时会出现图片丢失的情况 */ +export const queueDraw = new Queue(); +export const queueLoadImage = new Queue(); \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/common/types/cache.d.ts b/uni-im示例/uni_modules/Sansnn-uQRCode/common/types/cache.d.ts new file mode 100644 index 0000000..9b6c3ce --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/common/types/cache.d.ts @@ -0,0 +1,3 @@ +declare module '*/common/cache' { + export const cacheImageList: Array; +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/common/types/queue.d.ts b/uni-im示例/uni_modules/Sansnn-uQRCode/common/types/queue.d.ts new file mode 100644 index 0000000..f81ab97 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/common/types/queue.d.ts @@ -0,0 +1,4 @@ +declare module '*/common/queue' { + export const queueDraw: any; + export const queueLoadImage: any; +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/components/u-qrcode/u-qrcode.vue b/uni-im示例/uni_modules/Sansnn-uQRCode/components/u-qrcode/u-qrcode.vue new file mode 100644 index 0000000..4fefaac --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/components/u-qrcode/u-qrcode.vue @@ -0,0 +1,1131 @@ + + + + + diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/components/uqrcode/uqrcode.vue b/uni-im示例/uni_modules/Sansnn-uQRCode/components/uqrcode/uqrcode.vue new file mode 100644 index 0000000..4fefaac --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/components/uqrcode/uqrcode.vue @@ -0,0 +1,1131 @@ + + + + + diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/bridge/bridge-weex.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/bridge/bridge-weex.js new file mode 100644 index 0000000..27086ec --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/bridge/bridge-weex.js @@ -0,0 +1,241 @@ +const isWeex = typeof WXEnvironment !== 'undefined'; +const isWeexIOS = isWeex && /ios/i.test(WXEnvironment.platform); +const isWeexAndroid = isWeex && !isWeexIOS; + +import GLmethod from '../context-webgl/GLmethod'; + +const GCanvasModule = + (typeof weex !== 'undefined' && weex.requireModule) ? (weex.requireModule('gcanvas')) : + (typeof __weex_require__ !== 'undefined') ? (__weex_require__('@weex-module/gcanvas')) : {}; + +let isDebugging = false; + +let isComboDisabled = false; + +const logCommand = (function () { + const methodQuery = []; + Object.keys(GLmethod).forEach(key => { + methodQuery[GLmethod[key]] = key; + }) + const queryMethod = (id) => { + return methodQuery[parseInt(id)] || 'NotFoundMethod'; + } + const logCommand = (id, cmds) => { + const mId = cmds.split(',')[0]; + const mName = queryMethod(mId); + console.log(`=== callNative - componentId:${id}; method: ${mName}; cmds: ${cmds}`); + } + return logCommand; +})(); + +function joinArray(arr, sep) { + let res = ''; + for (let i = 0; i < arr.length; i++) { + if (i !== 0) { + res += sep; + } + res += arr[i]; + } + return res; +} + +const commandsCache = {} + +const GBridge = { + + callEnable: (ref, configArray) => { + + commandsCache[ref] = []; + + return GCanvasModule.enable({ + componentId: ref, + config: configArray + }); + }, + + callEnableDebug: () => { + isDebugging = true; + }, + + callEnableDisableCombo: () => { + isComboDisabled = true; + }, + + callSetContextType: function (componentId, context_type) { + GCanvasModule.setContextType(context_type, componentId); + }, + + callReset: function(id){ + GCanvasModule.resetComponent && canvasModule.resetComponent(componentId); + }, + + render: isWeexIOS ? function (componentId) { + return GCanvasModule.extendCallNative({ + contextId: componentId, + type: 0x60000001 + }); + } : function (componentId) { + return callGCanvasLinkNative(componentId, 0x60000001, 'render'); + }, + + render2d: isWeexIOS ? function (componentId, commands, callback) { + + if (isDebugging) { + console.log('>>> >>> render2d ==='); + console.log('>>> commands: ' + commands); + } + + GCanvasModule.render([commands, callback?true:false], componentId, callback); + + } : function (componentId, commands,callback) { + + if (isDebugging) { + console.log('>>> >>> render2d ==='); + console.log('>>> commands: ' + commands); + } + + callGCanvasLinkNative(componentId, 0x20000001, commands); + if(callback){ + callback(); + } + }, + + callExtendCallNative: isWeexIOS ? function (componentId, cmdArgs) { + + throw 'should not be here anymore ' + cmdArgs; + + } : function (componentId, cmdArgs) { + + throw 'should not be here anymore ' + cmdArgs; + + }, + + + flushNative: isWeexIOS ? function (componentId) { + + const cmdArgs = joinArray(commandsCache[componentId], ';'); + commandsCache[componentId] = []; + + if (isDebugging) { + console.log('>>> >>> flush native ==='); + console.log('>>> commands: ' + cmdArgs); + } + + const result = GCanvasModule.extendCallNative({ + "contextId": componentId, + "type": 0x60000000, + "args": cmdArgs + }); + + const res = result && result.result; + + if (isDebugging) { + console.log('>>> result: ' + res); + } + + return res; + + } : function (componentId) { + + const cmdArgs = joinArray(commandsCache[componentId], ';'); + commandsCache[componentId] = []; + + if (isDebugging) { + console.log('>>> >>> flush native ==='); + console.log('>>> commands: ' + cmdArgs); + } + + const result = callGCanvasLinkNative(componentId, 0x60000000, cmdArgs); + + if (isDebugging) { + console.log('>>> result: ' + result); + } + + return result; + }, + + callNative: function (componentId, cmdArgs, cache) { + + if (isDebugging) { + logCommand(componentId, cmdArgs); + } + + commandsCache[componentId].push(cmdArgs); + + if (!cache || isComboDisabled) { + return GBridge.flushNative(componentId); + } else { + return undefined; + } + }, + + texImage2D(componentId, ...args) { + if (isWeexIOS) { + if (args.length === 6) { + const [target, level, internalformat, format, type, image] = args; + GBridge.callNative( + componentId, + GLmethod.texImage2D + ',' + 6 + ',' + target + ',' + level + ',' + internalformat + ',' + format + ',' + type + ',' + image.src + ) + } else if (args.length === 9) { + const [target, level, internalformat, width, height, border, format, type, image] = args; + GBridge.callNative( + componentId, + GLmethod.texImage2D + ',' + 9 + ',' + target + ',' + level + ',' + internalformat + ',' + width + ',' + height + ',' + border + ',' + + + format + ',' + type + ',' + (image ? image.src : 0) + ) + } + } else if (isWeexAndroid) { + if (args.length === 6) { + const [target, level, internalformat, format, type, image] = args; + GCanvasModule.texImage2D(componentId, target, level, internalformat, format, type, image.src); + } else if (args.length === 9) { + const [target, level, internalformat, width, height, border, format, type, image] = args; + GCanvasModule.texImage2D(componentId, target, level, internalformat, width, height, border, format, type, (image ? image.src : 0)); + } + } + }, + + texSubImage2D(componentId, target, level, xoffset, yoffset, format, type, image) { + if (isWeexIOS) { + if (arguments.length === 8) { + GBridge.callNative( + componentId, + GLmethod.texSubImage2D + ',' + 6 + ',' + target + ',' + level + ',' + xoffset + ',' + yoffset, + ',' + format + ',' + type + ',' + image.src + ) + } + } else if (isWeexAndroid) { + GCanvasModule.texSubImage2D(componentId, target, level, xoffset, yoffset, format, type, image.src); + } + }, + + bindImageTexture(componentId, src, imageId) { + GCanvasModule.bindImageTexture([src, imageId], componentId); + }, + + perloadImage([url, id], callback) { + GCanvasModule.preLoadImage([url, id], function (image) { + image.url = url; + image.id = id; + callback(image); + }); + }, + + measureText(text, fontStyle, componentId) { + return GCanvasModule.measureText([text, fontStyle], componentId); + }, + + getImageData (componentId, x, y, w, h, callback) { + GCanvasModule.getImageData([x, y,w,h],componentId,callback); + }, + + putImageData (componentId, data, x, y, w, h, callback) { + GCanvasModule.putImageData([x, y,w,h,data],componentId,callback); + }, + + toTempFilePath(componentId, x, y, width, height, destWidth, destHeight, fileType, quality, callback){ + GCanvasModule.toTempFilePath([x, y, width,height, destWidth, destHeight, fileType, quality], componentId, callback); + } +} + +export default GBridge; \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/FillStyleLinearGradient.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/FillStyleLinearGradient.js new file mode 100644 index 0000000..3e7f03a --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/FillStyleLinearGradient.js @@ -0,0 +1,18 @@ +class FillStyleLinearGradient { + + constructor(x0, y0, x1, y1) { + this._start_pos = { _x: x0, _y: y0 }; + this._end_pos = { _x: x1, _y: y1 }; + this._stop_count = 0; + this._stops = [0, 0, 0, 0, 0]; + } + + addColorStop = function (pos, color) { + if (this._stop_count < 5 && 0.0 <= pos && pos <= 1.0) { + this._stops[this._stop_count] = { _pos: pos, _color: color }; + this._stop_count++; + } + } +} + +export default FillStyleLinearGradient; \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/FillStylePattern.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/FillStylePattern.js new file mode 100644 index 0000000..6e4f646 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/FillStylePattern.js @@ -0,0 +1,8 @@ +class FillStylePattern { + constructor(img, pattern) { + this._style = pattern; + this._img = img; + } +} + +export default FillStylePattern; \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/FillStyleRadialGradient.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/FillStyleRadialGradient.js new file mode 100644 index 0000000..7790596 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/FillStyleRadialGradient.js @@ -0,0 +1,17 @@ +class FillStyleRadialGradient { + constructor(x0, y0, r0, x1, y1, r1) { + this._start_pos = { _x: x0, _y: y0, _r: r0 }; + this._end_pos = { _x: x1, _y: y1, _r: r1 }; + this._stop_count = 0; + this._stops = [0, 0, 0, 0, 0]; + } + + addColorStop(pos, color) { + if (this._stop_count < 5 && 0.0 <= pos && pos <= 1.0) { + this._stops[this._stop_count] = { _pos: pos, _color: color }; + this._stop_count++; + } + } +} + +export default FillStyleRadialGradient; \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/RenderingContext.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/RenderingContext.js new file mode 100644 index 0000000..e6b8f48 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-2d/RenderingContext.js @@ -0,0 +1,666 @@ +import FillStylePattern from './FillStylePattern'; +import FillStyleLinearGradient from './FillStyleLinearGradient'; +import FillStyleRadialGradient from './FillStyleRadialGradient'; +import GImage from '../env/image.js'; +import { + ArrayBufferToBase64, + Base64ToUint8ClampedArray +} from '../env/tool.js'; + +export default class CanvasRenderingContext2D { + + _drawCommands = ''; + + _globalAlpha = 1.0; + + _fillStyle = 'rgb(0,0,0)'; + _strokeStyle = 'rgb(0,0,0)'; + + _lineWidth = 1; + _lineCap = 'butt'; + _lineJoin = 'miter'; + + _miterLimit = 10; + + _globalCompositeOperation = 'source-over'; + + _textAlign = 'start'; + _textBaseline = 'alphabetic'; + + _font = '10px sans-serif'; + + _savedGlobalAlpha = []; + + timer = null; + componentId = null; + + _notCommitDrawImageCache = []; + _needRedrawImageCache = []; + _redrawCommands = ''; + _autoSaveContext = true; + // _imageMap = new GHashMap(); + // _textureMap = new GHashMap(); + + constructor() { + this.className = 'CanvasRenderingContext2D'; + //this.save() + } + + setFillStyle(value) { + this.fillStyle = value; + } + + set fillStyle(value) { + this._fillStyle = value; + + if (typeof(value) == 'string') { + this._drawCommands = this._drawCommands.concat("F" + value + ";"); + } else if (value instanceof FillStylePattern) { + const image = value._img; + if (!image.complete) { + image.onload = () => { + var index = this._needRedrawImageCache.indexOf(image); + if (index > -1) { + this._needRedrawImageCache.splice(index, 1); + CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id); + this._redrawflush(true); + } + } + this._notCommitDrawImageCache.push(image); + } else { + CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id); + } + + //CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id); + this._drawCommands = this._drawCommands.concat("G" + image._id + "," + value._style + ";"); + } else if (value instanceof FillStyleLinearGradient) { + var command = "D" + value._start_pos._x.toFixed(2) + "," + value._start_pos._y.toFixed(2) + "," + + value._end_pos._x.toFixed(2) + "," + value._end_pos._y.toFixed(2) + "," + + value._stop_count; + for (var i = 0; i < value._stop_count; ++i) { + command += ("," + value._stops[i]._pos + "," + value._stops[i]._color); + } + this._drawCommands = this._drawCommands.concat(command + ";"); + } else if (value instanceof FillStyleRadialGradient) { + var command = "H" + value._start_pos._x.toFixed(2) + "," + value._start_pos._y.toFixed(2) + "," + value._start_pos._r + .toFixed(2) + "," + + value._end_pos._x.toFixed(2) + "," + value._end_pos._y.toFixed(2) + "," + value._end_pos._r.toFixed(2) + "," + + value._stop_count; + for (var i = 0; i < value._stop_count; ++i) { + command += ("," + value._stops[i]._pos + "," + value._stops[i]._color); + } + this._drawCommands = this._drawCommands.concat(command + ";"); + } + } + + get fillStyle() { + return this._fillStyle; + } + + get globalAlpha() { + return this._globalAlpha; + } + + setGlobalAlpha(value) { + this.globalAlpha = value; + } + + set globalAlpha(value) { + this._globalAlpha = value; + this._drawCommands = this._drawCommands.concat("a" + value.toFixed(2) + ";"); + } + + + get strokeStyle() { + return this._strokeStyle; + } + + setStrokeStyle(value) { + this.strokeStyle = value; + } + + set strokeStyle(value) { + + this._strokeStyle = value; + + if (typeof(value) == 'string') { + this._drawCommands = this._drawCommands.concat("S" + value + ";"); + } else if (value instanceof FillStylePattern) { + CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id); + this._drawCommands = this._drawCommands.concat("G" + image._id + "," + value._style + ";"); + } else if (value instanceof FillStyleLinearGradient) { + var command = "D" + value._start_pos._x.toFixed(2) + "," + value._start_pos._y.toFixed(2) + "," + + value._end_pos._x.toFixed(2) + "," + value._end_pos._y.toFixed(2) + "," + + value._stop_count; + + for (var i = 0; i < value._stop_count; ++i) { + command += ("," + value._stops[i]._pos + "," + value._stops[i]._color); + } + this._drawCommands = this._drawCommands.concat(command + ";"); + } else if (value instanceof FillStyleRadialGradient) { + var command = "H" + value._start_pos._x.toFixed(2) + "," + value._start_pos._y.toFixed(2) + "," + value._start_pos._r + .toFixed(2) + "," + + value._end_pos._x.toFixed(2) + "," + value._end_pos._y + ",".toFixed(2) + value._end_pos._r.toFixed(2) + "," + + value._stop_count; + + for (var i = 0; i < value._stop_count; ++i) { + command += ("," + value._stops[i]._pos + "," + value._stops[i]._color); + } + this._drawCommands = this._drawCommands.concat(command + ";"); + } + } + + get lineWidth() { + return this._lineWidth; + } + + setLineWidth(value) { + this.lineWidth = value; + } + + set lineWidth(value) { + this._lineWidth = value; + this._drawCommands = this._drawCommands.concat("W" + value + ";"); + } + + get lineCap() { + return this._lineCap; + } + + setLineCap(value) { + this.lineCap = value; + } + + set lineCap(value) { + this._lineCap = value; + this._drawCommands = this._drawCommands.concat("C" + value + ";"); + } + + get lineJoin() { + return this._lineJoin; + } + + setLineJoin(value) { + this.lineJoin = value + } + + set lineJoin(value) { + this._lineJoin = value; + this._drawCommands = this._drawCommands.concat("J" + value + ";"); + } + + get miterLimit() { + return this._miterLimit; + } + + setMiterLimit(value) { + this.miterLimit = value + } + + set miterLimit(value) { + this._miterLimit = value; + this._drawCommands = this._drawCommands.concat("M" + value + ";"); + } + + get globalCompositeOperation() { + return this._globalCompositeOperation; + } + + set globalCompositeOperation(value) { + + this._globalCompositeOperation = value; + let mode = 0; + switch (value) { + case "source-over": + mode = 0; + break; + case "source-atop": + mode = 5; + break; + case "source-in": + mode = 0; + break; + case "source-out": + mode = 2; + break; + case "destination-over": + mode = 4; + break; + case "destination-atop": + mode = 4; + break; + case "destination-in": + mode = 4; + break; + case "destination-out": + mode = 3; + break; + case "lighter": + mode = 1; + break; + case "copy": + mode = 2; + break; + case "xor": + mode = 6; + break; + default: + mode = 0; + } + + this._drawCommands = this._drawCommands.concat("B" + mode + ";"); + } + + get textAlign() { + return this._textAlign; + } + + setTextAlign(value) { + this.textAlign = value + } + + set textAlign(value) { + + this._textAlign = value; + let Align = 0; + switch (value) { + case "start": + Align = 0; + break; + case "end": + Align = 1; + break; + case "left": + Align = 2; + break; + case "center": + Align = 3; + break; + case "right": + Align = 4; + break; + default: + Align = 0; + } + + this._drawCommands = this._drawCommands.concat("A" + Align + ";"); + } + + get textBaseline() { + return this._textBaseline; + } + + setTextBaseline(value) { + this.textBaseline = value + } + + set textBaseline(value) { + this._textBaseline = value; + let baseline = 0; + switch (value) { + case "alphabetic": + baseline = 0; + break; + case "middle": + baseline = 1; + break; + case "top": + baseline = 2; + break; + case "hanging": + baseline = 3; + break; + case "bottom": + baseline = 4; + break; + case "ideographic": + baseline = 5; + break; + default: + baseline = 0; + break; + } + + this._drawCommands = this._drawCommands.concat("E" + baseline + ";"); + } + + get font() { + return this._font; + } + + setFontSize(size) { + var str = this._font; + var strs = str.trim().split(/\s+/); + for (var i = 0; i < strs.length; i++) { + var values = ["normal", "italic", "oblique", "normal", "small-caps", "normal", "bold", + "bolder", "lighter", "100", "200", "300", "400", "500", "600", "700", "800", "900", + "normal", "ultra-condensed", "extra-condensed", "condensed", "semi-condensed", + "semi-expanded", "expanded", "extra-expanded", "ultra-expanded" + ]; + + if (-1 == values.indexOf(strs[i].trim())) { + if (typeof size === 'string') { + strs[i] = size; + } else if (typeof size === 'number') { + strs[i] = String(size) + 'px'; + } + break; + } + } + this.font = strs.join(" "); + } + + set font(value) { + this._font = value; + this._drawCommands = this._drawCommands.concat("j" + value + ";"); + } + + setTransform(a, b, c, d, tx, ty) { + this._drawCommands = this._drawCommands.concat("t" + + (a === 1 ? "1" : a.toFixed(2)) + "," + + (b === 0 ? "0" : b.toFixed(2)) + "," + + (c === 0 ? "0" : c.toFixed(2)) + "," + + (d === 1 ? "1" : d.toFixed(2)) + "," + tx.toFixed(2) + "," + ty.toFixed(2) + ";"); + } + + transform(a, b, c, d, tx, ty) { + this._drawCommands = this._drawCommands.concat("f" + + (a === 1 ? "1" : a.toFixed(2)) + "," + + (b === 0 ? "0" : b.toFixed(2)) + "," + + (c === 0 ? "0" : c.toFixed(2)) + "," + + (d === 1 ? "1" : d.toFixed(2)) + "," + tx + "," + ty + ";"); + } + + resetTransform() { + this._drawCommands = this._drawCommands.concat("m;"); + } + + scale(a, d) { + this._drawCommands = this._drawCommands.concat("k" + a.toFixed(2) + "," + + d.toFixed(2) + ";"); + } + + rotate(angle) { + this._drawCommands = this._drawCommands + .concat("r" + angle.toFixed(6) + ";"); + } + + translate(tx, ty) { + this._drawCommands = this._drawCommands.concat("l" + tx.toFixed(2) + "," + ty.toFixed(2) + ";"); + } + + save() { + this._savedGlobalAlpha.push(this._globalAlpha); + this._drawCommands = this._drawCommands.concat("v;"); + } + + restore() { + this._drawCommands = this._drawCommands.concat("e;"); + this._globalAlpha = this._savedGlobalAlpha.pop(); + } + + createPattern(img, pattern) { + if (typeof img === 'string') { + var imgObj = new GImage(); + imgObj.src = img; + img = imgObj; + } + return new FillStylePattern(img, pattern); + } + + createLinearGradient(x0, y0, x1, y1) { + return new FillStyleLinearGradient(x0, y0, x1, y1); + } + + createRadialGradient = function(x0, y0, r0, x1, y1, r1) { + return new FillStyleRadialGradient(x0, y0, r0, x1, y1, r1); + }; + + createCircularGradient = function(x0, y0, r0) { + return new FillStyleRadialGradient(x0, y0, 0, x0, y0, r0); + }; + + strokeRect(x, y, w, h) { + this._drawCommands = this._drawCommands.concat("s" + x + "," + y + "," + w + "," + h + ";"); + } + + + clearRect(x, y, w, h) { + this._drawCommands = this._drawCommands.concat("c" + x + "," + y + "," + w + + "," + h + ";"); + } + + clip() { + this._drawCommands = this._drawCommands.concat("p;"); + } + + resetClip() { + this._drawCommands = this._drawCommands.concat("q;"); + } + + closePath() { + this._drawCommands = this._drawCommands.concat("o;"); + } + + moveTo(x, y) { + this._drawCommands = this._drawCommands.concat("g" + x.toFixed(2) + "," + y.toFixed(2) + ";"); + } + + lineTo(x, y) { + this._drawCommands = this._drawCommands.concat("i" + x.toFixed(2) + "," + y.toFixed(2) + ";"); + } + + quadraticCurveTo = function(cpx, cpy, x, y) { + this._drawCommands = this._drawCommands.concat("u" + cpx + "," + cpy + "," + x + "," + y + ";"); + } + + bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y, ) { + this._drawCommands = this._drawCommands.concat( + "z" + cp1x.toFixed(2) + "," + cp1y.toFixed(2) + "," + cp2x.toFixed(2) + "," + cp2y.toFixed(2) + "," + + x.toFixed(2) + "," + y.toFixed(2) + ";"); + } + + arcTo(x1, y1, x2, y2, radius) { + this._drawCommands = this._drawCommands.concat("h" + x1 + "," + y1 + "," + x2 + "," + y2 + "," + radius + ";"); + } + + beginPath() { + this._drawCommands = this._drawCommands.concat("b;"); + } + + + fillRect(x, y, w, h) { + this._drawCommands = this._drawCommands.concat("n" + x + "," + y + "," + w + + "," + h + ";"); + } + + rect(x, y, w, h) { + this._drawCommands = this._drawCommands.concat("w" + x + "," + y + "," + w + "," + h + ";"); + } + + fill() { + this._drawCommands = this._drawCommands.concat("L;"); + } + + stroke(path) { + this._drawCommands = this._drawCommands.concat("x;"); + } + + arc(x, y, radius, startAngle, endAngle, anticlockwise) { + + let ianticlockwise = 0; + if (anticlockwise) { + ianticlockwise = 1; + } + + this._drawCommands = this._drawCommands.concat( + "y" + x.toFixed(2) + "," + y.toFixed(2) + "," + + radius.toFixed(2) + "," + startAngle + "," + endAngle + "," + ianticlockwise + + ";" + ); + } + + fillText(text, x, y) { + let tmptext = text.replace(/!/g, "!!"); + tmptext = tmptext.replace(/,/g, "!,"); + tmptext = tmptext.replace(/;/g, "!;"); + this._drawCommands = this._drawCommands.concat("T" + tmptext + "," + x + "," + y + ",0.0;"); + } + + strokeText = function(text, x, y) { + let tmptext = text.replace(/!/g, "!!"); + tmptext = tmptext.replace(/,/g, "!,"); + tmptext = tmptext.replace(/;/g, "!;"); + this._drawCommands = this._drawCommands.concat("U" + tmptext + "," + x + "," + y + ",0.0;"); + } + + measureText(text) { + return CanvasRenderingContext2D.GBridge.measureText(text, this.font, this.componentId); + } + + isPointInPath = function(x, y) { + throw new Error('GCanvas not supported yet'); + } + + drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) { + if (typeof image === 'string') { + var imgObj = new GImage(); + imgObj.src = image; + image = imgObj; + } + if (image instanceof GImage) { + if (!image.complete) { + imgObj.onload = () => { + var index = this._needRedrawImageCache.indexOf(image); + if (index > -1) { + this._needRedrawImageCache.splice(index, 1); + CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id); + this._redrawflush(true); + } + } + this._notCommitDrawImageCache.push(image); + } else { + CanvasRenderingContext2D.GBridge.bindImageTexture(this.componentId, image.src, image._id); + } + var srcArgs = [image, sx, sy, sw, sh, dx, dy, dw, dh]; + var args = []; + for (var arg in srcArgs) { + if (typeof(srcArgs[arg]) != 'undefined') { + args.push(srcArgs[arg]); + } + } + this.__drawImage.apply(this, args); + //this.__drawImage(image,sx, sy, sw, sh, dx, dy, dw, dh); + } + } + + __drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) { + const numArgs = arguments.length; + + function drawImageCommands() { + + if (numArgs === 3) { + const x = parseFloat(sx) || 0.0; + const y = parseFloat(sy) || 0.0; + + return ("d" + image._id + ",0,0," + + image.width + "," + image.height + "," + + x + "," + y + "," + image.width + "," + image.height + ";"); + } else if (numArgs === 5) { + const x = parseFloat(sx) || 0.0; + const y = parseFloat(sy) || 0.0; + const width = parseInt(sw) || image.width; + const height = parseInt(sh) || image.height; + + return ("d" + image._id + ",0,0," + + image.width + "," + image.height + "," + + x + "," + y + "," + width + "," + height + ";"); + } else if (numArgs === 9) { + sx = parseFloat(sx) || 0.0; + sy = parseFloat(sy) || 0.0; + sw = parseInt(sw) || image.width; + sh = parseInt(sh) || image.height; + dx = parseFloat(dx) || 0.0; + dy = parseFloat(dy) || 0.0; + dw = parseInt(dw) || image.width; + dh = parseInt(dh) || image.height; + + return ("d" + image._id + "," + + sx + "," + sy + "," + sw + "," + sh + "," + + dx + "," + dy + "," + dw + "," + dh + ";"); + } + } + this._drawCommands += drawImageCommands(); + } + + _flush(reserve, callback) { + const commands = this._drawCommands; + this._drawCommands = ''; + CanvasRenderingContext2D.GBridge.render2d(this.componentId, commands, callback); + this._needRender = false; + } + + _redrawflush(reserve, callback) { + const commands = this._redrawCommands; + CanvasRenderingContext2D.GBridge.render2d(this.componentId, commands, callback); + if (this._needRedrawImageCache.length == 0) { + this._redrawCommands = ''; + } + } + + draw(reserve, callback) { + if (!reserve) { + this._globalAlpha = this._savedGlobalAlpha.pop(); + this._savedGlobalAlpha.push(this._globalAlpha); + this._redrawCommands = this._drawCommands; + this._needRedrawImageCache = this._notCommitDrawImageCache; + if (this._autoSaveContext) { + this._drawCommands = ("v;" + this._drawCommands); + this._autoSaveContext = false; + } else { + this._drawCommands = ("e;X;v;" + this._drawCommands); + } + } else { + this._needRedrawImageCache = this._needRedrawImageCache.concat(this._notCommitDrawImageCache); + this._redrawCommands += this._drawCommands; + if (this._autoSaveContext) { + this._drawCommands = ("v;" + this._drawCommands); + this._autoSaveContext = false; + } + } + this._notCommitDrawImageCache = []; + if (this._flush) { + this._flush(reserve, callback); + } + } + + getImageData(x, y, w, h, callback) { + CanvasRenderingContext2D.GBridge.getImageData(this.componentId, x, y, w, h, function(res) { + res.data = Base64ToUint8ClampedArray(res.data); + if (typeof(callback) == 'function') { + callback(res); + } + }); + } + + putImageData(data, x, y, w, h, callback) { + if (data instanceof Uint8ClampedArray) { + data = ArrayBufferToBase64(data); + CanvasRenderingContext2D.GBridge.putImageData(this.componentId, data, x, y, w, h, function(res) { + if (typeof(callback) == 'function') { + callback(res); + } + }); + } + } + + toTempFilePath(x, y, width, height, destWidth, destHeight, fileType, quality, callback) { + CanvasRenderingContext2D.GBridge.toTempFilePath(this.componentId, x, y, width, height, destWidth, destHeight, + fileType, quality, + function(res) { + if (typeof(callback) == 'function') { + callback(res); + } + }); + } +} diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/ActiveInfo.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/ActiveInfo.js new file mode 100644 index 0000000..b495129 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/ActiveInfo.js @@ -0,0 +1,11 @@ +export default class WebGLActiveInfo { + className = 'WebGLActiveInfo'; + + constructor({ + type, name, size + }) { + this.type = type; + this.name = name; + this.size = size; + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Buffer.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Buffer.js new file mode 100644 index 0000000..4800f67 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Buffer.js @@ -0,0 +1,21 @@ +import {getTransferedObjectUUID} from './classUtils'; + +const name = 'WebGLBuffer'; + +function uuid(id) { + return getTransferedObjectUUID(name, id); +} + +export default class WebGLBuffer { + className = name; + + constructor(id) { + this.id = id; + } + + static uuid = uuid; + + uuid() { + return uuid(this.id); + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Framebuffer.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Framebuffer.js new file mode 100644 index 0000000..28b46d3 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Framebuffer.js @@ -0,0 +1,21 @@ +import {getTransferedObjectUUID} from './classUtils'; + +const name = 'WebGLFrameBuffer'; + +function uuid(id) { + return getTransferedObjectUUID(name, id); +} + +export default class WebGLFramebuffer { + className = name; + + constructor(id) { + this.id = id; + } + + static uuid = uuid; + + uuid() { + return uuid(this.id); + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/GLenum.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/GLenum.js new file mode 100644 index 0000000..ac5544d --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/GLenum.js @@ -0,0 +1,298 @@ +export default { + "DEPTH_BUFFER_BIT": 256, + "STENCIL_BUFFER_BIT": 1024, + "COLOR_BUFFER_BIT": 16384, + "POINTS": 0, + "LINES": 1, + "LINE_LOOP": 2, + "LINE_STRIP": 3, + "TRIANGLES": 4, + "TRIANGLE_STRIP": 5, + "TRIANGLE_FAN": 6, + "ZERO": 0, + "ONE": 1, + "SRC_COLOR": 768, + "ONE_MINUS_SRC_COLOR": 769, + "SRC_ALPHA": 770, + "ONE_MINUS_SRC_ALPHA": 771, + "DST_ALPHA": 772, + "ONE_MINUS_DST_ALPHA": 773, + "DST_COLOR": 774, + "ONE_MINUS_DST_COLOR": 775, + "SRC_ALPHA_SATURATE": 776, + "FUNC_ADD": 32774, + "BLEND_EQUATION": 32777, + "BLEND_EQUATION_RGB": 32777, + "BLEND_EQUATION_ALPHA": 34877, + "FUNC_SUBTRACT": 32778, + "FUNC_REVERSE_SUBTRACT": 32779, + "BLEND_DST_RGB": 32968, + "BLEND_SRC_RGB": 32969, + "BLEND_DST_ALPHA": 32970, + "BLEND_SRC_ALPHA": 32971, + "CONSTANT_COLOR": 32769, + "ONE_MINUS_CONSTANT_COLOR": 32770, + "CONSTANT_ALPHA": 32771, + "ONE_MINUS_CONSTANT_ALPHA": 32772, + "BLEND_COLOR": 32773, + "ARRAY_BUFFER": 34962, + "ELEMENT_ARRAY_BUFFER": 34963, + "ARRAY_BUFFER_BINDING": 34964, + "ELEMENT_ARRAY_BUFFER_BINDING": 34965, + "STREAM_DRAW": 35040, + "STATIC_DRAW": 35044, + "DYNAMIC_DRAW": 35048, + "BUFFER_SIZE": 34660, + "BUFFER_USAGE": 34661, + "CURRENT_VERTEX_ATTRIB": 34342, + "FRONT": 1028, + "BACK": 1029, + "FRONT_AND_BACK": 1032, + "TEXTURE_2D": 3553, + "CULL_FACE": 2884, + "BLEND": 3042, + "DITHER": 3024, + "STENCIL_TEST": 2960, + "DEPTH_TEST": 2929, + "SCISSOR_TEST": 3089, + "POLYGON_OFFSET_FILL": 32823, + "SAMPLE_ALPHA_TO_COVERAGE": 32926, + "SAMPLE_COVERAGE": 32928, + "NO_ERROR": 0, + "INVALID_ENUM": 1280, + "INVALID_VALUE": 1281, + "INVALID_OPERATION": 1282, + "OUT_OF_MEMORY": 1285, + "CW": 2304, + "CCW": 2305, + "LINE_WIDTH": 2849, + "ALIASED_POINT_SIZE_RANGE": 33901, + "ALIASED_LINE_WIDTH_RANGE": 33902, + "CULL_FACE_MODE": 2885, + "FRONT_FACE": 2886, + "DEPTH_RANGE": 2928, + "DEPTH_WRITEMASK": 2930, + "DEPTH_CLEAR_VALUE": 2931, + "DEPTH_FUNC": 2932, + "STENCIL_CLEAR_VALUE": 2961, + "STENCIL_FUNC": 2962, + "STENCIL_FAIL": 2964, + "STENCIL_PASS_DEPTH_FAIL": 2965, + "STENCIL_PASS_DEPTH_PASS": 2966, + "STENCIL_REF": 2967, + "STENCIL_VALUE_MASK": 2963, + "STENCIL_WRITEMASK": 2968, + "STENCIL_BACK_FUNC": 34816, + "STENCIL_BACK_FAIL": 34817, + "STENCIL_BACK_PASS_DEPTH_FAIL": 34818, + "STENCIL_BACK_PASS_DEPTH_PASS": 34819, + "STENCIL_BACK_REF": 36003, + "STENCIL_BACK_VALUE_MASK": 36004, + "STENCIL_BACK_WRITEMASK": 36005, + "VIEWPORT": 2978, + "SCISSOR_BOX": 3088, + "COLOR_CLEAR_VALUE": 3106, + "COLOR_WRITEMASK": 3107, + "UNPACK_ALIGNMENT": 3317, + "PACK_ALIGNMENT": 3333, + "MAX_TEXTURE_SIZE": 3379, + "MAX_VIEWPORT_DIMS": 3386, + "SUBPIXEL_BITS": 3408, + "RED_BITS": 3410, + "GREEN_BITS": 3411, + "BLUE_BITS": 3412, + "ALPHA_BITS": 3413, + "DEPTH_BITS": 3414, + "STENCIL_BITS": 3415, + "POLYGON_OFFSET_UNITS": 10752, + "POLYGON_OFFSET_FACTOR": 32824, + "TEXTURE_BINDING_2D": 32873, + "SAMPLE_BUFFERS": 32936, + "SAMPLES": 32937, + "SAMPLE_COVERAGE_VALUE": 32938, + "SAMPLE_COVERAGE_INVERT": 32939, + "COMPRESSED_TEXTURE_FORMATS": 34467, + "DONT_CARE": 4352, + "FASTEST": 4353, + "NICEST": 4354, + "GENERATE_MIPMAP_HINT": 33170, + "BYTE": 5120, + "UNSIGNED_BYTE": 5121, + "SHORT": 5122, + "UNSIGNED_SHORT": 5123, + "INT": 5124, + "UNSIGNED_INT": 5125, + "FLOAT": 5126, + "DEPTH_COMPONENT": 6402, + "ALPHA": 6406, + "RGB": 6407, + "RGBA": 6408, + "LUMINANCE": 6409, + "LUMINANCE_ALPHA": 6410, + "UNSIGNED_SHORT_4_4_4_4": 32819, + "UNSIGNED_SHORT_5_5_5_1": 32820, + "UNSIGNED_SHORT_5_6_5": 33635, + "FRAGMENT_SHADER": 35632, + "VERTEX_SHADER": 35633, + "MAX_VERTEX_ATTRIBS": 34921, + "MAX_VERTEX_UNIFORM_VECTORS": 36347, + "MAX_VARYING_VECTORS": 36348, + "MAX_COMBINED_TEXTURE_IMAGE_UNITS": 35661, + "MAX_VERTEX_TEXTURE_IMAGE_UNITS": 35660, + "MAX_TEXTURE_IMAGE_UNITS": 34930, + "MAX_FRAGMENT_UNIFORM_VECTORS": 36349, + "SHADER_TYPE": 35663, + "DELETE_STATUS": 35712, + "LINK_STATUS": 35714, + "VALIDATE_STATUS": 35715, + "ATTACHED_SHADERS": 35717, + "ACTIVE_UNIFORMS": 35718, + "ACTIVE_ATTRIBUTES": 35721, + "SHADING_LANGUAGE_VERSION": 35724, + "CURRENT_PROGRAM": 35725, + "NEVER": 512, + "LESS": 513, + "EQUAL": 514, + "LEQUAL": 515, + "GREATER": 516, + "NOTEQUAL": 517, + "GEQUAL": 518, + "ALWAYS": 519, + "KEEP": 7680, + "REPLACE": 7681, + "INCR": 7682, + "DECR": 7683, + "INVERT": 5386, + "INCR_WRAP": 34055, + "DECR_WRAP": 34056, + "VENDOR": 7936, + "RENDERER": 7937, + "VERSION": 7938, + "NEAREST": 9728, + "LINEAR": 9729, + "NEAREST_MIPMAP_NEAREST": 9984, + "LINEAR_MIPMAP_NEAREST": 9985, + "NEAREST_MIPMAP_LINEAR": 9986, + "LINEAR_MIPMAP_LINEAR": 9987, + "TEXTURE_MAG_FILTER": 10240, + "TEXTURE_MIN_FILTER": 10241, + "TEXTURE_WRAP_S": 10242, + "TEXTURE_WRAP_T": 10243, + "TEXTURE": 5890, + "TEXTURE_CUBE_MAP": 34067, + "TEXTURE_BINDING_CUBE_MAP": 34068, + "TEXTURE_CUBE_MAP_POSITIVE_X": 34069, + "TEXTURE_CUBE_MAP_NEGATIVE_X": 34070, + "TEXTURE_CUBE_MAP_POSITIVE_Y": 34071, + "TEXTURE_CUBE_MAP_NEGATIVE_Y": 34072, + "TEXTURE_CUBE_MAP_POSITIVE_Z": 34073, + "TEXTURE_CUBE_MAP_NEGATIVE_Z": 34074, + "MAX_CUBE_MAP_TEXTURE_SIZE": 34076, + "TEXTURE0": 33984, + "TEXTURE1": 33985, + "TEXTURE2": 33986, + "TEXTURE3": 33987, + "TEXTURE4": 33988, + "TEXTURE5": 33989, + "TEXTURE6": 33990, + "TEXTURE7": 33991, + "TEXTURE8": 33992, + "TEXTURE9": 33993, + "TEXTURE10": 33994, + "TEXTURE11": 33995, + "TEXTURE12": 33996, + "TEXTURE13": 33997, + "TEXTURE14": 33998, + "TEXTURE15": 33999, + "TEXTURE16": 34000, + "TEXTURE17": 34001, + "TEXTURE18": 34002, + "TEXTURE19": 34003, + "TEXTURE20": 34004, + "TEXTURE21": 34005, + "TEXTURE22": 34006, + "TEXTURE23": 34007, + "TEXTURE24": 34008, + "TEXTURE25": 34009, + "TEXTURE26": 34010, + "TEXTURE27": 34011, + "TEXTURE28": 34012, + "TEXTURE29": 34013, + "TEXTURE30": 34014, + "TEXTURE31": 34015, + "ACTIVE_TEXTURE": 34016, + "REPEAT": 10497, + "CLAMP_TO_EDGE": 33071, + "MIRRORED_REPEAT": 33648, + "FLOAT_VEC2": 35664, + "FLOAT_VEC3": 35665, + "FLOAT_VEC4": 35666, + "INT_VEC2": 35667, + "INT_VEC3": 35668, + "INT_VEC4": 35669, + "BOOL": 35670, + "BOOL_VEC2": 35671, + "BOOL_VEC3": 35672, + "BOOL_VEC4": 35673, + "FLOAT_MAT2": 35674, + "FLOAT_MAT3": 35675, + "FLOAT_MAT4": 35676, + "SAMPLER_2D": 35678, + "SAMPLER_CUBE": 35680, + "VERTEX_ATTRIB_ARRAY_ENABLED": 34338, + "VERTEX_ATTRIB_ARRAY_SIZE": 34339, + "VERTEX_ATTRIB_ARRAY_STRIDE": 34340, + "VERTEX_ATTRIB_ARRAY_TYPE": 34341, + "VERTEX_ATTRIB_ARRAY_NORMALIZED": 34922, + "VERTEX_ATTRIB_ARRAY_POINTER": 34373, + "VERTEX_ATTRIB_ARRAY_BUFFER_BINDING": 34975, + "IMPLEMENTATION_COLOR_READ_TYPE": 35738, + "IMPLEMENTATION_COLOR_READ_FORMAT": 35739, + "COMPILE_STATUS": 35713, + "LOW_FLOAT": 36336, + "MEDIUM_FLOAT": 36337, + "HIGH_FLOAT": 36338, + "LOW_INT": 36339, + "MEDIUM_INT": 36340, + "HIGH_INT": 36341, + "FRAMEBUFFER": 36160, + "RENDERBUFFER": 36161, + "RGBA4": 32854, + "RGB5_A1": 32855, + "RGB565": 36194, + "DEPTH_COMPONENT16": 33189, + "STENCIL_INDEX8": 36168, + "DEPTH_STENCIL": 34041, + "RENDERBUFFER_WIDTH": 36162, + "RENDERBUFFER_HEIGHT": 36163, + "RENDERBUFFER_INTERNAL_FORMAT": 36164, + "RENDERBUFFER_RED_SIZE": 36176, + "RENDERBUFFER_GREEN_SIZE": 36177, + "RENDERBUFFER_BLUE_SIZE": 36178, + "RENDERBUFFER_ALPHA_SIZE": 36179, + "RENDERBUFFER_DEPTH_SIZE": 36180, + "RENDERBUFFER_STENCIL_SIZE": 36181, + "FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE": 36048, + "FRAMEBUFFER_ATTACHMENT_OBJECT_NAME": 36049, + "FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL": 36050, + "FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE": 36051, + "COLOR_ATTACHMENT0": 36064, + "DEPTH_ATTACHMENT": 36096, + "STENCIL_ATTACHMENT": 36128, + "DEPTH_STENCIL_ATTACHMENT": 33306, + "NONE": 0, + "FRAMEBUFFER_COMPLETE": 36053, + "FRAMEBUFFER_INCOMPLETE_ATTACHMENT": 36054, + "FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT": 36055, + "FRAMEBUFFER_INCOMPLETE_DIMENSIONS": 36057, + "FRAMEBUFFER_UNSUPPORTED": 36061, + "FRAMEBUFFER_BINDING": 36006, + "RENDERBUFFER_BINDING": 36007, + "MAX_RENDERBUFFER_SIZE": 34024, + "INVALID_FRAMEBUFFER_OPERATION": 1286, + "UNPACK_FLIP_Y_WEBGL": 37440, + "UNPACK_PREMULTIPLY_ALPHA_WEBGL": 37441, + "CONTEXT_LOST_WEBGL": 37442, + "UNPACK_COLORSPACE_CONVERSION_WEBGL": 37443, + "BROWSER_DEFAULT_WEBGL": 37444 +}; \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/GLmethod.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/GLmethod.js new file mode 100644 index 0000000..f2659be --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/GLmethod.js @@ -0,0 +1,142 @@ +let i = 1; + +const GLmethod = {}; + +GLmethod.activeTexture = i++; //1 +GLmethod.attachShader = i++; +GLmethod.bindAttribLocation = i++; +GLmethod.bindBuffer = i++; +GLmethod.bindFramebuffer = i++; +GLmethod.bindRenderbuffer = i++; +GLmethod.bindTexture = i++; +GLmethod.blendColor = i++; +GLmethod.blendEquation = i++; +GLmethod.blendEquationSeparate = i++; //10 +GLmethod.blendFunc = i++; +GLmethod.blendFuncSeparate = i++; +GLmethod.bufferData = i++; +GLmethod.bufferSubData = i++; +GLmethod.checkFramebufferStatus = i++; +GLmethod.clear = i++; +GLmethod.clearColor = i++; +GLmethod.clearDepth = i++; +GLmethod.clearStencil = i++; +GLmethod.colorMask = i++; //20 +GLmethod.compileShader = i++; +GLmethod.compressedTexImage2D = i++; +GLmethod.compressedTexSubImage2D = i++; +GLmethod.copyTexImage2D = i++; +GLmethod.copyTexSubImage2D = i++; +GLmethod.createBuffer = i++; +GLmethod.createFramebuffer = i++; +GLmethod.createProgram = i++; +GLmethod.createRenderbuffer = i++; +GLmethod.createShader = i++; //30 +GLmethod.createTexture = i++; +GLmethod.cullFace = i++; +GLmethod.deleteBuffer = i++; +GLmethod.deleteFramebuffer = i++; +GLmethod.deleteProgram = i++; +GLmethod.deleteRenderbuffer = i++; +GLmethod.deleteShader = i++; +GLmethod.deleteTexture = i++; +GLmethod.depthFunc = i++; +GLmethod.depthMask = i++; //40 +GLmethod.depthRange = i++; +GLmethod.detachShader = i++; +GLmethod.disable = i++; +GLmethod.disableVertexAttribArray = i++; +GLmethod.drawArrays = i++; +GLmethod.drawArraysInstancedANGLE = i++; +GLmethod.drawElements = i++; +GLmethod.drawElementsInstancedANGLE = i++; +GLmethod.enable = i++; +GLmethod.enableVertexAttribArray = i++; //50 +GLmethod.flush = i++; +GLmethod.framebufferRenderbuffer = i++; +GLmethod.framebufferTexture2D = i++; +GLmethod.frontFace = i++; +GLmethod.generateMipmap = i++; +GLmethod.getActiveAttrib = i++; +GLmethod.getActiveUniform = i++; +GLmethod.getAttachedShaders = i++; +GLmethod.getAttribLocation = i++; +GLmethod.getBufferParameter = i++; //60 +GLmethod.getContextAttributes = i++; +GLmethod.getError = i++; +GLmethod.getExtension = i++; +GLmethod.getFramebufferAttachmentParameter = i++; +GLmethod.getParameter = i++; +GLmethod.getProgramInfoLog = i++; +GLmethod.getProgramParameter = i++; +GLmethod.getRenderbufferParameter = i++; +GLmethod.getShaderInfoLog = i++; +GLmethod.getShaderParameter = i++; //70 +GLmethod.getShaderPrecisionFormat = i++; +GLmethod.getShaderSource = i++; +GLmethod.getSupportedExtensions = i++; +GLmethod.getTexParameter = i++; +GLmethod.getUniform = i++; +GLmethod.getUniformLocation = i++; +GLmethod.getVertexAttrib = i++; +GLmethod.getVertexAttribOffset = i++; +GLmethod.isBuffer = i++; +GLmethod.isContextLost = i++; //80 +GLmethod.isEnabled = i++; +GLmethod.isFramebuffer = i++; +GLmethod.isProgram = i++; +GLmethod.isRenderbuffer = i++; +GLmethod.isShader = i++; +GLmethod.isTexture = i++; +GLmethod.lineWidth = i++; +GLmethod.linkProgram = i++; +GLmethod.pixelStorei = i++; +GLmethod.polygonOffset = i++; //90 +GLmethod.readPixels = i++; +GLmethod.renderbufferStorage = i++; +GLmethod.sampleCoverage = i++; +GLmethod.scissor = i++; +GLmethod.shaderSource = i++; +GLmethod.stencilFunc = i++; +GLmethod.stencilFuncSeparate = i++; +GLmethod.stencilMask = i++; +GLmethod.stencilMaskSeparate = i++; +GLmethod.stencilOp = i++; //100 +GLmethod.stencilOpSeparate = i++; +GLmethod.texImage2D = i++; +GLmethod.texParameterf = i++; +GLmethod.texParameteri = i++; +GLmethod.texSubImage2D = i++; +GLmethod.uniform1f = i++; +GLmethod.uniform1fv = i++; +GLmethod.uniform1i = i++; +GLmethod.uniform1iv = i++; +GLmethod.uniform2f = i++; //110 +GLmethod.uniform2fv = i++; +GLmethod.uniform2i = i++; +GLmethod.uniform2iv = i++; +GLmethod.uniform3f = i++; +GLmethod.uniform3fv = i++; +GLmethod.uniform3i = i++; +GLmethod.uniform3iv = i++; +GLmethod.uniform4f = i++; +GLmethod.uniform4fv = i++; +GLmethod.uniform4i = i++; //120 +GLmethod.uniform4iv = i++; +GLmethod.uniformMatrix2fv = i++; +GLmethod.uniformMatrix3fv = i++; +GLmethod.uniformMatrix4fv = i++; +GLmethod.useProgram = i++; +GLmethod.validateProgram = i++; +GLmethod.vertexAttrib1f = i++; //new +GLmethod.vertexAttrib2f = i++; //new +GLmethod.vertexAttrib3f = i++; //new +GLmethod.vertexAttrib4f = i++; //new //130 +GLmethod.vertexAttrib1fv = i++; //new +GLmethod.vertexAttrib2fv = i++; //new +GLmethod.vertexAttrib3fv = i++; //new +GLmethod.vertexAttrib4fv = i++; //new +GLmethod.vertexAttribPointer = i++; +GLmethod.viewport = i++; + +export default GLmethod; \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/GLtype.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/GLtype.js new file mode 100644 index 0000000..695abcb --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/GLtype.js @@ -0,0 +1,23 @@ +const GLtype = {}; + +[ + "GLbitfield", + "GLboolean", + "GLbyte", + "GLclampf", + "GLenum", + "GLfloat", + "GLint", + "GLintptr", + "GLsizei", + "GLsizeiptr", + "GLshort", + "GLubyte", + "GLuint", + "GLushort" +].sort().map((typeName, i) => GLtype[typeName] = 1 >> (i + 1)); + +export default GLtype; + + + diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Program.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Program.js new file mode 100644 index 0000000..6f5691c --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Program.js @@ -0,0 +1,21 @@ +import {getTransferedObjectUUID} from './classUtils'; + +const name = 'WebGLProgram'; + +function uuid(id) { + return getTransferedObjectUUID(name, id); +} + +export default class WebGLProgram { + className = name; + + constructor(id) { + this.id = id; + } + + static uuid = uuid; + + uuid() { + return uuid(this.id); + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Renderbuffer.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Renderbuffer.js new file mode 100644 index 0000000..d3182ae --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Renderbuffer.js @@ -0,0 +1,21 @@ +import {getTransferedObjectUUID} from './classUtils'; + +const name = 'WebGLRenderBuffer'; + +function uuid(id) { + return getTransferedObjectUUID(name, id); +} + +export default class WebGLRenderbuffer { + className = name; + + constructor(id) { + this.id = id; + } + + static uuid = uuid; + + uuid() { + return uuid(this.id); + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/RenderingContext.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/RenderingContext.js new file mode 100644 index 0000000..5f9608f --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/RenderingContext.js @@ -0,0 +1,1191 @@ +import GLenum from './GLenum'; +import ActiveInfo from './ActiveInfo'; +import Buffer from './Buffer'; +import Framebuffer from './Framebuffer'; +import Renderbuffer from './Renderbuffer'; +import Texture from './Texture'; +import Program from './Program'; +import Shader from './Shader'; +import ShaderPrecisionFormat from './ShaderPrecisionFormat'; +import UniformLocation from './UniformLocation'; +import GLmethod from './GLmethod'; + +const processArray = (array, checkArrayType = false) => { + + function joinArray(arr, sep) { + let res = ''; + for (let i = 0; i < arr.length; i++) { + if (i !== 0) { + res += sep; + } + res += arr[i]; + } + return res; + } + + let type = 'Float32Array'; + if (checkArrayType) { + if (array instanceof Uint8Array) { + type = 'Uint8Array' + } else if (array instanceof Uint16Array) { + type = 'Uint16Array'; + } else if (array instanceof Uint32Array) { + type = 'Uint32Array'; + } else if (array instanceof Float32Array) { + type = 'Float32Array'; + } else { + throw new Error('Check array type failed. Array type is ' + typeof array); + } + } + + const ArrayTypes = { + Uint8Array: 1, + Uint16Array: 2, + Uint32Array: 4, + Float32Array: 14 + }; + return ArrayTypes[type] + ',' + btoa(joinArray(array, ',')) +} + +export default class WebGLRenderingContext { + + // static GBridge = null; + + className = 'WebGLRenderingContext'; + + constructor(canvas, type, attrs) { + this._canvas = canvas; + this._type = type; + this._version = 'WebGL 1.0'; + this._attrs = attrs; + this._map = new Map(); + + Object.keys(GLenum) + .forEach(name => Object.defineProperty(this, name, { + value: GLenum[name] + })); + } + + get canvas() { + return this._canvas; + } + + activeTexture = function (textureUnit) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.activeTexture + ',' + textureUnit, + true + ); + } + + attachShader = function (progarm, shader) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.attachShader + ',' + progarm.id + ',' + shader.id, + true + ); + } + + bindAttribLocation = function (program, index, name) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.bindAttribLocation + ',' + program.id + ',' + index + ',' + name, + true + ) + } + + bindBuffer = function (target, buffer) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.bindBuffer + ',' + target + ',' + (buffer ? buffer.id : 0), + true + ); + } + + bindFramebuffer = function (target, framebuffer) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.bindFramebuffer + ',' + target + ',' + (framebuffer ? framebuffer.id : 0), + true + ) + } + + bindRenderbuffer = function (target, renderBuffer) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.bindRenderbuffer + ',' + target + ',' + (renderBuffer ? renderBuffer.id : 0), + true + ) + } + + bindTexture = function (target, texture) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.bindTexture + ',' + target + ',' + (texture ? texture.id : 0), + true + ) + } + + blendColor = function (r, g, b, a) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.blendColor + ',' + target + ',' + r + ',' + g + ',' + b + ',' + a, + true + ) + } + + blendEquation = function (mode) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.blendEquation + ',' + mode, + true + ) + } + + blendEquationSeparate = function (modeRGB, modeAlpha) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.blendEquationSeparate + ',' + modeRGB + ',' + modeAlpha, + true + ) + } + + + blendFunc = function (sfactor, dfactor) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.blendFunc + ',' + sfactor + ',' + dfactor, + true + ); + } + + blendFuncSeparate = function (srcRGB, dstRGB, srcAlpha, dstAlpha) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.blendFuncSeparate + ',' + srcRGB + ',' + dstRGB + ',' + srcAlpha + ',' + dstAlpha, + true + ); + } + + bufferData = function (target, data, usage) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.bufferData + ',' + target + ',' + processArray(data, true) + ',' + usage, + true + ) + } + + bufferSubData = function (target, offset, data) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.bufferSubData + ',' + target + ',' + offset + ',' + processArray(data, true), + true + ) + } + + checkFramebufferStatus = function (target) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.checkFramebufferStatus + ',' + target + ); + return Number(result); + } + + clear = function (mask) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.clear + ',' + mask + ); + this._canvas._needRender = true; + } + + clearColor = function (r, g, b, a) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.clearColor + ',' + r + ',' + g + ',' + b, + true + ) + } + + clearDepth = function (depth) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.clearDepth + ',' + depth, + true + ) + } + + clearStencil = function (s) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.clearStencil + ',' + s + ); + } + + colorMask = function (r, g, b, a) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.colorMask + ',' + r + ',' + g + ',' + b + ',' + a + ) + } + + compileShader = function (shader) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.compileShader + ',' + shader.id, + true + ) + } + + compressedTexImage2D = function (target, level, internalformat, width, height, border, pixels) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.compressedTexImage2D + ',' + target + ',' + level + ',' + internalformat + ',' + + width + ',' + height + ',' + border + ',' + processArray(pixels), + true + ) + } + + compressedTexSubImage2D = function (target, level, xoffset, yoffset, width, height, format, pixels) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.compressedTexSubImage2D + ',' + target + ',' + level + ',' + xoffset + ',' + yoffset + ',' + + width + ',' + height + ',' + format + ',' + processArray(pixels), + true + ) + } + + + copyTexImage2D = function (target, level, internalformat, x, y, width, height, border) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.copyTexImage2D + ',' + target + ',' + level + ',' + internalformat + ',' + x + ',' + y + ',' + + width + ',' + height + ',' + border, + true + ); + } + + copyTexSubImage2D = function (target, level, xoffset, yoffset, x, y, width, height) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.copyTexSubImage2D + ',' + target + ',' + level + ',' + xoffset + ',' + yoffset + ',' + x + ',' + y + ',' + + width + ',' + height + ); + } + + createBuffer = function () { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.createBuffer + '' + ); + const buffer = new Buffer(result); + this._map.set(buffer.uuid(), buffer); + return buffer; + } + + createFramebuffer = function () { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.createFramebuffer + '' + ); + const framebuffer = new Framebuffer(result); + this._map.set(framebuffer.uuid(), framebuffer); + return framebuffer; + } + + + createProgram = function () { + const id = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.createProgram + '' + ); + const program = new Program(id); + this._map.set(program.uuid(), program); + return program; + } + + createRenderbuffer = function () { + const id = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.createRenderbuffer + '' + ) + const renderBuffer = new Renderbuffer(id); + this._map.set(renderBuffer.uuid(), renderBuffer); + return renderBuffer; + } + + createShader = function (type) { + const id = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.createShader + ',' + type + ) + const shader = new Shader(id, type); + this._map.set(shader.uuid(), shader); + return shader; + } + + createTexture = function () { + const id = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.createTexture + '' + ); + const texture = new Texture(id); + this._map.set(texture.uuid(), texture); + return texture; + } + + cullFace = function (mode) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.cullFace + ',' + mode, + true + ) + } + + + deleteBuffer = function (buffer) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.deleteBuffer + ',' + buffer.id, + true + ) + } + + deleteFramebuffer = function (framebuffer) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.deleteFramebuffer + ',' + framebuffer.id, + true + ) + } + + deleteProgram = function (program) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.deleteProgram + ',' + program.id, + true + ) + } + + deleteRenderbuffer = function (renderbuffer) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.deleteRenderbuffer + ',' + renderbuffer.id, + true + ) + } + + deleteShader = function (shader) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.deleteShader + ',' + shader.id, + true + ) + } + + deleteTexture = function (texture) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.deleteTexture + ',' + texture.id, + true + ) + } + + depthFunc = function (func) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.depthFunc + ',' + func + ) + } + + depthMask = function (flag) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.depthMask + ',' + Number(flag), + true + ) + } + + depthRange = function (zNear, zFar) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.depthRange + ',' + zNear + ',' + zFar, + true + ) + } + + detachShader = function (program, shader) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.detachShader + ',' + program.id + ',' + shader.id, + true + ) + } + + disable = function (cap) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.disable + ',' + cap, + true + ) + } + + disableVertexAttribArray = function (index) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.disableVertexAttribArray + ',' + index, + true + ); + } + + drawArrays = function (mode, first, count) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.drawArrays + ',' + mode + ',' + first + ',' + count + ) + this._canvas._needRender = true; + } + + drawElements = function (mode, count, type, offset) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.drawElements + ',' + mode + ',' + count + ',' + type + ',' + offset + ';' + ); + this._canvas._needRender = true; + } + + enable = function (cap) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.enable + ',' + cap, + true + ); + } + + enableVertexAttribArray = function (index) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.enableVertexAttribArray + ',' + index, + true + ) + } + + + flush = function () { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.flush + '' + ) + } + + framebufferRenderbuffer = function (target, attachment, textarget, texture, level) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.framebufferRenderbuffer + ',' + target + ',' + attachment + ',' + textarget + ',' + (texture ? texture.id : 0) + ',' + level, + true + ) + } + + framebufferTexture2D = function (target, attachment, textarget, texture, level) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.framebufferTexture2D + ',' + target + ',' + attachment + ',' + textarget + ',' + (texture ? texture.id : 0) + ',' + level, + true + ) + } + + frontFace = function (mode) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.frontFace + ',' + mode, + true + ) + } + + generateMipmap = function (target) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.generateMipmap + ',' + target, + true + ) + } + + getActiveAttrib = function (progarm, index) { + const resultString = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getActiveAttrib + ',' + progarm.id + ',' + index + ) + const [type, size, name] = resultString.split(','); + return new ActiveInfo({ + type: Number(type), + size: Number(size), + name + }); + } + + getActiveUniform = function (progarm, index) { + const resultString = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getActiveUniform + ',' + progarm.id + ',' + index + ); + const [type, size, name] = resultString.split(','); + return new ActiveInfo({ + type: Number(type), + size: Number(size), + name + }) + } + + getAttachedShaders = function (progarm) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getAttachedShaders + ',' + progarm.id + ); + const [type, ...ids] = result; + return ids.map(id => this._map.get(Shader.uuid(id))); + } + + getAttribLocation = function (progarm, name) { + return WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getAttribLocation + ',' + progarm.id + ',' + name + ) + } + + getBufferParameter = function (target, pname) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getBufferParameter + ',' + target + ',' + pname + ); + const [type, res] = getBufferParameter; + return res; + } + + getError = function () { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getError + '' + ) + return result; + } + + getExtension = function (name) { + return null; + } + + getFramebufferAttachmentParameter = function (target, attachment, pname) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getFramebufferAttachmentParameter + ',' + target + ',' + attachment + ',' + pname + ) + switch (pname) { + case GLenum.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME: + return this._map.get(Renderbuffer.uuid(result)) || this._map.get(Texture.uuid(result)) || null; + default: + return result; + } + } + + getParameter = function (pname) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getParameter + ',' + pname + ) + switch (pname) { + case GLenum.VERSION: + return this._version; + case GLenum.ARRAY_BUFFER_BINDING: // buffer + case GLenum.ELEMENT_ARRAY_BUFFER_BINDING: // buffer + return this._map.get(Buffer.uuid(result)) || null; + case GLenum.CURRENT_PROGRAM: // program + return this._map.get(Program.uuid(result)) || null; + case GLenum.FRAMEBUFFER_BINDING: // framebuffer + return this._map.get(Framebuffer.uuid(result)) || null; + case GLenum.RENDERBUFFER_BINDING: // renderbuffer + return this._map.get(Renderbuffer.uuid(result)) || null; + case GLenum.TEXTURE_BINDING_2D: // texture + case GLenum.TEXTURE_BINDING_CUBE_MAP: // texture + return this._map.get(Texture.uuid(result)) || null; + case GLenum.ALIASED_LINE_WIDTH_RANGE: // Float32Array + case GLenum.ALIASED_POINT_SIZE_RANGE: // Float32Array + case GLenum.BLEND_COLOR: // Float32Array + case GLenum.COLOR_CLEAR_VALUE: // Float32Array + case GLenum.DEPTH_RANGE: // Float32Array + case GLenum.MAX_VIEWPORT_DIMS: // Int32Array + case GLenum.SCISSOR_BOX: // Int32Array + case GLenum.VIEWPORT: // Int32Array + case GLenum.COMPRESSED_TEXTURE_FORMATS: // Uint32Array + default: + const [type, ...res] = result.split(','); + if (res.length === 1) { + return Number(res[0]); + } else { + return res.map(Number); + } + } + } + + getProgramInfoLog = function (progarm) { + return WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getProgramInfoLog + ',' + progarm.id + ) + } + + getProgramParameter = function (program, pname) { + const res = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getProgramParameter + ',' + program.id + ',' + pname + ); + + const [type, result] = res.split(',').map(i => parseInt(i)); + + if (type === 1) { + return Boolean(result); + } else if (type === 2) { + return result; + } else { + throw new Error('Unrecongized program paramater ' + res + ', type: ' + typeof res); + } + } + + + getRenderbufferParameter = function (target, pname) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getRenderbufferParameter + ',' + target + ',' + pname + ) + return result; + } + + + getShaderInfoLog = function (shader) { + return WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getShaderInfoLog + ',' + shader.id + ); + } + + getShaderParameter = function (shader, pname) { + return WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getShaderParameter + ',' + shader.id + ',' + pname + ) + } + + getShaderPrecisionFormat = function (shaderType, precisionType) { + const [rangeMin, rangeMax, precision] = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getShaderPrecisionFormat + ',' + shaderType + ',' + precisionType + ); + const shaderPrecisionFormat = new ShaderPrecisionFormat({ + rangeMin: Number(rangeMin), + rangeMax: Number(rangeMax), + precision: Number(precision) + }); + return shaderPrecisionFormat; + } + + getShaderSource = function (shader) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getShaderSource + ',' + shader.id + ); + return result; + } + + getSupportedExtensions = function () { + return Object.keys({}); + } + + getTexParameter = function (target, pname) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getTexParameter + ',' + target + ',' + pname + ) + return result; + } + + getUniformLocation = function (program, name) { + const id = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getUniformLocation + ',' + program.id + ',' + name + ); + if (id === -1) { + return null; + } else { + return new UniformLocation(Number(id)); + } + } + + getVertexAttrib = function (index, pname) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getVertexAttrib + ',' + index + ',' + pname + ); + switch (pname) { + case GLenum.VERTEX_ATTRIB_ARRAY_BUFFER_BINDING: + return this._map.get(Buffer.uuid(result)) || null; + case GLenum.CURRENT_VERTEX_ATTRIB: // Float32Array + default: + return result; + } + } + + getVertexAttribOffset = function (index, pname) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.getVertexAttribOffset + ',' + index + ',' + pname + ) + return Number(result); + } + + isBuffer = function (buffer) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.isBuffer + ',' + buffer.id + ) + return Boolean(result); + } + + isContextLost = function () { + return false; + } + + isEnabled = function (cap) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.isEnabled + ',' + cap + ) + return Boolean(result); + } + + isFramebuffer = function (framebuffer) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.isFramebuffer + ',' + framebuffer.id + ) + return Boolean(result); + } + + isProgram = function (program) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.isProgram + ',' + program.id + ) + return Boolean(result); + } + + isRenderbuffer = function (renderBuffer) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.isRenderbuffer + ',' + renderbuffer.id + ) + return Boolean(result); + } + + isShader = function (shader) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.isShader + ',' + shader.id + ) + return Boolean(result); + } + + isTexture = function (texture) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.isTexture + ',' + texture.id + ); + return Boolean(result); + } + + lineWidth = function (width) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.lineWidth + ',' + width, + true + ) + } + + linkProgram = function (program) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.linkProgram + ',' + program.id, + true + ); + } + + + pixelStorei = function (pname, param) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.pixelStorei + ',' + pname + ',' + Number(param) + ) + } + + polygonOffset = function (factor, units) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.polygonOffset + ',' + factor + ',' + units + ) + } + + readPixels = function (x, y, width, height, format, type, pixels) { + const result = WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.readPixels + ',' + x + ',' + y + ',' + width + ',' + height + ',' + format + ',' + type + ) + return result; + } + + renderbufferStorage = function (target, internalFormat, width, height) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.renderbufferStorage + ',' + target + ',' + internalFormat + ',' + width + ',' + height, + true + ) + } + + sampleCoverage = function (value, invert) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.sampleCoverage + ',' + value + ',' + Number(invert), + true + ) + } + + scissor = function (x, y, width, height) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.scissor + ',' + x + ',' + y + ',' + width + ',' + height, + true + ) + } + + shaderSource = function (shader, source) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.shaderSource + ',' + shader.id + ',' + source + ) + } + + stencilFunc = function (func, ref, mask) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.stencilFunc + ',' + func + ',' + ref + ',' + mask, + true + ) + } + + stencilFuncSeparate = function (face, func, ref, mask) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.stencilFuncSeparate + ',' + face + ',' + func + ',' + ref + ',' + mask, + true + ) + } + + stencilMask = function (mask) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.stencilMask + ',' + mask, + true + ) + } + + stencilMaskSeparate = function (face, mask) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.stencilMaskSeparate + ',' + face + ',' + mask, + true + ) + } + + stencilOp = function (fail, zfail, zpass) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.stencilOp + ',' + fail + ',' + zfail + ',' + zpass + ) + } + + stencilOpSeparate = function (face, fail, zfail, zpass) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.stencilOp + ',' + face + ',' + fail + ',' + zfail + ',' + zpass, + true + ) + } + + texImage2D = function (...args) { + WebGLRenderingContext.GBridge.texImage2D(this._canvas.id, ...args); + } + + + texParameterf = function (target, pname, param) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.texParameterf + ',' + target + ',' + pname + ',' + param, + true + ) + } + + texParameteri = function (target, pname, param) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.texParameteri + ',' + target + ',' + pname + ',' + param + ) + } + + texSubImage2D = function (...args) { + WebGLRenderingContext.GBridge.texSubImage2D(this._canvas.id, ...args); + } + + uniform1f = function (location, v0) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniform1f + ',' + location.id + ',' + v0 + ) + } + + uniform1fv = function (location, value) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniform1fv + ',' + location.id + ',' + processArray(value), + true + ) + } + + uniform1i = function (location, v0) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniform1i + ',' + location.id + ',' + v0, + // true + ) + } + + uniform1iv = function (location, value) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniform1iv + ',' + location.id + ',' + processArray(value), + true + ) + } + + uniform2f = function (location, v0, v1) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniform2f + ',' + location.id + ',' + v0 + ',' + v1, + true + ) + } + + uniform2fv = function (location, value) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniform2fv + ',' + location.id + ',' + processArray(value), + true + ) + } + + uniform2i = function (location, v0, v1) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniform2i + ',' + location.id + ',' + v0 + ',' + v1, + true + ) + } + + uniform2iv = function (location, value) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniform2iv + ',' + location.id + ',' + processArray(value), + true + ) + } + + uniform3f = function (location, v0, v1, v2) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniform3f + ',' + location.id + ',' + v0 + ',' + v1 + ',' + v2, + true + ) + } + + uniform3fv = function (location, value) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniform3fv + ',' + location.id + ',' + processArray(value), + true + ) + } + + uniform3i = function (location, v0, v1, v2) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniform3i + ',' + location.id + ',' + v0 + ',' + v1 + ',' + v2, + true + ) + } + + uniform3iv = function (location, value) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniform3iv + ',' + location.id + ',' + processArray(value), + true + ) + } + + uniform4f = function (location, v0, v1, v2, v3) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniform4f + ',' + location.id + ',' + v0 + ',' + v1 + ',' + v2 + ',' + v3, + true + ) + } + + uniform4fv = function (location, value) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniform4fv + ',' + location.id + ',' + processArray(value), + true + ) + } + + uniform4i = function (location, v0, v1, v2, v3) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniform4i + ',' + location.id + ',' + v0 + ',' + v1 + ',' + v2 + ',' + v3, + true + ) + } + + uniform4iv = function (location, value) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniform4iv + ',' + location.id + ',' + processArray(value, true), + true + ) + } + + uniformMatrix2fv = function (location, transpose, value) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniformMatrix2fv + ',' + location.id + ',' + Number(transpose) + ',' + processArray(value), + true + ) + } + + uniformMatrix3fv = function (location, transpose, value) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniformMatrix3fv + ',' + location.id + ',' + Number(transpose) + ',' + processArray(value), + true + ) + } + + uniformMatrix4fv = function (location, transpose, value) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.uniformMatrix4fv + ',' + location.id + ',' + Number(transpose) + ',' + processArray(value), + true + ); + } + + useProgram = function (progarm) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.useProgram + ',' + progarm.id + '', + true + ) + } + + + validateProgram = function (program) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.validateProgram + ',' + program.id, + true + ) + } + + vertexAttrib1f = function (index, v0) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.vertexAttrib1f + ',' + index + ',' + v0, + true + ) + } + + vertexAttrib2f = function (index, v0, v1) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.vertexAttrib2f + ',' + index + ',' + v0 + ',' + v1, + true + ) + } + + vertexAttrib3f = function (index, v0, v1, v2) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.vertexAttrib3f + ',' + index + ',' + v0 + ',' + v1 + ',' + v2, + true + ) + } + + vertexAttrib4f = function (index, v0, v1, v2, v3) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.vertexAttrib4f + ',' + index + ',' + v0 + ',' + v1 + ',' + v2 + ',' + v3, + true + ) + } + + vertexAttrib1fv = function (index, value) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.vertexAttrib1fv + ',' + index + ',' + processArray(value), + true + ) + } + + vertexAttrib2fv = function (index, value) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.vertexAttrib2fv + ',' + index + ',' + processArray(value), + true + ) + } + + vertexAttrib3fv = function (index, value) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.vertexAttrib3fv + ',' + index + ',' + processArray(value), + true + ) + } + + vertexAttrib4fv = function (index, value) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.vertexAttrib4fv + ',' + index + ',' + processArray(value), + true + ) + } + + vertexAttribPointer = function (index, size, type, normalized, stride, offset) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.vertexAttribPointer + ',' + index + ',' + size + ',' + type + ',' + Number(normalized) + ',' + stride + ',' + offset, + true + ) + } + + viewport = function (x, y, width, height) { + WebGLRenderingContext.GBridge.callNative( + this._canvas.id, + GLmethod.viewport + ',' + x + ',' + y + ',' + width + ',' + height, + true + ) + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Shader.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Shader.js new file mode 100644 index 0000000..a763886 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Shader.js @@ -0,0 +1,22 @@ +import {getTransferedObjectUUID} from './classUtils'; + +const name = 'WebGLShader'; + +function uuid(id) { + return getTransferedObjectUUID(name, id); +} + +export default class WebGLShader { + className = name; + + constructor(id, type) { + this.id = id; + this.type = type; + } + + static uuid = uuid; + + uuid() { + return uuid(this.id); + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/ShaderPrecisionFormat.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/ShaderPrecisionFormat.js new file mode 100644 index 0000000..208d6c1 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/ShaderPrecisionFormat.js @@ -0,0 +1,11 @@ +export default class WebGLShaderPrecisionFormat { + className = 'WebGLShaderPrecisionFormat'; + + constructor({ + rangeMin, rangeMax, precision + }) { + this.rangeMin = rangeMin; + this.rangeMax = rangeMax; + this.precision = precision; + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Texture.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Texture.js new file mode 100644 index 0000000..de4d806 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/Texture.js @@ -0,0 +1,22 @@ +import {getTransferedObjectUUID} from './classUtils'; + +const name = 'WebGLTexture'; + +function uuid(id) { + return getTransferedObjectUUID(name, id); +} + +export default class WebGLTexture { + className = name; + + constructor(id, type) { + this.id = id; + this.type = type; + } + + static uuid = uuid; + + uuid() { + return uuid(this.id); + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/UniformLocation.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/UniformLocation.js new file mode 100644 index 0000000..f5e99dc --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/UniformLocation.js @@ -0,0 +1,22 @@ +import {getTransferedObjectUUID} from './classUtils'; + +const name = 'WebGLUniformLocation'; + +function uuid(id) { + return getTransferedObjectUUID(name, id); +} + +export default class WebGLUniformLocation { + className = name; + + constructor(id, type) { + this.id = id; + this.type = type; + } + + static uuid = uuid; + + uuid() { + return uuid(this.id); + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/classUtils.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/classUtils.js new file mode 100644 index 0000000..88716be --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/context-webgl/classUtils.js @@ -0,0 +1,3 @@ +export function getTransferedObjectUUID(name, id) { + return `${name.toLowerCase()}-${id}`; +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/canvas.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/canvas.js new file mode 100644 index 0000000..a8d9bb9 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/canvas.js @@ -0,0 +1,74 @@ +import GContext2D from '../context-2d/RenderingContext'; +import GContextWebGL from '../context-webgl/RenderingContext'; + +export default class GCanvas { + + // static GBridge = null; + + id = null; + + _needRender = true; + + constructor(id, { disableAutoSwap }) { + this.id = id; + + this._disableAutoSwap = disableAutoSwap; + if (disableAutoSwap) { + this._swapBuffers = () => { + GCanvas.GBridge.render(this.id); + } + } + } + + getContext(type) { + + let context = null; + + if (type.match(/webgl/i)) { + context = new GContextWebGL(this); + + context.componentId = this.id; + + if (!this._disableAutoSwap) { + const render = () => { + if (this._needRender) { + GCanvas.GBridge.render(this.id); + this._needRender = false; + } + } + setInterval(render, 16); + } + + GCanvas.GBridge.callSetContextType(this.id, 1); // 0 for 2d; 1 for webgl + } else if (type.match(/2d/i)) { + context = new GContext2D(this); + + context.componentId = this.id; + +// const render = ( callback ) => { +// +// const commands = context._drawCommands; +// context._drawCommands = ''; +// +// GCanvas.GBridge.render2d(this.id, commands, callback); +// this._needRender = false; +// } +// //draw方法触发 +// context._flush = render; +// //setInterval(render, 16); + + GCanvas.GBridge.callSetContextType(this.id, 0); + } else { + throw new Error('not supported context ' + type); + } + + return context; + + } + + reset() { + GCanvas.GBridge.callReset(this.id); + } + + +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/image.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/image.js new file mode 100644 index 0000000..9499a51 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/image.js @@ -0,0 +1,96 @@ +let incId = 1; + +const noop = function () { }; + +class GImage { + + static GBridge = null; + + constructor() { + this._id = incId++; + this._width = 0; + this._height = 0; + this._src = undefined; + this._onload = noop; + this._onerror = noop; + this.complete = false; + } + + get width() { + return this._width; + } + set width(v) { + this._width = v; + } + + get height() { + return this._height; + } + + set height(v) { + this._height = v; + } + + get src() { + return this._src; + } + + set src(v) { + + if (v.startsWith('//')) { + v = 'http:' + v; + } + + this._src = v; + + GImage.GBridge.perloadImage([this._src, this._id], (data) => { + if (typeof data === 'string') { + data = JSON.parse(data); + } + if (data.error) { + var evt = { type: 'error', target: this }; + this.onerror(evt); + } else { + this.complete = true; + this.width = typeof data.width === 'number' ? data.width : 0; + this.height = typeof data.height === 'number' ? data.height : 0; + var evt = { type: 'load', target: this }; + this.onload(evt); + } + }); + } + + addEventListener(name, listener) { + if (name === 'load') { + this.onload = listener; + } else if (name === 'error') { + this.onerror = listener; + } + } + + removeEventListener(name, listener) { + if (name === 'load') { + this.onload = noop; + } else if (name === 'error') { + this.onerror = noop; + } + } + + get onload() { + return this._onload; + } + + set onload(v) { + this._onload = v; + } + + get onerror() { + return this._onerror; + } + + set onerror(v) { + this._onerror = v; + } +} + +export default GImage; \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/tool.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/tool.js new file mode 100644 index 0000000..d3fb398 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/env/tool.js @@ -0,0 +1,24 @@ + +export function ArrayBufferToBase64 (buffer) { + var binary = ''; + var bytes = new Uint8ClampedArray(buffer); + for (var len = bytes.byteLength, i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); +} + +export function Base64ToUint8ClampedArray(base64String) { + const padding = '='.repeat((4 - base64String.length % 4) % 4); + const base64 = (base64String + padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + + const rawData = atob(base64); + const outputArray = new Uint8ClampedArray(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/index.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/index.js new file mode 100644 index 0000000..a34ad58 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/gcanvas/index.js @@ -0,0 +1,39 @@ +import GCanvas from './env/canvas'; +import GImage from './env/image'; + +import GWebGLRenderingContext from './context-webgl/RenderingContext'; +import GContext2D from './context-2d/RenderingContext'; + +import GBridgeWeex from './bridge/bridge-weex'; + +export let Image = GImage; + +export let WeexBridge = GBridgeWeex; + +export function enable(el, { bridge, debug, disableAutoSwap, disableComboCommands } = {}) { + + const GBridge = GImage.GBridge = GCanvas.GBridge = GWebGLRenderingContext.GBridge = GContext2D.GBridge = bridge; + + GBridge.callEnable(el.ref, [ + 0, // renderMode: 0--RENDERMODE_WHEN_DIRTY, 1--RENDERMODE_CONTINUOUSLY + -1, // hybridLayerType: 0--LAYER_TYPE_NONE 1--LAYER_TYPE_SOFTWARE 2--LAYER_TYPE_HARDWARE + false, // supportScroll + false, // newCanvasMode + 1, // compatible + 'white',// clearColor + false // sameLevel: newCanvasMode = true && true => GCanvasView and Webview is same level + ]); + + if (debug === true) { + GBridge.callEnableDebug(); + } + if (disableComboCommands) { + GBridge.callEnableDisableCombo(); + } + + var canvas = new GCanvas(el.ref, { disableAutoSwap }); + canvas.width = el.style.width; + canvas.height = el.style.height; + + return canvas; +}; \ No newline at end of file diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/uqrcode/package.json b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/uqrcode/package.json new file mode 100644 index 0000000..0932bc4 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/uqrcode/package.json @@ -0,0 +1,12 @@ +{ + "name": "uqrcode", + "version": "3.5.1", + "description": "", + "main": "uqrcode.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/uqrcode/uqrcode.js b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/uqrcode/uqrcode.js new file mode 100644 index 0000000..2290ab3 --- /dev/null +++ b/uni-im示例/uni_modules/Sansnn-uQRCode/js_sdk/uqrcode/uqrcode.js @@ -0,0 +1,34 @@ +//--------------------------------------------------------------------- +// uQRCode二维码生成插件 v4.0.6 +// +// uQRCode是一款基于Javascript环境开发的二维码生成插件,适用所有Javascript运行环境的前端应用和Node.js。 +// +// Copyright (c) Sansnn uQRCode All rights reserved. +// +// Licensed under the Apache License, Version 2.0. +// http://www.apache.org/licenses/LICENSE-2.0 +// +// github地址: +// https://github.com/Sansnn/uQRCode +// +// npm地址: +// https://www.npmjs.com/package/uqrcodejs +// +// uni-app插件市场地址: +// https://ext.dcloud.net.cn/plugin?id=1287 +// +// 复制使用请保留本段注释,感谢支持开源! +// +//--------------------------------------------------------------------- + +//--------------------------------------------------------------------- +// 当前文件格式为 es,将 bundle 保留为 ES 模块文件,适用于其他打包工具以及支持 + + diff --git a/uni-im示例/uni_modules/uni-badge/package.json b/uni-im示例/uni_modules/uni-badge/package.json new file mode 100644 index 0000000..b0bac93 --- /dev/null +++ b/uni-im示例/uni_modules/uni-badge/package.json @@ -0,0 +1,85 @@ +{ + "id": "uni-badge", + "displayName": "uni-badge 数字角标", + "version": "1.2.2", + "description": "数字角标(徽章)组件,在元素周围展示消息提醒,一般用于列表、九宫格、按钮等地方。", + "keywords": [ + "", + "badge", + "uni-ui", + "uniui", + "数字角标", + "徽章" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "y", + "联盟": "y" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-badge/readme.md b/uni-im示例/uni_modules/uni-badge/readme.md new file mode 100644 index 0000000..bdf175d --- /dev/null +++ b/uni-im示例/uni_modules/uni-badge/readme.md @@ -0,0 +1,10 @@ +## Badge 数字角标 +> **组件名:uni-badge** +> 代码块: `uBadge` + +数字角标一般和其它控件(列表、9宫格等)配合使用,用于进行数量提示,默认为实心灰色背景, + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-badge) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/uni-im示例/uni_modules/uni-captcha/changelog.md b/uni-im示例/uni_modules/uni-captcha/changelog.md new file mode 100644 index 0000000..fdfea67 --- /dev/null +++ b/uni-im示例/uni_modules/uni-captcha/changelog.md @@ -0,0 +1,37 @@ +## 0.6.4(2023-01-16) +- 修复 部分情况下APP端无法获取验证码的问题 +## 0.6.3(2023-01-11) +- 修复 抖音小程序无法显示的Bug +- 修复 刷新时兼容 device_uuid +## 0.6.1(2022-06-23) +- 修复:部分返回值,不符合响应体规范的问题 +## 0.6.0(2022-05-27) +- 新增:支持在`uni-config-center`中根据场景值配置 +- 修复:弹窗式验证码,输入内容后点击取消,重新打开验证码的值仍然存在的问题 +## 0.5.2(2022-05-19) +- 修复在Vue3的兼容问题 +## 0.5.1(2022-05-18) +- 修复在某些情况下微信小程序端验证码显示错误的问题 +## 0.5.0(2022-05-17) +- 新增支持在`uni-captcha-co`->`config`配置验证码 +## 0.4.1(2022-05-16) +- 新增示例项目 +## 0.4.0(2022-05-16) +- 集成创建、刷新、显示验证码的云端一体验证码组件 +- 云对象`uni-captcha-co`集成获取验证码的api,`getImageCaptcha` +## 0.3.1(2022-05-13) +- 新增 返回值符合响应体规范 +## 0.3.0(2022-05-13) +- 新增 支持 uni-config-center 配置 +## 0.2.2(2022-04-25) +- 修复 0.2.1 版本引起的使用 image 组件验证码不显示的Bug +## 0.2.1(2022-04-18) +- 更新 优化字体 +## 0.2.0(2022-04-14) +- 新增 使用 svg 表现形式更好 +- 新增 使用字体,可以任意替换默认字体 +- 新增 支持设置字体大小 +- 新增 支持忽略某些字符 +- 注意 更新之后请重新上传公共模块 +## 0.1.0(2021-03-01) +- 调整为uni_modules目录规范 diff --git a/uni-im示例/uni_modules/uni-captcha/components/uni-captcha/uni-captcha.vue b/uni-im示例/uni_modules/uni-captcha/components/uni-captcha/uni-captcha.vue new file mode 100644 index 0000000..3d33343 --- /dev/null +++ b/uni-im示例/uni_modules/uni-captcha/components/uni-captcha/uni-captcha.vue @@ -0,0 +1,167 @@ + + + + + \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-captcha/components/uni-popup-captcha/uni-popup-captcha.vue b/uni-im示例/uni_modules/uni-captcha/components/uni-popup-captcha/uni-popup-captcha.vue new file mode 100644 index 0000000..b89b003 --- /dev/null +++ b/uni-im示例/uni_modules/uni-captcha/components/uni-popup-captcha/uni-popup-captcha.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-captcha/package.json b/uni-im示例/uni_modules/uni-captcha/package.json new file mode 100644 index 0000000..ea5c89d --- /dev/null +++ b/uni-im示例/uni_modules/uni-captcha/package.json @@ -0,0 +1,81 @@ +{ + "id": "uni-captcha", + "displayName": "uni-captcha", + "version": "0.6.4", + "description": "云端一体图形验证码组件", + "keywords": [ + "captcha", + "图形验证码", + "人机验证", + "防刷", + "防脚本" +], + "repository": "https://gitee.com/dcloud/uni-captcha", + "engines": { + "HBuilderX": "^3.1.0" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "", + "type": "unicloud-template-function" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "u", + "app-nvue": "u" + }, + "H5-mobile": { + "Safari": "u", + "Android Browser": "u", + "微信浏览器(Android)": "u", + "QQ浏览器(Android)": "u" + }, + "H5-pc": { + "Chrome": "u", + "IE": "u", + "Edge": "u", + "Firefox": "u", + "Safari": "u" + }, + "小程序": { + "微信": "u", + "阿里": "u", + "百度": "u", + "字节跳动": "u", + "QQ": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "u" + } + } + } + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-captcha/readme.md b/uni-im示例/uni_modules/uni-captcha/readme.md new file mode 100644 index 0000000..d929f63 --- /dev/null +++ b/uni-im示例/uni_modules/uni-captcha/readme.md @@ -0,0 +1,3 @@ +

+文档已移至 uni-captcha文档 +

\ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/common/uni-captcha/LICENSE.md b/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/common/uni-captcha/LICENSE.md new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/common/uni-captcha/LICENSE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/common/uni-captcha/fonts/font.ttf b/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/common/uni-captcha/fonts/font.ttf new file mode 100644 index 0000000..a60ce88 Binary files /dev/null and b/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/common/uni-captcha/fonts/font.ttf differ diff --git a/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/common/uni-captcha/index.js b/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/common/uni-captcha/index.js new file mode 100644 index 0000000..6e5c3c8 --- /dev/null +++ b/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/common/uni-captcha/index.js @@ -0,0 +1 @@ +"use strict";var e=require("assert"),t=require("path");function n(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var o=n(e),r=n(t);const s={10001:"uni-captcha-create-fail",10002:"uni-captcha-verify-fail",10003:"uni-captcha-refresh-fail",10101:"uni-captcha-deviceId-required",10102:"uni-captcha-text-required",10103:"uni-captcha-verify-overdue",10104:"uni-captcha-verify-fail",50403:"uni-captcha-interior-fail"};function a(e){const t=.2*Math.random()-.1;switch(e.type){case"M":case"L":e.x+=t,e.y+=t;break;case"Q":case"C":e.x+=t,e.y+=t,e.x1+=t,e.y1+=t}return e}function i(e,t,n,o,r,s,a){let i,l,c,u,p,h;if(e<=0||e>=1)throw RangeError("spliteCurveAt requires position > 0 && position < 1");return u=[],p=0,i={},l={},c={},i.x=t,i.y=n,l.x=o,l.y=r,c.x=s,c.y=a,h=e,u[p++]=i.x,u[p++]=i.y,u[p++]=i.x+=(l.x-i.x)*h,u[p++]=i.y+=(l.y-i.y)*h,l.x+=(c.x-l.x)*h,l.y+=(c.y-l.y)*h,u[p++]=i.x+(l.x-i.x)*h,u[p++]=i.y+(l.y-i.y)*h,u[p++]=l.x,u[p++]=l.y,u[p++]=c.x,u[p++]=c.y,u}function l(e,t){return Math.random()*(t-e)+e}var c=function(e,t){const n=e[0];o.default(n,"expect a string");const r=t.fontSize,s=r/t.font.unitsPerEm,c=t.font.charToGlyph(n),u=c.advanceWidth?c.advanceWidth*s:0,p=t.x-u/2,h=(t.ascender+t.descender)*s,f=t.y+h/2,d=c.getPath(p,f,r);d.commands.forEach(a),d.commands=function(e,t){const n=[];for(let o=0;ot.truncateLineProbability){const e=l(-.1,.1);n.push(r),n.push({type:"L",x:(r.x+s.x)/2+e,y:(r.y+s.y)/2+e})}else n.push(r)}else if("Q"===r.type&&o>=1){const s=e[o-1];if(("L"===s.type||"M"===s.type)&&Math.random()>t.truncateCurveProbability){const e=s.x,o=s.y,a=l(-.1,.1),c=r.x1+a,u=r.y1+a,p=r.x+a,h=r.y+a,f=i(l(t.truncateCurvePositionMin,t.truncateCurvePositionMax),e,o,c,u,p,h),d={type:"Q",x1:f[2],y1:f[3],x:f[4],y:f[5]},g={type:"L",x:f[4],y:f[5]},m={type:"Q",x1:f[6],y1:f[7],x:f[8],y:f[9]},y={type:"L",x:f[8],y:f[9]};n.push(d),n.push(g),n.push(m),n.push(y)}}else n.push(r)}return n}(d.commands,t);return d.toPathData()};function u(){this.table=new Uint16Array(16),this.trans=new Uint16Array(288)}function p(e,t){this.source=e,this.sourceIndex=0,this.tag=0,this.bitcount=0,this.dest=t,this.destLen=0,this.ltree=new u,this.dtree=new u}var h=new u,f=new u,d=new Uint8Array(30),g=new Uint16Array(30),m=new Uint8Array(30),y=new Uint16Array(30),v=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),b=new u,S=new Uint8Array(320);function x(e,t,n,o){var r,s;for(r=0;r>>=1,t}function O(e,t,n){if(!t)return n;for(;e.bitcount<24;)e.tag|=e.source[e.sourceIndex++]<>>16-t;return e.tag>>>=t,e.bitcount-=t,o+n}function w(e,t){for(;e.bitcount<24;)e.tag|=e.source[e.sourceIndex++]<>>=1,++r,n+=t.table[r],o-=t.table[r]}while(o>=0);return e.tag=s,e.bitcount-=r,t.trans[n+o]}function k(e,t,n){var o,r,s,a,i,l;for(o=O(e,5,257),r=O(e,5,1),s=O(e,4,4),a=0;a<19;++a)S[a]=0;for(a=0;a8;)e.sourceIndex--,e.bitcount-=8;if((t=256*(t=e.source[e.sourceIndex+1])+e.source[e.sourceIndex])!==(65535&~(256*e.source[e.sourceIndex+3]+e.source[e.sourceIndex+2])))return-3;for(e.sourceIndex+=4,n=t;n;--n)e.dest[e.destLen++]=e.source[e.sourceIndex++];return e.bitcount=0,0}!function(e,t){var n;for(n=0;n<7;++n)e.table[n]=0;for(e.table[7]=24,e.table[8]=152,e.table[9]=112,n=0;n<24;++n)e.trans[n]=256+n;for(n=0;n<144;++n)e.trans[24+n]=n;for(n=0;n<8;++n)e.trans[168+n]=280+n;for(n=0;n<112;++n)e.trans[176+n]=144+n;for(n=0;n<5;++n)t.table[n]=0;for(t.table[5]=32,n=0;n<32;++n)t.trans[n]=n}(h,f),x(d,g,4,3),x(m,y,2,1),d[28]=0,g[28]=258;var C=function(e,t){var n,o,r=new p(e,t);do{switch(n=E(r),O(r,2,0)){case 0:o=D(r);break;case 1:o=R(r,h,f);break;case 2:k(r,r.ltree,r.dtree),o=R(r,r.ltree,r.dtree);break;default:o=-3}if(0!==o)throw new Error("Data error")}while(!n);return r.destLenthis.x2&&(this.x2=e)),"number"==typeof t&&((isNaN(this.y1)||isNaN(this.y2))&&(this.y1=t,this.y2=t),tthis.y2&&(this.y2=t))},I.prototype.addX=function(e){this.addPoint(e,null)},I.prototype.addY=function(e){this.addPoint(null,e)},I.prototype.addBezier=function(e,t,n,o,r,s,a,i){const l=[e,t],c=[n,o],u=[r,s],p=[a,i];this.addPoint(e,t),this.addPoint(a,i);for(let e=0;e<=1;e++){const t=6*l[e]-12*c[e]+6*u[e],n=-3*l[e]+9*c[e]-9*u[e]+3*p[e],o=3*c[e]-3*l[e];if(0===n){if(0===t)continue;const n=-o/t;0=0&&n>0&&(e+=" "),e+=t(o)}return e}e=void 0!==e?e:2;let o="";for(let e=0;e=0&&e<=255,"Byte value should be between 0 and 255."),[e]},F.BYTE=H(1),A.CHAR=function(e){return[e.charCodeAt(0)]},F.CHAR=H(1),A.CHARARRAY=function(e){const t=[];for(let n=0;n>8&255,255&e]},F.USHORT=H(2),A.SHORT=function(e){return e>=32768&&(e=-(65536-e)),[e>>8&255,255&e]},F.SHORT=H(2),A.UINT24=function(e){return[e>>16&255,e>>8&255,255&e]},F.UINT24=H(3),A.ULONG=function(e){return[e>>24&255,e>>16&255,e>>8&255,255&e]},F.ULONG=H(4),A.LONG=function(e){return e>=2147483648&&(e=-(4294967296-e)),[e>>24&255,e>>16&255,e>>8&255,255&e]},F.LONG=H(4),A.FIXED=A.ULONG,F.FIXED=F.ULONG,A.FWORD=A.SHORT,F.FWORD=F.SHORT,A.UFWORD=A.USHORT,F.UFWORD=F.USHORT,A.LONGDATETIME=function(e){return[0,0,0,0,e>>24&255,e>>16&255,e>>8&255,255&e]},F.LONGDATETIME=H(8),A.TAG=function(e){return N.argument(4===e.length,"Tag should be exactly 4 ASCII characters."),[e.charCodeAt(0),e.charCodeAt(1),e.charCodeAt(2),e.charCodeAt(3)]},F.TAG=H(4),A.Card8=A.BYTE,F.Card8=F.BYTE,A.Card16=A.USHORT,F.Card16=F.USHORT,A.OffSize=A.BYTE,F.OffSize=F.BYTE,A.SID=A.USHORT,F.SID=F.USHORT,A.NUMBER=function(e){return e>=-107&&e<=107?[e+139]:e>=108&&e<=1131?[247+((e-=108)>>8),255&e]:e>=-1131&&e<=-108?[251+((e=-e-108)>>8),255&e]:e>=-32768&&e<=32767?A.NUMBER16(e):A.NUMBER32(e)},F.NUMBER=function(e){return A.NUMBER(e).length},A.NUMBER16=function(e){return[28,e>>8&255,255&e]},F.NUMBER16=H(3),A.NUMBER32=function(e){return[29,e>>24&255,e>>16&255,e>>8&255,255&e]},F.NUMBER32=H(5),A.REAL=function(e){let t=e.toString();const n=/\.(\d*?)(?:9{5,20}|0{5,20})\d{0,2}(?:e(.+)|$)/.exec(t);if(n){const o=parseFloat("1e"+((n[2]?+n[2]:0)+n[1].length));t=(Math.round(e*o)/o).toString()}let o="";for(let e=0,n=t.length;e>8&255,t[t.length]=255&o}return t},F.UTF16=function(e){return 2*e.length};const z={"x-mac-croatian":"ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®Š™´¨≠ŽØ∞±≤≥∆µ∂∑∏š∫ªºΩžø¿¡¬√ƒ≈ƫȅ ÀÃÕŒœĐ—“”‘’÷◊©⁄€‹›Æ»–·‚„‰ÂćÁčÈÍÎÏÌÓÔđÒÚÛÙıˆ˜¯πË˚¸Êæˇ","x-mac-cyrillic":"АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ†°Ґ£§•¶І®©™Ђђ≠Ѓѓ∞±≤≥іµґЈЄєЇїЉљЊњјЅ¬√ƒ≈∆«»… ЋћЌќѕ–—“”‘’÷„ЎўЏџ№Ёёяабвгдежзийклмнопрстуфхцчшщъыьэю","x-mac-gaelic":"ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØḂ±≤≥ḃĊċḊḋḞḟĠġṀæøṁṖṗɼƒſṠ«»… ÀÃÕŒœ–—“”‘’ṡẛÿŸṪ€‹›Ŷŷṫ·Ỳỳ⁊ÂÊÁËÈÍÎÏÌÓÔ♣ÒÚÛÙıÝýŴŵẄẅẀẁẂẃ","x-mac-greek":"Ĺ²É³ÖÜ΅àâä΄¨çéèê룙î‰ôö¦€ùûü†ΓΔΘΛΞΠß®©ΣΪ§≠°·Α±≤≥¥ΒΕΖΗΙΚΜΦΫΨΩάΝ¬ΟΡ≈Τ«»… ΥΧΆΈœ–―“”‘’÷ΉΊΌΎέήίόΏύαβψδεφγηιξκλμνοπώρστθωςχυζϊϋΐΰ­","x-mac-icelandic":"ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûüݰ¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€ÐðÞþý·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ","x-mac-inuit":"ᐃᐄᐅᐆᐊᐋᐱᐲᐳᐴᐸᐹᑉᑎᑏᑐᑑᑕᑖᑦᑭᑮᑯᑰᑲᑳᒃᒋᒌᒍᒎᒐᒑ°ᒡᒥᒦ•¶ᒧ®©™ᒨᒪᒫᒻᓂᓃᓄᓅᓇᓈᓐᓯᓰᓱᓲᓴᓵᔅᓕᓖᓗᓘᓚᓛᓪᔨᔩᔪᔫᔭ… ᔮᔾᕕᕖᕗ–—“”‘’ᕘᕙᕚᕝᕆᕇᕈᕉᕋᕌᕐᕿᖀᖁᖂᖃᖄᖅᖏᖐᖑᖒᖓᖔᖕᙱᙲᙳᙴᙵᙶᖖᖠᖡᖢᖣᖤᖥᖦᕼŁł","x-mac-ce":"ÄĀāÉĄÖÜáąČäčĆć鏟ĎíďĒēĖóėôöõúĚěü†°Ę£§•¶ß®©™ę¨≠ģĮįĪ≤≥īĶ∂∑łĻļĽľĹĺŅņѬ√ńŇ∆«»… ňŐÕőŌ–—“”‘’÷◊ōŔŕŘ‹›řŖŗŠ‚„šŚśÁŤťÍŽžŪÓÔūŮÚůŰűŲųÝýķŻŁżĢˇ",macintosh:"ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€‹›fifl‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ","x-mac-romanian":"ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ĂȘ∞±≤≥¥µ∂∑∏π∫ªºΩăș¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€‹›Țț‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ","x-mac-turkish":"ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸĞğİıŞş‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙˆ˜¯˘˙˚¸˝˛ˇ"};P.MACSTRING=function(e,t,n,o){const r=z[o];if(void 0===r)return;let s="";for(let o=0;o=-128&&e<=127}function X(e,t,n){let o=0;const r=e.length;for(;t>8&255,t+256&255)}return s}A.MACSTRING=function(e,t){const n=function(e){if(!W){W={};for(let e in z)W[e]=new String(e)}const t=W[e];if(void 0===t)return;if(_){const e=_.get(t);if(void 0!==e)return e}const n=z[e];if(void 0===n)return;const o={};for(let e=0;e=128&&(r=n[r],void 0===r))return;o[t]=r}return o},F.MACSTRING=function(e,t){const n=A.MACSTRING(e,t);return void 0!==n?n.length:0},A.VARDELTAS=function(e){let t=0;const n=[];for(;t=-128&&o<=127?V(e,t,n):j(e,t,n)}return n},A.INDEX=function(e){let t=1;const n=[t],o=[];for(let r=0;r>8,t[s+1]=255&a,t=t.concat(o[n])}return t},F.TABLE=function(e){let t=0;const n=e.fields.length;for(let o=0;o0)return new ce(this.data,this.offset+t).parseStruct(e)},ce.prototype.parseListOfLists=function(e){const t=this.parseOffset16List(),n=t.length,o=this.relativeOffset,r=new Array(n);for(let o=0;o=0;r-=1){const n=pe.getUShort(e,t+4+8*r),s=pe.getUShort(e,t+4+8*r+2);if(3===n&&(0===s||1===s||10===s)){o=pe.getULong(e,t+4+8*r+4);break}}if(-1===o)throw new Error("No valid cmap sub-tables found.");const r=new pe.Parser(e,t+o);if(n.format=r.parseUShort(),12===n.format)!function(e,t){let n;t.parseUShort(),e.length=t.parseULong(),e.language=t.parseULong(),e.groupCount=n=t.parseULong(),e.glyphIndexMap={};for(let o=0;o>1,t.skip("uShort",3),e.glyphIndexMap={};const a=new pe.Parser(n,o+r+14),i=new pe.Parser(n,o+r+16+2*s),l=new pe.Parser(n,o+r+16+4*s),c=new pe.Parser(n,o+r+16+6*s);let u=o+r+16+8*s;for(let t=0;t0?(s=e.parseByte(),0==(t&r)&&(s=-s),s=n+s):s=(t&r)>0?n:n+e.parseShort(),s}function Ee(e,t,n){const o=new pe.Parser(t,n);let r,s;if(e.numberOfContours=o.parseShort(),e._xMin=o.parseShort(),e._yMin=o.parseShort(),e._xMax=o.parseShort(),e._yMax=o.parseShort(),e.numberOfContours>0){const t=e.endPointIndices=[];for(let n=0;n0){const t=o.parseByte();for(let n=0;n0){const a=[];let i;if(n>0){for(let e=0;e=0,a.push(i);let e=0;for(let t=0;t0?(2&r)>0?(n.dx=o.parseShort(),n.dy=o.parseShort()):n.matchedPoints=[o.parseUShort(),o.parseUShort()]:(2&r)>0?(n.dx=o.parseChar(),n.dy=o.parseChar()):n.matchedPoints=[o.parseByte(),o.parseByte()],(8&r)>0?n.xScale=n.yScale=o.parseF2Dot14():(64&r)>0?(n.xScale=o.parseF2Dot14(),n.yScale=o.parseF2Dot14()):(128&r)>0&&(n.xScale=o.parseF2Dot14(),n.scale01=o.parseF2Dot14(),n.scale10=o.parseF2Dot14(),n.yScale=o.parseF2Dot14()),e.components.push(n),t=!!(32&r)}if(256&r){e.instructionLength=o.parseUShort(),e.instructions=[];for(let t=0;tt.points.length-1||o.matchedPoints[1]>r.points.length-1)throw Error("Matched points out of range in "+t.name);const n=t.points[o.matchedPoints[0]];let s=r.points[o.matchedPoints[1]];const a={xScale:o.xScale,scale01:o.scale01,scale10:o.scale10,yScale:o.yScale,dx:0,dy:0};s=Oe([s],a)[0],a.dx=n.x-s.x,a.dy=n.y-s.y,e=Oe(r.points,a)}t.points=t.points.concat(e)}}return we(t.points)}var Re={getPath:we,parse:function(e,t,n,o){const r=new Ie.GlyphSet(o);for(let s=0;s>4,s=15&o;if(15===r)break;if(t+=n[r],15===s)break;t+=n[s]}return parseFloat(t)}(e);if(t>=32&&t<=246)return t-139;if(t>=247&&t<=250)return n=e.parseByte(),256*(t-247)+n+108;if(t>=251&&t<=254)return n=e.parseByte(),256*-(t-251)-n-108;throw new Error("Invalid b0 "+t)}function Pe(e,t,n){t=void 0!==t?t:0;const o=new pe.Parser(e,t),r=[];let s=[];for(n=void 0!==n?n:e.length;o.relativeOffset>1,l.length=0,d=!0}return function n(p){let x,U,T,E,O,w,k,R,D,C,L,I,M=0;for(;M1&&!d&&(v=l.shift()+h,d=!0),y+=l.pop(),b(m,y);break;case 5:for(;l.length>0;)m+=l.shift(),y+=l.shift(),i.lineTo(m,y);break;case 6:for(;l.length>0&&(m+=l.shift(),i.lineTo(m,y),0!==l.length);)y+=l.shift(),i.lineTo(m,y);break;case 7:for(;l.length>0&&(y+=l.shift(),i.lineTo(m,y),0!==l.length);)m+=l.shift(),i.lineTo(m,y);break;case 8:for(;l.length>0;)o=m+l.shift(),r=y+l.shift(),s=o+l.shift(),a=r+l.shift(),m=s+l.shift(),y=a+l.shift(),i.curveTo(o,r,s,a,m,y);break;case 10:O=l.pop()+u,w=c[O],w&&n(w);break;case 11:return;case 12:switch(B=p[M],M+=1,B){case 35:o=m+l.shift(),r=y+l.shift(),s=o+l.shift(),a=r+l.shift(),k=s+l.shift(),R=a+l.shift(),D=k+l.shift(),C=R+l.shift(),L=D+l.shift(),I=C+l.shift(),m=L+l.shift(),y=I+l.shift(),l.shift(),i.curveTo(o,r,s,a,k,R),i.curveTo(D,C,L,I,m,y);break;case 34:o=m+l.shift(),r=y,s=o+l.shift(),a=r+l.shift(),k=s+l.shift(),R=a,D=k+l.shift(),C=a,L=D+l.shift(),I=y,m=L+l.shift(),i.curveTo(o,r,s,a,k,R),i.curveTo(D,C,L,I,m,y);break;case 36:o=m+l.shift(),r=y+l.shift(),s=o+l.shift(),a=r+l.shift(),k=s+l.shift(),R=a,D=k+l.shift(),C=a,L=D+l.shift(),I=C+l.shift(),m=L+l.shift(),i.curveTo(o,r,s,a,k,R),i.curveTo(D,C,L,I,m,y);break;case 37:o=m+l.shift(),r=y+l.shift(),s=o+l.shift(),a=r+l.shift(),k=s+l.shift(),R=a+l.shift(),D=k+l.shift(),C=R+l.shift(),L=D+l.shift(),I=C+l.shift(),Math.abs(L-m)>Math.abs(I-y)?m=L+l.shift():y=I+l.shift(),i.curveTo(o,r,s,a,k,R),i.curveTo(D,C,L,I,m,y);break;default:console.log("Glyph "+t.index+": unknown operator 1200"+B),l.length=0}break;case 14:l.length>0&&!d&&(v=l.shift()+h,d=!0),g&&(i.closePath(),g=!1);break;case 18:S();break;case 19:case 20:S(),M+=f+7>>3;break;case 21:l.length>2&&!d&&(v=l.shift()+h,d=!0),y+=l.pop(),m+=l.pop(),b(m,y);break;case 22:l.length>1&&!d&&(v=l.shift()+h,d=!0),m+=l.pop(),b(m,y);break;case 23:S();break;case 24:for(;l.length>2;)o=m+l.shift(),r=y+l.shift(),s=o+l.shift(),a=r+l.shift(),m=s+l.shift(),y=a+l.shift(),i.curveTo(o,r,s,a,m,y);m+=l.shift(),y+=l.shift(),i.lineTo(m,y);break;case 25:for(;l.length>6;)m+=l.shift(),y+=l.shift(),i.lineTo(m,y);o=m+l.shift(),r=y+l.shift(),s=o+l.shift(),a=r+l.shift(),m=s+l.shift(),y=a+l.shift(),i.curveTo(o,r,s,a,m,y);break;case 26:for(l.length%2&&(m+=l.shift());l.length>0;)o=m,r=y+l.shift(),s=o+l.shift(),a=r+l.shift(),m=s,y=a+l.shift(),i.curveTo(o,r,s,a,m,y);break;case 27:for(l.length%2&&(y+=l.shift());l.length>0;)o=m+l.shift(),r=y,s=o+l.shift(),a=r+l.shift(),m=s+l.shift(),y=a,i.curveTo(o,r,s,a,m,y);break;case 28:x=p[M],U=p[M+1],l.push((x<<24|U<<16)>>16),M+=2;break;case 29:O=l.pop()+e.gsubrsBias,w=e.gsubrs[O],w&&n(w);break;case 30:for(;l.length>0&&(o=m,r=y+l.shift(),s=o+l.shift(),a=r+l.shift(),m=s+l.shift(),y=a+(1===l.length?l.shift():0),i.curveTo(o,r,s,a,m,y),0!==l.length);)o=m+l.shift(),r=y,s=o+l.shift(),a=r+l.shift(),y=a+l.shift(),m=s+(1===l.length?l.shift():0),i.curveTo(o,r,s,a,m,y);break;case 31:for(;l.length>0&&(o=m+l.shift(),r=y,s=o+l.shift(),a=r+l.shift(),y=a+l.shift(),m=s+(1===l.length?l.shift():0),i.curveTo(o,r,s,a,m,y),0!==l.length);)o=m,r=y+l.shift(),s=o+l.shift(),a=r+l.shift(),m=s+l.shift(),y=a+(1===l.length?l.shift():0),i.curveTo(o,r,s,a,m,y);break;default:B<32?console.log("Glyph "+t.index+": unknown operator "+B):B<247?l.push(B-139):B<251?(x=p[M],M+=1,l.push(256*(B-247)+x+108)):B<255?(x=p[M],M+=1,l.push(256*-(B-251)-x-108)):(x=p[M],U=p[M+1],T=p[M+2],E=p[M+3],M+=4,l.push((x<<24|U<<16|T<<8|E)/65536))}}}(n),t.advanceWidth=v,i}function Ve(e,t){let n,o=de.indexOf(e);return o>=0&&(n=o),o=t.indexOf(e),o>=0?n=o+de.length:(n=de.length+t.length,t.push(e)),n}function je(e,t,n){const o={};for(let r=0;r=o)throw new Error("CFF table CID Font FDSelect has bad FD index value "+s+" (FD count "+o+")");r.push(s)}else{if(3!==i)throw new Error("CFF Table CID Font FDSelect table has unsupported format "+i);{const e=a.parseCard16();let t,i=a.parseCard16();if(0!==i)throw new Error("CFF Table CID Font FDSelect format 3 range has bad initial GID "+i);for(let l=0;l=o)throw new Error("CFF table CID Font FDSelect has bad FD index value "+s+" (FD count "+o+")");if(t>n)throw new Error("CFF Table CID Font FDSelect format 3 range has bad GID "+t);for(;i=1&&(n.ulCodePageRange1=o.parseULong(),n.ulCodePageRange2=o.parseULong()),n.version>=2&&(n.sxHeight=o.parseShort(),n.sCapHeight=o.parseShort(),n.usDefaultChar=o.parseUShort(),n.usBreakChar=o.parseUShort(),n.usMaxContent=o.parseUShort()),n},make:function(e){return new oe.Table("OS/2",[{name:"version",type:"USHORT",value:3},{name:"xAvgCharWidth",type:"SHORT",value:0},{name:"usWeightClass",type:"USHORT",value:0},{name:"usWidthClass",type:"USHORT",value:0},{name:"fsType",type:"USHORT",value:0},{name:"ySubscriptXSize",type:"SHORT",value:650},{name:"ySubscriptYSize",type:"SHORT",value:699},{name:"ySubscriptXOffset",type:"SHORT",value:0},{name:"ySubscriptYOffset",type:"SHORT",value:140},{name:"ySuperscriptXSize",type:"SHORT",value:650},{name:"ySuperscriptYSize",type:"SHORT",value:699},{name:"ySuperscriptXOffset",type:"SHORT",value:0},{name:"ySuperscriptYOffset",type:"SHORT",value:479},{name:"yStrikeoutSize",type:"SHORT",value:49},{name:"yStrikeoutPosition",type:"SHORT",value:258},{name:"sFamilyClass",type:"SHORT",value:0},{name:"bFamilyType",type:"BYTE",value:0},{name:"bSerifStyle",type:"BYTE",value:0},{name:"bWeight",type:"BYTE",value:0},{name:"bProportion",type:"BYTE",value:0},{name:"bContrast",type:"BYTE",value:0},{name:"bStrokeVariation",type:"BYTE",value:0},{name:"bArmStyle",type:"BYTE",value:0},{name:"bLetterform",type:"BYTE",value:0},{name:"bMidline",type:"BYTE",value:0},{name:"bXHeight",type:"BYTE",value:0},{name:"ulUnicodeRange1",type:"ULONG",value:0},{name:"ulUnicodeRange2",type:"ULONG",value:0},{name:"ulUnicodeRange3",type:"ULONG",value:0},{name:"ulUnicodeRange4",type:"ULONG",value:0},{name:"achVendID",type:"CHARARRAY",value:"XXXX"},{name:"fsSelection",type:"USHORT",value:0},{name:"usFirstCharIndex",type:"USHORT",value:0},{name:"usLastCharIndex",type:"USHORT",value:0},{name:"sTypoAscender",type:"SHORT",value:0},{name:"sTypoDescender",type:"SHORT",value:0},{name:"sTypoLineGap",type:"SHORT",value:0},{name:"usWinAscent",type:"USHORT",value:0},{name:"usWinDescent",type:"USHORT",value:0},{name:"ulCodePageRange1",type:"ULONG",value:0},{name:"ulCodePageRange2",type:"ULONG",value:0},{name:"sxHeight",type:"SHORT",value:0},{name:"sCapHeight",type:"SHORT",value:0},{name:"usDefaultChar",type:"USHORT",value:0},{name:"usBreakChar",type:"USHORT",value:0},{name:"usMaxContext",type:"USHORT",value:0}],e)},unicodeRanges:gt,getUnicodeRange:function(e){for(let t=0;t=n.begin&&e=ye.length){const e=o.parseChar();n.names.push(o.parseString(e))}break;case 2.5:n.numberOfGlyphs=o.parseUShort(),n.offset=new Array(n.numberOfGlyphs);for(let e=0;et.value.tag?1:-1})),t.fields=t.fields.concat(o),t.fields=t.fields.concat(r),t}function kt(e,t,n){for(let n=0;n0){return e.glyphs.get(o).getMetrics()}}return n}function Rt(e){let t=0;for(let n=0;nm||void 0===l)&&m>0&&(l=m),c 123 are reserved for internal usage");f|=1<0?tt.make(w):void 0,D=yt.make(),C=$e.make(e.glyphs,{version:e.getEnglishName("version"),fullName:T,familyName:x,weightName:U,postScriptName:E,unitsPerEm:e.unitsPerEm,fontBBox:[0,d.yMin,d.ascender,d.advanceWidthMax]}),L=e.metas&&Object.keys(e.metas).length>0?Ut.make(e.metas):void 0,I=[g,m,y,v,k,S,D,C,b];R&&I.push(R),e.tables.gsub&&I.push(xt.make(e.tables.gsub)),L&&I.push(L);const M=wt(I),B=Et(M.encode()),G=M.fields;let N=!1;for(let e=0;e>>1,s=e[r].tag;if(s===t)return r;s>>1,s=e[r];if(s===t)return r;s=0)return o[r].script;if(t){const t={tag:e,script:{defaultLangSys:{reserved:0,reqFeatureIndex:65535,featureIndexes:[]},langSysRecords:[]}};return o.splice(-1-r,0,t),t.script}}},getLangSysTable:function(e,t,n){const o=this.getScriptTable(e,n);if(o){if(!t||"dflt"===t||"DFLT"===t)return o.defaultLangSys;const e=Ct(o.langSysRecords,t);if(e>=0)return o.langSysRecords[e].langSys;if(n){const n={tag:t,langSys:{reserved:0,reqFeatureIndex:65535,featureIndexes:[]}};return o.langSysRecords.splice(-1-e,0,n),n.langSys}}},getFeatureTable:function(e,t,n,o){const r=this.getLangSysTable(e,t,o);if(r){let e;const t=r.featureIndexes,s=this.font.tables[this.tableName].features;for(let o=0;o=s[o-1].tag,"Features must be added in alphabetical order."),e={tag:n,feature:{params:0,lookupListIndexes:[]}},s.push(e),t.push(o),e.feature}}},getLookupTables:function(e,t,n,o,r){const s=this.getFeatureTable(e,t,n,r),a=[];if(s){let e;const t=s.lookupListIndexes,n=this.font.tables[this.tableName].lookups;for(let r=0;r=0){const e=s.ligatureSets[c];for(let t=0;t0&&e<0?n:o<0&&e>0?-n:e*o},Zt={x:1,y:0,axis:"x",distance:function(e,t,n,o){return(n?e.xo:e.x)-(o?t.xo:t.x)},interpolate:function(e,t,n,o){let r,s,a,i,l,c,u;if(!o||o===this)return r=e.xo-t.xo,s=e.xo-n.xo,l=t.x-t.xo,c=n.x-n.xo,a=Math.abs(r),i=Math.abs(s),u=a+i,0===u?void(e.x=e.xo+(l+c)/2):void(e.x=e.xo+(l*i+c*a)/u);r=o.distance(e,t,!0,!0),s=o.distance(e,n,!0,!0),l=o.distance(t,t,!1,!0),c=o.distance(n,n,!1,!0),a=Math.abs(r),i=Math.abs(s),u=a+i,0!==u?Zt.setRelative(e,e,(l*i+c*a)/u,o,!0):Zt.setRelative(e,e,(l+c)/2,o,!0)},normalSlope:Number.NEGATIVE_INFINITY,setRelative:function(e,t,n,o,r){if(!o||o===this)return void(e.x=(r?t.xo:t.x)+n);const s=r?t.xo:t.x,a=r?t.yo:t.y,i=s+n*o.x,l=a+n*o.y;e.x=i+(e.y-l)/o.normalSlope},slope:0,touch:function(e){e.xTouched=!0},touched:function(e){return e.xTouched},untouch:function(e){e.xTouched=!1}},Qt={x:0,y:1,axis:"y",distance:function(e,t,n,o){return(n?e.yo:e.y)-(o?t.yo:t.y)},interpolate:function(e,t,n,o){let r,s,a,i,l,c,u;if(!o||o===this)return r=e.yo-t.yo,s=e.yo-n.yo,l=t.y-t.yo,c=n.y-n.yo,a=Math.abs(r),i=Math.abs(s),u=a+i,0===u?void(e.y=e.yo+(l+c)/2):void(e.y=e.yo+(l*i+c*a)/u);r=o.distance(e,t,!0,!0),s=o.distance(e,n,!0,!0),l=o.distance(t,t,!1,!0),c=o.distance(n,n,!1,!0),a=Math.abs(r),i=Math.abs(s),u=a+i,0!==u?Qt.setRelative(e,e,(l*i+c*a)/u,o,!0):Qt.setRelative(e,e,(l+c)/2,o,!0)},normalSlope:0,setRelative:function(e,t,n,o,r){if(!o||o===this)return void(e.y=(r?t.yo:t.y)+n);const s=r?t.xo:t.x,a=r?t.yo:t.y,i=s+n*o.x,l=a+n*o.y;e.y=l+o.normalSlope*(e.x-i)},slope:Number.POSITIVE_INFINITY,touch:function(e){e.yTouched=!0},touched:function(e){return e.yTouched},untouch:function(e){e.yTouched=!1}};function $t(e,t){this.x=e,this.y=t,this.axis=void 0,this.slope=t/e,this.normalSlope=-e/t,Object.freeze(this)}function Kt(e,t){const n=Math.sqrt(e*e+t*t);return t/=n,1===(e/=n)&&0===t?Zt:0===e&&1===t?Qt:new $t(e,t)}function Jt(e,t,n,o){this.x=this.xo=Math.round(64*e)/64,this.y=this.yo=Math.round(64*t)/64,this.lastPointOfContour=n,this.onCurve=o,this.prevPointOnContour=void 0,this.nextPointOnContour=void 0,this.xTouched=!1,this.yTouched=!1,Object.preventExtensions(this)}Object.freeze(Zt),Object.freeze(Qt),$t.prototype.distance=function(e,t,n,o){return this.x*Zt.distance(e,t,n,o)+this.y*Qt.distance(e,t,n,o)},$t.prototype.interpolate=function(e,t,n,o){let r,s,a,i,l,c,u;a=o.distance(e,t,!0,!0),i=o.distance(e,n,!0,!0),r=o.distance(t,t,!1,!0),s=o.distance(n,n,!1,!0),l=Math.abs(a),c=Math.abs(i),u=l+c,0!==u?this.setRelative(e,e,(r*c+s*l)/u,o,!0):this.setRelative(e,e,(r+s)/2,o,!0)},$t.prototype.setRelative=function(e,t,n,o,r){o=o||this;const s=r?t.xo:t.x,a=r?t.yo:t.y,i=s+n*o.x,l=a+n*o.y,c=o.normalSlope,u=this.slope,p=e.x,h=e.y;e.x=(u*p-c*i+l-h)/(u-c),e.y=u*(e.x-p)+h},$t.prototype.touch=function(e){e.xTouched=!0,e.yTouched=!0},Jt.prototype.nextTouched=function(e){let t=this.nextPointOnContour;for(;!e.touched(t)&&t!==this;)t=t.nextPointOnContour;return t},Jt.prototype.prevTouched=function(e){let t=this.prevPointOnContour;for(;!e.touched(t)&&t!==this;)t=t.prevPointOnContour;return t};const en=Object.freeze(new Jt(0,0)),tn={cvCutIn:17/16,deltaBase:9,deltaShift:.125,loop:1,minDis:1,autoFlip:!0};function nn(e,t){switch(this.env=e,this.stack=[],this.prog=t,e){case"glyf":this.zp0=this.zp1=this.zp2=1,this.rp0=this.rp1=this.rp2=0;case"prep":this.fv=this.pv=this.dpv=Zt,this.round=Wt}}function on(e){const t=e.tZone=new Array(e.gZone.length);for(let e=0;e=176&&o<=183)r+=o-176+1;else if(o>=184&&o<=191)r+=2*(o-184+1);else if(t&&1===s&&27===o)break}while(s>0);e.ip=r}function sn(e,t){exports.DEBUG&&console.log(t.step,"SVTCA["+e.axis+"]"),t.fv=t.pv=t.dpv=e}function an(e,t){exports.DEBUG&&console.log(t.step,"SPVTCA["+e.axis+"]"),t.pv=t.dpv=e}function ln(e,t){exports.DEBUG&&console.log(t.step,"SFVTCA["+e.axis+"]"),t.fv=e}function cn(e,t){const n=t.stack,o=n.pop(),r=n.pop(),s=t.z2[o],a=t.z1[r];let i,l;exports.DEBUG&&console.log("SPVTL["+e+"]",o,r),e?(i=s.y-a.y,l=a.x-s.x):(i=a.x-s.x,l=a.y-s.y),t.pv=t.dpv=Kt(i,l)}function un(e,t){const n=t.stack,o=n.pop(),r=n.pop(),s=t.z2[o],a=t.z1[r];let i,l;exports.DEBUG&&console.log("SFVTL["+e+"]",o,r),e?(i=s.y-a.y,l=a.x-s.x):(i=a.x-s.x,l=a.y-s.y),t.fv=Kt(i,l)}function pn(e){exports.DEBUG&&console.log(e.step,"POP[]"),e.stack.pop()}function hn(e,t){const n=t.stack.pop(),o=t.z0[n],r=t.fv,s=t.pv;exports.DEBUG&&console.log(t.step,"MDAP["+e+"]",n);let a=s.distance(o,en);e&&(a=t.round(a)),r.setRelative(o,en,a,s),r.touch(o),t.rp0=t.rp1=n}function fn(e,t){const n=t.z2,o=n.length-2;let r,s,a;exports.DEBUG&&console.log(t.step,"IUP["+e.axis+"]");for(let t=0;t1?"loop "+(t.loop-i)+": ":"")+"SHP["+(e?"rp1":"rp2")+"]",o)}t.loop=1}function gn(e,t){const n=t.stack,o=e?t.rp1:t.rp2,r=(e?t.z0:t.z1)[o],s=t.fv,a=t.pv,i=n.pop(),l=t.z2[t.contours[i]];let c=l;exports.DEBUG&&console.log(t.step,"SHC["+e+"]",i);const u=a.distance(r,r,!1,!0);do{c!==r&&s.setRelative(c,c,u,a),c=c.nextPointOnContour}while(c!==l)}function mn(e,t){const n=t.stack,o=e?t.rp1:t.rp2,r=(e?t.z0:t.z1)[o],s=t.fv,a=t.pv,i=n.pop();let l,c;switch(exports.DEBUG&&console.log(t.step,"SHZ["+e+"]",i),i){case 0:l=t.tZone;break;case 1:l=t.gZone;break;default:throw new Error("Invalid zone")}const u=a.distance(r,r,!1,!0),p=l.length-2;for(let e=0;e",i),t.stack.push(Math.round(64*i))}function xn(e,t){const n=t.stack,o=n.pop(),r=t.fv,s=t.pv,a=t.ppem,i=t.deltaBase+16*(e-1),l=t.deltaShift,c=t.z0;exports.DEBUG&&console.log(t.step,"DELTAP["+e+"]",o,n);for(let e=0;e>4)!==a)continue;let u=(15&o)-8;u>=0&&u++,exports.DEBUG&&console.log(t.step,"DELTAPFIX",e,"by",u*l);const p=c[e];r.setRelative(p,p,u*l,s)}}function Un(e,t){const n=t.stack,o=n.pop();exports.DEBUG&&console.log(t.step,"ROUND[]"),n.push(64*t.round(o/64))}function Tn(e,t){const n=t.stack,o=n.pop(),r=t.ppem,s=t.deltaBase+16*(e-1),a=t.deltaShift;exports.DEBUG&&console.log(t.step,"DELTAC["+e+"]",o,n);for(let e=0;e>4)!==r)continue;let i=(15&o)-8;i>=0&&i++;const l=i*a;exports.DEBUG&&console.log(t.step,"DELTACFIX",e,"by",l),t.cvt[e]+=l}}function En(e,t){const n=t.stack,o=n.pop(),r=n.pop(),s=t.z2[o],a=t.z1[r];let i,l;exports.DEBUG&&console.log("SDPVTL["+e+"]",o,r),e?(i=s.y-a.y,l=a.x-s.x):(i=a.x-s.x,l=a.y-s.y),t.dpv=Kt(i,l)}function On(e,t){const n=t.stack,o=t.prog;let r=t.ip;exports.DEBUG&&console.log(t.step,"PUSHB["+e+"]");for(let t=0;t=0?1:-1,m=Math.abs(m),e&&(v=s.cvt[i],o&&Math.abs(m-v)":"_")+(o?"R":"_")+(0===r?"Gr":1===r?"Bl":2===r?"Wh":"")+"]",e?i+"("+s.cvt[i]+","+v+")":"",l,"(d =",g,"->",y*m,")"),s.rp1=s.rp0,s.rp2=l,t&&(s.rp0=l)}function Rn(e){(e=e||{}).empty||(Nt(e.familyName,"When creating a new Font object, familyName is required."),Nt(e.styleName,"When creating a new Font object, styleName is required."),Nt(e.unitsPerEm,"When creating a new Font object, unitsPerEm is required."),Nt(e.ascender,"When creating a new Font object, ascender is required."),Nt(e.descender,"When creating a new Font object, descender is required."),Nt(e.descender<0,"Descender should be negative (e.g. -512)."),this.names={fontFamily:{en:e.familyName||" "},fontSubfamily:{en:e.styleName||" "},fullName:{en:e.fullName||e.familyName+" "+e.styleName},postScriptName:{en:e.postScriptName||e.familyName+e.styleName},designer:{en:e.designer||" "},designerURL:{en:e.designerURL||" "},manufacturer:{en:e.manufacturer||" "},manufacturerURL:{en:e.manufacturerURL||" "},license:{en:e.license||" "},licenseURL:{en:e.licenseURL||" "},version:{en:e.version||"Version 0.1"},description:{en:e.description||" "},copyright:{en:e.copyright||" "},trademark:{en:e.trademark||" "}},this.unitsPerEm=e.unitsPerEm||1e3,this.ascender=e.ascender,this.descender=e.descender,this.createdTimestamp=e.createdTimestamp,this.tables={os2:{usWeightClass:e.weightClass||this.usWeightClasses.MEDIUM,usWidthClass:e.widthClass||this.usWidthClasses.MEDIUM,fsSelection:e.fsSelection||this.fsSelectionValues.REGULAR}}),this.supported=!0,this.glyphs=new Ie.GlyphSet(this,e.glyphs||[]),this.encoding=new ve(this),this.substitution=new It(this),this.tables=this.tables||{},Object.defineProperty(this,"hinting",{get:function(){return this._hinting?this._hinting:"truetype"===this.outlinesFormat?this._hinting=new zt(this):void 0}})}function Dn(e,t){const n=JSON.stringify(e);let o=256;for(let e in t){let r=parseInt(e);if(r&&!(r<256)){if(JSON.stringify(t[e])===n)return r;o<=r&&(o=r+1)}}return t[o]=e,o}function Cn(e,t,n){const o=Dn(t.name,n);return[{name:"tag_"+e,type:"TAG",value:t.tag},{name:"minValue_"+e,type:"FIXED",value:t.minValue<<16},{name:"defaultValue_"+e,type:"FIXED",value:t.defaultValue<<16},{name:"maxValue_"+e,type:"FIXED",value:t.maxValue<<16},{name:"flags_"+e,type:"USHORT",value:0},{name:"nameID_"+e,type:"USHORT",value:o}]}function Ln(e,t,n){const o={},r=new pe.Parser(e,t);return o.tag=r.parseTag(),o.minValue=r.parseFixed(),o.defaultValue=r.parseFixed(),o.maxValue=r.parseFixed(),r.skip("uShort",1),o.name=n[r.parseUShort()]||{},o}function In(e,t,n,o){const r=[{name:"nameID_"+e,type:"USHORT",value:Dn(t.name,o)},{name:"flags_"+e,type:"USHORT",value:0}];for(let o=0;o2)return;const n=this.font;let o=this._prepState;if(!o||o.ppem!==t){let e=this._fpgmState;if(!e){nn.prototype=tn,e=this._fpgmState=new nn("fpgm",n.tables.fpgm),e.funcs=[],e.font=n,exports.DEBUG&&(console.log("---EXEC FPGM---"),e.step=-1);try{At(e)}catch(e){return console.log("Hinting error in FPGM:"+e),void(this._errorState=3)}}nn.prototype=e,o=this._prepState=new nn("prep",n.tables.prep),o.ppem=t;const r=n.tables.cvt;if(r){const e=o.cvt=new Array(r.length),s=t/n.unitsPerEm;for(let t=0;t1))try{return Ft(e,o)}catch(e){return this._errorState<1&&(console.log("Hinting error:"+e),console.log("Note: further hinting errors are silenced")),void(this._errorState=1)}},Ft=function(e,t){const n=t.ppem/t.font.unitsPerEm,o=n;let r,s,a,i=e.components;if(nn.prototype=t,i){const l=t.font;s=[],r=[];for(let e=0;e1?"loop "+(e.loop-n)+": ":"")+"SHPIX[]",a,r),o.setRelative(i,i,r),o.touch(i)}e.loop=1},function(e){const t=e.stack,n=e.rp1,o=e.rp2;let r=e.loop;const s=e.z0[n],a=e.z1[o],i=e.fv,l=e.dpv,c=e.z2;for(;r--;){const u=t.pop(),p=c[u];exports.DEBUG&&console.log(e.step,(e.loop>1?"loop "+(e.loop-r)+": ":"")+"IP[]",u,n,"<->",o),i.interpolate(p,s,a,l),i.touch(p)}e.loop=1},yn.bind(void 0,0),yn.bind(void 0,1),function(e){const t=e.stack,n=e.rp0,o=e.z0[n];let r=e.loop;const s=e.fv,a=e.pv,i=e.z1;for(;r--;){const n=t.pop(),l=i[n];exports.DEBUG&&console.log(e.step,(e.loop>1?"loop "+(e.loop-r)+": ":"")+"ALIGNRP[]",n),s.setRelative(l,o,0,a),s.touch(l)}e.loop=1},function(e){exports.DEBUG&&console.log(e.step,"RTDG[]"),e.round=qt},vn.bind(void 0,0),vn.bind(void 0,1),function(e){const t=e.prog;let n=e.ip;const o=e.stack,r=t[++n];exports.DEBUG&&console.log(e.step,"NPUSHB[]",r);for(let e=0;en?1:0)},function(e){const t=e.stack,n=t.pop(),o=t.pop();exports.DEBUG&&console.log(e.step,"GTEQ[]",n,o),t.push(o>=n?1:0)},function(e){const t=e.stack,n=t.pop(),o=t.pop();exports.DEBUG&&console.log(e.step,"EQ[]",n,o),t.push(n===o?1:0)},function(e){const t=e.stack,n=t.pop(),o=t.pop();exports.DEBUG&&console.log(e.step,"NEQ[]",n,o),t.push(n!==o?1:0)},function(e){const t=e.stack,n=t.pop();exports.DEBUG&&console.log(e.step,"ODD[]",n),t.push(Math.trunc(n)%2?1:0)},function(e){const t=e.stack,n=t.pop();exports.DEBUG&&console.log(e.step,"EVEN[]",n),t.push(Math.trunc(n)%2?0:1)},function(e){let t=e.stack.pop();exports.DEBUG&&console.log(e.step,"IF[]",t),t||(rn(e,!0),exports.DEBUG&&console.log(e.step,"EIF[]"))},function(e){exports.DEBUG&&console.log(e.step,"EIF[]")},function(e){const t=e.stack,n=t.pop(),o=t.pop();exports.DEBUG&&console.log(e.step,"AND[]",n,o),t.push(n&&o?1:0)},function(e){const t=e.stack,n=t.pop(),o=t.pop();exports.DEBUG&&console.log(e.step,"OR[]",n,o),t.push(n||o?1:0)},function(e){const t=e.stack,n=t.pop();exports.DEBUG&&console.log(e.step,"NOT[]",n),t.push(n?0:1)},xn.bind(void 0,1),function(e){const t=e.stack.pop();exports.DEBUG&&console.log(e.step,"SDB[]",t),e.deltaBase=t},function(e){const t=e.stack.pop();exports.DEBUG&&console.log(e.step,"SDS[]",t),e.deltaShift=Math.pow(.5,t)},function(e){const t=e.stack,n=t.pop(),o=t.pop();exports.DEBUG&&console.log(e.step,"ADD[]",n,o),t.push(o+n)},function(e){const t=e.stack,n=t.pop(),o=t.pop();exports.DEBUG&&console.log(e.step,"SUB[]",n,o),t.push(o-n)},function(e){const t=e.stack,n=t.pop(),o=t.pop();exports.DEBUG&&console.log(e.step,"DIV[]",n,o),t.push(64*o/n)},function(e){const t=e.stack,n=t.pop(),o=t.pop();exports.DEBUG&&console.log(e.step,"MUL[]",n,o),t.push(o*n/64)},function(e){const t=e.stack,n=t.pop();exports.DEBUG&&console.log(e.step,"ABS[]",n),t.push(Math.abs(n))},function(e){const t=e.stack;let n=t.pop();exports.DEBUG&&console.log(e.step,"NEG[]",n),t.push(-n)},function(e){const t=e.stack,n=t.pop();exports.DEBUG&&console.log(e.step,"FLOOR[]",n),t.push(64*Math.floor(n/64))},function(e){const t=e.stack,n=t.pop();exports.DEBUG&&console.log(e.step,"CEILING[]",n),t.push(64*Math.ceil(n/64))},Un.bind(void 0,0),Un.bind(void 0,1),Un.bind(void 0,2),Un.bind(void 0,3),void 0,void 0,void 0,void 0,function(e){const t=e.stack,n=t.pop(),o=t.pop();exports.DEBUG&&console.log(e.step,"WCVTF[]",n,o),e.cvt[o]=n*e.ppem/e.font.unitsPerEm},xn.bind(void 0,2),xn.bind(void 0,3),Tn.bind(void 0,1),Tn.bind(void 0,2),Tn.bind(void 0,3),function(e){let t,n=e.stack.pop();switch(exports.DEBUG&&console.log(e.step,"SROUND[]",n),e.round=Yt,192&n){case 0:t=.5;break;case 64:t=1;break;case 128:t=2;break;default:throw new Error("invalid SROUND value")}switch(e.srPeriod=t,48&n){case 0:e.srPhase=0;break;case 16:e.srPhase=.25*t;break;case 32:e.srPhase=.5*t;break;case 48:e.srPhase=.75*t;break;default:throw new Error("invalid SROUND value")}n&=15,e.srThreshold=0===n?0:(n/8-.5)*t},function(e){let t,n=e.stack.pop();switch(exports.DEBUG&&console.log(e.step,"S45ROUND[]",n),e.round=Yt,192&n){case 0:t=Math.sqrt(2)/2;break;case 64:t=Math.sqrt(2);break;case 128:t=2*Math.sqrt(2);break;default:throw new Error("invalid S45ROUND value")}switch(e.srPeriod=t,48&n){case 0:e.srPhase=0;break;case 16:e.srPhase=.25*t;break;case 32:e.srPhase=.5*t;break;case 48:e.srPhase=.75*t;break;default:throw new Error("invalid S45ROUND value")}n&=15,e.srThreshold=0===n?0:(n/8-.5)*t},void 0,void 0,function(e){exports.DEBUG&&console.log(e.step,"ROFF[]"),e.round=_t},void 0,function(e){exports.DEBUG&&console.log(e.step,"RUTG[]"),e.round=Vt},function(e){exports.DEBUG&&console.log(e.step,"RDTG[]"),e.round=jt},pn,pn,void 0,void 0,void 0,void 0,void 0,function(e){const t=e.stack.pop();exports.DEBUG&&console.log(e.step,"SCANCTRL[]",t)},En.bind(void 0,0),En.bind(void 0,1),function(e){const t=e.stack,n=t.pop();let o=0;exports.DEBUG&&console.log(e.step,"GETINFO[]",n),1&n&&(o=35),32&n&&(o|=4096),t.push(o)},void 0,function(e){const t=e.stack,n=t.pop(),o=t.pop(),r=t.pop();exports.DEBUG&&console.log(e.step,"ROLL[]"),t.push(o),t.push(n),t.push(r)},function(e){const t=e.stack,n=t.pop(),o=t.pop();exports.DEBUG&&console.log(e.step,"MAX[]",n,o),t.push(Math.max(o,n))},function(e){const t=e.stack,n=t.pop(),o=t.pop();exports.DEBUG&&console.log(e.step,"MIN[]",n,o),t.push(Math.min(o,n))},function(e){const t=e.stack.pop();exports.DEBUG&&console.log(e.step,"SCANTYPE[]",t)},function(e){const t=e.stack.pop();let n=e.stack.pop();switch(exports.DEBUG&&console.log(e.step,"INSTCTRL[]",t,n),t){case 1:return void(e.inhibitGridFit=!!n);case 2:return void(e.ignoreCvt=!!n);default:throw new Error("invalid INSTCTRL[] selector")}},void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,On.bind(void 0,1),On.bind(void 0,2),On.bind(void 0,3),On.bind(void 0,4),On.bind(void 0,5),On.bind(void 0,6),On.bind(void 0,7),On.bind(void 0,8),wn.bind(void 0,1),wn.bind(void 0,2),wn.bind(void 0,3),wn.bind(void 0,4),wn.bind(void 0,5),wn.bind(void 0,6),wn.bind(void 0,7),wn.bind(void 0,8),kn.bind(void 0,0,0,0,0,0),kn.bind(void 0,0,0,0,0,1),kn.bind(void 0,0,0,0,0,2),kn.bind(void 0,0,0,0,0,3),kn.bind(void 0,0,0,0,1,0),kn.bind(void 0,0,0,0,1,1),kn.bind(void 0,0,0,0,1,2),kn.bind(void 0,0,0,0,1,3),kn.bind(void 0,0,0,1,0,0),kn.bind(void 0,0,0,1,0,1),kn.bind(void 0,0,0,1,0,2),kn.bind(void 0,0,0,1,0,3),kn.bind(void 0,0,0,1,1,0),kn.bind(void 0,0,0,1,1,1),kn.bind(void 0,0,0,1,1,2),kn.bind(void 0,0,0,1,1,3),kn.bind(void 0,0,1,0,0,0),kn.bind(void 0,0,1,0,0,1),kn.bind(void 0,0,1,0,0,2),kn.bind(void 0,0,1,0,0,3),kn.bind(void 0,0,1,0,1,0),kn.bind(void 0,0,1,0,1,1),kn.bind(void 0,0,1,0,1,2),kn.bind(void 0,0,1,0,1,3),kn.bind(void 0,0,1,1,0,0),kn.bind(void 0,0,1,1,0,1),kn.bind(void 0,0,1,1,0,2),kn.bind(void 0,0,1,1,0,3),kn.bind(void 0,0,1,1,1,0),kn.bind(void 0,0,1,1,1,1),kn.bind(void 0,0,1,1,1,2),kn.bind(void 0,0,1,1,1,3),kn.bind(void 0,1,0,0,0,0),kn.bind(void 0,1,0,0,0,1),kn.bind(void 0,1,0,0,0,2),kn.bind(void 0,1,0,0,0,3),kn.bind(void 0,1,0,0,1,0),kn.bind(void 0,1,0,0,1,1),kn.bind(void 0,1,0,0,1,2),kn.bind(void 0,1,0,0,1,3),kn.bind(void 0,1,0,1,0,0),kn.bind(void 0,1,0,1,0,1),kn.bind(void 0,1,0,1,0,2),kn.bind(void 0,1,0,1,0,3),kn.bind(void 0,1,0,1,1,0),kn.bind(void 0,1,0,1,1,1),kn.bind(void 0,1,0,1,1,2),kn.bind(void 0,1,0,1,1,3),kn.bind(void 0,1,1,0,0,0),kn.bind(void 0,1,1,0,0,1),kn.bind(void 0,1,1,0,0,2),kn.bind(void 0,1,1,0,0,3),kn.bind(void 0,1,1,0,1,0),kn.bind(void 0,1,1,0,1,1),kn.bind(void 0,1,1,0,1,2),kn.bind(void 0,1,1,0,1,3),kn.bind(void 0,1,1,1,0,0),kn.bind(void 0,1,1,1,0,1),kn.bind(void 0,1,1,1,0,2),kn.bind(void 0,1,1,1,0,3),kn.bind(void 0,1,1,1,1,0),kn.bind(void 0,1,1,1,1,1),kn.bind(void 0,1,1,1,1,2),kn.bind(void 0,1,1,1,1,3)],Rn.prototype.hasChar=function(e){return null!==this.encoding.charToGlyphIndex(e)},Rn.prototype.charToGlyphIndex=function(e){return this.encoding.charToGlyphIndex(e)},Rn.prototype.charToGlyph=function(e){const t=this.charToGlyphIndex(e);let n=this.glyphs.get(t);return n||(n=this.glyphs.get(0)),n},Rn.prototype.stringToGlyphs=function(e,t){t=t||this.defaultRenderOptions;const n=[];for(let t=0;t>1;e1&&console.warn("Only the first kern subtable is supported."),e.skip("uLong");const n=255&e.parseUShort();if(e.skip("uShort"),0===n){const n=e.parseUShort();e.skip("uShort",3);for(let o=0;o{const t=jn.loadSync(e);Qn.font=t,Qn.ascender=t.ascender,Qn.descender=t.descender}};const Kn=$n.options,Jn=function(e,t){return Math.round(e+Math.random()*(t-e))};const eo=function(e,t){return{text:(e+t).toString(),equation:e+"+"+t}},to=function(e,t){return{text:(e-t).toString(),equation:e+"-"+t}};function no(e,t,n){return 6*(n=(n+1)%1)<1?e+(t-e)*n*6:2*n<1?t:3*n<2?e+(t-e)*(2/3-n)*6:e}var oo={int:Jn,greyColor:function(e,t){const n=Jn(e=e||1,t=t||9).toString(16);return`#${n}${n}${n}`},captchaText:function(e){"number"==typeof e&&(e={size:e});const t=(e=e||{}).size||4,n=e.ignoreChars||"";let o=-1,r="",s=e.charPreset||Kn.charPreset;n&&(s=function(e,t){return e.split("").filter(e=>-1===t.indexOf(e))}(s,n));const a=s.length-1;for(;++o>16,o=t>>8&255,r=255&t,s=Math.max(n,o,r),a=Math.min(n,o,r);return(s+a)/510}(e):1;let r,s;o>=.5?(r=Math.round(100*o)-45,s=Math.round(100*o)-25):(r=Math.round(100*o)+25,s=Math.round(100*o)+45);const a=Jn(r,s)/100,i=a<.5?a*(a+n):a+n-a*n,l=2*a-i,c=Math.floor(255*no(l,i,t+1/3)),u=Math.floor(255*no(l,i,t));return"#"+(Math.floor(255*no(l,i,t-1/3))|u<<8|c<<16|1<<24).toString(16).slice(1)}};const ro=$n.options,so=function(e,t){e=e||oo.captchaText();const n=(t=Object.assign({},ro,t)).width,o=t.height,r=t.background||t.backgroundColor;r&&(t.color=!0);const s=r?``:"",a=[].concat(function(e,t,n){const o=n.color,r=[],s=n.inverse?7:1,a=n.inverse?15:9;let i=-1;for(;++i`)}return r}(n,o,t)).concat(function(e,t,n,o,r){const s=e.length,a=(t-2)/(s+1),i=o.inverse?10:0,l=o.inverse?14:4;let u=-1;const p=[],h=r||o.color?oo.color(o.background):oo.greyColor(i,l);for(;++u`)}return p}(e,n,o,t)).sort(()=>Math.random()-.5).join("");return`${``}${s}${a}`};var ao=so,io=oo.captchaText,lo=function(e){const t=e.text||oo.captchaText(e);return{text:t,data:so(t,e)}},co=function(e){const t=oo.mathExpr(e.mathMin,e.mathMax,e.mathOperator);return{text:t.text,data:so(t.equation,e)}},uo=ro,po=$n.loadFont;ao.randomText=io,ao.create=lo,ao.createMathExpr=co,ao.options=uo,ao.loadFont=po;var ho=ao;const fo=Object.prototype.toString;function go(e){return"[object Object]"===fo.call(e)}function mo(){"development"===process.env.NODE_ENV&&console.log(...arguments)}const yo=async function(){};function vo(e){return yo.constructor===e.constructor?async function(){const t=await e.apply(this,arguments);return go(t)&&(t.msg&&(t.message=t.msg,t.errMsg=t.msg),0===t.code?t.errCode=t.code:t.errCode=s[t.code]||t.code),t}:function(){const t=e.apply(this,arguments);return go(t)&&(t.msg&&(t.message=t.msg,t.errMsg=t.msg),0===t.code?t.errCode=t.code:t.errCode=s[t.code]||t.code),t}}const bo=uniCloud.database(),So=bo.collection("opendb-verify-codes");class xo{async setVerifyCode({clientIP:e,deviceId:t,code:n,expiresDate:o,scene:r}){if(!t)return{code:10101,msg:"deviceId不可为空"};if(!n)return{code:10102,msg:"验证码不可为空"};o||(o=180);const s=Date.now(),a={device_uuid:t,scene:r,code:n.toLocaleLowerCase(),state:0,ip:e,created_date:s,expired_date:s+1e3*o};return mo("addRes",await So.add(a)),{code:0,deviceId:t}}async verifyCode({deviceId:e,code:t,scene:n}){if(!e)return{code:10101,msg:"deviceId不可为空"};if(!t)return{code:10102,msg:"验证码不可为空"};const o=Date.now(),r={device_uuid:e,scene:n,code:t.toLocaleLowerCase(),state:0},s=await So.where(r).orderBy("created_date","desc").limit(1).get();if(mo("verifyRecord:",s),s&&s.data&&s.data.length>0){const e=s.data[0];if(e.expired_date{e.scene&&delete e.scene,this.pluginConfig.scene[n]=Object.assign({},t,e[n])})}}}{constructor(){super(),this.DEVICEID2opts={}}mergeConfig(e){const t=go(this.pluginConfig.scene)?this.pluginConfig.scene[e.scene]:e.scene;return Object.assign({},go(t)?t:this.pluginConfig,e)}async create(e={}){if(!e.scene)throw new Error("scene验证码场景不可为空");e=this.mergeConfig(e);let{scene:t,expiresDate:n,deviceId:o,clientIP:r,...s}=e;if(o=o||__ctx__.DEVICEID,r=r||__ctx__.CLIENTIP,!o)throw new Error("deviceId不可为空");const a=new xo;try{const{text:i,base64:l}=function(e={}){const{uniPlatform:t=""}=e;let n;n=e.mathExpr?ho.createMathExpr(e):ho.create(e);let o="data:image/svg+xml;utf8,"+n.data.replace(/#/g,"%23");return(!t||["mp-toutiao","h5","web","app","app-plus"].indexOf(t)>-1)&&(o=o.replace(/"/g,"'").replace(//g,"%3E")),{text:n.text,base64:o}}(s),c=await a.setVerifyCode({clientIP:r,deviceId:o,code:i,expiresDate:n,scene:t});return c.code>0?{...c,code:10001}:(this.DEVICEID2opts[o]=e,{code:0,msg:"验证码获取成功",captchaBase64:l})}catch(e){return{code:10001,msg:"验证码生成失败:"+e.message}}}async verify({deviceId:e,captcha:t,scene:n}){if(!(e=e||__ctx__.DEVICEID))throw new Error("deviceId不可为空");if(!n)throw new Error("scene验证码场景不可为空");const o=new xo;try{const r=await o.verifyCode({deviceId:e,code:t,scene:n});return r.code>0?r:{code:0,msg:"验证码通过"}}catch(e){return{code:10002,msg:"验证码校验失败:"+e.message}}}async refresh(e={}){let{scene:t,expiresDate:n,deviceId:o,...r}=e;if(o=o||__ctx__.DEVICEID,!o)throw new Error("deviceId不可为空");if(!t)throw new Error("scene验证码场景不可为空");const s=await So.where(bo.command.or([{device_uuid:o,scene:t},{deviceId:o,scene:t}])).orderBy("created_date","desc").limit(1).get();if(s&&s.data&&s.data.length>0){const e=s.data[0];await So.doc(e._id).update({state:2}),Object.keys(r).length>0&&(this.DEVICEID2opts[o]=Object.assign({},this.DEVICEID2opts[o],r));let a={};try{a=await this.create(Object.assign({},this.DEVICEID2opts[o],{deviceId:o,scene:t,expiresDate:n}))}catch(e){return{code:50403,msg:e.message}}return a.code>0?{...a,code:50403}:{code:0,msg:"验证码刷新成功",captchaBase64:a.captchaBase64}}return{code:10003,msg:`验证码刷新失败:无此设备在 ${t} 场景信息,请重新获取`}}}const Eo=new xo;Object.keys(Eo).forEach(e=>{To.prototype[e]=vo(Eo[e])});const Oo=new To,wo=new Proxy(Oo,{get(e,t){if(t in e)return"function"==typeof e[t]?vo(e[t]).bind(wo):e[t]}});module.exports=wo; diff --git a/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/common/uni-captcha/package.json b/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/common/uni-captcha/package.json new file mode 100644 index 0000000..164a9d6 --- /dev/null +++ b/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/common/uni-captcha/package.json @@ -0,0 +1,16 @@ +{ + "name": "uni-captcha", + "version": "0.6.4", + "description": "uni-captcha", + "main": "index.js", + "homepage": "https://ext.dcloud.net.cn/plugin?id=4048", + "repository": { + "type": "git", + "url": "git+https://gitee.com/dcloud/uni-captcha" + }, + "author": "DCloud", + "license": "Apache-2.0", + "dependencies": { + "uni-config-center": "file:../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center" + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/uni-captcha-co/config.js b/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/uni-captcha-co/config.js new file mode 100644 index 0000000..c37b38b --- /dev/null +++ b/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/uni-captcha-co/config.js @@ -0,0 +1,17 @@ +module.exports = { + "image-captcha":{ + "width": 150, //图片宽度 + "height": 44, //图片高度 + "background": "#FFFAE8", //验证码背景色,设置空字符`''`不使用背景颜色 + // "size": 4, //验证码长度,最多 6 个字符 + // "noise": 4, //验证码干扰线条数 + // "color": false, //字体是否使用随机颜色,当设置`background`后恒为`true` + // "fontSize": 40, //字体大小 + // "ignoreChars": '', //忽略那些字符 + // "mathExpr": false, //是否使用数学表达式 + // "mathMin": 1, //表达式所使用的最小数字 + // "mathMax": 9, //表达式所使用的最大数字 + // "mathOperator": '' //表达式所使用的运算符,支持 `+`、`-`。不传随机使用 + // "expiresDate":180 //验证码过期时间(s) + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/uni-captcha-co/index.obj.js b/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/uni-captcha-co/index.obj.js new file mode 100644 index 0000000..09b36ac --- /dev/null +++ b/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/uni-captcha-co/index.obj.js @@ -0,0 +1,32 @@ +// 开发文档: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj +//导入验证码公共模块 +const uniCaptcha = require('uni-captcha') +//获取数据库对象 +const db = uniCloud.database(); +//获取数据表opendb-verify-codes对象 +const verifyCodes = db.collection('opendb-verify-codes') +module.exports = { + async getImageCaptcha({ + scene + }) { + //获取设备id + let { + deviceId, + platform + } = this.getClientInfo(); + //根据:设备id、场景值、状态,查找记录是否存在 + let res = await verifyCodes.where({ + scene, + deviceId, + state: 0 + }).limit(1).get() + //如果已存在则调用刷新接口,反之调用插件接口 + let action = res.data.length ? 'refresh' : 'create' + //执行并返回结果 + //导入配置,配置优先级说明:此处配置 > uni-config-center + return await uniCaptcha[action]({ + scene, //来源客户端传递,表示:使用场景值,用于防止不同功能的验证码混用 + uniPlatform: platform + }) + } +} diff --git a/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/uni-captcha-co/package.json b/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/uni-captcha-co/package.json new file mode 100644 index 0000000..b5188c3 --- /dev/null +++ b/uni-im示例/uni_modules/uni-captcha/uniCloud/cloudfunctions/uni-captcha-co/package.json @@ -0,0 +1,10 @@ +{ + "name": "uni-captcha-co", + "dependencies": { + "uni-captcha": "file:../common/uni-captcha", + "uni-config-center": "file:../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center" + }, + "extensions": { + "uni-cloud-jql": {} + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-captcha/uniCloud/database/opendb-verify-codes.schema.json b/uni-im示例/uni_modules/uni-captcha/uniCloud/database/opendb-verify-codes.schema.json new file mode 100644 index 0000000..1f3be59 --- /dev/null +++ b/uni-im示例/uni_modules/uni-captcha/uniCloud/database/opendb-verify-codes.schema.json @@ -0,0 +1,45 @@ +{ + "bsonType": "object", + "properties": { + "_id": { + "description": "ID,系统自动生成" + }, + "code": { + "bsonType": "string", + "description": "验证码" + }, + "create_date": { + "bsonType": "timestamp", + "description": "创建时间" + }, + "device_uuid": { + "bsonType": "string", + "description": "设备UUID,常用于图片验证码" + }, + "email": { + "bsonType": "string", + "description": "邮箱" + }, + "expired_date": { + "bsonType": "timestamp", + "description": "过期时间" + }, + "ip": { + "bsonType": "string", + "description": "请求时客户端IP地址" + }, + "mobile": { + "bsonType": "string", + "description": "手机号码" + }, + "scene": { + "bsonType": "string", + "description": "使用验证码的场景,如:login, bind, unbind, pay" + }, + "state": { + "bsonType": "int", + "description": "验证状态:0 未验证、1 已验证、2 已作废" + } + }, + "required": [] +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-cloud-s2s/changelog.md b/uni-im示例/uni_modules/uni-cloud-s2s/changelog.md new file mode 100644 index 0000000..727d5b2 --- /dev/null +++ b/uni-im示例/uni_modules/uni-cloud-s2s/changelog.md @@ -0,0 +1,2 @@ +## 1.0.1(2023-03-02) +- 修复 方法名错误 diff --git a/uni-im示例/uni_modules/uni-cloud-s2s/package.json b/uni-im示例/uni_modules/uni-cloud-s2s/package.json new file mode 100644 index 0000000..339d219 --- /dev/null +++ b/uni-im示例/uni_modules/uni-cloud-s2s/package.json @@ -0,0 +1,83 @@ +{ + "id": "uni-cloud-s2s", + "displayName": "服务空间与服务器安全通讯模块", + "version": "1.0.1", + "description": "用于解决服务空间与服务器通讯时互相信任问题", + "keywords": [ + "安全通讯", + "服务器请求云函数", + "云函数请求服务器" +], + "repository": "", + "engines": { + "HBuilderX": "^3.1.0" + }, + "dcloudext": { + "type": "unicloud-template-function", + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "Vue": { + "vue2": "u", + "vue3": "u" + }, + "App": { + "app-vue": "u", + "app-nvue": "u" + }, + "H5-mobile": { + "Safari": "u", + "Android Browser": "u", + "微信浏览器(Android)": "u", + "QQ浏览器(Android)": "u" + }, + "H5-pc": { + "Chrome": "u", + "IE": "u", + "Edge": "u", + "Firefox": "u", + "Safari": "u" + }, + "小程序": { + "微信": "u", + "阿里": "u", + "百度": "u", + "字节跳动": "u", + "QQ": "u", + "钉钉": "u", + "快手": "u", + "飞书": "u", + "京东": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + } + } + } + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-cloud-s2s/readme.md b/uni-im示例/uni_modules/uni-cloud-s2s/readme.md new file mode 100644 index 0000000..3c8ed0c --- /dev/null +++ b/uni-im示例/uni_modules/uni-cloud-s2s/readme.md @@ -0,0 +1,3 @@ +# uni-cloud-s2s + +文档见:[外部服务器如何与uniCloud安全通讯](https://uniapp.dcloud.net.cn/uniCloud/uni-cloud-s2s.html) \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-cloud-s2s/uniCloud/cloudfunctions/common/uni-cloud-s2s/index.js b/uni-im示例/uni_modules/uni-cloud-s2s/uniCloud/cloudfunctions/common/uni-cloud-s2s/index.js new file mode 100644 index 0000000..da9a36c --- /dev/null +++ b/uni-im示例/uni_modules/uni-cloud-s2s/uniCloud/cloudfunctions/common/uni-cloud-s2s/index.js @@ -0,0 +1 @@ +"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("crypto"),t=require("path");function s(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}require("fs");var o=s(e),n=s(t);const i="uni-cloud-s2s",r={code:5e4,message:"Config error"},c={code:51e3,message:"Access denied"};class a extends Error{constructor(e){super(e.message),this.errMsg=e.message||"",this.code=this.errCode=e.code,this.errSubject=e.subject,this.forceReturn=e.forceReturn||!1,this.cause=e.cause,Object.defineProperties(this,{message:{get(){return this.errMsg},set(e){this.errMsg=e}}})}toJSON(e=0){if(!(e>=10))return e++,{errCode:this.errCode,errMsg:this.errMsg,errSubject:this.errSubject,cause:this.cause&&this.cause.toJSON?this.cause.toJSON(e):this.cause}}}const d=Object.prototype.toString;const h=50002,u=Object.create(null);["string","boolean","number","null"].forEach((e=>{u[e]=function(t,s){if(function(e){return d.call(e).slice(8,-1).toLowerCase()}(t)!==e)return{code:h,message:`${s} is invalid`}}}));const f="Unicloud-S2s-Authorization";class g{constructor(e){const{config:t}=e||{};this.config=t;const{connectCode:s}=t||{};if(this.connectCode=s,!s||"string"!=typeof s)throw new a({subject:i,code:r.code,message:"Invalid connectCode in config"})}getHeadersValue(e={},t,s){const o=Object.keys(e||{}).find((e=>e.toLowerCase()===t.toLowerCase()));return o?e[o]:s}verifyHttpInfo(e){const t=this.getHeadersValue(e.headers,f,""),[s="",o=""]=t.split(" ");if(s.toLowerCase()==="CONNECTCODE".toLowerCase()&&o===this.config.connectCode)return!0;throw new a({subject:i,code:c.code,message:`Invalid CONNECTCODE in headers['${f}']`})}getSecureHeaders(e){return{[f]:`CONNECTCODE ${this.config.connectCode}`}}}function l(e){return function(t){const{content:s,signKey:n}=t||{};return o.default.createHash(e).update(s+"\n"+n).digest("hex")}}const p={md5:l("md5"),sha1:l("sha1"),sha256:l("md5"),"hmac-sha256":function(e){const{content:t,signKey:s}=e||{};return o.default.createHmac("sha256",s).update(t).digest("hex")}};function m(e){const{timestamp:t,data:s={},signKey:o,hashMethod:n="hmac-sha256"}=e||{},i=p[n],r=["number","string","boolean"],c=Object.keys(s).sort(),a=[];for(let e=0;ee.toLowerCase()===t.toLowerCase()));return o?e[o]:s}getHttpData(e){const t=e.httpMethod.toLowerCase(),s=this.getHttpHeaders(e),o=this.getHeadersValue(s,"Content-Type","");if("get"===t)return e.queryStringParameters;if("post"!==t)throw new a({subject:i,code:c.code,message:`Invalid http method, expected "POST" or "get", got "${t}"`});if(0===o.indexOf("application/json"))return JSON.parse(e.body);if(0===o.indexOf("application/x-www-form-urlencoded"))return require("querystring").parse(e.body);throw new a({subject:i,code:c.code,message:`Invalid content type of POST method, expected "application/json" or "application/x-www-form-urlencoded", got "${o}"`})}verifyHttpInfo(e){const t=e.headers||{},s=this.getHeadersValue(t,"Unicloud-S2s-Timestamp","0");let[o,n]=this.getHeadersValue(t,"Unicloud-S2s-Signature","").split(" ");if(o=o.toLowerCase(),o!==this.hashMethod)throw new a({subject:i,code:c.code,message:`Invalid hash method, expected "${this.hashMethod}", got "${o}"`});const r=parseInt(s),d=Date.now();if(Math.abs(d-r)>1e3*this.timeDiffTolerance)throw new a({subject:i,code:c.code,message:`Invalid timestamp, server timestamp is ${d}, ${r} exceed max timeDiffTolerance(${this.timeDiffTolerance} seconds)`});return m({timestamp:r,data:this.getHttpData(e),signKey:this.signKey,hashMethod:this.hashMethod})===n}getSecureHeaders(e){const{data:t}=e||{},s=Date.now(),o=m({timestamp:s,data:t,signKey:this.signKey,hashMethod:this.hashMethod});return{"Unicloud-S2s-Timestamp":s+"","Unicloud-S2s-Signature":this.hashMethod+" "+o}}}const y=require("uni-config-center")({pluginId:i});class b{constructor(){this.config=y.config();const e=n.default.resolve(require.resolve("uni-config-center"),i,"config.json");if(!this.config)throw new a({subject:i,code:r.code,message:`${i} config required, please check your config file: ${e}`});if("connectCode"===this.config.type)this.verifier=new g({config:this.config});else{if(!function(e){return"sign"===e.type}(this.config))throw new a({subject:i,code:r.code,message:`Invalid ${i} config, expected policy is "code" or "sign", got ${this.config.policy}`});this.verifier=new w({config:this.config})}}verifyHttpInfo(e){if(!e)throw new a({subject:i,code:c.code,message:"Access denied, httpInfo required"});return this.verifier.verifyHttpInfo(e)}getSecureHeaders(e){return this.verifier.getSecureHeaders(e)}}exports.getSecureHeaders=function(e){return(new b).getSecureHeaders(e)},exports.verifyHttpInfo=function(e){const t=(new b).verifyHttpInfo(e);if(!t)throw new a({subject:i,code:c.code,message:c.message});return t}; diff --git a/uni-im示例/uni_modules/uni-cloud-s2s/uniCloud/cloudfunctions/common/uni-cloud-s2s/package.json b/uni-im示例/uni_modules/uni-cloud-s2s/uniCloud/cloudfunctions/common/uni-cloud-s2s/package.json new file mode 100644 index 0000000..e99f52c --- /dev/null +++ b/uni-im示例/uni_modules/uni-cloud-s2s/uniCloud/cloudfunctions/common/uni-cloud-s2s/package.json @@ -0,0 +1,11 @@ +{ + "name": "uni-cloud-s2s", + "version": "1.0.1", + "description": "", + "keywords": [], + "author": "DCloud", + "main": "index.js", + "dependencies": { + "uni-config-center": "file:../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center" + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-config-center/changelog.md b/uni-im示例/uni_modules/uni-config-center/changelog.md new file mode 100644 index 0000000..57dbcb5 --- /dev/null +++ b/uni-im示例/uni_modules/uni-config-center/changelog.md @@ -0,0 +1,6 @@ +## 0.0.3(2022-11-11) +- 修复 config 方法获取根节点为数组格式配置时错误的转化为了对象的Bug +## 0.0.2(2021-04-16) +- 修改插件package信息 +## 0.0.1(2021-03-15) +- 初始化项目 diff --git a/uni-im示例/uni_modules/uni-config-center/package.json b/uni-im示例/uni_modules/uni-config-center/package.json new file mode 100644 index 0000000..bace866 --- /dev/null +++ b/uni-im示例/uni_modules/uni-config-center/package.json @@ -0,0 +1,81 @@ +{ + "id": "uni-config-center", + "displayName": "uni-config-center", + "version": "0.0.3", + "description": "uniCloud 配置中心", + "keywords": [ + "配置", + "配置中心" +], + "repository": "", + "engines": { + "HBuilderX": "^3.1.0" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "", + "type": "unicloud-template-function" + }, + "directories": { + "example": "../../../scripts/dist" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "u", + "app-nvue": "u" + }, + "H5-mobile": { + "Safari": "u", + "Android Browser": "u", + "微信浏览器(Android)": "u", + "QQ浏览器(Android)": "u" + }, + "H5-pc": { + "Chrome": "u", + "IE": "u", + "Edge": "u", + "Firefox": "u", + "Safari": "u" + }, + "小程序": { + "微信": "u", + "阿里": "u", + "百度": "u", + "字节跳动": "u", + "QQ": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "u" + } + } + } + } +} diff --git a/uni-im示例/uni_modules/uni-config-center/readme.md b/uni-im示例/uni_modules/uni-config-center/readme.md new file mode 100644 index 0000000..03f7fc2 --- /dev/null +++ b/uni-im示例/uni_modules/uni-config-center/readme.md @@ -0,0 +1,93 @@ +# 为什么使用uni-config-center + +实际开发中很多插件需要配置文件才可以正常运行,如果每个插件都单独进行配置的话就会产生下面这样的目录结构 + +```bash +cloudfunctions +└─────common 公共模块 + ├─plugin-a // 插件A对应的目录 + │ ├─index.js + │ ├─config.json // plugin-a对应的配置文件 + │ └─other-file.cert // plugin-a依赖的其他文件 + └─plugin-b // plugin-b对应的目录 + ├─index.js + └─config.json // plugin-b对应的配置文件 +``` + +假设插件作者要发布一个项目模板,里面使用了很多需要配置的插件,无论是作者发布还是用户使用都是一个大麻烦。 + +uni-config-center就是用了统一管理这些配置文件的,使用uni-config-center后的目录结构如下 + +```bash +cloudfunctions +└─────common 公共模块 + ├─plugin-a // 插件A对应的目录 + │ └─index.js + ├─plugin-b // plugin-b对应的目录 + │ └─index.js + └─uni-config-center + ├─index.js // config-center入口文件 + ├─plugin-a + │ ├─config.json // plugin-a对应的配置文件 + │ └─other-file.cert // plugin-a依赖的其他文件 + └─plugin-b + └─config.json // plugin-b对应的配置文件 +``` + +使用uni-config-center后的优势 + +- 配置文件统一管理,分离插件主体和配置信息,更新插件更方便 +- 支持对config.json设置schema,插件使用者在HBuilderX内编写config.json文件时会有更好的提示(后续HBuilderX会提供支持) + +# 用法 + +在要使用uni-config-center的公共模块或云函数内引入uni-config-center依赖,请参考:[使用公共模块](https://uniapp.dcloud.net.cn/uniCloud/cf-common) + +```js +const createConfig = require('uni-config-center') + +const uniIdConfig = createConfig({ + pluginId: 'uni-id', // 插件id + defaultConfig: { // 默认配置 + tokenExpiresIn: 7200, + tokenExpiresThreshold: 600, + }, + customMerge: function(defaultConfig, userConfig) { // 自定义默认配置和用户配置的合并规则,不设置的情况侠会对默认配置和用户配置进行深度合并 + // defaudltConfig 默认配置 + // userConfig 用户配置 + return Object.assign(defaultConfig, userConfig) + } +}) + + +// 以如下配置为例 +// { +// "tokenExpiresIn": 7200, +// "passwordErrorLimit": 6, +// "bindTokenToDevice": false, +// "passwordErrorRetryTime": 3600, +// "app-plus": { +// "tokenExpiresIn": 2592000 +// }, +// "service": { +// "sms": { +// "codeExpiresIn": 300 +// } +// } +// } + +// 获取配置 +uniIdConfig.config() // 获取全部配置,注意:uni-config-center内不存在对应插件目录时会返回空对象 +uniIdConfig.config('tokenExpiresIn') // 指定键值获取配置,返回:7200 +uniIdConfig.config('service.sms.codeExpiresIn') // 指定键值获取配置,返回:300 +uniIdConfig.config('tokenExpiresThreshold', 600) // 指定键值获取配置,如果不存在则取传入的默认值,返回:600 + +// 获取文件绝对路径 +uniIdConfig.resolve('custom-token.js') // 获取uni-config-center/uni-id/custom-token.js文件的路径 + +// 引用文件(require) +uniIDConfig.requireFile('custom-token.js') // 使用require方式引用uni-config-center/uni-id/custom-token.js文件。文件不存在时返回undefined,文件内有其他错误导致require失败时会抛出错误。 + +// 判断是否包含某文件 +uniIDConfig.hasFile('custom-token.js') // 配置目录是否包含某文件,true: 文件存在,false: 文件不存在 +``` \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/index.js b/uni-im示例/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/index.js new file mode 100644 index 0000000..00ba62f --- /dev/null +++ b/uni-im示例/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/index.js @@ -0,0 +1 @@ +"use strict";var t=require("fs"),r=require("path");function e(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var n=e(t),o=e(r),i="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};var u=function(t){var r={exports:{}};return t(r,r.exports),r.exports}((function(t,r){var e="__lodash_hash_undefined__",n=9007199254740991,o="[object Arguments]",u="[object Function]",c="[object Object]",a=/^\[object .+?Constructor\]$/,f=/^(?:0|[1-9]\d*)$/,s={};s["[object Float32Array]"]=s["[object Float64Array]"]=s["[object Int8Array]"]=s["[object Int16Array]"]=s["[object Int32Array]"]=s["[object Uint8Array]"]=s["[object Uint8ClampedArray]"]=s["[object Uint16Array]"]=s["[object Uint32Array]"]=!0,s[o]=s["[object Array]"]=s["[object ArrayBuffer]"]=s["[object Boolean]"]=s["[object DataView]"]=s["[object Date]"]=s["[object Error]"]=s[u]=s["[object Map]"]=s["[object Number]"]=s[c]=s["[object RegExp]"]=s["[object Set]"]=s["[object String]"]=s["[object WeakMap]"]=!1;var l="object"==typeof i&&i&&i.Object===Object&&i,h="object"==typeof self&&self&&self.Object===Object&&self,p=l||h||Function("return this")(),_=r&&!r.nodeType&&r,v=_&&t&&!t.nodeType&&t,d=v&&v.exports===_,y=d&&l.process,g=function(){try{var t=v&&v.require&&v.require("util").types;return t||y&&y.binding&&y.binding("util")}catch(t){}}(),b=g&&g.isTypedArray;function j(t,r,e){switch(e.length){case 0:return t.call(r);case 1:return t.call(r,e[0]);case 2:return t.call(r,e[0],e[1]);case 3:return t.call(r,e[0],e[1],e[2])}return t.apply(r,e)}var w,O,m,A=Array.prototype,z=Function.prototype,M=Object.prototype,x=p["__core-js_shared__"],C=z.toString,F=M.hasOwnProperty,U=(w=/[^.]+$/.exec(x&&x.keys&&x.keys.IE_PROTO||""))?"Symbol(src)_1."+w:"",S=M.toString,I=C.call(Object),P=RegExp("^"+C.call(F).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),T=d?p.Buffer:void 0,q=p.Symbol,E=p.Uint8Array,$=T?T.allocUnsafe:void 0,D=(O=Object.getPrototypeOf,m=Object,function(t){return O(m(t))}),k=Object.create,B=M.propertyIsEnumerable,N=A.splice,L=q?q.toStringTag:void 0,R=function(){try{var t=vt(Object,"defineProperty");return t({},"",{}),t}catch(t){}}(),G=T?T.isBuffer:void 0,V=Math.max,W=Date.now,H=vt(p,"Map"),J=vt(Object,"create"),K=function(){function t(){}return function(r){if(!xt(r))return{};if(k)return k(r);t.prototype=r;var e=new t;return t.prototype=void 0,e}}();function Q(t){var r=-1,e=null==t?0:t.length;for(this.clear();++r-1},X.prototype.set=function(t,r){var e=this.__data__,n=nt(e,t);return n<0?(++this.size,e.push([t,r])):e[n][1]=r,this},Y.prototype.clear=function(){this.size=0,this.__data__={hash:new Q,map:new(H||X),string:new Q}},Y.prototype.delete=function(t){var r=_t(this,t).delete(t);return this.size-=r?1:0,r},Y.prototype.get=function(t){return _t(this,t).get(t)},Y.prototype.has=function(t){return _t(this,t).has(t)},Y.prototype.set=function(t,r){var e=_t(this,t),n=e.size;return e.set(t,r),this.size+=e.size==n?0:1,this},Z.prototype.clear=function(){this.__data__=new X,this.size=0},Z.prototype.delete=function(t){var r=this.__data__,e=r.delete(t);return this.size=r.size,e},Z.prototype.get=function(t){return this.__data__.get(t)},Z.prototype.has=function(t){return this.__data__.has(t)},Z.prototype.set=function(t,r){var e=this.__data__;if(e instanceof X){var n=e.__data__;if(!H||n.length<199)return n.push([t,r]),this.size=++e.size,this;e=this.__data__=new Y(n)}return e.set(t,r),this.size=e.size,this};var it,ut=function(t,r,e){for(var n=-1,o=Object(t),i=e(t),u=i.length;u--;){var c=i[it?u:++n];if(!1===r(o[c],c,o))break}return t};function ct(t){return null==t?void 0===t?"[object Undefined]":"[object Null]":L&&L in Object(t)?function(t){var r=F.call(t,L),e=t[L];try{t[L]=void 0;var n=!0}catch(t){}var o=S.call(t);n&&(r?t[L]=e:delete t[L]);return o}(t):function(t){return S.call(t)}(t)}function at(t){return Ct(t)&&ct(t)==o}function ft(t){return!(!xt(t)||function(t){return!!U&&U in t}(t))&&(zt(t)?P:a).test(function(t){if(null!=t){try{return C.call(t)}catch(t){}try{return t+""}catch(t){}}return""}(t))}function st(t){if(!xt(t))return function(t){var r=[];if(null!=t)for(var e in Object(t))r.push(e);return r}(t);var r=yt(t),e=[];for(var n in t)("constructor"!=n||!r&&F.call(t,n))&&e.push(n);return e}function lt(t,r,e,n,o){t!==r&&ut(r,(function(i,u){if(o||(o=new Z),xt(i))!function(t,r,e,n,o,i,u){var a=gt(t,e),f=gt(r,e),s=u.get(f);if(s)return void rt(t,e,s);var l=i?i(a,f,e+"",t,r,u):void 0,h=void 0===l;if(h){var p=Ot(f),_=!p&&At(f),v=!p&&!_&&Ft(f);l=f,p||_||v?Ot(a)?l=a:Ct(j=a)&&mt(j)?l=function(t,r){var e=-1,n=t.length;r||(r=Array(n));for(;++e-1&&t%1==0&&t0){if(++r>=800)return arguments[0]}else r=0;return t.apply(void 0,arguments)}}(pt);function jt(t,r){return t===r||t!=t&&r!=r}var wt=at(function(){return arguments}())?at:function(t){return Ct(t)&&F.call(t,"callee")&&!B.call(t,"callee")},Ot=Array.isArray;function mt(t){return null!=t&&Mt(t.length)&&!zt(t)}var At=G||function(){return!1};function zt(t){if(!xt(t))return!1;var r=ct(t);return r==u||"[object GeneratorFunction]"==r||"[object AsyncFunction]"==r||"[object Proxy]"==r}function Mt(t){return"number"==typeof t&&t>-1&&t%1==0&&t<=n}function xt(t){var r=typeof t;return null!=t&&("object"==r||"function"==r)}function Ct(t){return null!=t&&"object"==typeof t}var Ft=b?function(t){return function(r){return t(r)}}(b):function(t){return Ct(t)&&Mt(t.length)&&!!s[ct(t)]};function Ut(t){return mt(t)?tt(t,!0):st(t)}var St,It=(St=function(t,r,e){lt(t,r,e)},ht((function(t,r){var e=-1,n=r.length,o=n>1?r[n-1]:void 0,i=n>2?r[2]:void 0;for(o=St.length>3&&"function"==typeof o?(n--,o):void 0,i&&function(t,r,e){if(!xt(e))return!1;var n=typeof r;return!!("number"==n?mt(e)&&dt(r,e.length):"string"==n&&r in e)&&jt(e[r],t)}(r[0],r[1],i)&&(o=n<3?void 0:o,n=1),t=Object(t);++ec.call(t,r);class f{constructor({pluginId:t,defaultConfig:r={},customMerge:e,root:n}){this.pluginId=t,this.defaultConfig=r,this.pluginConfigPath=o.default.resolve(n||__dirname,t),this.customMerge=e,this._config=void 0}resolve(t){return o.default.resolve(this.pluginConfigPath,t)}hasFile(t){return n.default.existsSync(this.resolve(t))}requireFile(t){try{return require(this.resolve(t))}catch(t){if("MODULE_NOT_FOUND"===t.code)return;throw t}}_getUserConfig(){return this.requireFile("config.json")}config(t,r){if(!this._config){const t=this._getUserConfig();this._config=Array.isArray(t)?t:(this.customMerge||u)(this.defaultConfig,t)}let e=this._config;return t?function(t,r,e){if("number"==typeof r)return t[r];if("symbol"==typeof r)return a(t,r)?t[r]:e;const n="string"!=typeof(o=r)?o:o.split(".").reduce(((t,r)=>(r.split(/\[([^}]+)\]/g).forEach((r=>r&&t.push(r))),t)),[]);var o;let i=t;for(let t=0;t { + var al = [] + attrs.forEach(key => { + al.push(this[key]) + }) + return al + }, (newValue, oldValue) => { + this.paginationInternal.pageSize = this.pageSize + + let needReset = false + for (let i = 2; i < newValue.length; i++) { + if (newValue[i] != oldValue[i]) { + needReset = true + break + } + } + if (needReset) { + this.clear() + this.reset() + } + if (newValue[0] != oldValue[0]) { + this.paginationInternal.current = this.pageCurrent + } + + this._execLoadData() + }) + + // #ifdef H5 + if (process.env.NODE_ENV === 'development') { + this._debugDataList = [] + if (!window.unidev) { + window.unidev = { + clientDB: { + data: [] + } + } + } + unidev.clientDB.data.push(this._debugDataList) + } + // #endif + + // #ifdef MP-TOUTIAO + let changeName + let events = this.$scope.dataset.eventOpts + for (var i = 0; i < events.length; i++) { + let event = events[i] + if (event[0].includes('^load')) { + changeName = event[1][0][0] + } + } + if (changeName) { + let parent = this.$parent + let maxDepth = 16 + this._changeDataFunction = null + while (parent && maxDepth > 0) { + let fun = parent[changeName] + if (fun && typeof fun === 'function') { + this._changeDataFunction = fun + maxDepth = 0 + break + } + parent = parent.$parent + maxDepth--; + } + } + // #endif + + // if (!this.manual) { + // this.loadData() + // } + }, + // #ifdef H5 + beforeDestroy() { + if (process.env.NODE_ENV === 'development' && window.unidev) { + var cd = this._debugDataList + var dl = unidev.clientDB.data + for (var i = dl.length - 1; i >= 0; i--) { + if (dl[i] === cd) { + dl.splice(i, 1) + break + } + } + } + }, + // #endif + methods: { + loadData(args1, args2) { + let callback = null + if (typeof args1 === 'object') { + if (args1.clear) { + this.clear() + this.reset() + } + if (args1.current !== undefined) { + this.paginationInternal.current = args1.current + } + if (typeof args2 === 'function') { + callback = args2 + } + } else if (typeof args1 === 'function') { + callback = args1 + } + + this._execLoadData(callback) + }, + loadMore() { + if (this._isEnded) { + return + } + this._execLoadData() + }, + refresh() { + this.clear() + this._execLoadData() + }, + clear() { + this._isEnded = false + this.listData = [] + }, + reset() { + this.paginationInternal.current = 1 + }, + remove(id, { + action, + callback, + confirmTitle, + confirmContent + } = {}) { + if (!id || !id.length) { + return + } + uni.showModal({ + title: confirmTitle || '提示', + content: confirmContent || '是否删除该数据', + showCancel: true, + success: (res) => { + if (!res.confirm) { + return + } + this._execRemove(id, action, callback) + } + }) + }, + _execLoadData(callback) { + if (this.loading) { + return + } + this.loading = true + this.errorMessage = '' + + this._getExec().then((res) => { + this.loading = false + const { + data, + count + } = res.result + this._isEnded = data.length < this.pageSize + + callback && callback(data, this._isEnded) + this._dispatchEvent(events.load, data) + + if (this.getone) { + this.listData = data.length ? data[0] : undefined + } else if (this.pageData === pageMode.add) { + this.listData.push(...data) + if (this.listData.length) { + this.paginationInternal.current++ + } + } else if (this.pageData === pageMode.replace) { + this.listData = data + this.paginationInternal.count = count + } + + // #ifdef H5 + if (process.env.NODE_ENV === 'development') { + this._debugDataList.length = 0 + this._debugDataList.push(...JSON.parse(JSON.stringify(this.listData))) + } + // #endif + }).catch((err) => { + this.loading = false + this.errorMessage = err + callback && callback() + this.$emit(events.error, err) + }) + }, + _getExec() { + let exec = this.db + if (this.action) { + exec = exec.action(this.action) + } + + exec = exec.collection(this.collection) + + if (!(!this.where || !Object.keys(this.where).length)) { + exec = exec.where(this.where) + } + if (this.field) { + exec = exec.field(this.field) + } + if (this.orderby) { + exec = exec.orderBy(this.orderby) + } + + const { + current, + size + } = this.paginationInternal + exec = exec.skip(size * (current - 1)).limit(size).get({ + getCount: this.getcount + }) + + return exec + }, + _execRemove(id, action, callback) { + if (!this.collection || !id) { + return + } + + const ids = Array.isArray(id) ? id : [id] + if (!ids.length) { + return + } + + uni.showLoading({ + mask: true + }) + + let exec = this.db + if (action) { + exec = exec.action(action) + } + + exec.collection(this.collection).where({ + _id: dbCmd.in(ids) + }).remove().then((res) => { + callback && callback(res.result) + if (this.pageData === pageMode.replace) { + this.refresh() + } else { + this.removeData(ids) + } + }).catch((err) => { + uni.showModal({ + content: err.message, + showCancel: false + }) + }).finally(() => { + uni.hideLoading() + }) + }, + removeData(ids) { + let il = ids.slice(0) + let dl = this.listData + for (let i = dl.length - 1; i >= 0; i--) { + let index = il.indexOf(dl[i]._id) + if (index >= 0) { + dl.splice(i, 1) + il.splice(index, 1) + } + } + }, + _dispatchEvent(type, data) { + if (this._changeDataFunction) { + this._changeDataFunction(data, this._isEnded) + } else { + this.$emit(type, data, this._isEnded) + } + } + } +} diff --git a/uni-im示例/uni_modules/uni-data-checkbox/components/uni-data-checkbox/uni-data-checkbox.vue b/uni-im示例/uni_modules/uni-data-checkbox/components/uni-data-checkbox/uni-data-checkbox.vue new file mode 100644 index 0000000..3c75d9f --- /dev/null +++ b/uni-im示例/uni_modules/uni-data-checkbox/components/uni-data-checkbox/uni-data-checkbox.vue @@ -0,0 +1,821 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-data-checkbox/package.json b/uni-im示例/uni_modules/uni-data-checkbox/package.json new file mode 100644 index 0000000..113c350 --- /dev/null +++ b/uni-im示例/uni_modules/uni-data-checkbox/package.json @@ -0,0 +1,84 @@ +{ + "id": "uni-data-checkbox", + "displayName": "uni-data-checkbox 数据选择器", + "version": "1.0.3", + "description": "通过数据驱动的单选框和复选框", + "keywords": [ + "uni-ui", + "checkbox", + "单选", + "多选", + "单选多选" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "^3.1.1" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": ["uni-load-more","uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/uni-im示例/uni_modules/uni-data-checkbox/readme.md b/uni-im示例/uni_modules/uni-data-checkbox/readme.md new file mode 100644 index 0000000..6eb253d --- /dev/null +++ b/uni-im示例/uni_modules/uni-data-checkbox/readme.md @@ -0,0 +1,18 @@ + + +## DataCheckbox 数据驱动的单选复选框 +> **组件名:uni-data-checkbox** +> 代码块: `uDataCheckbox` + + +本组件是基于uni-app基础组件checkbox的封装。本组件要解决问题包括: + +1. 数据绑定型组件:给本组件绑定一个data,会自动渲染一组候选内容。再以往,开发者需要编写不少代码实现类似功能 +2. 自动的表单校验:组件绑定了data,且符合[uni-forms](https://ext.dcloud.net.cn/plugin?id=2773)组件的表单校验规范,搭配使用会自动实现表单校验 +3. 本组件合并了单选多选 +4. 本组件有若干风格选择,如普通的单选多选框、并列button风格、tag风格。开发者可以快速选择需要的风格。但作为一个封装组件,样式代码虽然不用自己写了,却会牺牲一定的样式自定义性 + +在uniCloud开发中,`DB Schema`中配置了enum枚举等类型后,在web控制台的[自动生成表单](https://uniapp.dcloud.io/uniCloud/schema?id=autocode)功能中,会自动生成``uni-data-checkbox``组件并绑定好data + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-data-checkbox) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-data-picker/changelog.md b/uni-im示例/uni_modules/uni-data-picker/changelog.md new file mode 100644 index 0000000..7edcd87 --- /dev/null +++ b/uni-im示例/uni_modules/uni-data-picker/changelog.md @@ -0,0 +1,68 @@ +## 1.0.9(2022-11-30) +- 修复 v-for 为使用 key 值控制台 warning +## 1.0.8(2022-09-16) +- 可以使用 uni-scss 控制主题色 +## 1.0.7(2022-07-06) +- 优化 pc端图标位置不正确的问题 +## 1.0.6(2022-07-05) +- 优化 显示样式 +## 1.0.5(2022-07-04) +- 修复 uni-data-picker 在 uni-forms-item 中宽度不正确的bug +## 1.0.4(2022-04-19) +- 修复 字节小程序 本地数据无法选择下一级的Bug +## 1.0.3(2022-02-25) +- 修复 nvue 不支持的 v-show 的 bug +## 1.0.2(2022-02-25) +- 修复 条件编译 nvue 不支持的 css 样式 +## 1.0.1(2021-11-23) +- 修复 由上个版本引发的map、v-model等属性不生效的bug +## 1.0.0(2021-11-19) +- 优化 组件 UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-data-picker](https://uniapp.dcloud.io/component/uniui/uni-data-picker) +## 0.4.9(2021-10-28) +- 修复 VUE2 v-model 概率无效的 bug +## 0.4.8(2021-10-27) +- 修复 v-model 概率无效的 bug +## 0.4.7(2021-10-25) +- 新增 属性 spaceInfo 服务空间配置 HBuilderX 3.2.11+ +- 修复 树型 uniCloud 数据类型为 int 时报错的 bug +## 0.4.6(2021-10-19) +- 修复 非 VUE3 v-model 为 0 时无法选中的 bug +## 0.4.5(2021-09-26) +- 新增 清除已选项的功能(通过 clearIcon 属性配置是否显示按钮),同时提供 clear 方法以供调用,二者等效 +- 修复 readonly 为 true 时报错的 bug +## 0.4.4(2021-09-26) +- 修复 上一版本造成的 map 属性失效的 bug +- 新增 ellipsis 属性,支持配置 tab 选项长度过长时是否自动省略 +## 0.4.3(2021-09-24) +- 修复 某些情况下级联未触发的 bug +## 0.4.2(2021-09-23) +- 新增 提供 show 和 hide 方法,开发者可以通过 ref 调用 +- 新增 选项内容过长自动添加省略号 +## 0.4.1(2021-09-15) +- 新增 map 属性 字段映射,将 text/value 映射到数据中的其他字段 +## 0.4.0(2021-07-13) +- 组件兼容 vue3,如何创建 vue3 项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 0.3.5(2021-06-04) +- 修复 无法加载云端数据的问题 +## 0.3.4(2021-05-28) +- 修复 v-model 无效问题 +- 修复 loaddata 为空数据组时加载时间过长问题 +- 修复 上个版本引出的本地数据无法选择带有 children 的 2 级节点 +## 0.3.3(2021-05-12) +- 新增 组件示例地址 +## 0.3.2(2021-04-22) +- 修复 非树形数据有 where 属性查询报错的问题 +## 0.3.1(2021-04-15) +- 修复 本地数据概率无法回显时问题 +## 0.3.0(2021-04-07) +- 新增 支持云端非树形表结构数据 +- 修复 根节点 parent_field 字段等于 null 时选择界面错乱问题 +## 0.2.0(2021-03-15) +- 修复 nodeclick、popupopened、popupclosed 事件无法触发的问题 +## 0.1.9(2021-03-09) +- 修复 微信小程序某些情况下无法选择的问题 +## 0.1.8(2021-02-05) +- 优化 部分样式在 nvue 上的兼容表现 +## 0.1.7(2021-02-05) +- 调整为 uni_modules 目录规范 diff --git a/uni-im示例/uni_modules/uni-data-picker/components/uni-data-picker/keypress.js b/uni-im示例/uni_modules/uni-data-picker/components/uni-data-picker/keypress.js new file mode 100644 index 0000000..6ef26a2 --- /dev/null +++ b/uni-im示例/uni_modules/uni-data-picker/components/uni-data-picker/keypress.js @@ -0,0 +1,45 @@ +// #ifdef H5 +export default { + name: 'Keypress', + props: { + disable: { + type: Boolean, + default: false + } + }, + mounted () { + const keyNames = { + esc: ['Esc', 'Escape'], + tab: 'Tab', + enter: 'Enter', + space: [' ', 'Spacebar'], + up: ['Up', 'ArrowUp'], + left: ['Left', 'ArrowLeft'], + right: ['Right', 'ArrowRight'], + down: ['Down', 'ArrowDown'], + delete: ['Backspace', 'Delete', 'Del'] + } + const listener = ($event) => { + if (this.disable) { + return + } + const keyName = Object.keys(keyNames).find(key => { + const keyName = $event.key + const value = keyNames[key] + return value === keyName || (Array.isArray(value) && value.includes(keyName)) + }) + if (keyName) { + // 避免和其他按键事件冲突 + setTimeout(() => { + this.$emit(keyName, {}) + }, 0) + } + } + document.addEventListener('keyup', listener) + this.$once('hook:beforeDestroy', () => { + document.removeEventListener('keyup', listener) + }) + }, + render: () => {} +} +// #endif diff --git a/uni-im示例/uni_modules/uni-data-picker/components/uni-data-picker/uni-data-picker.vue b/uni-im示例/uni_modules/uni-data-picker/components/uni-data-picker/uni-data-picker.vue new file mode 100644 index 0000000..4553627 --- /dev/null +++ b/uni-im示例/uni_modules/uni-data-picker/components/uni-data-picker/uni-data-picker.vue @@ -0,0 +1,554 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-picker.js b/uni-im示例/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-picker.js new file mode 100644 index 0000000..c12fd54 --- /dev/null +++ b/uni-im示例/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-picker.js @@ -0,0 +1,563 @@ +export default { + props: { + localdata: { + type: [Array, Object], + default () { + return [] + } + }, + spaceInfo: { + type: Object, + default () { + return {} + } + }, + collection: { + type: String, + default: '' + }, + action: { + type: String, + default: '' + }, + field: { + type: String, + default: '' + }, + orderby: { + type: String, + default: '' + }, + where: { + type: [String, Object], + default: '' + }, + pageData: { + type: String, + default: 'add' + }, + pageCurrent: { + type: Number, + default: 1 + }, + pageSize: { + type: Number, + default: 20 + }, + getcount: { + type: [Boolean, String], + default: false + }, + getone: { + type: [Boolean, String], + default: false + }, + gettree: { + type: [Boolean, String], + default: false + }, + manual: { + type: Boolean, + default: false + }, + value: { + type: [Array, String, Number], + default () { + return [] + } + }, + modelValue: { + type: [Array, String, Number], + default () { + return [] + } + }, + preload: { + type: Boolean, + default: false + }, + stepSearh: { + type: Boolean, + default: true + }, + selfField: { + type: String, + default: '' + }, + parentField: { + type: String, + default: '' + }, + multiple: { + type: Boolean, + default: false + }, + map: { + type: Object, + default() { + return { + text: "text", + value: "value" + } + } + } + }, + data() { + return { + loading: false, + errorMessage: '', + loadMore: { + contentdown: '', + contentrefresh: '', + contentnomore: '' + }, + dataList: [], + selected: [], + selectedIndex: 0, + page: { + current: this.pageCurrent, + size: this.pageSize, + count: 0 + } + } + }, + computed: { + isLocaldata() { + return !this.collection.length + }, + postField() { + let fields = [this.field]; + if (this.parentField) { + fields.push(`${this.parentField} as parent_value`); + } + return fields.join(','); + }, + dataValue() { + let isModelValue = Array.isArray(this.modelValue) ? (this.modelValue.length > 0) : (this.modelValue !== null || this.modelValue !== undefined) + return isModelValue ? this.modelValue : this.value + }, + hasValue() { + if (typeof this.dataValue === 'number') { + return true + } + return (this.dataValue != null) && (this.dataValue.length > 0) + } + }, + created() { + this.$watch(() => { + var al = []; + ['pageCurrent', + 'pageSize', + 'spaceInfo', + 'value', + 'modelValue', + 'localdata', + 'collection', + 'action', + 'field', + 'orderby', + 'where', + 'getont', + 'getcount', + 'gettree' + ].forEach(key => { + al.push(this[key]) + }); + return al + }, (newValue, oldValue) => { + let needReset = false + for (let i = 2; i < newValue.length; i++) { + if (newValue[i] != oldValue[i]) { + needReset = true + break + } + } + if (newValue[0] != oldValue[0]) { + this.page.current = this.pageCurrent + } + this.page.size = this.pageSize + + this.onPropsChange() + }) + this._treeData = [] + }, + methods: { + onPropsChange() { + this._treeData = [] + }, + getCommand(options = {}) { + /* eslint-disable no-undef */ + let db = uniCloud.database(this.spaceInfo) + + const action = options.action || this.action + if (action) { + db = db.action(action) + } + + const collection = options.collection || this.collection + db = db.collection(collection) + + const where = options.where || this.where + if (!(!where || !Object.keys(where).length)) { + db = db.where(where) + } + + const field = options.field || this.field + if (field) { + db = db.field(field) + } + + const orderby = options.orderby || this.orderby + if (orderby) { + db = db.orderBy(orderby) + } + + const current = options.pageCurrent !== undefined ? options.pageCurrent : this.page.current + const size = options.pageSize !== undefined ? options.pageSize : this.page.size + const getCount = options.getcount !== undefined ? options.getcount : this.getcount + const getTree = options.gettree !== undefined ? options.gettree : this.gettree + + const getOptions = { + getCount, + getTree + } + if (options.getTreePath) { + getOptions.getTreePath = options.getTreePath + } + + db = db.skip(size * (current - 1)).limit(size).get(getOptions) + + return db + }, + getNodeData(callback) { + if (this.loading) { + return + } + this.loading = true + this.getCommand({ + field: this.postField, + where: this._pathWhere() + }).then((res) => { + this.loading = false + this.selected = res.result.data + callback && callback() + }).catch((err) => { + this.loading = false + this.errorMessage = err + }) + }, + getTreePath(callback) { + if (this.loading) { + return + } + this.loading = true + + this.getCommand({ + field: this.postField, + getTreePath: { + startWith: `${this.selfField}=='${this.dataValue}'` + } + }).then((res) => { + this.loading = false + let treePath = [] + this._extractTreePath(res.result.data, treePath) + this.selected = treePath + callback && callback() + }).catch((err) => { + this.loading = false + this.errorMessage = err + }) + }, + loadData() { + if (this.isLocaldata) { + this._processLocalData() + return + } + + if (this.dataValue != null) { + this._loadNodeData((data) => { + this._treeData = data + this._updateBindData() + this._updateSelected() + }) + return + } + + if (this.stepSearh) { + this._loadNodeData((data) => { + this._treeData = data + this._updateBindData() + }) + } else { + this._loadAllData((data) => { + this._treeData = [] + this._extractTree(data, this._treeData, null) + this._updateBindData() + }) + } + }, + _loadAllData(callback) { + if (this.loading) { + return + } + this.loading = true + + this.getCommand({ + field: this.postField, + gettree: true, + startwith: `${this.selfField}=='${this.dataValue}'` + }).then((res) => { + this.loading = false + callback(res.result.data) + this.onDataChange() + }).catch((err) => { + this.loading = false + this.errorMessage = err + }) + }, + _loadNodeData(callback, pw) { + if (this.loading) { + return + } + this.loading = true + + this.getCommand({ + field: this.postField, + where: pw || this._postWhere(), + pageSize: 500 + }).then((res) => { + this.loading = false + callback(res.result.data) + this.onDataChange() + }).catch((err) => { + this.loading = false + this.errorMessage = err + }) + }, + _pathWhere() { + let result = [] + let where_field = this._getParentNameByField(); + if (where_field) { + result.push(`${where_field} == '${this.dataValue}'`) + } + + if (this.where) { + return `(${this.where}) && (${result.join(' || ')})` + } + + return result.join(' || ') + }, + _postWhere() { + let result = [] + let selected = this.selected + let parentField = this.parentField + if (parentField) { + result.push(`${parentField} == null || ${parentField} == ""`) + } + if (selected.length) { + for (var i = 0; i < selected.length - 1; i++) { + result.push(`${parentField} == '${selected[i].value}'`) + } + } + + let where = [] + if (this.where) { + where.push(`(${this.where})`) + } + if (result.length) { + where.push(`(${result.join(' || ')})`) + } + + return where.join(' && ') + }, + _nodeWhere() { + let result = [] + let selected = this.selected + if (selected.length) { + result.push(`${this.parentField} == '${selected[selected.length - 1].value}'`) + } + + if (this.where) { + return `(${this.where}) && (${result.join(' || ')})` + } + + return result.join(' || ') + }, + _getParentNameByField() { + const fields = this.field.split(','); + let where_field = null; + for (let i = 0; i < fields.length; i++) { + const items = fields[i].split('as'); + if (items.length < 2) { + continue; + } + if (items[1].trim() === 'value') { + where_field = items[0].trim(); + break; + } + } + return where_field + }, + _isTreeView() { + return (this.parentField && this.selfField) + }, + _updateSelected() { + var dl = this.dataList + var sl = this.selected + let textField = this.map.text + let valueField = this.map.value + for (var i = 0; i < sl.length; i++) { + var value = sl[i].value + var dl2 = dl[i] + for (var j = 0; j < dl2.length; j++) { + var item2 = dl2[j] + if (item2[valueField] === value) { + sl[i].text = item2[textField] + break + } + } + } + }, + _updateBindData(node) { + const { + dataList, + hasNodes + } = this._filterData(this._treeData, this.selected) + + let isleaf = this._stepSearh === false && !hasNodes + + if (node) { + node.isleaf = isleaf + } + + this.dataList = dataList + this.selectedIndex = dataList.length - 1 + + if (!isleaf && this.selected.length < dataList.length) { + this.selected.push({ + value: null, + text: "请选择" + }) + } + + return { + isleaf, + hasNodes + } + }, + _filterData(data, paths) { + let dataList = [] + let hasNodes = true + + dataList.push(data.filter((item) => { + return (item.parent_value === null || item.parent_value === undefined || item.parent_value === '') + })) + for (let i = 0; i < paths.length; i++) { + var value = paths[i].value + var nodes = data.filter((item) => { + return item.parent_value === value + }) + + if (nodes.length) { + dataList.push(nodes) + } else { + hasNodes = false + } + } + + return { + dataList, + hasNodes + } + }, + _extractTree(nodes, result, parent_value) { + let list = result || [] + let valueField = this.map.value + for (let i = 0; i < nodes.length; i++) { + let node = nodes[i] + + let child = {} + for (let key in node) { + if (key !== 'children') { + child[key] = node[key] + } + } + if (parent_value !== null && parent_value !== undefined && parent_value !== '') { + child.parent_value = parent_value + } + result.push(child) + + let children = node.children + if (children) { + this._extractTree(children, result, node[valueField]) + } + } + }, + _extractTreePath(nodes, result) { + let list = result || [] + for (let i = 0; i < nodes.length; i++) { + let node = nodes[i] + + let child = {} + for (let key in node) { + if (key !== 'children') { + child[key] = node[key] + } + } + result.push(child) + + let children = node.children + if (children) { + this._extractTreePath(children, result) + } + } + }, + _findNodePath(key, nodes, path = []) { + let textField = this.map.text + let valueField = this.map.value + for (let i = 0; i < nodes.length; i++) { + let node = nodes[i] + let children = node.children + let text = node[textField] + let value = node[valueField] + + path.push({ + value, + text + }) + + if (value === key) { + return path + } + + if (children) { + const p = this._findNodePath(key, children, path) + if (p.length) { + return p + } + } + + path.pop() + } + return [] + }, + _processLocalData() { + this._treeData = [] + this._extractTree(this.localdata, this._treeData) + + var inputValue = this.dataValue + if (inputValue === undefined) { + return + } + + if (Array.isArray(inputValue)) { + inputValue = inputValue[inputValue.length - 1] + if (typeof inputValue === 'object' && inputValue[this.map.value]) { + inputValue = inputValue[this.map.value] + } + } + + this.selected = this._findNodePath(inputValue, this.localdata) + } + } +} diff --git a/uni-im示例/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-pickerview.vue b/uni-im示例/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-pickerview.vue new file mode 100644 index 0000000..159b54d --- /dev/null +++ b/uni-im示例/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-pickerview.vue @@ -0,0 +1,335 @@ + + + + diff --git a/uni-im示例/uni_modules/uni-data-picker/package.json b/uni-im示例/uni_modules/uni-data-picker/package.json new file mode 100644 index 0000000..04b4610 --- /dev/null +++ b/uni-im示例/uni_modules/uni-data-picker/package.json @@ -0,0 +1,90 @@ +{ + "id": "uni-data-picker", + "displayName": "uni-data-picker 数据驱动的picker选择器", + "version": "1.0.9", + "description": "单列、多列级联选择器,常用于省市区城市选择、公司部门选择、多级分类等场景", + "keywords": [ + "uni-ui", + "uniui", + "picker", + "级联", + "省市区", + "" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": [ + "uni-load-more", + "uni-icons", + "uni-scss" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "u" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y", + "京东": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-data-picker/readme.md b/uni-im示例/uni_modules/uni-data-picker/readme.md new file mode 100644 index 0000000..6cda224 --- /dev/null +++ b/uni-im示例/uni_modules/uni-data-picker/readme.md @@ -0,0 +1,22 @@ +## DataPicker 级联选择 +> **组件名:uni-data-picker** +> 代码块: `uDataPicker` +> 关联组件:`uni-data-pickerview`、`uni-load-more`。 + + +`` 是一个选择类[datacom组件](https://uniapp.dcloud.net.cn/component/datacom)。 + +支持单列、和多列级联选择。列数没有限制,如果屏幕显示不全,顶部tab区域会左右滚动。 + +候选数据支持一次性加载完毕,也支持懒加载,比如示例图中,选择了“北京”后,动态加载北京的区县数据。 + +`` 组件尤其适用于地址选择、分类选择等选择类。 + +`` 支持本地数据、云端静态数据(json),uniCloud云数据库数据。 + +`` 可以通过JQL直连uniCloud云数据库,配套[DB Schema](https://uniapp.dcloud.net.cn/uniCloud/schema),可在schema2code中自动生成前端页面,还支持服务器端校验。 + +在uniCloud数据表中新建表“uni-id-address”和“opendb-city-china”,这2个表的schema自带foreignKey关联。在“uni-id-address”表的表结构页面使用schema2code生成前端页面,会自动生成地址管理的维护页面,自动从“opendb-city-china”表包含的中国所有省市区信息里选择地址。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-data-picker) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-dateformat/changelog.md b/uni-im示例/uni_modules/uni-dateformat/changelog.md new file mode 100644 index 0000000..d551d7b --- /dev/null +++ b/uni-im示例/uni_modules/uni-dateformat/changelog.md @@ -0,0 +1,10 @@ +## 1.0.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-dateformat](https://uniapp.dcloud.io/component/uniui/uni-dateformat) +## 0.0.5(2021-07-08) +- 调整 默认时间不再是当前时间,而是显示'-'字符 +## 0.0.4(2021-05-12) +- 新增 组件示例地址 +## 0.0.3(2021-02-04) +- 调整为uni_modules目录规范 +- 修复 iOS 平台日期格式化出错的问题 diff --git a/uni-im示例/uni_modules/uni-dateformat/components/uni-dateformat/date-format.js b/uni-im示例/uni_modules/uni-dateformat/components/uni-dateformat/date-format.js new file mode 100644 index 0000000..e00d559 --- /dev/null +++ b/uni-im示例/uni_modules/uni-dateformat/components/uni-dateformat/date-format.js @@ -0,0 +1,200 @@ +// yyyy-MM-dd hh:mm:ss.SSS 所有支持的类型 +function pad(str, length = 2) { + str += '' + while (str.length < length) { + str = '0' + str + } + return str.slice(-length) +} + +const parser = { + yyyy: (dateObj) => { + return pad(dateObj.year, 4) + }, + yy: (dateObj) => { + return pad(dateObj.year) + }, + MM: (dateObj) => { + return pad(dateObj.month) + }, + M: (dateObj) => { + return dateObj.month + }, + dd: (dateObj) => { + return pad(dateObj.day) + }, + d: (dateObj) => { + return dateObj.day + }, + hh: (dateObj) => { + return pad(dateObj.hour) + }, + h: (dateObj) => { + return dateObj.hour + }, + mm: (dateObj) => { + return pad(dateObj.minute) + }, + m: (dateObj) => { + return dateObj.minute + }, + ss: (dateObj) => { + return pad(dateObj.second) + }, + s: (dateObj) => { + return dateObj.second + }, + SSS: (dateObj) => { + return pad(dateObj.millisecond, 3) + }, + S: (dateObj) => { + return dateObj.millisecond + }, +} + +// 这都n年了iOS依然不认识2020-12-12,需要转换为2020/12/12 +function getDate(time) { + if (time instanceof Date) { + return time + } + switch (typeof time) { + case 'string': + { + // 2020-12-12T12:12:12.000Z、2020-12-12T12:12:12.000 + if (time.indexOf('T') > -1) { + return new Date(time) + } + return new Date(time.replace(/-/g, '/')) + } + default: + return new Date(time) + } +} + +export function formatDate(date, format = 'yyyy/MM/dd hh:mm:ss') { + if (!date && date !== 0) { + return '' + } + date = getDate(date) + const dateObj = { + year: date.getFullYear(), + month: date.getMonth() + 1, + day: date.getDate(), + hour: date.getHours(), + minute: date.getMinutes(), + second: date.getSeconds(), + millisecond: date.getMilliseconds() + } + const tokenRegExp = /yyyy|yy|MM|M|dd|d|hh|h|mm|m|ss|s|SSS|SS|S/ + let flag = true + let result = format + while (flag) { + flag = false + result = result.replace(tokenRegExp, function(matched) { + flag = true + return parser[matched](dateObj) + }) + } + return result +} + +export function friendlyDate(time, { + locale = 'zh', + threshold = [60000, 3600000], + format = 'yyyy/MM/dd hh:mm:ss' +}) { + if (time === '-') { + return time + } + if (!time && time !== 0) { + return '' + } + const localeText = { + zh: { + year: '年', + month: '月', + day: '天', + hour: '小时', + minute: '分钟', + second: '秒', + ago: '前', + later: '后', + justNow: '刚刚', + soon: '马上', + template: '{num}{unit}{suffix}' + }, + en: { + year: 'year', + month: 'month', + day: 'day', + hour: 'hour', + minute: 'minute', + second: 'second', + ago: 'ago', + later: 'later', + justNow: 'just now', + soon: 'soon', + template: '{num} {unit} {suffix}' + } + } + const text = localeText[locale] || localeText.zh + let date = getDate(time) + let ms = date.getTime() - Date.now() + let absMs = Math.abs(ms) + if (absMs < threshold[0]) { + return ms < 0 ? text.justNow : text.soon + } + if (absMs >= threshold[1]) { + return formatDate(date, format) + } + let num + let unit + let suffix = text.later + if (ms < 0) { + suffix = text.ago + ms = -ms + } + const seconds = Math.floor((ms) / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + const months = Math.floor(days / 30) + const years = Math.floor(months / 12) + switch (true) { + case years > 0: + num = years + unit = text.year + break + case months > 0: + num = months + unit = text.month + break + case days > 0: + num = days + unit = text.day + break + case hours > 0: + num = hours + unit = text.hour + break + case minutes > 0: + num = minutes + unit = text.minute + break + default: + num = seconds + unit = text.second + break + } + + if (locale === 'en') { + if (num === 1) { + num = 'a' + } else { + unit += 's' + } + } + + return text.template.replace(/{\s*num\s*}/g, num + '').replace(/{\s*unit\s*}/g, unit).replace(/{\s*suffix\s*}/g, + suffix) +} diff --git a/uni-im示例/uni_modules/uni-dateformat/components/uni-dateformat/uni-dateformat.vue b/uni-im示例/uni_modules/uni-dateformat/components/uni-dateformat/uni-dateformat.vue new file mode 100644 index 0000000..c5ed030 --- /dev/null +++ b/uni-im示例/uni_modules/uni-dateformat/components/uni-dateformat/uni-dateformat.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-dateformat/package.json b/uni-im示例/uni_modules/uni-dateformat/package.json new file mode 100644 index 0000000..786a670 --- /dev/null +++ b/uni-im示例/uni_modules/uni-dateformat/package.json @@ -0,0 +1,88 @@ +{ + "id": "uni-dateformat", + "displayName": "uni-dateformat 日期格式化", + "version": "1.0.0", + "description": "日期格式化组件,可以将日期格式化为1分钟前、刚刚等形式", + "keywords": [ + "uni-ui", + "uniui", + "日期格式化", + "时间格式化", + "格式化时间", + "" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "y", + "联盟": "y" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-dateformat/readme.md b/uni-im示例/uni_modules/uni-dateformat/readme.md new file mode 100644 index 0000000..37ddb6e --- /dev/null +++ b/uni-im示例/uni_modules/uni-dateformat/readme.md @@ -0,0 +1,11 @@ + + +### DateFormat 日期格式化 +> **组件名:uni-dateformat** +> 代码块: `uDateformat` + + +日期格式化组件。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-dateformat) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-datetime-picker/changelog.md b/uni-im示例/uni_modules/uni-datetime-picker/changelog.md new file mode 100644 index 0000000..56d8a1c --- /dev/null +++ b/uni-im示例/uni_modules/uni-datetime-picker/changelog.md @@ -0,0 +1,105 @@ +## 2.2.12(2022-12-01) +- 修复 vue3 下 i18n 国际化初始值不正确的bug +## 2.2.11(2022-09-19) +- 修复,支付宝小程序样式错乱,[详情](https://github.com/dcloudio/uni-app/issues/3861) +## 2.2.10(2022-09-19) +- 修复,反向选择日期范围,日期显示异常,[详情](https://ask.dcloud.net.cn/question/153401?item_id=212892&rf=false) +## 2.2.9(2022-09-16) +- 可以使用 uni-scss 控制主题色 +## 2.2.8(2022-09-08) +- 修复 close事件无效的 bug +## 2.2.7(2022-09-05) +- 修复 移动端 maskClick 无效的 bug,详见:[https://ask.dcloud.net.cn/question/140824?item_id=209458&rf=false](https://ask.dcloud.net.cn/question/140824?item_id=209458&rf=false) +## 2.2.6(2022-06-30) +- 优化 组件样式,调整了组件图标大小、高度、颜色等,与uni-ui风格保持一致 +## 2.2.5(2022-06-24) +- 修复 日历顶部年月及底部确认未国际化 bug +## 2.2.4(2022-03-31) +- 修复 Vue3 下动态赋值,单选类型未响应的 bug +## 2.2.3(2022-03-28) +- 修复 Vue3 下动态赋值未响应的 bug +## 2.2.2(2021-12-10) +- 修复 clear-icon 属性在小程序平台不生效的 bug +## 2.2.1(2021-12-10) +- 修复 日期范围选在小程序平台,必须多点击一次才能取消选中状态的 bug +## 2.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-datetime-picker](https://uniapp.dcloud.io/component/uniui/uni-datetime-picker) +## 2.1.5(2021-11-09) +- 新增 提供组件设计资源,组件样式调整 +## 2.1.4(2021-09-10) +- 修复 hide-second 在移动端的 bug +- 修复 单选赋默认值时,赋值日期未高亮的 bug +- 修复 赋默认值时,移动端未正确显示时间的 bug +## 2.1.3(2021-09-09) +- 新增 hide-second 属性,支持只使用时分,隐藏秒 +## 2.1.2(2021-09-03) +- 优化 取消选中时(范围选)直接开始下一次选择, 避免多点一次 +- 优化 移动端支持清除按钮,同时支持通过 ref 调用组件的 clear 方法 +- 优化 调整字号大小,美化日历界面 +- 修复 因国际化导致的 placeholder 失效的 bug +## 2.1.1(2021-08-24) +- 新增 支持国际化 +- 优化 范围选择器在 pc 端过宽的问题 +## 2.1.0(2021-08-09) +- 新增 适配 vue3 +## 2.0.19(2021-08-09) +- 新增 支持作为 uni-forms 子组件相关功能 +- 修复 在 uni-forms 中使用时,选择时间报 NAN 错误的 bug +## 2.0.18(2021-08-05) +- 修复 type 属性动态赋值无效的 bug +- 修复 ‘确认’按钮被 tabbar 遮盖 bug +- 修复 组件未赋值时范围选左、右日历相同的 bug +## 2.0.17(2021-08-04) +- 修复 范围选未正确显示当前值的 bug +- 修复 h5 平台(移动端)报错 'cale' of undefined 的 bug +## 2.0.16(2021-07-21) +- 新增 return-type 属性支持返回 date 日期对象 +## 2.0.15(2021-07-14) +- 修复 单选日期类型,初始赋值后不在当前日历的 bug +- 新增 clearIcon 属性,显示框的清空按钮可配置显示隐藏(仅 pc 有效) +- 优化 移动端移除显示框的清空按钮,无实际用途 +## 2.0.14(2021-07-14) +- 修复 组件赋值为空,界面未更新的 bug +- 修复 start 和 end 不能动态赋值的 bug +- 修复 范围选类型,用户选择后再次选择右侧日历(结束日期)显示不正确的 bug +## 2.0.13(2021-07-08) +- 修复 范围选择不能动态赋值的 bug +## 2.0.12(2021-07-08) +- 修复 范围选择的初始时间在一个月内时,造成无法选择的bug +## 2.0.11(2021-07-08) +- 优化 弹出层在超出视窗边缘定位不准确的问题 +## 2.0.10(2021-07-08) +- 修复 范围起始点样式的背景色与今日样式的字体前景色融合,导致日期字体看不清的 bug +- 优化 弹出层在超出视窗边缘被遮盖的问题 +## 2.0.9(2021-07-07) +- 新增 maskClick 事件 +- 修复 特殊情况日历 rpx 布局错误的 bug,rpx -> px +- 修复 范围选择时清空返回值不合理的bug,['', ''] -> [] +## 2.0.8(2021-07-07) +- 新增 日期时间显示框支持插槽 +## 2.0.7(2021-07-01) +- 优化 添加 uni-icons 依赖 +## 2.0.6(2021-05-22) +- 修复 图标在小程序上不显示的 bug +- 优化 重命名引用组件,避免潜在组件命名冲突 +## 2.0.5(2021-05-20) +- 优化 代码目录扁平化 +## 2.0.4(2021-05-12) +- 新增 组件示例地址 +## 2.0.3(2021-05-10) +- 修复 ios 下不识别 '-' 日期格式的 bug +- 优化 pc 下弹出层添加边框和阴影 +## 2.0.2(2021-05-08) +- 修复 在 admin 中获取弹出层定位错误的bug +## 2.0.1(2021-05-08) +- 修复 type 属性向下兼容,默认值从 date 变更为 datetime +## 2.0.0(2021-04-30) +- 支持日历形式的日期+时间的范围选择 + > 注意:此版本不向后兼容,不再支持单独时间选择(type=time)及相关的 hide-second 属性(时间选可使用内置组件 picker) +## 1.0.6(2021-03-18) +- 新增 hide-second 属性,时间支持仅选择时、分 +- 修复 选择跟显示的日期不一样的 bug +- 修复 chang事件触发2次的 bug +- 修复 分、秒 end 范围错误的 bug +- 优化 更好的 nvue 适配 diff --git a/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar-item.vue b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar-item.vue new file mode 100644 index 0000000..a2201d3 --- /dev/null +++ b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar-item.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar.vue b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar.vue new file mode 100644 index 0000000..b376470 --- /dev/null +++ b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar.vue @@ -0,0 +1,924 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/en.json b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/en.json new file mode 100644 index 0000000..9acf1ab --- /dev/null +++ b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/en.json @@ -0,0 +1,22 @@ +{ + "uni-datetime-picker.selectDate": "select date", + "uni-datetime-picker.selectTime": "select time", + "uni-datetime-picker.selectDateTime": "select datetime", + "uni-datetime-picker.startDate": "start date", + "uni-datetime-picker.endDate": "end date", + "uni-datetime-picker.startTime": "start time", + "uni-datetime-picker.endTime": "end time", + "uni-datetime-picker.ok": "ok", + "uni-datetime-picker.clear": "clear", + "uni-datetime-picker.cancel": "cancel", + "uni-datetime-picker.year": "-", + "uni-datetime-picker.month": "", + "uni-calender.MON": "MON", + "uni-calender.TUE": "TUE", + "uni-calender.WED": "WED", + "uni-calender.THU": "THU", + "uni-calender.FRI": "FRI", + "uni-calender.SAT": "SAT", + "uni-calender.SUN": "SUN", + "uni-calender.confirm": "confirm" +} diff --git a/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/index.js b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/index.js new file mode 100644 index 0000000..de7509c --- /dev/null +++ b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/index.js @@ -0,0 +1,8 @@ +import en from './en.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hans.json b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hans.json new file mode 100644 index 0000000..d2df5e7 --- /dev/null +++ b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hans.json @@ -0,0 +1,22 @@ +{ + "uni-datetime-picker.selectDate": "选择日期", + "uni-datetime-picker.selectTime": "选择时间", + "uni-datetime-picker.selectDateTime": "选择日期时间", + "uni-datetime-picker.startDate": "开始日期", + "uni-datetime-picker.endDate": "结束日期", + "uni-datetime-picker.startTime": "开始时间", + "uni-datetime-picker.endTime": "结束时间", + "uni-datetime-picker.ok": "确定", + "uni-datetime-picker.clear": "清除", + "uni-datetime-picker.cancel": "取消", + "uni-datetime-picker.year": "年", + "uni-datetime-picker.month": "月", + "uni-calender.SUN": "日", + "uni-calender.MON": "一", + "uni-calender.TUE": "二", + "uni-calender.WED": "三", + "uni-calender.THU": "四", + "uni-calender.FRI": "五", + "uni-calender.SAT": "六", + "uni-calender.confirm": "确认" +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hant.json b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hant.json new file mode 100644 index 0000000..d23fa3c --- /dev/null +++ b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "uni-datetime-picker.selectDate": "選擇日期", + "uni-datetime-picker.selectTime": "選擇時間", + "uni-datetime-picker.selectDateTime": "選擇日期時間", + "uni-datetime-picker.startDate": "開始日期", + "uni-datetime-picker.endDate": "結束日期", + "uni-datetime-picker.startTime": "開始时间", + "uni-datetime-picker.endTime": "結束时间", + "uni-datetime-picker.ok": "確定", + "uni-datetime-picker.clear": "清除", + "uni-datetime-picker.cancel": "取消", + "uni-datetime-picker.year": "年", + "uni-datetime-picker.month": "月", + "uni-calender.SUN": "日", + "uni-calender.MON": "一", + "uni-calender.TUE": "二", + "uni-calender.WED": "三", + "uni-calender.THU": "四", + "uni-calender.FRI": "五", + "uni-calender.SAT": "六", + "uni-calender.confirm": "確認" +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/keypress.js b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/keypress.js new file mode 100644 index 0000000..9601aba --- /dev/null +++ b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/keypress.js @@ -0,0 +1,45 @@ +// #ifdef H5 +export default { + name: 'Keypress', + props: { + disable: { + type: Boolean, + default: false + } + }, + mounted () { + const keyNames = { + esc: ['Esc', 'Escape'], + tab: 'Tab', + enter: 'Enter', + space: [' ', 'Spacebar'], + up: ['Up', 'ArrowUp'], + left: ['Left', 'ArrowLeft'], + right: ['Right', 'ArrowRight'], + down: ['Down', 'ArrowDown'], + delete: ['Backspace', 'Delete', 'Del'] + } + const listener = ($event) => { + if (this.disable) { + return + } + const keyName = Object.keys(keyNames).find(key => { + const keyName = $event.key + const value = keyNames[key] + return value === keyName || (Array.isArray(value) && value.includes(keyName)) + }) + if (keyName) { + // 避免和其他按键事件冲突 + setTimeout(() => { + this.$emit(keyName, {}) + }, 0) + } + } + document.addEventListener('keyup', listener) + this.$once('hook:beforeDestroy', () => { + document.removeEventListener('keyup', listener) + }) + }, + render: () => {} +} +// #endif \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/time-picker.vue b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/time-picker.vue new file mode 100644 index 0000000..b4ba172 --- /dev/null +++ b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/time-picker.vue @@ -0,0 +1,946 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue new file mode 100644 index 0000000..8bac504 --- /dev/null +++ b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue @@ -0,0 +1,1012 @@ + + + + diff --git a/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/util.js b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/util.js new file mode 100644 index 0000000..efa5773 --- /dev/null +++ b/uni-im示例/uni_modules/uni-datetime-picker/components/uni-datetime-picker/util.js @@ -0,0 +1,410 @@ +class Calendar { + constructor({ + date, + selected, + startDate, + endDate, + range, + // multipleStatus + } = {}) { + // 当前日期 + this.date = this.getDate(new Date()) // 当前初入日期 + // 打点信息 + this.selected = selected || []; + // 范围开始 + this.startDate = startDate + // 范围结束 + this.endDate = endDate + this.range = range + // 多选状态 + this.cleanMultipleStatus() + // 每周日期 + this.weeks = {} + // this._getWeek(this.date.fullDate) + // this.multipleStatus = multipleStatus + this.lastHover = false + } + /** + * 设置日期 + * @param {Object} date + */ + setDate(date) { + this.selectDate = this.getDate(date) + this._getWeek(this.selectDate.fullDate) + } + + /** + * 清理多选状态 + */ + cleanMultipleStatus() { + this.multipleStatus = { + before: '', + after: '', + data: [] + } + } + + /** + * 重置开始日期 + */ + resetSatrtDate(startDate) { + // 范围开始 + this.startDate = startDate + + } + + /** + * 重置结束日期 + */ + resetEndDate(endDate) { + // 范围结束 + this.endDate = endDate + } + + /** + * 获取任意时间 + */ + getDate(date, AddDayCount = 0, str = 'day') { + if (!date) { + date = new Date() + } + if (typeof date !== 'object') { + date = date.replace(/-/g, '/') + } + const dd = new Date(date) + switch (str) { + case 'day': + dd.setDate(dd.getDate() + AddDayCount) // 获取AddDayCount天后的日期 + break + case 'month': + if (dd.getDate() === 31) { + dd.setDate(dd.getDate() + AddDayCount) + } else { + dd.setMonth(dd.getMonth() + AddDayCount) // 获取AddDayCount天后的日期 + } + break + case 'year': + dd.setFullYear(dd.getFullYear() + AddDayCount) // 获取AddDayCount天后的日期 + break + } + const y = dd.getFullYear() + const m = dd.getMonth() + 1 < 10 ? '0' + (dd.getMonth() + 1) : dd.getMonth() + 1 // 获取当前月份的日期,不足10补0 + const d = dd.getDate() < 10 ? '0' + dd.getDate() : dd.getDate() // 获取当前几号,不足10补0 + return { + fullDate: y + '-' + m + '-' + d, + year: y, + month: m, + date: d, + day: dd.getDay() + } + } + + + /** + * 获取上月剩余天数 + */ + _getLastMonthDays(firstDay, full) { + let dateArr = [] + for (let i = firstDay; i > 0; i--) { + const beforeDate = new Date(full.year, full.month - 1, -i + 1).getDate() + dateArr.push({ + date: beforeDate, + month: full.month - 1, + disable: true + }) + } + return dateArr + } + /** + * 获取本月天数 + */ + _currentMonthDys(dateData, full) { + let dateArr = [] + let fullDate = this.date.fullDate + for (let i = 1; i <= dateData; i++) { + let isinfo = false + let nowDate = full.year + '-' + (full.month < 10 ? + full.month : full.month) + '-' + (i < 10 ? + '0' + i : i) + // 是否今天 + let isDay = fullDate === nowDate + // 获取打点信息 + let info = this.selected && this.selected.find((item) => { + if (this.dateEqual(nowDate, item.date)) { + return item + } + }) + + // 日期禁用 + let disableBefore = true + let disableAfter = true + if (this.startDate) { + // let dateCompBefore = this.dateCompare(this.startDate, fullDate) + // disableBefore = this.dateCompare(dateCompBefore ? this.startDate : fullDate, nowDate) + disableBefore = this.dateCompare(this.startDate, nowDate) + } + + if (this.endDate) { + // let dateCompAfter = this.dateCompare(fullDate, this.endDate) + // disableAfter = this.dateCompare(nowDate, dateCompAfter ? this.endDate : fullDate) + disableAfter = this.dateCompare(nowDate, this.endDate) + } + let multiples = this.multipleStatus.data + let checked = false + let multiplesStatus = -1 + if (this.range) { + if (multiples) { + multiplesStatus = multiples.findIndex((item) => { + return this.dateEqual(item, nowDate) + }) + } + if (multiplesStatus !== -1) { + checked = true + } + } + let data = { + fullDate: nowDate, + year: full.year, + date: i, + multiple: this.range ? checked : false, + beforeMultiple: this.isLogicBefore(nowDate, this.multipleStatus.before, this.multipleStatus.after), + afterMultiple: this.isLogicAfter(nowDate, this.multipleStatus.before, this.multipleStatus.after), + month: full.month, + disable: !(disableBefore && disableAfter), + isDay, + userChecked: false + } + if (info) { + data.extraInfo = info + } + + dateArr.push(data) + } + return dateArr + } + /** + * 获取下月天数 + */ + _getNextMonthDays(surplus, full) { + let dateArr = [] + for (let i = 1; i < surplus + 1; i++) { + dateArr.push({ + date: i, + month: Number(full.month) + 1, + disable: true + }) + } + return dateArr + } + + /** + * 获取当前日期详情 + * @param {Object} date + */ + getInfo(date) { + if (!date) { + date = new Date() + } + const dateInfo = this.canlender.find(item => item.fullDate === this.getDate(date).fullDate) + return dateInfo + } + + /** + * 比较时间大小 + */ + dateCompare(startDate, endDate) { + // 计算截止时间 + startDate = new Date(startDate.replace('-', '/').replace('-', '/')) + // 计算详细项的截止时间 + endDate = new Date(endDate.replace('-', '/').replace('-', '/')) + if (startDate <= endDate) { + return true + } else { + return false + } + } + + /** + * 比较时间是否相等 + */ + dateEqual(before, after) { + // 计算截止时间 + before = new Date(before.replace('-', '/').replace('-', '/')) + // 计算详细项的截止时间 + after = new Date(after.replace('-', '/').replace('-', '/')) + if (before.getTime() - after.getTime() === 0) { + return true + } else { + return false + } + } + + /** + * 比较真实起始日期 + */ + + isLogicBefore(currentDay, before, after) { + let logicBefore = before + if (before && after) { + logicBefore = this.dateCompare(before, after) ? before : after + } + return this.dateEqual(logicBefore, currentDay) + } + + isLogicAfter(currentDay, before, after) { + let logicAfter = after + if (before && after) { + logicAfter = this.dateCompare(before, after) ? after : before + } + return this.dateEqual(logicAfter, currentDay) + } + + /** + * 获取日期范围内所有日期 + * @param {Object} begin + * @param {Object} end + */ + geDateAll(begin, end) { + var arr = [] + var ab = begin.split('-') + var ae = end.split('-') + var db = new Date() + db.setFullYear(ab[0], ab[1] - 1, ab[2]) + var de = new Date() + de.setFullYear(ae[0], ae[1] - 1, ae[2]) + var unixDb = db.getTime() - 24 * 60 * 60 * 1000 + var unixDe = de.getTime() - 24 * 60 * 60 * 1000 + for (var k = unixDb; k <= unixDe;) { + k = k + 24 * 60 * 60 * 1000 + arr.push(this.getDate(new Date(parseInt(k))).fullDate) + } + return arr + } + + /** + * 获取多选状态 + */ + setMultiple(fullDate) { + let { + before, + after + } = this.multipleStatus + if (!this.range) return + if (before && after) { + if (!this.lastHover) { + this.lastHover = true + return + } + this.multipleStatus.before = fullDate + this.multipleStatus.after = '' + this.multipleStatus.data = [] + this.multipleStatus.fulldate = '' + this.lastHover = false + } else { + if (!before) { + this.multipleStatus.before = fullDate + this.lastHover = false + } else { + this.multipleStatus.after = fullDate + if (this.dateCompare(this.multipleStatus.before, this.multipleStatus.after)) { + this.multipleStatus.data = this.geDateAll(this.multipleStatus.before, this.multipleStatus + .after); + } else { + this.multipleStatus.data = this.geDateAll(this.multipleStatus.after, this.multipleStatus + .before); + } + this.lastHover = true + } + } + this._getWeek(fullDate) + } + + /** + * 鼠标 hover 更新多选状态 + */ + setHoverMultiple(fullDate) { + let { + before, + after + } = this.multipleStatus + + if (!this.range) return + if (this.lastHover) return + + if (!before) { + this.multipleStatus.before = fullDate + } else { + this.multipleStatus.after = fullDate + if (this.dateCompare(this.multipleStatus.before, this.multipleStatus.after)) { + this.multipleStatus.data = this.geDateAll(this.multipleStatus.before, this.multipleStatus.after); + } else { + this.multipleStatus.data = this.geDateAll(this.multipleStatus.after, this.multipleStatus.before); + } + } + this._getWeek(fullDate) + } + + /** + * 更新默认值多选状态 + */ + setDefaultMultiple(before, after) { + this.multipleStatus.before = before + this.multipleStatus.after = after + if (before && after) { + if (this.dateCompare(before, after)) { + this.multipleStatus.data = this.geDateAll(before, after); + this._getWeek(after) + } else { + this.multipleStatus.data = this.geDateAll(after, before); + this._getWeek(before) + } + } + } + + /** + * 获取每周数据 + * @param {Object} dateData + */ + _getWeek(dateData) { + const { + fullDate, + year, + month, + date, + day + } = this.getDate(dateData) + let firstDay = new Date(year, month - 1, 1).getDay() + let currentDay = new Date(year, month, 0).getDate() + let dates = { + lastMonthDays: this._getLastMonthDays(firstDay, this.getDate(dateData)), // 上个月末尾几天 + currentMonthDys: this._currentMonthDys(currentDay, this.getDate(dateData)), // 本月天数 + nextMonthDays: [], // 下个月开始几天 + weeks: [] + } + let canlender = [] + const surplus = 42 - (dates.lastMonthDays.length + dates.currentMonthDys.length) + dates.nextMonthDays = this._getNextMonthDays(surplus, this.getDate(dateData)) + canlender = canlender.concat(dates.lastMonthDays, dates.currentMonthDys, dates.nextMonthDays) + let weeks = {} + // 拼接数组 上个月开始几天 + 本月天数+ 下个月开始几天 + for (let i = 0; i < canlender.length; i++) { + if (i % 7 === 0) { + weeks[parseInt(i / 7)] = new Array(7) + } + weeks[parseInt(i / 7)][i % 7] = canlender[i] + } + this.canlender = canlender + this.weeks = weeks + } + + //静态方法 + // static init(date) { + // if (!this.instance) { + // this.instance = new Calendar(date); + // } + // return this.instance; + // } +} + + +export default Calendar diff --git a/uni-im示例/uni_modules/uni-datetime-picker/package.json b/uni-im示例/uni_modules/uni-datetime-picker/package.json new file mode 100644 index 0000000..1f45ddb --- /dev/null +++ b/uni-im示例/uni_modules/uni-datetime-picker/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-datetime-picker", + "displayName": "uni-datetime-picker 日期选择器", + "version": "2.2.12", + "description": "uni-datetime-picker 日期时间选择器,支持日历,支持范围选择", + "keywords": [ + "uni-datetime-picker", + "uni-ui", + "uniui", + "日期时间选择器", + "日期时间" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "n" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/uni-im示例/uni_modules/uni-datetime-picker/readme.md b/uni-im示例/uni_modules/uni-datetime-picker/readme.md new file mode 100644 index 0000000..162fbef --- /dev/null +++ b/uni-im示例/uni_modules/uni-datetime-picker/readme.md @@ -0,0 +1,21 @@ + + +> `重要通知:组件升级更新 2.0.0 后,支持日期+时间范围选择,组件 ui 将使用日历选择日期,ui 变化较大,同时支持 PC 和 移动端。此版本不向后兼容,不再支持单独的时间选择(type=time)及相关的 hide-second 属性(时间选可使用内置组件 picker)。若仍需使用旧版本,可在插件市场下载*非uni_modules版本*,旧版本将不再维护` + +## DatetimePicker 时间选择器 + +> **组件名:uni-datetime-picker** +> 代码块: `uDatetimePicker` + + +该组件的优势是,支持**时间戳**输入和输出(起始时间、终止时间也支持时间戳),可**同时选择**日期和时间。 + +若只是需要单独选择日期和时间,不需要时间戳输入和输出,可使用原生的 picker 组件。 + +**_点击 picker 默认值规则:_** + +- 若设置初始值 value, 会显示在 picker 显示框中 +- 若无初始值 value,则初始值 value 为当前本地时间 Date.now(), 但不会显示在 picker 显示框中 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-datetime-picker) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-easyinput/changelog.md b/uni-im示例/uni_modules/uni-easyinput/changelog.md new file mode 100644 index 0000000..765401a --- /dev/null +++ b/uni-im示例/uni_modules/uni-easyinput/changelog.md @@ -0,0 +1,97 @@ +## 1.1.9(2023-04-11) +- 修复 vue3 下 keyboardheightchange 事件报错的bug +## 1.1.8(2023-03-29) +- 优化 trim 属性默认值 +## 1.1.7(2023-03-29) +- 新增 cursor-spacing 属性 +## 1.1.6(2023-01-28) +- 新增 keyboardheightchange 事件,可监听键盘高度变化 +## 1.1.5(2022-11-29) +- 优化 主题样式 +## 1.1.4(2022-10-27) +- 修复 props 中背景颜色无默认值的bug +## 1.1.0(2022-06-30) + +- 新增 在 uni-forms 1.4.0 中使用可以在 blur 时校验内容 +- 新增 clear 事件,点击右侧叉号图标触发 +- 新增 change 事件 ,仅在输入框失去焦点或用户按下回车时触发 +- 优化 组件样式,组件获取焦点时高亮显示,图标颜色调整等 + +## 1.0.5(2022-06-07) + +- 优化 clearable 显示策略 + +## 1.0.4(2022-06-07) + +- 优化 clearable 显示策略 + +## 1.0.3(2022-05-20) + +- 修复 关闭图标某些情况下无法取消的 bug + +## 1.0.2(2022-04-12) + +- 修复 默认值不生效的 bug + +## 1.0.1(2022-04-02) + +- 修复 value 不能为 0 的 bug + +## 1.0.0(2021-11-19) + +- 优化 组件 UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-easyinput](https://uniapp.dcloud.io/component/uniui/uni-easyinput) + +## 0.1.4(2021-08-20) + +- 修复 在 uni-forms 的动态表单中默认值校验不通过的 bug + +## 0.1.3(2021-08-11) + +- 修复 在 uni-forms 中重置表单,错误信息无法清除的问题 + +## 0.1.2(2021-07-30) + +- 优化 vue3 下事件警告的问题 + +## 0.1.1 + +- 优化 errorMessage 属性支持 Boolean 类型 + +## 0.1.0(2021-07-13) + +- 组件兼容 vue3,如何创建 vue3 项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) + +## 0.0.16(2021-06-29) + +- 修复 confirmType 属性(仅 type="text" 生效)导致多行文本框无法换行的 bug + +## 0.0.15(2021-06-21) + +- 修复 passwordIcon 属性拼写错误的 bug + +## 0.0.14(2021-06-18) + +- 新增 passwordIcon 属性,当 type=password 时是否显示小眼睛图标 +- 修复 confirmType 属性不生效的问题 + +## 0.0.13(2021-06-04) + +- 修复 disabled 状态可清出内容的 bug + +## 0.0.12(2021-05-12) + +- 新增 组件示例地址 + +## 0.0.11(2021-05-07) + +- 修复 input-border 属性不生效的问题 + +## 0.0.10(2021-04-30) + +- 修复 ios 遮挡文字、显示一半的问题 + +## 0.0.9(2021-02-05) + +- 调整为 uni_modules 目录规范 +- 优化 兼容 nvue 页面 diff --git a/uni-im示例/uni_modules/uni-easyinput/components/uni-easyinput/common.js b/uni-im示例/uni_modules/uni-easyinput/components/uni-easyinput/common.js new file mode 100644 index 0000000..df9abe1 --- /dev/null +++ b/uni-im示例/uni_modules/uni-easyinput/components/uni-easyinput/common.js @@ -0,0 +1,56 @@ +/** + * @desc 函数防抖 + * @param func 目标函数 + * @param wait 延迟执行毫秒数 + * @param immediate true - 立即执行, false - 延迟执行 + */ +export const debounce = function(func, wait = 1000, immediate = true) { + let timer; + console.log(1); + return function() { + console.log(123); + let context = this, + args = arguments; + if (timer) clearTimeout(timer); + if (immediate) { + let callNow = !timer; + timer = setTimeout(() => { + timer = null; + }, wait); + if (callNow) func.apply(context, args); + } else { + timer = setTimeout(() => { + func.apply(context, args); + }, wait) + } + } +} +/** + * @desc 函数节流 + * @param func 函数 + * @param wait 延迟执行毫秒数 + * @param type 1 使用表时间戳,在时间段开始的时候触发 2 使用表定时器,在时间段结束的时候触发 + */ +export const throttle = (func, wait = 1000, type = 1) => { + let previous = 0; + let timeout; + return function() { + let context = this; + let args = arguments; + if (type === 1) { + let now = Date.now(); + + if (now - previous > wait) { + func.apply(context, args); + previous = now; + } + } else if (type === 2) { + if (!timeout) { + timeout = setTimeout(() => { + timeout = null; + func.apply(context, args) + }, wait) + } + } + } +} diff --git a/uni-im示例/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue b/uni-im示例/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue new file mode 100644 index 0000000..2c7993a --- /dev/null +++ b/uni-im示例/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue @@ -0,0 +1,657 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-easyinput/package.json b/uni-im示例/uni_modules/uni-easyinput/package.json new file mode 100644 index 0000000..bd128e1 --- /dev/null +++ b/uni-im示例/uni_modules/uni-easyinput/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-easyinput", + "displayName": "uni-easyinput 增强输入框", + "version": "1.1.9", + "description": "Easyinput 组件是对原生input组件的增强", + "keywords": [ + "uni-ui", + "uniui", + "input", + "uni-easyinput", + "输入框" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-easyinput/readme.md b/uni-im示例/uni_modules/uni-easyinput/readme.md new file mode 100644 index 0000000..f1faf8f --- /dev/null +++ b/uni-im示例/uni_modules/uni-easyinput/readme.md @@ -0,0 +1,11 @@ + + +### Easyinput 增强输入框 +> **组件名:uni-easyinput** +> 代码块: `uEasyinput` + + +easyinput 组件是对原生input组件的增强 ,是专门为配合表单组件[uni-forms](https://ext.dcloud.net.cn/plugin?id=2773)而设计的,easyinput 内置了边框,图标等,同时包含 input 所有功能 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-easyinput) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-fab/changelog.md b/uni-im示例/uni_modules/uni-fab/changelog.md new file mode 100644 index 0000000..0048ff9 --- /dev/null +++ b/uni-im示例/uni_modules/uni-fab/changelog.md @@ -0,0 +1,21 @@ +## 1.2.4(2022-09-07) +小程序端由于 style 使用了对象导致报错,[详情](https://ask.dcloud.net.cn/question/152790?item_id=211778&rf=false) +## 1.2.3(2022-09-05) +- 修复 nvue 环境下,具有 tabBar 时,fab 组件下部位置无法正常获取 --window-bottom 的bug,详见:[https://ask.dcloud.net.cn/question/110638?notification_id=826310](https://ask.dcloud.net.cn/question/110638?notification_id=826310) +## 1.2.2(2021-12-29) +- 更新 组件依赖 +## 1.2.1(2021-11-19) +- 修复 阴影颜色不正确的bug +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-fab](https://uniapp.dcloud.io/component/uniui/uni-fab) +## 1.1.1(2021-11-09) +- 新增 提供组件设计资源,组件样式调整 +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.7(2021-05-12) +- 新增 组件示例地址 +## 1.0.6(2021-02-05) +- 调整为uni_modules目录规范 +- 优化 按钮背景色调整 +- 优化 兼容pc端 diff --git a/uni-im示例/uni_modules/uni-fab/components/uni-fab/uni-fab.vue b/uni-im示例/uni_modules/uni-fab/components/uni-fab/uni-fab.vue new file mode 100644 index 0000000..96c4739 --- /dev/null +++ b/uni-im示例/uni_modules/uni-fab/components/uni-fab/uni-fab.vue @@ -0,0 +1,487 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-fab/package.json b/uni-im示例/uni_modules/uni-fab/package.json new file mode 100644 index 0000000..6636170 --- /dev/null +++ b/uni-im示例/uni_modules/uni-fab/package.json @@ -0,0 +1,84 @@ +{ + "id": "uni-fab", + "displayName": "uni-fab 悬浮按钮", + "version": "1.2.4", + "description": "悬浮按钮 fab button ,点击可展开一个图标按钮菜单。", + "keywords": [ + "uni-ui", + "uniui", + "按钮", + "悬浮按钮", + "fab" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": ["uni-scss","uni-icons"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/uni-im示例/uni_modules/uni-fab/readme.md b/uni-im示例/uni_modules/uni-fab/readme.md new file mode 100644 index 0000000..9a444e8 --- /dev/null +++ b/uni-im示例/uni_modules/uni-fab/readme.md @@ -0,0 +1,9 @@ +## Fab 悬浮按钮 +> **组件名:uni-fab** +> 代码块: `uFab` + + +点击可展开一个图形按钮菜单 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-fab) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-file-picker/changelog.md b/uni-im示例/uni_modules/uni-file-picker/changelog.md new file mode 100644 index 0000000..e1621c3 --- /dev/null +++ b/uni-im示例/uni_modules/uni-file-picker/changelog.md @@ -0,0 +1,65 @@ +## 1.0.3(2022-12-21) +- 新增 sourceType 属性, 可以自定义图片和视频选择的来源 +## 1.0.2(2022-07-04) +- 修复 在uni-forms下样式不生效的bug +## 1.0.1(2021-11-23) +- 修复 参数为对象的情况下,url在某些情况显示错误的bug +## 1.0.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-file-picker](https://uniapp.dcloud.io/component/uniui/uni-file-picker) +## 0.2.16(2021-11-08) +- 修复 传入空对象 ,显示错误的Bug +## 0.2.15(2021-08-30) +- 修复 return-type="object" 时且存在v-model时,无法删除文件的Bug +## 0.2.14(2021-08-23) +- 新增 参数中返回 fileID 字段 +## 0.2.13(2021-08-23) +- 修复 腾讯云传入fileID 不能回显的bug +- 修复 选择图片后,不能放大的问题 +## 0.2.12(2021-08-17) +- 修复 由于 0.2.11 版本引起的不能回显图片的Bug +## 0.2.11(2021-08-16) +- 新增 clearFiles(index) 方法,可以手动删除指定文件 +- 修复 v-model 值设为 null 报错的Bug +## 0.2.10(2021-08-13) +- 修复 return-type="object" 时,无法删除文件的Bug +## 0.2.9(2021-08-03) +- 修复 auto-upload 属性失效的Bug +## 0.2.8(2021-07-31) +- 修复 fileExtname属性不指定值报错的Bug +## 0.2.7(2021-07-31) +- 修复 在某种场景下图片不回显的Bug +## 0.2.6(2021-07-30) +- 修复 return-type为object下,返回值不正确的Bug +## 0.2.5(2021-07-30) +- 修复(重要) H5 平台下如果和uni-forms组件一同使用导致页面卡死的问题 +## 0.2.3(2021-07-28) +- 优化 调整示例代码 +## 0.2.2(2021-07-27) +- 修复 vue3 下赋值错误的Bug +- 优化 h5平台下上传文件导致页面卡死的问题 +## 0.2.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 0.1.1(2021-07-02) +- 修复 sourceType 缺少默认值导致 ios 无法选择文件 +## 0.1.0(2021-06-30) +- 优化 解耦与uniCloud的强绑定关系 ,如不绑定服务空间,默认autoUpload为false且不可更改 +## 0.0.11(2021-06-30) +- 修复 由 0.0.10 版本引发的 returnType 属性失效的问题 +## 0.0.10(2021-06-29) +- 优化 文件上传后进度条消失时机 +## 0.0.9(2021-06-29) +- 修复 在uni-forms 中,删除文件 ,获取的值不对的Bug +## 0.0.8(2021-06-15) +- 修复 删除文件时无法触发 v-model 的Bug +## 0.0.7(2021-05-12) +- 新增 组件示例地址 +## 0.0.6(2021-04-09) +- 修复 选择的文件非 file-extname 字段指定的扩展名报错的Bug +## 0.0.5(2021-04-09) +- 优化 更新组件示例 +## 0.0.4(2021-04-09) +- 优化 file-extname 字段支持字符串写法,多个扩展名需要用逗号分隔 +## 0.0.3(2021-02-05) +- 调整为uni_modules目录规范 +- 修复 微信小程序不指定 fileExtname 属性选择失败的Bug diff --git a/uni-im示例/uni_modules/uni-file-picker/components/uni-file-picker/choose-and-upload-file.js b/uni-im示例/uni_modules/uni-file-picker/components/uni-file-picker/choose-and-upload-file.js new file mode 100644 index 0000000..aff0864 --- /dev/null +++ b/uni-im示例/uni_modules/uni-file-picker/components/uni-file-picker/choose-and-upload-file.js @@ -0,0 +1,224 @@ +'use strict'; + +const ERR_MSG_OK = 'chooseAndUploadFile:ok'; +const ERR_MSG_FAIL = 'chooseAndUploadFile:fail'; + +function chooseImage(opts) { + const { + count, + sizeType = ['original', 'compressed'], + sourceType, + extension + } = opts + return new Promise((resolve, reject) => { + uni.chooseImage({ + count, + sizeType, + sourceType, + extension, + success(res) { + resolve(normalizeChooseAndUploadFileRes(res, 'image')); + }, + fail(res) { + reject({ + errMsg: res.errMsg.replace('chooseImage:fail', ERR_MSG_FAIL), + }); + }, + }); + }); +} + +function chooseVideo(opts) { + const { + camera, + compressed, + maxDuration, + sourceType, + extension + } = opts; + return new Promise((resolve, reject) => { + uni.chooseVideo({ + camera, + compressed, + maxDuration, + sourceType, + extension, + success(res) { + const { + tempFilePath, + duration, + size, + height, + width + } = res; + resolve(normalizeChooseAndUploadFileRes({ + errMsg: 'chooseVideo:ok', + tempFilePaths: [tempFilePath], + tempFiles: [ + { + name: (res.tempFile && res.tempFile.name) || '', + path: tempFilePath, + size, + type: (res.tempFile && res.tempFile.type) || '', + width, + height, + duration, + fileType: 'video', + cloudPath: '', + }, ], + }, 'video')); + }, + fail(res) { + reject({ + errMsg: res.errMsg.replace('chooseVideo:fail', ERR_MSG_FAIL), + }); + }, + }); + }); +} + +function chooseAll(opts) { + const { + count, + extension + } = opts; + return new Promise((resolve, reject) => { + let chooseFile = uni.chooseFile; + if (typeof wx !== 'undefined' && + typeof wx.chooseMessageFile === 'function') { + chooseFile = wx.chooseMessageFile; + } + if (typeof chooseFile !== 'function') { + return reject({ + errMsg: ERR_MSG_FAIL + ' 请指定 type 类型,该平台仅支持选择 image 或 video。', + }); + } + chooseFile({ + type: 'all', + count, + extension, + success(res) { + resolve(normalizeChooseAndUploadFileRes(res)); + }, + fail(res) { + reject({ + errMsg: res.errMsg.replace('chooseFile:fail', ERR_MSG_FAIL), + }); + }, + }); + }); +} + +function normalizeChooseAndUploadFileRes(res, fileType) { + res.tempFiles.forEach((item, index) => { + if (!item.name) { + item.name = item.path.substring(item.path.lastIndexOf('/') + 1); + } + if (fileType) { + item.fileType = fileType; + } + item.cloudPath = + Date.now() + '_' + index + item.name.substring(item.name.lastIndexOf('.')); + }); + if (!res.tempFilePaths) { + res.tempFilePaths = res.tempFiles.map((file) => file.path); + } + return res; +} + +function uploadCloudFiles(files, max = 5, onUploadProgress) { + files = JSON.parse(JSON.stringify(files)) + const len = files.length + let count = 0 + let self = this + return new Promise(resolve => { + while (count < max) { + next() + } + + function next() { + let cur = count++ + if (cur >= len) { + !files.find(item => !item.url && !item.errMsg) && resolve(files) + return + } + const fileItem = files[cur] + const index = self.files.findIndex(v => v.uuid === fileItem.uuid) + fileItem.url = '' + delete fileItem.errMsg + + uniCloud + .uploadFile({ + filePath: fileItem.path, + cloudPath: fileItem.cloudPath, + fileType: fileItem.fileType, + onUploadProgress: res => { + res.index = index + onUploadProgress && onUploadProgress(res) + } + }) + .then(res => { + fileItem.url = res.fileID + fileItem.index = index + if (cur < len) { + next() + } + }) + .catch(res => { + fileItem.errMsg = res.errMsg || res.message + fileItem.index = index + if (cur < len) { + next() + } + }) + } + }) +} + + + + + +function uploadFiles(choosePromise, { + onChooseFile, + onUploadProgress +}) { + return choosePromise + .then((res) => { + if (onChooseFile) { + const customChooseRes = onChooseFile(res); + if (typeof customChooseRes !== 'undefined') { + return Promise.resolve(customChooseRes).then((chooseRes) => typeof chooseRes === 'undefined' ? + res : chooseRes); + } + } + return res; + }) + .then((res) => { + if (res === false) { + return { + errMsg: ERR_MSG_OK, + tempFilePaths: [], + tempFiles: [], + }; + } + return res + }) +} + +function chooseAndUploadFile(opts = { + type: 'all' +}) { + if (opts.type === 'image') { + return uploadFiles(chooseImage(opts), opts); + } + else if (opts.type === 'video') { + return uploadFiles(chooseVideo(opts), opts); + } + return uploadFiles(chooseAll(opts), opts); +} + +export { + chooseAndUploadFile, + uploadCloudFiles +}; diff --git a/uni-im示例/uni_modules/uni-file-picker/components/uni-file-picker/uni-file-picker.vue b/uni-im示例/uni_modules/uni-file-picker/components/uni-file-picker/uni-file-picker.vue new file mode 100644 index 0000000..9bb9829 --- /dev/null +++ b/uni-im示例/uni_modules/uni-file-picker/components/uni-file-picker/uni-file-picker.vue @@ -0,0 +1,663 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-file-picker/components/uni-file-picker/upload-file.vue b/uni-im示例/uni_modules/uni-file-picker/components/uni-file-picker/upload-file.vue new file mode 100644 index 0000000..625d92e --- /dev/null +++ b/uni-im示例/uni_modules/uni-file-picker/components/uni-file-picker/upload-file.vue @@ -0,0 +1,325 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-file-picker/components/uni-file-picker/upload-image.vue b/uni-im示例/uni_modules/uni-file-picker/components/uni-file-picker/upload-image.vue new file mode 100644 index 0000000..2a29bc2 --- /dev/null +++ b/uni-im示例/uni_modules/uni-file-picker/components/uni-file-picker/upload-image.vue @@ -0,0 +1,292 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-file-picker/components/uni-file-picker/utils.js b/uni-im示例/uni_modules/uni-file-picker/components/uni-file-picker/utils.js new file mode 100644 index 0000000..60aaa3e --- /dev/null +++ b/uni-im示例/uni_modules/uni-file-picker/components/uni-file-picker/utils.js @@ -0,0 +1,109 @@ +/** + * 获取文件名和后缀 + * @param {String} name + */ +export const get_file_ext = (name) => { + const last_len = name.lastIndexOf('.') + const len = name.length + return { + name: name.substring(0, last_len), + ext: name.substring(last_len + 1, len) + } +} + +/** + * 获取扩展名 + * @param {Array} fileExtname + */ +export const get_extname = (fileExtname) => { + if (!Array.isArray(fileExtname)) { + let extname = fileExtname.replace(/(\[|\])/g, '') + return extname.split(',') + } else { + return fileExtname + } + return [] +} + +/** + * 获取文件和检测是否可选 + */ +export const get_files_and_is_max = (res, _extname) => { + let filePaths = [] + let files = [] + if(!_extname || _extname.length === 0){ + return { + filePaths, + files + } + } + res.tempFiles.forEach(v => { + let fileFullName = get_file_ext(v.name) + const extname = fileFullName.ext.toLowerCase() + if (_extname.indexOf(extname) !== -1) { + files.push(v) + filePaths.push(v.path) + } + }) + if (files.length !== res.tempFiles.length) { + uni.showToast({ + title: `当前选择了${res.tempFiles.length}个文件 ,${res.tempFiles.length - files.length} 个文件格式不正确`, + icon: 'none', + duration: 5000 + }) + } + + return { + filePaths, + files + } +} + + +/** + * 获取图片信息 + * @param {Object} filepath + */ +export const get_file_info = (filepath) => { + return new Promise((resolve, reject) => { + uni.getImageInfo({ + src: filepath, + success(res) { + resolve(res) + }, + fail(err) { + reject(err) + } + }) + }) +} +/** + * 获取封装数据 + */ +export const get_file_data = async (files, type = 'image') => { + // 最终需要上传数据库的数据 + let fileFullName = get_file_ext(files.name) + const extname = fileFullName.ext.toLowerCase() + let filedata = { + name: files.name, + uuid: files.uuid, + extname: extname || '', + cloudPath: files.cloudPath, + fileType: files.fileType, + url: files.path || files.path, + size: files.size, //单位是字节 + image: {}, + path: files.path, + video: {} + } + if (type === 'image') { + const imageinfo = await get_file_info(files.path) + delete filedata.video + filedata.image.width = imageinfo.width + filedata.image.height = imageinfo.height + filedata.image.location = imageinfo.path + } else { + delete filedata.image + } + return filedata +} diff --git a/uni-im示例/uni_modules/uni-file-picker/package.json b/uni-im示例/uni_modules/uni-file-picker/package.json new file mode 100644 index 0000000..8d5227b --- /dev/null +++ b/uni-im示例/uni_modules/uni-file-picker/package.json @@ -0,0 +1,83 @@ +{ + "id": "uni-file-picker", + "displayName": "uni-file-picker 文件选择上传", + "version": "1.0.3", + "description": "文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间", + "keywords": [ + "uni-ui", + "uniui", + "图片上传", + "文件上传" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "n" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/uni-im示例/uni_modules/uni-file-picker/readme.md b/uni-im示例/uni_modules/uni-file-picker/readme.md new file mode 100644 index 0000000..c8399a5 --- /dev/null +++ b/uni-im示例/uni_modules/uni-file-picker/readme.md @@ -0,0 +1,11 @@ + +## FilePicker 文件选择上传 + +> **组件名:uni-file-picker** +> 代码块: `uFilePicker` + + +文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-file-picker) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-forms/changelog.md b/uni-im示例/uni_modules/uni-forms/changelog.md new file mode 100644 index 0000000..8218df5 --- /dev/null +++ b/uni-im示例/uni_modules/uni-forms/changelog.md @@ -0,0 +1,92 @@ +## 1.4.9(2023-02-10) +- 修复 required 参数无法动态绑定 +## 1.4.8(2022-08-23) +- 优化 根据 rules 自动添加 required 的问题 +## 1.4.7(2022-08-22) +- 修复 item 未设置 require 属性,rules 设置 require 后,星号也显示的 bug,详见:[https://ask.dcloud.net.cn/question/151540](https://ask.dcloud.net.cn/question/151540) +## 1.4.6(2022-07-13) +- 修复 model 需要校验的值没有声明对应字段时,导致第一次不触发校验的bug +## 1.4.5(2022-07-05) +- 新增 更多表单示例 +- 优化 子表单组件过期提示的问题 +- 优化 子表单组件uni-datetime-picker、uni-data-select、uni-data-picker的显示样式 +## 1.4.4(2022-07-04) +- 更新 删除组件日志 +## 1.4.3(2022-07-04) +- 修复 由 1.4.0 引发的 label 插槽不生效的bug +## 1.4.2(2022-07-04) +- 修复 子组件找不到 setValue 报错的bug +## 1.4.1(2022-07-04) +- 修复 uni-data-picker 在 uni-forms-item 中报错的bug +- 修复 uni-data-picker 在 uni-forms-item 中宽度不正确的bug +## 1.4.0(2022-06-30) +- 【重要】组件逻辑重构,部分用法用旧版本不兼容,请注意兼容问题 +- 【重要】组件使用 Provide/Inject 方式注入依赖,提供了自定义表单组件调用 uni-forms 校验表单的能力 +- 新增 model 属性,等同于原 value/modelValue 属性,旧属性即将废弃 +- 新增 validateTrigger 属性的 blur 值,仅 uni-easyinput 生效 +- 新增 onFieldChange 方法,可以对子表单进行校验,可替代binddata方法 +- 新增 子表单的 setRules 方法,配合自定义校验函数使用 +- 新增 uni-forms-item 的 setRules 方法,配置动态表单使用可动态更新校验规则 +- 优化 动态表单校验方式,废弃拼接name的方式 +## 1.3.3(2022-06-22) +- 修复 表单校验顺序无序问题 +## 1.3.2(2021-12-09) +- +## 1.3.1(2021-11-19) +- 修复 label 插槽不生效的bug +## 1.3.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-forms](https://uniapp.dcloud.io/component/uniui/uni-forms) +## 1.2.7(2021-08-13) +- 修复 没有添加校验规则的字段依然报错的Bug +## 1.2.6(2021-08-11) +- 修复 重置表单错误信息无法清除的问题 +## 1.2.5(2021-08-11) +- 优化 组件文档 +## 1.2.4(2021-08-11) +- 修复 表单验证只生效一次的问题 +## 1.2.3(2021-07-30) +- 优化 vue3下事件警告的问题 +## 1.2.2(2021-07-26) +- 修复 vue2 下条件编译导致destroyed生命周期失效的Bug +- 修复 1.2.1 引起的示例在小程序平台报错的Bug +## 1.2.1(2021-07-22) +- 修复 动态校验表单,默认值为空的情况下校验失效的Bug +- 修复 不指定name属性时,运行报错的Bug +- 优化 label默认宽度从65调整至70,使required为true且四字时不换行 +- 优化 组件示例,新增动态校验示例代码 +- 优化 组件文档,使用方式更清晰 +## 1.2.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.1.2(2021-06-25) +- 修复 pattern 属性在微信小程序平台无效的问题 +## 1.1.1(2021-06-22) +- 修复 validate-trigger属性为submit且err-show-type属性为toast时不能弹出的Bug +## 1.1.0(2021-06-22) +- 修复 只写setRules方法而导致校验不生效的Bug +- 修复 由上个办法引发的错误提示文字错位的Bug +## 1.0.48(2021-06-21) +- 修复 不设置 label 属性 ,无法设置label插槽的问题 +## 1.0.47(2021-06-21) +- 修复 不设置label属性,label-width属性不生效的bug +- 修复 setRules 方法与rules属性冲突的问题 +## 1.0.46(2021-06-04) +- 修复 动态删减数据导致报错的问题 +## 1.0.45(2021-06-04) +- 新增 modelValue 属性 ,value 即将废弃 +## 1.0.44(2021-06-02) +- 新增 uni-forms-item 可以设置单独的 rules +- 新增 validate 事件增加 keepitem 参数,可以选择那些字段不过滤 +- 优化 submit 事件重命名为 validate +## 1.0.43(2021-05-12) +- 新增 组件示例地址 +## 1.0.42(2021-04-30) +- 修复 自定义检验器失效的问题 +## 1.0.41(2021-03-05) +- 更新 校验器 +- 修复 表单规则设置类型为 number 的情况下,值为0校验失败的Bug +## 1.0.40(2021-03-04) +- 修复 动态显示uni-forms-item的情况下,submit 方法获取值错误的Bug +## 1.0.39(2021-02-05) +- 调整为uni_modules目录规范 +- 修复 校验器传入 int 等类型 ,返回String类型的Bug diff --git a/uni-im示例/uni_modules/uni-forms/components/uni-forms-item/uni-forms-item.vue b/uni-im示例/uni_modules/uni-forms/components/uni-forms-item/uni-forms-item.vue new file mode 100644 index 0000000..91fe351 --- /dev/null +++ b/uni-im示例/uni_modules/uni-forms/components/uni-forms-item/uni-forms-item.vue @@ -0,0 +1,627 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-forms/components/uni-forms/uni-forms.vue b/uni-im示例/uni_modules/uni-forms/components/uni-forms/uni-forms.vue new file mode 100644 index 0000000..ed2f6d9 --- /dev/null +++ b/uni-im示例/uni_modules/uni-forms/components/uni-forms/uni-forms.vue @@ -0,0 +1,397 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-forms/components/uni-forms/utils.js b/uni-im示例/uni_modules/uni-forms/components/uni-forms/utils.js new file mode 100644 index 0000000..6da2421 --- /dev/null +++ b/uni-im示例/uni_modules/uni-forms/components/uni-forms/utils.js @@ -0,0 +1,293 @@ +/** + * 简单处理对象拷贝 + * @param {Obejct} 被拷贝对象 + * @@return {Object} 拷贝对象 + */ +export const deepCopy = (val) => { + return JSON.parse(JSON.stringify(val)) +} +/** + * 过滤数字类型 + * @param {String} format 数字类型 + * @@return {Boolean} 返回是否为数字类型 + */ +export const typeFilter = (format) => { + return format === 'int' || format === 'double' || format === 'number' || format === 'timestamp'; +} + +/** + * 把 value 转换成指定的类型,用于处理初始值,原因是初始值需要入库不能为 undefined + * @param {String} key 字段名 + * @param {any} value 字段值 + * @param {Object} rules 表单校验规则 + */ +export const getValue = (key, value, rules) => { + const isRuleNumType = rules.find(val => val.format && typeFilter(val.format)); + const isRuleBoolType = rules.find(val => (val.format && val.format === 'boolean') || val.format === 'bool'); + // 输入类型为 number + if (!!isRuleNumType) { + if (!value && value !== 0) { + value = null + } else { + value = isNumber(Number(value)) ? Number(value) : value + } + } + + // 输入类型为 boolean + if (!!isRuleBoolType) { + value = isBoolean(value) ? value : false + } + + return value; +} + +/** + * 获取表单数据 + * @param {String|Array} name 真实名称,需要使用 realName 获取 + * @param {Object} data 原始数据 + * @param {any} value 需要设置的值 + */ +export const setDataValue = (field, formdata, value) => { + formdata[field] = value + return value || '' +} + +/** + * 获取表单数据 + * @param {String|Array} field 真实名称,需要使用 realName 获取 + * @param {Object} data 原始数据 + */ +export const getDataValue = (field, data) => { + return objGet(data, field) +} + +/** + * 获取表单类型 + * @param {String|Array} field 真实名称,需要使用 realName 获取 + */ +export const getDataValueType = (field, data) => { + const value = getDataValue(field, data) + return { + type: type(value), + value + } +} + +/** + * 获取表单可用的真实name + * @param {String|Array} name 表单name + * @@return {String} 表单可用的真实name + */ +export const realName = (name, data = {}) => { + const base_name = _basePath(name) + if (typeof base_name === 'object' && Array.isArray(base_name) && base_name.length > 1) { + const realname = base_name.reduce((a, b) => a += `#${b}`, '_formdata_') + return realname + } + return base_name[0] || name +} + +/** + * 判断是否表单可用的真实name + * @param {String|Array} name 表单name + * @@return {String} 表单可用的真实name + */ +export const isRealName = (name) => { + const reg = /^_formdata_#*/ + return reg.test(name) +} + +/** + * 获取表单数据的原始格式 + * @@return {Object|Array} object 需要解析的数据 + */ +export const rawData = (object = {}, name) => { + let newData = JSON.parse(JSON.stringify(object)) + let formData = {} + for(let i in newData){ + let path = name2arr(i) + objSet(formData,path,newData[i]) + } + return formData +} + +/** + * 真实name还原为 array + * @param {*} name + */ +export const name2arr = (name) => { + let field = name.replace('_formdata_#', '') + field = field.split('#').map(v => (isNumber(v) ? Number(v) : v)) + return field +} + +/** + * 对象中设置值 + * @param {Object|Array} object 源数据 + * @param {String| Array} path 'a.b.c' 或 ['a',0,'b','c'] + * @param {String} value 需要设置的值 + */ +export const objSet = (object, path, value) => { + if (typeof object !== 'object') return object; + _basePath(path).reduce((o, k, i, _) => { + if (i === _.length - 1) { + // 若遍历结束直接赋值 + o[k] = value + return null + } else if (k in o) { + // 若存在对应路径,则返回找到的对象,进行下一次遍历 + return o[k] + } else { + // 若不存在对应路径,则创建对应对象,若下一路径是数字,新对象赋值为空数组,否则赋值为空对象 + o[k] = /^[0-9]{1,}$/.test(_[i + 1]) ? [] : {} + return o[k] + } + }, object) + // 返回object + return object; +} + +// 处理 path, path有三种形式:'a[0].b.c'、'a.0.b.c' 和 ['a','0','b','c'],需要统一处理成数组,便于后续使用 +function _basePath(path) { + // 若是数组,则直接返回 + if (Array.isArray(path)) return path + // 若有 '[',']',则替换成将 '[' 替换成 '.',去掉 ']' + return path.replace(/\[/g, '.').replace(/\]/g, '').split('.') +} + +/** + * 从对象中获取值 + * @param {Object|Array} object 源数据 + * @param {String| Array} path 'a.b.c' 或 ['a',0,'b','c'] + * @param {String} defaultVal 如果无法从调用链中获取值的默认值 + */ +export const objGet = (object, path, defaultVal = 'undefined') => { + // 先将path处理成统一格式 + let newPath = _basePath(path) + // 递归处理,返回最后结果 + let val = newPath.reduce((o, k) => { + return (o || {})[k] + }, object); + return !val || val !== undefined ? val : defaultVal +} + + +/** + * 是否为 number 类型 + * @param {any} num 需要判断的值 + * @return {Boolean} 是否为 number + */ +export const isNumber = (num) => { + return !isNaN(Number(num)) +} + +/** + * 是否为 boolean 类型 + * @param {any} bool 需要判断的值 + * @return {Boolean} 是否为 boolean + */ +export const isBoolean = (bool) => { + return (typeof bool === 'boolean') +} +/** + * 是否有必填字段 + * @param {Object} rules 规则 + * @return {Boolean} 是否有必填字段 + */ +export const isRequiredField = (rules) => { + let isNoField = false; + for (let i = 0; i < rules.length; i++) { + const ruleData = rules[i]; + if (ruleData.required) { + isNoField = true; + break; + } + } + return isNoField; +} + + +/** + * 获取数据类型 + * @param {Any} obj 需要获取数据类型的值 + */ +export const type = (obj) => { + var class2type = {}; + + // 生成class2type映射 + "Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item, index) { + class2type["[object " + item + "]"] = item.toLowerCase(); + }) + if (obj == null) { + return obj + ""; + } + return typeof obj === "object" || typeof obj === "function" ? + class2type[Object.prototype.toString.call(obj)] || "object" : + typeof obj; +} + +/** + * 判断两个值是否相等 + * @param {any} a 值 + * @param {any} b 值 + * @return {Boolean} 是否相等 + */ +export const isEqual = (a, b) => { + //如果a和b本来就全等 + if (a === b) { + //判断是否为0和-0 + return a !== 0 || 1 / a === 1 / b; + } + //判断是否为null和undefined + if (a == null || b == null) { + return a === b; + } + //接下来判断a和b的数据类型 + var classNameA = toString.call(a), + classNameB = toString.call(b); + //如果数据类型不相等,则返回false + if (classNameA !== classNameB) { + return false; + } + //如果数据类型相等,再根据不同数据类型分别判断 + switch (classNameA) { + case '[object RegExp]': + case '[object String]': + //进行字符串转换比较 + return '' + a === '' + b; + case '[object Number]': + //进行数字转换比较,判断是否为NaN + if (+a !== +a) { + return +b !== +b; + } + //判断是否为0或-0 + return +a === 0 ? 1 / +a === 1 / b : +a === +b; + case '[object Date]': + case '[object Boolean]': + return +a === +b; + } + //如果是对象类型 + if (classNameA == '[object Object]') { + //获取a和b的属性长度 + var propsA = Object.getOwnPropertyNames(a), + propsB = Object.getOwnPropertyNames(b); + if (propsA.length != propsB.length) { + return false; + } + for (var i = 0; i < propsA.length; i++) { + var propName = propsA[i]; + //如果对应属性对应值不相等,则返回false + if (a[propName] !== b[propName]) { + return false; + } + } + return true; + } + //如果是数组类型 + if (classNameA == '[object Array]') { + if (a.toString() == b.toString()) { + return true; + } + return false; + } +} diff --git a/uni-im示例/uni_modules/uni-forms/components/uni-forms/validate.js b/uni-im示例/uni_modules/uni-forms/components/uni-forms/validate.js new file mode 100644 index 0000000..1834c6c --- /dev/null +++ b/uni-im示例/uni_modules/uni-forms/components/uni-forms/validate.js @@ -0,0 +1,486 @@ +var pattern = { + email: /^\S+?@\S+?\.\S+?$/, + idcard: /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/, + url: new RegExp( + "^(?!mailto:)(?:(?:http|https|ftp)://|//)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$", + 'i') +}; + +const FORMAT_MAPPING = { + "int": 'integer', + "bool": 'boolean', + "double": 'number', + "long": 'number', + "password": 'string' + // "fileurls": 'array' +} + +function formatMessage(args, resources = '') { + var defaultMessage = ['label'] + defaultMessage.forEach((item) => { + if (args[item] === undefined) { + args[item] = '' + } + }) + + let str = resources + for (let key in args) { + let reg = new RegExp('{' + key + '}') + str = str.replace(reg, args[key]) + } + return str +} + +function isEmptyValue(value, type) { + if (value === undefined || value === null) { + return true; + } + + if (typeof value === 'string' && !value) { + return true; + } + + if (Array.isArray(value) && !value.length) { + return true; + } + + if (type === 'object' && !Object.keys(value).length) { + return true; + } + + return false; +} + +const types = { + integer(value) { + return types.number(value) && parseInt(value, 10) === value; + }, + string(value) { + return typeof value === 'string'; + }, + number(value) { + if (isNaN(value)) { + return false; + } + return typeof value === 'number'; + }, + "boolean": function(value) { + return typeof value === 'boolean'; + }, + "float": function(value) { + return types.number(value) && !types.integer(value); + }, + array(value) { + return Array.isArray(value); + }, + object(value) { + return typeof value === 'object' && !types.array(value); + }, + date(value) { + return value instanceof Date; + }, + timestamp(value) { + if (!this.integer(value) || Math.abs(value).toString().length > 16) { + return false + } + return true; + }, + file(value) { + return typeof value.url === 'string'; + }, + email(value) { + return typeof value === 'string' && !!value.match(pattern.email) && value.length < 255; + }, + url(value) { + return typeof value === 'string' && !!value.match(pattern.url); + }, + pattern(reg, value) { + try { + return new RegExp(reg).test(value); + } catch (e) { + return false; + } + }, + method(value) { + return typeof value === 'function'; + }, + idcard(value) { + return typeof value === 'string' && !!value.match(pattern.idcard); + }, + 'url-https'(value) { + return this.url(value) && value.startsWith('https://'); + }, + 'url-scheme'(value) { + return value.startsWith('://'); + }, + 'url-web'(value) { + return false; + } +} + +class RuleValidator { + + constructor(message) { + this._message = message + } + + async validateRule(fieldKey, fieldValue, value, data, allData) { + var result = null + + let rules = fieldValue.rules + + let hasRequired = rules.findIndex((item) => { + return item.required + }) + if (hasRequired < 0) { + if (value === null || value === undefined) { + return result + } + if (typeof value === 'string' && !value.length) { + return result + } + } + + var message = this._message + + if (rules === undefined) { + return message['default'] + } + + for (var i = 0; i < rules.length; i++) { + let rule = rules[i] + let vt = this._getValidateType(rule) + + Object.assign(rule, { + label: fieldValue.label || `["${fieldKey}"]` + }) + + if (RuleValidatorHelper[vt]) { + result = RuleValidatorHelper[vt](rule, value, message) + if (result != null) { + break + } + } + + if (rule.validateExpr) { + let now = Date.now() + let resultExpr = rule.validateExpr(value, allData, now) + if (resultExpr === false) { + result = this._getMessage(rule, rule.errorMessage || this._message['default']) + break + } + } + + if (rule.validateFunction) { + result = await this.validateFunction(rule, value, data, allData, vt) + if (result !== null) { + break + } + } + } + + if (result !== null) { + result = message.TAG + result + } + + return result + } + + async validateFunction(rule, value, data, allData, vt) { + let result = null + try { + let callbackMessage = null + const res = await rule.validateFunction(rule, value, allData || data, (message) => { + callbackMessage = message + }) + if (callbackMessage || (typeof res === 'string' && res) || res === false) { + result = this._getMessage(rule, callbackMessage || res, vt) + } + } catch (e) { + result = this._getMessage(rule, e.message, vt) + } + return result + } + + _getMessage(rule, message, vt) { + return formatMessage(rule, message || rule.errorMessage || this._message[vt] || message['default']) + } + + _getValidateType(rule) { + var result = '' + if (rule.required) { + result = 'required' + } else if (rule.format) { + result = 'format' + } else if (rule.arrayType) { + result = 'arrayTypeFormat' + } else if (rule.range) { + result = 'range' + } else if (rule.maximum !== undefined || rule.minimum !== undefined) { + result = 'rangeNumber' + } else if (rule.maxLength !== undefined || rule.minLength !== undefined) { + result = 'rangeLength' + } else if (rule.pattern) { + result = 'pattern' + } else if (rule.validateFunction) { + result = 'validateFunction' + } + return result + } +} + +const RuleValidatorHelper = { + required(rule, value, message) { + if (rule.required && isEmptyValue(value, rule.format || typeof value)) { + return formatMessage(rule, rule.errorMessage || message.required); + } + + return null + }, + + range(rule, value, message) { + const { + range, + errorMessage + } = rule; + + let list = new Array(range.length); + for (let i = 0; i < range.length; i++) { + const item = range[i]; + if (types.object(item) && item.value !== undefined) { + list[i] = item.value; + } else { + list[i] = item; + } + } + + let result = false + if (Array.isArray(value)) { + result = (new Set(value.concat(list)).size === list.length); + } else { + if (list.indexOf(value) > -1) { + result = true; + } + } + + if (!result) { + return formatMessage(rule, errorMessage || message['enum']); + } + + return null + }, + + rangeNumber(rule, value, message) { + if (!types.number(value)) { + return formatMessage(rule, rule.errorMessage || message.pattern.mismatch); + } + + let { + minimum, + maximum, + exclusiveMinimum, + exclusiveMaximum + } = rule; + let min = exclusiveMinimum ? value <= minimum : value < minimum; + let max = exclusiveMaximum ? value >= maximum : value > maximum; + + if (minimum !== undefined && min) { + return formatMessage(rule, rule.errorMessage || message['number'][exclusiveMinimum ? + 'exclusiveMinimum' : 'minimum' + ]) + } else if (maximum !== undefined && max) { + return formatMessage(rule, rule.errorMessage || message['number'][exclusiveMaximum ? + 'exclusiveMaximum' : 'maximum' + ]) + } else if (minimum !== undefined && maximum !== undefined && (min || max)) { + return formatMessage(rule, rule.errorMessage || message['number'].range) + } + + return null + }, + + rangeLength(rule, value, message) { + if (!types.string(value) && !types.array(value)) { + return formatMessage(rule, rule.errorMessage || message.pattern.mismatch); + } + + let min = rule.minLength; + let max = rule.maxLength; + let val = value.length; + + if (min !== undefined && val < min) { + return formatMessage(rule, rule.errorMessage || message['length'].minLength) + } else if (max !== undefined && val > max) { + return formatMessage(rule, rule.errorMessage || message['length'].maxLength) + } else if (min !== undefined && max !== undefined && (val < min || val > max)) { + return formatMessage(rule, rule.errorMessage || message['length'].range) + } + + return null + }, + + pattern(rule, value, message) { + if (!types['pattern'](rule.pattern, value)) { + return formatMessage(rule, rule.errorMessage || message.pattern.mismatch); + } + + return null + }, + + format(rule, value, message) { + var customTypes = Object.keys(types); + var format = FORMAT_MAPPING[rule.format] ? FORMAT_MAPPING[rule.format] : (rule.format || rule.arrayType); + + if (customTypes.indexOf(format) > -1) { + if (!types[format](value)) { + return formatMessage(rule, rule.errorMessage || message.typeError); + } + } + + return null + }, + + arrayTypeFormat(rule, value, message) { + if (!Array.isArray(value)) { + return formatMessage(rule, rule.errorMessage || message.typeError); + } + + for (let i = 0; i < value.length; i++) { + const element = value[i]; + let formatResult = this.format(rule, element, message) + if (formatResult !== null) { + return formatResult + } + } + + return null + } +} + +class SchemaValidator extends RuleValidator { + + constructor(schema, options) { + super(SchemaValidator.message); + + this._schema = schema + this._options = options || null + } + + updateSchema(schema) { + this._schema = schema + } + + async validate(data, allData) { + let result = this._checkFieldInSchema(data) + if (!result) { + result = await this.invokeValidate(data, false, allData) + } + return result.length ? result[0] : null + } + + async validateAll(data, allData) { + let result = this._checkFieldInSchema(data) + if (!result) { + result = await this.invokeValidate(data, true, allData) + } + return result + } + + async validateUpdate(data, allData) { + let result = this._checkFieldInSchema(data) + if (!result) { + result = await this.invokeValidateUpdate(data, false, allData) + } + return result.length ? result[0] : null + } + + async invokeValidate(data, all, allData) { + let result = [] + let schema = this._schema + for (let key in schema) { + let value = schema[key] + let errorMessage = await this.validateRule(key, value, data[key], data, allData) + if (errorMessage != null) { + result.push({ + key, + errorMessage + }) + if (!all) break + } + } + return result + } + + async invokeValidateUpdate(data, all, allData) { + let result = [] + for (let key in data) { + let errorMessage = await this.validateRule(key, this._schema[key], data[key], data, allData) + if (errorMessage != null) { + result.push({ + key, + errorMessage + }) + if (!all) break + } + } + return result + } + + _checkFieldInSchema(data) { + var keys = Object.keys(data) + var keys2 = Object.keys(this._schema) + if (new Set(keys.concat(keys2)).size === keys2.length) { + return '' + } + + var noExistFields = keys.filter((key) => { + return keys2.indexOf(key) < 0; + }) + var errorMessage = formatMessage({ + field: JSON.stringify(noExistFields) + }, SchemaValidator.message.TAG + SchemaValidator.message['defaultInvalid']) + return [{ + key: 'invalid', + errorMessage + }] + } +} + +function Message() { + return { + TAG: "", + default: '验证错误', + defaultInvalid: '提交的字段{field}在数据库中并不存在', + validateFunction: '验证无效', + required: '{label}必填', + 'enum': '{label}超出范围', + timestamp: '{label}格式无效', + whitespace: '{label}不能为空', + typeError: '{label}类型无效', + date: { + format: '{label}日期{value}格式无效', + parse: '{label}日期无法解析,{value}无效', + invalid: '{label}日期{value}无效' + }, + length: { + minLength: '{label}长度不能少于{minLength}', + maxLength: '{label}长度不能超过{maxLength}', + range: '{label}必须介于{minLength}和{maxLength}之间' + }, + number: { + minimum: '{label}不能小于{minimum}', + maximum: '{label}不能大于{maximum}', + exclusiveMinimum: '{label}不能小于等于{minimum}', + exclusiveMaximum: '{label}不能大于等于{maximum}', + range: '{label}必须介于{minimum}and{maximum}之间' + }, + pattern: { + mismatch: '{label}格式不匹配' + } + }; +} + + +SchemaValidator.message = new Message(); + +export default SchemaValidator diff --git a/uni-im示例/uni_modules/uni-forms/package.json b/uni-im示例/uni_modules/uni-forms/package.json new file mode 100644 index 0000000..1925611 --- /dev/null +++ b/uni-im示例/uni_modules/uni-forms/package.json @@ -0,0 +1,88 @@ +{ + "id": "uni-forms", + "displayName": "uni-forms 表单", + "version": "1.4.9", + "description": "由输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据", + "keywords": [ + "uni-ui", + "表单", + "校验", + "表单校验", + "表单验证" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, +"dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y", + "京东": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/uni-im示例/uni_modules/uni-forms/readme.md b/uni-im示例/uni_modules/uni-forms/readme.md new file mode 100644 index 0000000..63d5a04 --- /dev/null +++ b/uni-im示例/uni_modules/uni-forms/readme.md @@ -0,0 +1,23 @@ + + +## Forms 表单 + +> **组件名:uni-forms** +> 代码块: `uForms`、`uni-forms-item` +> 关联组件:`uni-forms-item`、`uni-easyinput`、`uni-data-checkbox`、`uni-group`。 + + +uni-app的内置组件已经有了 `
`组件,用于提交表单内容。 + +然而几乎每个表单都需要做表单验证,为了方便做表单验证,减少重复开发,`uni ui` 又基于 ``组件封装了 ``组件,内置了表单验证功能。 + +`` 提供了 `rules`属性来描述校验规则、``子组件来包裹具体的表单项,以及给原生或三方组件提供了 `binddata()` 来设置表单值。 + +每个要校验的表单项,不管input还是checkbox,都必须放在``组件中,且一个``组件只能放置一个表单项。 + +``组件内部预留了显示error message的区域,默认是在表单项的底部。 + +另外,``组件下面的各个表单项,可以通过``包裹为不同的分组。同一``下的不同表单项目将聚拢在一起,同其他group保持垂直间距。``仅影响视觉效果。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-forms) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-icons/changelog.md b/uni-im示例/uni_modules/uni-icons/changelog.md new file mode 100644 index 0000000..6449885 --- /dev/null +++ b/uni-im示例/uni_modules/uni-icons/changelog.md @@ -0,0 +1,22 @@ +## 1.3.5(2022-01-24) +- 优化 size 属性可以传入不带单位的字符串数值 +## 1.3.4(2022-01-24) +- 优化 size 支持其他单位 +## 1.3.3(2022-01-17) +- 修复 nvue 有些图标不显示的bug,兼容老版本图标 +## 1.3.2(2021-12-01) +- 优化 示例可复制图标名称 +## 1.3.1(2021-11-23) +- 优化 兼容旧组件 type 值 +## 1.3.0(2021-11-19) +- 新增 更多图标 +- 优化 自定义图标使用方式 +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-icons](https://uniapp.dcloud.io/component/uniui/uni-icons) +## 1.1.7(2021-11-08) +## 1.2.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.1.5(2021-05-12) +- 新增 组件示例地址 +## 1.1.4(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/uni-im示例/uni_modules/uni-icons/components/uni-icons/icons.js b/uni-im示例/uni_modules/uni-icons/components/uni-icons/icons.js new file mode 100644 index 0000000..7889936 --- /dev/null +++ b/uni-im示例/uni_modules/uni-icons/components/uni-icons/icons.js @@ -0,0 +1,1169 @@ +export default { + "id": "2852637", + "name": "uniui图标库", + "font_family": "uniicons", + "css_prefix_text": "uniui-", + "description": "", + "glyphs": [ + { + "icon_id": "25027049", + "name": "yanse", + "font_class": "color", + "unicode": "e6cf", + "unicode_decimal": 59087 + }, + { + "icon_id": "25027048", + "name": "wallet", + "font_class": "wallet", + "unicode": "e6b1", + "unicode_decimal": 59057 + }, + { + "icon_id": "25015720", + "name": "settings-filled", + "font_class": "settings-filled", + "unicode": "e6ce", + "unicode_decimal": 59086 + }, + { + "icon_id": "25015434", + "name": "shimingrenzheng-filled", + "font_class": "auth-filled", + "unicode": "e6cc", + "unicode_decimal": 59084 + }, + { + "icon_id": "24934246", + "name": "shop-filled", + "font_class": "shop-filled", + "unicode": "e6cd", + "unicode_decimal": 59085 + }, + { + "icon_id": "24934159", + "name": "staff-filled-01", + "font_class": "staff-filled", + "unicode": "e6cb", + "unicode_decimal": 59083 + }, + { + "icon_id": "24932461", + "name": "VIP-filled", + "font_class": "vip-filled", + "unicode": "e6c6", + "unicode_decimal": 59078 + }, + { + "icon_id": "24932462", + "name": "plus_circle_fill", + "font_class": "plus-filled", + "unicode": "e6c7", + "unicode_decimal": 59079 + }, + { + "icon_id": "24932463", + "name": "folder_add-filled", + "font_class": "folder-add-filled", + "unicode": "e6c8", + "unicode_decimal": 59080 + }, + { + "icon_id": "24932464", + "name": "yanse-filled", + "font_class": "color-filled", + "unicode": "e6c9", + "unicode_decimal": 59081 + }, + { + "icon_id": "24932465", + "name": "tune-filled", + "font_class": "tune-filled", + "unicode": "e6ca", + "unicode_decimal": 59082 + }, + { + "icon_id": "24932455", + "name": "a-rilidaka-filled", + "font_class": "calendar-filled", + "unicode": "e6c0", + "unicode_decimal": 59072 + }, + { + "icon_id": "24932456", + "name": "notification-filled", + "font_class": "notification-filled", + "unicode": "e6c1", + "unicode_decimal": 59073 + }, + { + "icon_id": "24932457", + "name": "wallet-filled", + "font_class": "wallet-filled", + "unicode": "e6c2", + "unicode_decimal": 59074 + }, + { + "icon_id": "24932458", + "name": "paihangbang-filled", + "font_class": "medal-filled", + "unicode": "e6c3", + "unicode_decimal": 59075 + }, + { + "icon_id": "24932459", + "name": "gift-filled", + "font_class": "gift-filled", + "unicode": "e6c4", + "unicode_decimal": 59076 + }, + { + "icon_id": "24932460", + "name": "fire-filled", + "font_class": "fire-filled", + "unicode": "e6c5", + "unicode_decimal": 59077 + }, + { + "icon_id": "24928001", + "name": "refreshempty", + "font_class": "refreshempty", + "unicode": "e6bf", + "unicode_decimal": 59071 + }, + { + "icon_id": "24926853", + "name": "location-ellipse", + "font_class": "location-filled", + "unicode": "e6af", + "unicode_decimal": 59055 + }, + { + "icon_id": "24926735", + "name": "person-filled", + "font_class": "person-filled", + "unicode": "e69d", + "unicode_decimal": 59037 + }, + { + "icon_id": "24926703", + "name": "personadd-filled", + "font_class": "personadd-filled", + "unicode": "e698", + "unicode_decimal": 59032 + }, + { + "icon_id": "24923351", + "name": "back", + "font_class": "back", + "unicode": "e6b9", + "unicode_decimal": 59065 + }, + { + "icon_id": "24923352", + "name": "forward", + "font_class": "forward", + "unicode": "e6ba", + "unicode_decimal": 59066 + }, + { + "icon_id": "24923353", + "name": "arrowthinright", + "font_class": "arrow-right", + "unicode": "e6bb", + "unicode_decimal": 59067 + }, + { + "icon_id": "24923353", + "name": "arrowthinright", + "font_class": "arrowthinright", + "unicode": "e6bb", + "unicode_decimal": 59067 + }, + { + "icon_id": "24923354", + "name": "arrowthinleft", + "font_class": "arrow-left", + "unicode": "e6bc", + "unicode_decimal": 59068 + }, + { + "icon_id": "24923354", + "name": "arrowthinleft", + "font_class": "arrowthinleft", + "unicode": "e6bc", + "unicode_decimal": 59068 + }, + { + "icon_id": "24923355", + "name": "arrowthinup", + "font_class": "arrow-up", + "unicode": "e6bd", + "unicode_decimal": 59069 + }, + { + "icon_id": "24923355", + "name": "arrowthinup", + "font_class": "arrowthinup", + "unicode": "e6bd", + "unicode_decimal": 59069 + }, + { + "icon_id": "24923356", + "name": "arrowthindown", + "font_class": "arrow-down", + "unicode": "e6be", + "unicode_decimal": 59070 + },{ + "icon_id": "24923356", + "name": "arrowthindown", + "font_class": "arrowthindown", + "unicode": "e6be", + "unicode_decimal": 59070 + }, + { + "icon_id": "24923349", + "name": "arrowdown", + "font_class": "bottom", + "unicode": "e6b8", + "unicode_decimal": 59064 + },{ + "icon_id": "24923349", + "name": "arrowdown", + "font_class": "arrowdown", + "unicode": "e6b8", + "unicode_decimal": 59064 + }, + { + "icon_id": "24923346", + "name": "arrowright", + "font_class": "right", + "unicode": "e6b5", + "unicode_decimal": 59061 + }, + { + "icon_id": "24923346", + "name": "arrowright", + "font_class": "arrowright", + "unicode": "e6b5", + "unicode_decimal": 59061 + }, + { + "icon_id": "24923347", + "name": "arrowup", + "font_class": "top", + "unicode": "e6b6", + "unicode_decimal": 59062 + }, + { + "icon_id": "24923347", + "name": "arrowup", + "font_class": "arrowup", + "unicode": "e6b6", + "unicode_decimal": 59062 + }, + { + "icon_id": "24923348", + "name": "arrowleft", + "font_class": "left", + "unicode": "e6b7", + "unicode_decimal": 59063 + }, + { + "icon_id": "24923348", + "name": "arrowleft", + "font_class": "arrowleft", + "unicode": "e6b7", + "unicode_decimal": 59063 + }, + { + "icon_id": "24923334", + "name": "eye", + "font_class": "eye", + "unicode": "e651", + "unicode_decimal": 58961 + }, + { + "icon_id": "24923335", + "name": "eye-filled", + "font_class": "eye-filled", + "unicode": "e66a", + "unicode_decimal": 58986 + }, + { + "icon_id": "24923336", + "name": "eye-slash", + "font_class": "eye-slash", + "unicode": "e6b3", + "unicode_decimal": 59059 + }, + { + "icon_id": "24923337", + "name": "eye-slash-filled", + "font_class": "eye-slash-filled", + "unicode": "e6b4", + "unicode_decimal": 59060 + }, + { + "icon_id": "24923305", + "name": "info-filled", + "font_class": "info-filled", + "unicode": "e649", + "unicode_decimal": 58953 + }, + { + "icon_id": "24923299", + "name": "reload-01", + "font_class": "reload", + "unicode": "e6b2", + "unicode_decimal": 59058 + }, + { + "icon_id": "24923195", + "name": "mic_slash_fill", + "font_class": "micoff-filled", + "unicode": "e6b0", + "unicode_decimal": 59056 + }, + { + "icon_id": "24923165", + "name": "map-pin-ellipse", + "font_class": "map-pin-ellipse", + "unicode": "e6ac", + "unicode_decimal": 59052 + }, + { + "icon_id": "24923166", + "name": "map-pin", + "font_class": "map-pin", + "unicode": "e6ad", + "unicode_decimal": 59053 + }, + { + "icon_id": "24923167", + "name": "location", + "font_class": "location", + "unicode": "e6ae", + "unicode_decimal": 59054 + }, + { + "icon_id": "24923064", + "name": "starhalf", + "font_class": "starhalf", + "unicode": "e683", + "unicode_decimal": 59011 + }, + { + "icon_id": "24923065", + "name": "star", + "font_class": "star", + "unicode": "e688", + "unicode_decimal": 59016 + }, + { + "icon_id": "24923066", + "name": "star-filled", + "font_class": "star-filled", + "unicode": "e68f", + "unicode_decimal": 59023 + }, + { + "icon_id": "24899646", + "name": "a-rilidaka", + "font_class": "calendar", + "unicode": "e6a0", + "unicode_decimal": 59040 + }, + { + "icon_id": "24899647", + "name": "fire", + "font_class": "fire", + "unicode": "e6a1", + "unicode_decimal": 59041 + }, + { + "icon_id": "24899648", + "name": "paihangbang", + "font_class": "medal", + "unicode": "e6a2", + "unicode_decimal": 59042 + }, + { + "icon_id": "24899649", + "name": "font", + "font_class": "font", + "unicode": "e6a3", + "unicode_decimal": 59043 + }, + { + "icon_id": "24899650", + "name": "gift", + "font_class": "gift", + "unicode": "e6a4", + "unicode_decimal": 59044 + }, + { + "icon_id": "24899651", + "name": "link", + "font_class": "link", + "unicode": "e6a5", + "unicode_decimal": 59045 + }, + { + "icon_id": "24899652", + "name": "notification", + "font_class": "notification", + "unicode": "e6a6", + "unicode_decimal": 59046 + }, + { + "icon_id": "24899653", + "name": "staff", + "font_class": "staff", + "unicode": "e6a7", + "unicode_decimal": 59047 + }, + { + "icon_id": "24899654", + "name": "VIP", + "font_class": "vip", + "unicode": "e6a8", + "unicode_decimal": 59048 + }, + { + "icon_id": "24899655", + "name": "folder_add", + "font_class": "folder-add", + "unicode": "e6a9", + "unicode_decimal": 59049 + }, + { + "icon_id": "24899656", + "name": "tune", + "font_class": "tune", + "unicode": "e6aa", + "unicode_decimal": 59050 + }, + { + "icon_id": "24899657", + "name": "shimingrenzheng", + "font_class": "auth", + "unicode": "e6ab", + "unicode_decimal": 59051 + }, + { + "icon_id": "24899565", + "name": "person", + "font_class": "person", + "unicode": "e699", + "unicode_decimal": 59033 + }, + { + "icon_id": "24899566", + "name": "email-filled", + "font_class": "email-filled", + "unicode": "e69a", + "unicode_decimal": 59034 + }, + { + "icon_id": "24899567", + "name": "phone-filled", + "font_class": "phone-filled", + "unicode": "e69b", + "unicode_decimal": 59035 + }, + { + "icon_id": "24899568", + "name": "phone", + "font_class": "phone", + "unicode": "e69c", + "unicode_decimal": 59036 + }, + { + "icon_id": "24899570", + "name": "email", + "font_class": "email", + "unicode": "e69e", + "unicode_decimal": 59038 + }, + { + "icon_id": "24899571", + "name": "personadd", + "font_class": "personadd", + "unicode": "e69f", + "unicode_decimal": 59039 + }, + { + "icon_id": "24899558", + "name": "chatboxes-filled", + "font_class": "chatboxes-filled", + "unicode": "e692", + "unicode_decimal": 59026 + }, + { + "icon_id": "24899559", + "name": "contact", + "font_class": "contact", + "unicode": "e693", + "unicode_decimal": 59027 + }, + { + "icon_id": "24899560", + "name": "chatbubble-filled", + "font_class": "chatbubble-filled", + "unicode": "e694", + "unicode_decimal": 59028 + }, + { + "icon_id": "24899561", + "name": "contact-filled", + "font_class": "contact-filled", + "unicode": "e695", + "unicode_decimal": 59029 + }, + { + "icon_id": "24899562", + "name": "chatboxes", + "font_class": "chatboxes", + "unicode": "e696", + "unicode_decimal": 59030 + }, + { + "icon_id": "24899563", + "name": "chatbubble", + "font_class": "chatbubble", + "unicode": "e697", + "unicode_decimal": 59031 + }, + { + "icon_id": "24881290", + "name": "upload-filled", + "font_class": "upload-filled", + "unicode": "e68e", + "unicode_decimal": 59022 + }, + { + "icon_id": "24881292", + "name": "upload", + "font_class": "upload", + "unicode": "e690", + "unicode_decimal": 59024 + }, + { + "icon_id": "24881293", + "name": "weixin", + "font_class": "weixin", + "unicode": "e691", + "unicode_decimal": 59025 + }, + { + "icon_id": "24881274", + "name": "compose", + "font_class": "compose", + "unicode": "e67f", + "unicode_decimal": 59007 + }, + { + "icon_id": "24881275", + "name": "qq", + "font_class": "qq", + "unicode": "e680", + "unicode_decimal": 59008 + }, + { + "icon_id": "24881276", + "name": "download-filled", + "font_class": "download-filled", + "unicode": "e681", + "unicode_decimal": 59009 + }, + { + "icon_id": "24881277", + "name": "pengyouquan", + "font_class": "pyq", + "unicode": "e682", + "unicode_decimal": 59010 + }, + { + "icon_id": "24881279", + "name": "sound", + "font_class": "sound", + "unicode": "e684", + "unicode_decimal": 59012 + }, + { + "icon_id": "24881280", + "name": "trash-filled", + "font_class": "trash-filled", + "unicode": "e685", + "unicode_decimal": 59013 + }, + { + "icon_id": "24881281", + "name": "sound-filled", + "font_class": "sound-filled", + "unicode": "e686", + "unicode_decimal": 59014 + }, + { + "icon_id": "24881282", + "name": "trash", + "font_class": "trash", + "unicode": "e687", + "unicode_decimal": 59015 + }, + { + "icon_id": "24881284", + "name": "videocam-filled", + "font_class": "videocam-filled", + "unicode": "e689", + "unicode_decimal": 59017 + }, + { + "icon_id": "24881285", + "name": "spinner-cycle", + "font_class": "spinner-cycle", + "unicode": "e68a", + "unicode_decimal": 59018 + }, + { + "icon_id": "24881286", + "name": "weibo", + "font_class": "weibo", + "unicode": "e68b", + "unicode_decimal": 59019 + }, + { + "icon_id": "24881288", + "name": "videocam", + "font_class": "videocam", + "unicode": "e68c", + "unicode_decimal": 59020 + }, + { + "icon_id": "24881289", + "name": "download", + "font_class": "download", + "unicode": "e68d", + "unicode_decimal": 59021 + }, + { + "icon_id": "24879601", + "name": "help", + "font_class": "help", + "unicode": "e679", + "unicode_decimal": 59001 + }, + { + "icon_id": "24879602", + "name": "navigate-filled", + "font_class": "navigate-filled", + "unicode": "e67a", + "unicode_decimal": 59002 + }, + { + "icon_id": "24879603", + "name": "plusempty", + "font_class": "plusempty", + "unicode": "e67b", + "unicode_decimal": 59003 + }, + { + "icon_id": "24879604", + "name": "smallcircle", + "font_class": "smallcircle", + "unicode": "e67c", + "unicode_decimal": 59004 + }, + { + "icon_id": "24879605", + "name": "minus-filled", + "font_class": "minus-filled", + "unicode": "e67d", + "unicode_decimal": 59005 + }, + { + "icon_id": "24879606", + "name": "micoff", + "font_class": "micoff", + "unicode": "e67e", + "unicode_decimal": 59006 + }, + { + "icon_id": "24879588", + "name": "closeempty", + "font_class": "closeempty", + "unicode": "e66c", + "unicode_decimal": 58988 + }, + { + "icon_id": "24879589", + "name": "clear", + "font_class": "clear", + "unicode": "e66d", + "unicode_decimal": 58989 + }, + { + "icon_id": "24879590", + "name": "navigate", + "font_class": "navigate", + "unicode": "e66e", + "unicode_decimal": 58990 + }, + { + "icon_id": "24879591", + "name": "minus", + "font_class": "minus", + "unicode": "e66f", + "unicode_decimal": 58991 + }, + { + "icon_id": "24879592", + "name": "image", + "font_class": "image", + "unicode": "e670", + "unicode_decimal": 58992 + }, + { + "icon_id": "24879593", + "name": "mic", + "font_class": "mic", + "unicode": "e671", + "unicode_decimal": 58993 + }, + { + "icon_id": "24879594", + "name": "paperplane", + "font_class": "paperplane", + "unicode": "e672", + "unicode_decimal": 58994 + }, + { + "icon_id": "24879595", + "name": "close", + "font_class": "close", + "unicode": "e673", + "unicode_decimal": 58995 + }, + { + "icon_id": "24879596", + "name": "help-filled", + "font_class": "help-filled", + "unicode": "e674", + "unicode_decimal": 58996 + }, + { + "icon_id": "24879597", + "name": "plus-filled", + "font_class": "paperplane-filled", + "unicode": "e675", + "unicode_decimal": 58997 + }, + { + "icon_id": "24879598", + "name": "plus", + "font_class": "plus", + "unicode": "e676", + "unicode_decimal": 58998 + }, + { + "icon_id": "24879599", + "name": "mic-filled", + "font_class": "mic-filled", + "unicode": "e677", + "unicode_decimal": 58999 + }, + { + "icon_id": "24879600", + "name": "image-filled", + "font_class": "image-filled", + "unicode": "e678", + "unicode_decimal": 59000 + }, + { + "icon_id": "24855900", + "name": "locked-filled", + "font_class": "locked-filled", + "unicode": "e668", + "unicode_decimal": 58984 + }, + { + "icon_id": "24855901", + "name": "info", + "font_class": "info", + "unicode": "e669", + "unicode_decimal": 58985 + }, + { + "icon_id": "24855903", + "name": "locked", + "font_class": "locked", + "unicode": "e66b", + "unicode_decimal": 58987 + }, + { + "icon_id": "24855884", + "name": "camera-filled", + "font_class": "camera-filled", + "unicode": "e658", + "unicode_decimal": 58968 + }, + { + "icon_id": "24855885", + "name": "chat-filled", + "font_class": "chat-filled", + "unicode": "e659", + "unicode_decimal": 58969 + }, + { + "icon_id": "24855886", + "name": "camera", + "font_class": "camera", + "unicode": "e65a", + "unicode_decimal": 58970 + }, + { + "icon_id": "24855887", + "name": "circle", + "font_class": "circle", + "unicode": "e65b", + "unicode_decimal": 58971 + }, + { + "icon_id": "24855888", + "name": "checkmarkempty", + "font_class": "checkmarkempty", + "unicode": "e65c", + "unicode_decimal": 58972 + }, + { + "icon_id": "24855889", + "name": "chat", + "font_class": "chat", + "unicode": "e65d", + "unicode_decimal": 58973 + }, + { + "icon_id": "24855890", + "name": "circle-filled", + "font_class": "circle-filled", + "unicode": "e65e", + "unicode_decimal": 58974 + }, + { + "icon_id": "24855891", + "name": "flag", + "font_class": "flag", + "unicode": "e65f", + "unicode_decimal": 58975 + }, + { + "icon_id": "24855892", + "name": "flag-filled", + "font_class": "flag-filled", + "unicode": "e660", + "unicode_decimal": 58976 + }, + { + "icon_id": "24855893", + "name": "gear-filled", + "font_class": "gear-filled", + "unicode": "e661", + "unicode_decimal": 58977 + }, + { + "icon_id": "24855894", + "name": "home", + "font_class": "home", + "unicode": "e662", + "unicode_decimal": 58978 + }, + { + "icon_id": "24855895", + "name": "home-filled", + "font_class": "home-filled", + "unicode": "e663", + "unicode_decimal": 58979 + }, + { + "icon_id": "24855896", + "name": "gear", + "font_class": "gear", + "unicode": "e664", + "unicode_decimal": 58980 + }, + { + "icon_id": "24855897", + "name": "smallcircle-filled", + "font_class": "smallcircle-filled", + "unicode": "e665", + "unicode_decimal": 58981 + }, + { + "icon_id": "24855898", + "name": "map-filled", + "font_class": "map-filled", + "unicode": "e666", + "unicode_decimal": 58982 + }, + { + "icon_id": "24855899", + "name": "map", + "font_class": "map", + "unicode": "e667", + "unicode_decimal": 58983 + }, + { + "icon_id": "24855825", + "name": "refresh-filled", + "font_class": "refresh-filled", + "unicode": "e656", + "unicode_decimal": 58966 + }, + { + "icon_id": "24855826", + "name": "refresh", + "font_class": "refresh", + "unicode": "e657", + "unicode_decimal": 58967 + }, + { + "icon_id": "24855808", + "name": "cloud-upload", + "font_class": "cloud-upload", + "unicode": "e645", + "unicode_decimal": 58949 + }, + { + "icon_id": "24855809", + "name": "cloud-download-filled", + "font_class": "cloud-download-filled", + "unicode": "e646", + "unicode_decimal": 58950 + }, + { + "icon_id": "24855810", + "name": "cloud-download", + "font_class": "cloud-download", + "unicode": "e647", + "unicode_decimal": 58951 + }, + { + "icon_id": "24855811", + "name": "cloud-upload-filled", + "font_class": "cloud-upload-filled", + "unicode": "e648", + "unicode_decimal": 58952 + }, + { + "icon_id": "24855813", + "name": "redo", + "font_class": "redo", + "unicode": "e64a", + "unicode_decimal": 58954 + }, + { + "icon_id": "24855814", + "name": "images-filled", + "font_class": "images-filled", + "unicode": "e64b", + "unicode_decimal": 58955 + }, + { + "icon_id": "24855815", + "name": "undo-filled", + "font_class": "undo-filled", + "unicode": "e64c", + "unicode_decimal": 58956 + }, + { + "icon_id": "24855816", + "name": "more", + "font_class": "more", + "unicode": "e64d", + "unicode_decimal": 58957 + }, + { + "icon_id": "24855817", + "name": "more-filled", + "font_class": "more-filled", + "unicode": "e64e", + "unicode_decimal": 58958 + }, + { + "icon_id": "24855818", + "name": "undo", + "font_class": "undo", + "unicode": "e64f", + "unicode_decimal": 58959 + }, + { + "icon_id": "24855819", + "name": "images", + "font_class": "images", + "unicode": "e650", + "unicode_decimal": 58960 + }, + { + "icon_id": "24855821", + "name": "paperclip", + "font_class": "paperclip", + "unicode": "e652", + "unicode_decimal": 58962 + }, + { + "icon_id": "24855822", + "name": "settings", + "font_class": "settings", + "unicode": "e653", + "unicode_decimal": 58963 + }, + { + "icon_id": "24855823", + "name": "search", + "font_class": "search", + "unicode": "e654", + "unicode_decimal": 58964 + }, + { + "icon_id": "24855824", + "name": "redo-filled", + "font_class": "redo-filled", + "unicode": "e655", + "unicode_decimal": 58965 + }, + { + "icon_id": "24841702", + "name": "list", + "font_class": "list", + "unicode": "e644", + "unicode_decimal": 58948 + }, + { + "icon_id": "24841489", + "name": "mail-open-filled", + "font_class": "mail-open-filled", + "unicode": "e63a", + "unicode_decimal": 58938 + }, + { + "icon_id": "24841491", + "name": "hand-thumbsdown-filled", + "font_class": "hand-down-filled", + "unicode": "e63c", + "unicode_decimal": 58940 + }, + { + "icon_id": "24841492", + "name": "hand-thumbsdown", + "font_class": "hand-down", + "unicode": "e63d", + "unicode_decimal": 58941 + }, + { + "icon_id": "24841493", + "name": "hand-thumbsup-filled", + "font_class": "hand-up-filled", + "unicode": "e63e", + "unicode_decimal": 58942 + }, + { + "icon_id": "24841494", + "name": "hand-thumbsup", + "font_class": "hand-up", + "unicode": "e63f", + "unicode_decimal": 58943 + }, + { + "icon_id": "24841496", + "name": "heart-filled", + "font_class": "heart-filled", + "unicode": "e641", + "unicode_decimal": 58945 + }, + { + "icon_id": "24841498", + "name": "mail-open", + "font_class": "mail-open", + "unicode": "e643", + "unicode_decimal": 58947 + }, + { + "icon_id": "24841488", + "name": "heart", + "font_class": "heart", + "unicode": "e639", + "unicode_decimal": 58937 + }, + { + "icon_id": "24839963", + "name": "loop", + "font_class": "loop", + "unicode": "e633", + "unicode_decimal": 58931 + }, + { + "icon_id": "24839866", + "name": "pulldown", + "font_class": "pulldown", + "unicode": "e632", + "unicode_decimal": 58930 + }, + { + "icon_id": "24813798", + "name": "scan", + "font_class": "scan", + "unicode": "e62a", + "unicode_decimal": 58922 + }, + { + "icon_id": "24813786", + "name": "bars", + "font_class": "bars", + "unicode": "e627", + "unicode_decimal": 58919 + }, + { + "icon_id": "24813788", + "name": "cart-filled", + "font_class": "cart-filled", + "unicode": "e629", + "unicode_decimal": 58921 + }, + { + "icon_id": "24813790", + "name": "checkbox", + "font_class": "checkbox", + "unicode": "e62b", + "unicode_decimal": 58923 + }, + { + "icon_id": "24813791", + "name": "checkbox-filled", + "font_class": "checkbox-filled", + "unicode": "e62c", + "unicode_decimal": 58924 + }, + { + "icon_id": "24813794", + "name": "shop", + "font_class": "shop", + "unicode": "e62f", + "unicode_decimal": 58927 + }, + { + "icon_id": "24813795", + "name": "headphones", + "font_class": "headphones", + "unicode": "e630", + "unicode_decimal": 58928 + }, + { + "icon_id": "24813796", + "name": "cart", + "font_class": "cart", + "unicode": "e631", + "unicode_decimal": 58929 + } + ] +} diff --git a/uni-im示例/uni_modules/uni-icons/components/uni-icons/uni-icons.vue b/uni-im示例/uni_modules/uni-icons/components/uni-icons/uni-icons.vue new file mode 100644 index 0000000..86e7444 --- /dev/null +++ b/uni-im示例/uni_modules/uni-icons/components/uni-icons/uni-icons.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-icons/components/uni-icons/uniicons.css b/uni-im示例/uni_modules/uni-icons/components/uni-icons/uniicons.css new file mode 100644 index 0000000..2f56eab --- /dev/null +++ b/uni-im示例/uni_modules/uni-icons/components/uni-icons/uniicons.css @@ -0,0 +1,663 @@ +.uniui-color:before { + content: "\e6cf"; +} + +.uniui-wallet:before { + content: "\e6b1"; +} + +.uniui-settings-filled:before { + content: "\e6ce"; +} + +.uniui-auth-filled:before { + content: "\e6cc"; +} + +.uniui-shop-filled:before { + content: "\e6cd"; +} + +.uniui-staff-filled:before { + content: "\e6cb"; +} + +.uniui-vip-filled:before { + content: "\e6c6"; +} + +.uniui-plus-filled:before { + content: "\e6c7"; +} + +.uniui-folder-add-filled:before { + content: "\e6c8"; +} + +.uniui-color-filled:before { + content: "\e6c9"; +} + +.uniui-tune-filled:before { + content: "\e6ca"; +} + +.uniui-calendar-filled:before { + content: "\e6c0"; +} + +.uniui-notification-filled:before { + content: "\e6c1"; +} + +.uniui-wallet-filled:before { + content: "\e6c2"; +} + +.uniui-medal-filled:before { + content: "\e6c3"; +} + +.uniui-gift-filled:before { + content: "\e6c4"; +} + +.uniui-fire-filled:before { + content: "\e6c5"; +} + +.uniui-refreshempty:before { + content: "\e6bf"; +} + +.uniui-location-filled:before { + content: "\e6af"; +} + +.uniui-person-filled:before { + content: "\e69d"; +} + +.uniui-personadd-filled:before { + content: "\e698"; +} + +.uniui-back:before { + content: "\e6b9"; +} + +.uniui-forward:before { + content: "\e6ba"; +} + +.uniui-arrow-right:before { + content: "\e6bb"; +} + +.uniui-arrowthinright:before { + content: "\e6bb"; +} + +.uniui-arrow-left:before { + content: "\e6bc"; +} + +.uniui-arrowthinleft:before { + content: "\e6bc"; +} + +.uniui-arrow-up:before { + content: "\e6bd"; +} + +.uniui-arrowthinup:before { + content: "\e6bd"; +} + +.uniui-arrow-down:before { + content: "\e6be"; +} + +.uniui-arrowthindown:before { + content: "\e6be"; +} + +.uniui-bottom:before { + content: "\e6b8"; +} + +.uniui-arrowdown:before { + content: "\e6b8"; +} + +.uniui-right:before { + content: "\e6b5"; +} + +.uniui-arrowright:before { + content: "\e6b5"; +} + +.uniui-top:before { + content: "\e6b6"; +} + +.uniui-arrowup:before { + content: "\e6b6"; +} + +.uniui-left:before { + content: "\e6b7"; +} + +.uniui-arrowleft:before { + content: "\e6b7"; +} + +.uniui-eye:before { + content: "\e651"; +} + +.uniui-eye-filled:before { + content: "\e66a"; +} + +.uniui-eye-slash:before { + content: "\e6b3"; +} + +.uniui-eye-slash-filled:before { + content: "\e6b4"; +} + +.uniui-info-filled:before { + content: "\e649"; +} + +.uniui-reload:before { + content: "\e6b2"; +} + +.uniui-micoff-filled:before { + content: "\e6b0"; +} + +.uniui-map-pin-ellipse:before { + content: "\e6ac"; +} + +.uniui-map-pin:before { + content: "\e6ad"; +} + +.uniui-location:before { + content: "\e6ae"; +} + +.uniui-starhalf:before { + content: "\e683"; +} + +.uniui-star:before { + content: "\e688"; +} + +.uniui-star-filled:before { + content: "\e68f"; +} + +.uniui-calendar:before { + content: "\e6a0"; +} + +.uniui-fire:before { + content: "\e6a1"; +} + +.uniui-medal:before { + content: "\e6a2"; +} + +.uniui-font:before { + content: "\e6a3"; +} + +.uniui-gift:before { + content: "\e6a4"; +} + +.uniui-link:before { + content: "\e6a5"; +} + +.uniui-notification:before { + content: "\e6a6"; +} + +.uniui-staff:before { + content: "\e6a7"; +} + +.uniui-vip:before { + content: "\e6a8"; +} + +.uniui-folder-add:before { + content: "\e6a9"; +} + +.uniui-tune:before { + content: "\e6aa"; +} + +.uniui-auth:before { + content: "\e6ab"; +} + +.uniui-person:before { + content: "\e699"; +} + +.uniui-email-filled:before { + content: "\e69a"; +} + +.uniui-phone-filled:before { + content: "\e69b"; +} + +.uniui-phone:before { + content: "\e69c"; +} + +.uniui-email:before { + content: "\e69e"; +} + +.uniui-personadd:before { + content: "\e69f"; +} + +.uniui-chatboxes-filled:before { + content: "\e692"; +} + +.uniui-contact:before { + content: "\e693"; +} + +.uniui-chatbubble-filled:before { + content: "\e694"; +} + +.uniui-contact-filled:before { + content: "\e695"; +} + +.uniui-chatboxes:before { + content: "\e696"; +} + +.uniui-chatbubble:before { + content: "\e697"; +} + +.uniui-upload-filled:before { + content: "\e68e"; +} + +.uniui-upload:before { + content: "\e690"; +} + +.uniui-weixin:before { + content: "\e691"; +} + +.uniui-compose:before { + content: "\e67f"; +} + +.uniui-qq:before { + content: "\e680"; +} + +.uniui-download-filled:before { + content: "\e681"; +} + +.uniui-pyq:before { + content: "\e682"; +} + +.uniui-sound:before { + content: "\e684"; +} + +.uniui-trash-filled:before { + content: "\e685"; +} + +.uniui-sound-filled:before { + content: "\e686"; +} + +.uniui-trash:before { + content: "\e687"; +} + +.uniui-videocam-filled:before { + content: "\e689"; +} + +.uniui-spinner-cycle:before { + content: "\e68a"; +} + +.uniui-weibo:before { + content: "\e68b"; +} + +.uniui-videocam:before { + content: "\e68c"; +} + +.uniui-download:before { + content: "\e68d"; +} + +.uniui-help:before { + content: "\e679"; +} + +.uniui-navigate-filled:before { + content: "\e67a"; +} + +.uniui-plusempty:before { + content: "\e67b"; +} + +.uniui-smallcircle:before { + content: "\e67c"; +} + +.uniui-minus-filled:before { + content: "\e67d"; +} + +.uniui-micoff:before { + content: "\e67e"; +} + +.uniui-closeempty:before { + content: "\e66c"; +} + +.uniui-clear:before { + content: "\e66d"; +} + +.uniui-navigate:before { + content: "\e66e"; +} + +.uniui-minus:before { + content: "\e66f"; +} + +.uniui-image:before { + content: "\e670"; +} + +.uniui-mic:before { + content: "\e671"; +} + +.uniui-paperplane:before { + content: "\e672"; +} + +.uniui-close:before { + content: "\e673"; +} + +.uniui-help-filled:before { + content: "\e674"; +} + +.uniui-paperplane-filled:before { + content: "\e675"; +} + +.uniui-plus:before { + content: "\e676"; +} + +.uniui-mic-filled:before { + content: "\e677"; +} + +.uniui-image-filled:before { + content: "\e678"; +} + +.uniui-locked-filled:before { + content: "\e668"; +} + +.uniui-info:before { + content: "\e669"; +} + +.uniui-locked:before { + content: "\e66b"; +} + +.uniui-camera-filled:before { + content: "\e658"; +} + +.uniui-chat-filled:before { + content: "\e659"; +} + +.uniui-camera:before { + content: "\e65a"; +} + +.uniui-circle:before { + content: "\e65b"; +} + +.uniui-checkmarkempty:before { + content: "\e65c"; +} + +.uniui-chat:before { + content: "\e65d"; +} + +.uniui-circle-filled:before { + content: "\e65e"; +} + +.uniui-flag:before { + content: "\e65f"; +} + +.uniui-flag-filled:before { + content: "\e660"; +} + +.uniui-gear-filled:before { + content: "\e661"; +} + +.uniui-home:before { + content: "\e662"; +} + +.uniui-home-filled:before { + content: "\e663"; +} + +.uniui-gear:before { + content: "\e664"; +} + +.uniui-smallcircle-filled:before { + content: "\e665"; +} + +.uniui-map-filled:before { + content: "\e666"; +} + +.uniui-map:before { + content: "\e667"; +} + +.uniui-refresh-filled:before { + content: "\e656"; +} + +.uniui-refresh:before { + content: "\e657"; +} + +.uniui-cloud-upload:before { + content: "\e645"; +} + +.uniui-cloud-download-filled:before { + content: "\e646"; +} + +.uniui-cloud-download:before { + content: "\e647"; +} + +.uniui-cloud-upload-filled:before { + content: "\e648"; +} + +.uniui-redo:before { + content: "\e64a"; +} + +.uniui-images-filled:before { + content: "\e64b"; +} + +.uniui-undo-filled:before { + content: "\e64c"; +} + +.uniui-more:before { + content: "\e64d"; +} + +.uniui-more-filled:before { + content: "\e64e"; +} + +.uniui-undo:before { + content: "\e64f"; +} + +.uniui-images:before { + content: "\e650"; +} + +.uniui-paperclip:before { + content: "\e652"; +} + +.uniui-settings:before { + content: "\e653"; +} + +.uniui-search:before { + content: "\e654"; +} + +.uniui-redo-filled:before { + content: "\e655"; +} + +.uniui-list:before { + content: "\e644"; +} + +.uniui-mail-open-filled:before { + content: "\e63a"; +} + +.uniui-hand-down-filled:before { + content: "\e63c"; +} + +.uniui-hand-down:before { + content: "\e63d"; +} + +.uniui-hand-up-filled:before { + content: "\e63e"; +} + +.uniui-hand-up:before { + content: "\e63f"; +} + +.uniui-heart-filled:before { + content: "\e641"; +} + +.uniui-mail-open:before { + content: "\e643"; +} + +.uniui-heart:before { + content: "\e639"; +} + +.uniui-loop:before { + content: "\e633"; +} + +.uniui-pulldown:before { + content: "\e632"; +} + +.uniui-scan:before { + content: "\e62a"; +} + +.uniui-bars:before { + content: "\e627"; +} + +.uniui-cart-filled:before { + content: "\e629"; +} + +.uniui-checkbox:before { + content: "\e62b"; +} + +.uniui-checkbox-filled:before { + content: "\e62c"; +} + +.uniui-shop:before { + content: "\e62f"; +} + +.uniui-headphones:before { + content: "\e630"; +} + +.uniui-cart:before { + content: "\e631"; +} diff --git a/uni-im示例/uni_modules/uni-icons/components/uni-icons/uniicons.ttf b/uni-im示例/uni_modules/uni-icons/components/uni-icons/uniicons.ttf new file mode 100644 index 0000000..835f33b Binary files /dev/null and b/uni-im示例/uni_modules/uni-icons/components/uni-icons/uniicons.ttf differ diff --git a/uni-im示例/uni_modules/uni-icons/package.json b/uni-im示例/uni_modules/uni-icons/package.json new file mode 100644 index 0000000..d1c4e77 --- /dev/null +++ b/uni-im示例/uni_modules/uni-icons/package.json @@ -0,0 +1,86 @@ +{ + "id": "uni-icons", + "displayName": "uni-icons 图标", + "version": "1.3.5", + "description": "图标组件,用于展示移动端常见的图标,可自定义颜色、大小。", + "keywords": [ + "uni-ui", + "uniui", + "icon", + "图标" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "^3.2.14" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-icons/readme.md b/uni-im示例/uni_modules/uni-icons/readme.md new file mode 100644 index 0000000..86234ba --- /dev/null +++ b/uni-im示例/uni_modules/uni-icons/readme.md @@ -0,0 +1,8 @@ +## Icons 图标 +> **组件名:uni-icons** +> 代码块: `uIcons` + +用于展示 icons 图标 。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-icons) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 diff --git a/uni-im示例/uni_modules/uni-id-common/changelog.md b/uni-im示例/uni_modules/uni-id-common/changelog.md new file mode 100644 index 0000000..a8ee8f2 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-common/changelog.md @@ -0,0 +1,32 @@ +## 1.0.16(2023-04-25) +- 新增maxTokenLength配置,用于限制数据库用户记录token数组的最大长度 +## 1.0.15(2023-04-06) +- 修复部分语言国际化出错的Bug +## 1.0.14(2023-03-07) +- 修复 admin用户包含其他角色时未包含在token的Bug +## 1.0.13(2022-07-21) +- 修复 创建token时未传角色权限信息生成的token不正确的bug +## 1.0.12(2022-07-15) +- 提升与旧版本uni-id的兼容性(补充读取配置文件时回退平台app-plus、h5),但是仍推荐使用新平台名进行配置(app、web) +## 1.0.11(2022-07-14) +- 修复 部分情况下报`read property 'reduce' of undefined`的错误 +## 1.0.10(2022-07-11) +- 将token存储在用户表的token字段内,与旧版本uni-id保持一致 +## 1.0.9(2022-07-01) +- checkToken兼容token内未缓存角色权限的情况,此时将查库获取角色权限 +## 1.0.8(2022-07-01) +- 修复clientDB默认依赖时部分情况下获取不到uni-id配置的Bug +## 1.0.7(2022-06-30) +- 修复config文件不合法时未抛出具体错误的Bug +## 1.0.6(2022-06-28) +- 移除插件内的数据表schema +## 1.0.5(2022-06-27) +- 修复使用多应用配置时报`Cannot read property 'appId' of undefined`的Bug +## 1.0.4(2022-06-27) +- 修复使用自定义token内容功能报错的Bug [详情](https://ask.dcloud.net.cn/question/147945) +## 1.0.2(2022-06-23) +- 对齐旧版本uni-id默认配置 +## 1.0.1(2022-06-22) +- 补充对uni-config-center的依赖 +## 1.0.0(2022-06-21) +- 提供uni-id token创建、校验、刷新接口,简化旧版uni-id公共模块 diff --git a/uni-im示例/uni_modules/uni-id-common/package.json b/uni-im示例/uni_modules/uni-id-common/package.json new file mode 100644 index 0000000..0ac0de9 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-common/package.json @@ -0,0 +1,84 @@ +{ + "id": "uni-id-common", + "displayName": "uni-id-common", + "version": "1.0.16", + "description": "包含uni-id token生成、校验、刷新功能的云函数公共模块", + "keywords": [ + "uni-id-common", + "uniCloud", + "token", + "权限" + ], + "repository": "https://gitcode.net/dcloud/uni-id-common", + "engines": { + "HBuilderX": "^3.1.0" + }, + "dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "", + "type": "unicloud-template-function" + }, + "uni_modules": { + "dependencies": ["uni-config-center"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "Vue": { + "vue2": "u", + "vue3": "u" + }, + "App": { + "app-vue": "u", + "app-nvue": "u" + }, + "H5-mobile": { + "Safari": "u", + "Android Browser": "u", + "微信浏览器(Android)": "u", + "QQ浏览器(Android)": "u" + }, + "H5-pc": { + "Chrome": "u", + "IE": "u", + "Edge": "u", + "Firefox": "u", + "Safari": "u" + }, + "小程序": { + "微信": "u", + "阿里": "u", + "百度": "u", + "字节跳动": "u", + "QQ": "u", + "钉钉": "u", + "快手": "u", + "飞书": "u", + "京东": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + } + } + } + } +} diff --git a/uni-im示例/uni_modules/uni-id-common/readme.md b/uni-im示例/uni_modules/uni-id-common/readme.md new file mode 100644 index 0000000..5f6a37a --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-common/readme.md @@ -0,0 +1,3 @@ +# uni-id-common + +文档请参考:[uni-id-common](https://uniapp.dcloud.net.cn/uniCloud/uni-id-common.html) \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-id-common/uniCloud/cloudfunctions/common/uni-id-common/index.js b/uni-im示例/uni_modules/uni-id-common/uniCloud/cloudfunctions/common/uni-id-common/index.js new file mode 100644 index 0000000..bcbc2c7 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-common/uniCloud/cloudfunctions/common/uni-id-common/index.js @@ -0,0 +1 @@ +"use strict";var e,t=(e=require("crypto"))&&"object"==typeof e&&"default"in e?e.default:e;const n={TOKEN_EXPIRED:"uni-id-token-expired",CHECK_TOKEN_FAILED:"uni-id-check-token-failed",PARAM_REQUIRED:"uni-id-param-required",ACCOUNT_EXISTS:"uni-id-account-exists",ACCOUNT_NOT_EXISTS:"uni-id-account-not-exists",ACCOUNT_CONFLICT:"uni-id-account-conflict",ACCOUNT_BANNED:"uni-id-account-banned",ACCOUNT_AUDITING:"uni-id-account-auditing",ACCOUNT_AUDIT_FAILED:"uni-id-account-audit-failed",ACCOUNT_CLOSED:"uni-id-account-closed"};function i(e){return!!e&&("object"==typeof e||"function"==typeof e)&&"function"==typeof e.then}function r(e){if(!e)return;const t=e.match(/^(\d+).(\d+).(\d+)/);return t?t.slice(1,4).map(e=>parseInt(e)):void 0}function o(e,t){const n=r(e),i=r(t);return n?i?function(e,t){const n=Math.max(e.length,t.length);for(let i=0;ir)return 1;if(n=e)throw new Error("Config error, tokenExpiresThreshold should be less than tokenExpiresIn");t>e/2&&console.warn(`Please check whether the tokenExpiresThreshold configuration is set too large, tokenExpiresThreshold: ${t}, tokenExpiresIn: ${e}`)}get customToken(){return this.uniId.interceptorMap.get("customToken")}isTokenInDb(e){return o(e,"1.0.10")>=0}async getUserRecord(){if(this.userRecord)return this.userRecord;const e=await C.doc(this.uid).get();if(this.userRecord=e.data[0],!this.userRecord)throw{errCode:n.ACCOUNT_NOT_EXISTS};switch(this.userRecord.status){case void 0:case 0:break;case 1:throw{errCode:n.ACCOUNT_BANNED};case 2:throw{errCode:n.ACCOUNT_AUDITING};case 3:throw{errCode:n.ACCOUNT_AUDIT_FAILED};case 4:throw{errCode:n.ACCOUNT_CLOSED}}if(this.oldTokenPayload){if(this.isTokenInDb(this.oldTokenPayload.uniIdVersion)){if(-1===(this.userRecord.token||[]).indexOf(this.oldToken))throw{errCode:n.CHECK_TOKEN_FAILED}}if(this.userRecord.valid_token_date&&this.userRecord.valid_token_date>1e3*this.oldTokenPayload.iat)throw{errCode:n.TOKEN_EXPIRED}}return this.userRecord}async updateUserRecord(e){await C.doc(this.uid).update(e)}async getUserPermission(){if(this.userPermission)return this.userPermission;const e=(await this.getUserRecord()).role||[];if(0===e.length)return this.userPermission={role:[],permission:[]},this.userPermission;if(e.includes("admin"))return this.userPermission={role:e,permission:[]},this.userPermission;const t=await T.where({role_id:_.in(e)}).get(),n=(i=t.data.reduce((e,t)=>(t.permission&&e.push(...t.permission),e),[]),Array.from(new Set(i)));var i;return this.userPermission={role:e,permission:n},this.userPermission}async _createToken({uid:e,role:t,permission:i}={}){if(!t||!i){const e=await this.getUserPermission();t=e.role,i=e.permission}let r={uid:e,role:t,permission:i};if(this.uniId.interceptorMap.has("customToken")){const n=this.uniId.interceptorMap.get("customToken");if("function"!=typeof n)throw new Error("Invalid custom token file");r=await n({uid:e,role:t,permission:i})}const o=Date.now(),{tokenSecret:s,tokenExpiresIn:c,maxTokenLength:a=10}=this.config,u=g({...r,uniIdVersion:"1.0.16"},s,{expiresIn:c}),d=await this.getUserRecord(),l=(d.token||[]).filter(e=>{try{const t=this._checkToken(e);if(d.valid_token_date&&d.valid_token_date>1e3*t.iat)return!1}catch(e){if(e.errCode===n.TOKEN_EXPIRED)return!1}return!0});return l.push(u),l.length>a&&l.splice(0,l.length-a),await this.updateUserRecord({last_login_ip:this.clientInfo.clientIP,last_login_date:o,token:l}),{token:u,tokenExpired:o+1e3*c}}async createToken({uid:e,role:t,permission:i}={}){if(!e)throw{errCode:n.PARAM_REQUIRED,errMsgValue:{param:"uid"}};this.uid=e;const{token:r,tokenExpired:o}=await this._createToken({uid:e,role:t,permission:i});return{errCode:0,token:r,tokenExpired:o}}async refreshToken({token:e}={}){if(!e)throw{errCode:n.PARAM_REQUIRED,errMsgValue:{param:"token"}};this.oldToken=e;const t=this._checkToken(e);this.uid=t.uid,this.oldTokenPayload=t;const{uid:i}=t,{role:r,permission:o}=await this.getUserPermission(),{token:s,tokenExpired:c}=await this._createToken({uid:i,role:r,permission:o});return{errCode:0,token:s,tokenExpired:c}}_checkToken(e){const{tokenSecret:t}=this.config;let i;try{i=k(e,t)}catch(e){if("TokenExpiredError"===e.name)throw{errCode:n.TOKEN_EXPIRED};throw{errCode:n.CHECK_TOKEN_FAILED}}return i}async checkToken(e,{autoRefresh:t=!0}={}){if(!e)throw{errCode:n.PARAM_REQUIRED,errMsgValue:{param:"token"}};this.oldToken=e;const i=this._checkToken(e);this.uid=i.uid,this.oldTokenPayload=i;const{tokenExpiresThreshold:r}=this.config,{uid:o,role:s,permission:c}=i,a={role:s,permission:c};if(!s&&!c){const{role:e,permission:t}=await this.getUserPermission();a.role=e,a.permission=t}if(!r||!t){const e={code:0,errCode:0,...i,...a};return delete e.uniIdVersion,e}const u=Date.now();let d={};1e3*i.exp-u<1e3*r&&(d=await this._createToken({uid:o}));const l={code:0,errCode:0,...i,...a,...d};return delete l.uniIdVersion,l}}var E=Object.freeze({__proto__:null,checkToken:async function(e,{autoRefresh:t=!0}={}){return new m({uniId:this}).checkToken(e,{autoRefresh:t})},createToken:async function({uid:e,role:t,permission:n}={}){return new m({uniId:this}).createToken({uid:e,role:t,permission:n})},refreshToken:async function({token:e}={}){return new m({uniId:this}).refreshToken({token:e})}});const w=require("uni-config-center")({pluginId:"uni-id"});class x{constructor({context:e,clientInfo:t,config:n}={}){this._clientInfo=e?function(e){return{appId:e.APPID,platform:e.PLATFORM,locale:e.LOCALE,clientIP:e.CLIENTIP,deviceId:e.DEVICEID}}(e):t,this.config=n||this._getOriginConfig(),this.interceptorMap=new Map,w.hasFile("custom-token.js")&&this.setInterceptor("customToken",require(w.resolve("custom-token.js")));this._i18n=uniCloud.initI18n({locale:this._clientInfo.locale,fallbackLocale:"zh-Hans",messages:JSON.parse(JSON.stringify(d))}),d[this._i18n.locale]||this._i18n.setLocale("zh-Hans")}setInterceptor(e,t){this.interceptorMap.set(e,t)}_t(...e){return this._i18n.t(...e)}_parseOriginConfig(e){return Array.isArray(e)?e:e[0]?Object.values(e):e}_getOriginConfig(){if(w.hasFile("config.json")){let e;try{e=w.config()}catch(e){throw new Error("Invalid uni-id config file\n"+e.message)}return this._parseOriginConfig(e)}try{return this._parseOriginConfig(require("uni-id/config.json"))}catch(e){throw new Error("Invalid uni-id config file")}}_getAppConfig(){const e=this._getOriginConfig();return Array.isArray(e)?e.find(e=>e.dcloudAppid===this._clientInfo.appId)||e.find(e=>e.isDefaultConfig):e}_getPlatformConfig(){const e=this._getAppConfig();if(!e)throw new Error(`Config for current app (${this._clientInfo.appId}) was not found, please check your config file or client appId`);let t;switch("app-plus"===this._clientInfo.platform&&(this._clientInfo.platform="app"),"h5"===this._clientInfo.platform&&(this._clientInfo.platform="web"),this._clientInfo.platform){case"web":t="h5";break;case"app":t="app-plus"}const n=[{tokenExpiresIn:7200,tokenExpiresThreshold:1200,passwordErrorLimit:6,passwordErrorRetryTime:3600},e];t&&e[t]&&n.push(e[t]),n.push(e[this._clientInfo.platform]);const i=Object.assign(...n);return["tokenSecret","tokenExpiresIn"].forEach(e=>{if(!i||!i[e])throw new Error(`Config parameter missing, ${e} is required`)}),i}_getConfig(){return this._getPlatformConfig()}}for(const e in E)x.prototype[e]=E[e];function y(e){const t=new x(e);return new Proxy(t,{get(e,t){if(t in e&&0!==t.indexOf("_")){if("function"==typeof e[t])return(n=e[t],function(){let e;try{e=n.apply(this,arguments)}catch(e){if(a(e))return c.call(this,e),e;throw e}return i(e)?e.then(e=>(a(e)&&c.call(this,e),e),e=>{if(a(e))return c.call(this,e),e;throw e}):(a(e)&&c.call(this,e),e)}).bind(e);if("context"!==t&&"config"!==t)return e[t]}var n}})}x.prototype.createInstance=y;const A={createInstance:y};module.exports=A; diff --git a/uni-im示例/uni_modules/uni-id-common/uniCloud/cloudfunctions/common/uni-id-common/package.json b/uni-im示例/uni_modules/uni-id-common/uniCloud/cloudfunctions/common/uni-id-common/package.json new file mode 100644 index 0000000..3b4ec05 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-common/uniCloud/cloudfunctions/common/uni-id-common/package.json @@ -0,0 +1,16 @@ +{ + "name": "uni-id-common", + "version": "1.0.16", + "description": "uni-id token生成、校验、刷新", + "main": "index.js", + "homepage": "https://uniapp.dcloud.io/uniCloud/uni-id-common.html", + "repository": { + "type": "git", + "url": "git+https://gitee.com/dcloud/uni-id-common.git" + }, + "author": "DCloud", + "license": "Apache-2.0", + "dependencies": { + "uni-config-center": "file:../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center" + } +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-id-pages/changelog.md b/uni-im示例/uni_modules/uni-id-pages/changelog.md new file mode 100644 index 0000000..f8065e6 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/changelog.md @@ -0,0 +1,168 @@ +## 1.1.14(2023-05-19) +- 修复 退出登录不会跳转至登录页的问题 +## 1.1.13(2023-05-10) +- 修复 启用摇树优化 报错的问题 +## 1.1.12(2023-05-05) +- uni-id-co 新增 调用 add-user 接口创建用户时允许触发 beforeRegister 钩子方法,beforeRegister 钩子[详见](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#before-register) +- uni-id-co 新增 自无 unionid 到有 unionid 状态进行登录时为用户补充 unionid 字段 +- uni-id-co 修复 i18n 在特定场景下报错的 bug +- uni-id-co 修复 跨平台解绑微信/QQ时无法解绑的 bug +- uni-id-co 修复 微信小程序等平台创建验证码时无法展示的 bug +- uni-id-co 修复 更新 push_clientid 时因 device_id 没有变化导致无法更新 +## 1.1.11(2023-03-24) +- 修复 tabbar页面因为token无效而强制跳转至登录页面(url参数包含`uniIdRedirectUrl`)后无法返回的问题 +## 1.1.10(2023-03-24) +- 修复 PC微信扫码登录跳转地址错误 +- uni-id-co 新增 请求鉴权支持 uni-cloud-s2s 模块验证签名 [uni-cloud-s2s文档](https://uniapp.dcloud.net.cn/uniCloud/uni-cloud-s2s.html) +## 1.1.9(2023-03-24) +- 修复 跳转至登录页面的url参数包含`uniIdRedirectUrl`后无法返回的问题 +## 1.1.8(2023-03-02) +- 修复 调试模式下没有对微信授权手机号登录方式进行配置检测 +## 1.1.7(2023-02-27) +- 【重要】新增 实名认证功能 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#frv) +## 1.1.6(2023-02-24) +- uni-id-co 新增 注册用户时允许配置默认角色 [文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#config-defult-role) +- uni-id-co 优化 `updateUserInfoByExternal`接口,允许修改头像、性别 +- uni-id-co 修复 请求签名密钥字段 `requestAuthSecret` 缺少为空判断 +- uni-id-co 修复 `externalRegister`接口头像未使用`avatar_file`字段保存 +- 修复 web微信登录回调地址不正确 +## 1.1.5(2023-02-23) +- 更新 微信小程序端 更新头像信息,如果是使用微信的头像则不再调用裁剪接口 +## 1.1.4(2023-02-21) +- 修复 部分情况下 `uniIdRedirectUrl` 参数无效的问题 +## 1.1.3(2023-02-20) +- 修复 非微信小程序端报`TypeError: uni.hideHomeButton is not a function`的问题 +## 1.1.2(2023-02-10) +- 新增 微信小程序端 首页需强制登录时,隐藏返回首页按钮 +- uni-id-co 新增 外部联登后修改用户信息接口(updateUserInfoByExternal) [文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-update-userinfo) +- uni-id-co 优化外部联登接口(登录、注册)逻辑 +## 1.1.1(2023-02-02) +- 新增 微信小程序端 支持选择使用微信资料的“头像”和“昵称” 设置用户资料 [详情参考](https://wdoc-76491.picgzc.qpic.cn/MTY4ODg1MDUyNzQyMDUxNw_21263_rTNhg68FTngQGdvQ_1647431233?w=1280&h=695.7176470588236) +## 1.1.0(2023-01-31) +- 【重要】优化 小程序端资源包大小(运行时大小为:731KB,发行后为:583KB;注:可以直接将本插件作为分包使用) +- 更新 微信小程序端 上传头像功能 用`wx.cropImage`实现图片裁剪 +- 修复 选择一键登录时会先显示 非密码登录页面的问题 +- 修复 一键登录 点击右上角的关闭按钮没有返回上一页的问题 +## 1.0.41(2023-01-16) +- 优化 压缩依赖的文件资源大小 +## 1.0.40(2023-01-16) +- 更新依赖的 验证码插件`uni-captcha`版本的版本为 0.6.4 修复 部分情况下APP端无法获取验证码的问题 [详情参考](https://ext.dcloud.net.cn/plugin?id=4048) +- 修复 客户端token过期后,点击退出登录按钮报错的问题 +- uni-id-co 修复 updateUser 接口`手机号`和`邮箱`参数值为空字符串时,修改无效的问题 +## 1.0.39(2022-12-28) +- uni-id-co 修复 URL化时第三方登录无法获取 uniPlatform 参数 +- uni-id-co 修复 validator error +## 1.0.38(2022-12-26) +- uni-id-co 优化 手机号与邮箱验证规则为空字符串时不校验 +## 1.0.37(2022-12-09) +- 优化admin端样式 +## 1.0.36(2022-12-08) +- uni-id-co 修复 `updateUser` 接口部分参数为空时数据修改异常 +## 1.0.35(2022-11-30) +- uni-id-co 新增 匹配到的用户不可在当前应用登录时的错误码 `uni-id-account-not-exists-in-current-app` [错误码说明](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#errcode) +## 1.0.34(2022-11-29) +- 优化 toast 错误提示时间为3秒 +- uni-id-co 修复 无法从 clientInfo 中获取 uniIdToken +## 1.0.33(2022-11-25) +- uni-id-co 新增 外部系统联登接口,可为外部系统创建与uni-id相对应的账号,使该账号可以使用依赖uniId的系统及功能 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external) +- uni-id-co 新增 URL化请求时鉴权签名验证 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#http-reqeust-auth) +- uni-id-co 修复 微信登录时用户未设置头像的报错问题 +## 1.0.32(2022-11-21) +- 新增 设置密码页面 +- 新增 登录后跳转设置密码页面配置项`setPasswordAfterLogin` [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-pwd-after-login) +- uni-id-co 新增 设置密码接口 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-pwd) +## 1.0.31(2022-11-16) +- uni-id-co 修复 验证码可能无法收到的bug +## 1.0.30(2022-11-11) +- uni-id-co 修复 用户只有openid时绑定微信/QQ报错 +## 1.0.29(2022-11-10) +- uni-id-co 支持URL化方式请求 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#adapter-http) +## 1.0.28(2022-11-09) +- uni-id-co 升级密码加密算法,支持hmac-sha256加密 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#password-safe) +- uni-id-co 新增 开发者可以自定义密码加密规则 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#custom-password-encrypt) +- uni-id-co 新增 支持将其他系统用户迁移至uni-id [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#move-users-to-uni-id) +## 1.0.27(2022-10-26) +- uni-id-co 新增 secureNetworkHandshakeByWeixin 接口,用于建立和微信小程序的安全网络连接 +## 1.0.26(2022-10-18) +- 修复 uni-id-pages 导入时异常的Bug +## 1.0.25(2022-10-14) +- uni-id-co 增加 微信授权手机号登录方式 [文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-weixin-mobile) +- uni-id-co 增加 解绑第三方平台账号 [文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-third-account) +- uni-id-co 微信绑定手机号支持通过`getPhoneNumber`事件回调的`code`绑定 [文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-mp-weixin) +- 修复 sendSmsCode 接口未在参数内传递 templateId 时 未能从配置文件读取 templateId 的Bug +## 1.0.24(2022-10-08) +- 修复 报uni-id-users表schema内错误的bug +## 1.0.23(2022-10-08) +- 修复 vue3下vite编译发行打包失败 +- 修复 某些情况下注册账号,报TypeErroe:Cannot read properties of undefined (reading ’showToast‘)的错误 +## 1.0.22(2022-09-23) +- 修复 某些情况下,修改密码报“两次输入密码不一致”的bug +## 1.0.21(2022-09-21) +- 修复 store.hasLogin的值在某些情况下会出错的bug +## 1.0.20(2022-09-21) +- 新增 store 账号信息状态管理,详情:用户中心页面 路径:`/uni_modules/uni-id-pages/pages/userinfo/userinfo` +## 1.0.19(2022-09-20) +- 修复 小程序端,使用将自定义节点设置成[虚拟节点](https://uniapp.dcloud.net.cn/tutorial/vue-api.html#%E5%85%B6%E4%BB%96%E9%85%8D%E7%BD%AE)的uni-ui组件,样式错乱的问题 +## 1.0.18(2022-09-20) +- 修复 微信小程序端 WXSS 编译报错的bug +## 1.0.17(2022.09-19) +- 修复 无法退出登录的bug +## 1.0.16(2022-09-19) +- 修复 在 Edge 浏览器下 input[type="password"] 会出现浏览器自带的密码查看按钮 +- 优化 退出登录重定向页面为 uniIdRouter.loginPage +- 新增 注册账号页面支持返回登录页面 +## 1.0.15(2022-09-19) +- 更新表结构,解决在uni-admin中部分clientDB操作没有权限的问题 +## 1.0.14(2022-09-16) +- 修改 配置项`isAdmin`默认值为`false` +## 1.0.13(2022-09-16) +- 新增 管理员注册页面 +- 新增 配置项`isAdmin`区分是否为管理端 +- 新增 登录成功后自动跳转;跳转优先级:路由携带(`uniIdRedirectUrl`参数) > 返回上一路由 > 跳转首页 +- uni-id-co 优化 注册管理员时管理员存在提示文案 +## 1.0.12(2022-09-07) +- 修复 getSupportedLoginType判断是否支持微信公众号、PC网页微信扫码登录方式报错的Bug +- 优化 适配pc端样式 +- 新增 邮箱验证码注册 +- 新增 邮箱验证码找回密码 +- 新增 退出登录(全局)回调事件:`uni-id-pages-logout`,支持通过[uni.$on](https://uniapp.dcloud.net.cn/api/window/communication.html#on)监听; +- 调整 抽离退出登录方法至`/uni_modules/uni-id-pages/common/common.js`中,方便在项目其他页面中调用 +- 调整 用户中心(路径:`/uni_modules/uni-id-pages/pages/userinfo/userinfo`)默认不再显示退出登录按钮。支持页面传参数`showLoginManage=true`恢复显示 +## 1.0.11(2022-09-01) +- 修复 iOS端,一键登录功能卡在showLoading的问题 +- 更新 合并密码强度与长度配置 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#config) +- uni-id-co 修复 调用 removeAuthorizedApp 接口报错的Bug +- uni-id-co 新增 管理端接口 updateUser [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#update-user) +- uni-id-co 调整 为兼容旧版本,未配置密码强度时提供最简单的密码规则校验(长度大于6即可) +- uni-id-co 调整 注册、登录时如果携带了token则尝试对此token进行登出操作 +- uni-id-co 调整 管理端接口 addUser 增加 mobile、email等参数 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#add-user) +## 1.0.10(2022-08-25) +- 修复 导入uni-id-pages插件时未自动导入uni-open-bridge-common的Bug +## 1.0.9(2022-08-23) +- 修复 uni-id-co 缺失uni-open-bridge-common依赖的Bug +## 1.0.8(2022-08-23) +- 新增 H5端支持微信登录(含微信公众号内的网页授权登录 和 普通浏览器内网页生成二维码,实现手机微信扫码登录)[详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#weixinlogin) +- 新增 登录成功(全局)回调事件:`uni-id-pages-login-success`,支持通过[uni.$on](https://uniapp.dcloud.net.cn/api/window/communication.html#on)监听; +- 新增 密码强度(是否必须包含大小写字母、数字和特殊符号以及长度)配置 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#config) +- 调整 uni-id-co 密码规则调整,废除之前的简单校验,允许配置密码强度 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#password-strength) +- 调整 uni-id-co 存储用户 openid 时同时以客户端 AppId 为 Key 的副本,参考:[微信登录](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-weixin)、[QQ登录](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-qq) +- 调整 uni-id-co 依赖 uni-open-bridge-common 存储用户 session_key、access_token 等信息 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#save-user-token) +- 新增 uni-id-co 增加 beforeRegister 钩子用户在注册前向用户记录内添加一些数据 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#before-register) +## 1.0.7(2022-07-19) +- 修复 uni-id-co接口 logout时没有删除token的Bug +## 1.0.6(2022-07-13) +- 新增 允许覆盖内置校验规则 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#custom-validator) +- 修复 app端clientInfo.appVersionCode为数字导致校验无法通过的Bug +## 1.0.5(2022-07-11) +修复 微信小程序调用uni-id-co接口报错的Bug [详情](https://ask.dcloud.net.cn/question/148877) +## 1.0.4(2022-07-06) +- uni-id-co增加clientInfo字段类型校验 +- 监听token更新时机,同步客户端push_clientid至uni-id-device表,改为:同步客户端push_clientid至uni-id-device表和opendb-device表 +## 1.0.3(2022-07-05) +新增监听token更新时机,同步客户端push_clientid至uni-id-device表 +## 1.0.2(2022-07-04) +修复微信小程序登录时无unionid报错的Bug [详情](https://ask.dcloud.net.cn/question/148016) +## 1.0.1(2022-06-28) +添加相关uni-id表 +## 1.0.0(2022-06-23) +正式版 diff --git a/uni-im示例/uni_modules/uni-id-pages/common/check-id-card.js b/uni-im示例/uni_modules/uni-id-pages/common/check-id-card.js new file mode 100644 index 0000000..b3ca0c3 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/common/check-id-card.js @@ -0,0 +1,16 @@ +function checkIdCard (idCardNumber) { + if (!idCardNumber || typeof idCardNumber !== 'string' || idCardNumber.length !== 18) return false + + const coefficient = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] + const checkCode = [1, 0, 'x', 9, 8, 7, 6, 5, 4, 3, 2] + const code = idCardNumber.substring(17) + + let sum = 0 + for (let i = 0; i < 17; i++) { + sum += Number(idCardNumber.charAt(i)) * coefficient[i] + } + + return checkCode[sum % 11].toString() === code.toLowerCase() +} + +export default checkIdCard diff --git a/uni-im示例/uni_modules/uni-id-pages/common/login-page.mixin.js b/uni-im示例/uni_modules/uni-id-pages/common/login-page.mixin.js new file mode 100644 index 0000000..2a26f7e --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/common/login-page.mixin.js @@ -0,0 +1,95 @@ +import { + mutations +} from '@/uni_modules/uni-id-pages/common/store.js' +import config from '@/uni_modules/uni-id-pages/config.js' +const mixin = { + data() { + return { + config, + uniIdRedirectUrl: '', + isMounted: false + } + }, + onUnload() { + // #ifdef H5 + document.onkeydown = false + // #endif + }, + mounted() { + this.isMounted = true + }, + onLoad(e) { + if (e.is_weixin_redirect) { + uni.showLoading({ + mask: true + }) + + if (window.location.href.includes('#')) { + // 将url通过 ? 分割获取后面的参数字符串 再通过 & 将每一个参数单独分割出来 + const paramsArr = window.location.href.split('?')[1].split('&') + paramsArr.forEach(item => { + const arr = item.split('=') + if (arr[0] == 'code') { + e.code = arr[1] + } + }) + } + this.$nextTick(n => { + // console.log(this.$refs.uniFabLogin); + this.$refs.uniFabLogin.login({ + code: e.code + }, 'weixin') + }) + } + + if (e.uniIdRedirectUrl) { + this.uniIdRedirectUrl = decodeURIComponent(e.uniIdRedirectUrl) + } + + // #ifdef MP-WEIXIN + if (getCurrentPages().length === 1) { + uni.hideHomeButton() + console.log('已隐藏:返回首页按钮'); + } + // #endif + }, + computed: { + needAgreements() { + if (this.isMounted) { + if (this.$refs.agreements) { + return this.$refs.agreements.needAgreements + } else { + return false + } + } + }, + agree: { + get() { + if (this.isMounted) { + if (this.$refs.agreements) { + return this.$refs.agreements.isAgree + } else { + return true + } + } + }, + set(agree) { + if (this.$refs.agreements) { + this.$refs.agreements.isAgree = agree + } else { + console.log('不存在 隐私政策协议组件'); + } + } + } + }, + methods: { + loginSuccess(e) { + mutations.loginSuccess({ + ...e, + uniIdRedirectUrl: this.uniIdRedirectUrl + }) + } + } +} + +export default mixin diff --git a/uni-im示例/uni_modules/uni-id-pages/common/login-page.scss b/uni-im示例/uni_modules/uni-id-pages/common/login-page.scss new file mode 100644 index 0000000..5de6899 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/common/login-page.scss @@ -0,0 +1,126 @@ +// 隐藏 edge 浏览器的密码查看按钮 + +/* #ifdef H5 */ +.input-box ::v-deep{ + .uni-input-input[type="password"] { + &::-ms-reveal { + display: none; + } + } +} +/* #endif */ + +.uni-content { + padding: 0 60rpx; +} + +.login-logo { + display: none; +} + +/* #ifndef APP-NVUE */ +@media screen and (min-width: 690px) { + .uni-content { + /* #ifndef H5 */ + padding: 0; + max-width: 300px; + margin-left: calc(50% - 200px); + /* #endif */ + /* #ifdef H5 */ + margin: 0 auto; + position: relative; + top: 100px; + padding: 30px 40px 80px 40px; + max-width: 450px; + max-height: 450px; + border-radius: 10px; + box-shadow: 0 0 20px #efefef; + background-color: #FFF; + /* #endif */ + } + /* #ifdef H5 */ + .login-logo { + display: flex; + justify-content: center; + } + + .login-logo image { + width: 60px; + height: 60px; + } + + .register-back{ + display: none; + } + + uni-button{ + padding-bottom: 1px; + } + + /* #endif */ +} + +.uni-content view { + box-sizing: border-box; +} +/* #endif */ + + + +.title { + /* #ifndef APP-NVUE */ + display: flex; + /* #endif */ + padding: 18px 0; + font-weight: 800; + flex-direction: column; +} + +.tip { + /* #ifndef APP-NVUE */ + display: flex; + /* #endif */ + color: #BDBDC0; + font-size: 11px; + margin: 6px 0; +} + + +/* #ifndef APP-NVUE */ +// 解决小程序端开启虚拟节点virtualHost引起的 class = input-box丢失的问题 [详情参考](https://uniapp.dcloud.net.cn/matter.html#%E5%90%84%E5%AE%B6%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6%E4%B8%8D%E5%90%8C-%E5%8F%AF%E8%83%BD%E5%AD%98%E5%9C%A8%E7%9A%84%E5%B9%B3%E5%8F%B0%E5%85%BC%E5%AE%B9%E9%97%AE%E9%A2%98) +.uni-content ::v-deep .uni-easyinput__content, +/* #endif */ + +.input-box { + height: 44px; + background-color: #F8F8F8 !important; + border-radius: 0; + font-size: 14px; + /* #ifndef APP-NVUE */ + display: flex; + /* #endif */ + flex: 1; +} + +.link { + color: #04498c; + cursor: pointer; +} + +.uni-content ::v-deep .uni-forms-item__inner { + padding-bottom: 8px; +} + +.uni-btn { + text-align: center; + height: 40px; + line-height: 40px; + margin: 15px 0 10px 0; + color: #FFF !important; + border-radius: 5px; + font-size: 16px; +} + +.uni-body.uni_modules-uni-id-pages-pages-login-login-withoutpwd{ + height: auto !important; +} diff --git a/uni-im示例/uni_modules/uni-id-pages/common/password.js b/uni-im示例/uni_modules/uni-id-pages/common/password.js new file mode 100644 index 0000000..196c240 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/common/password.js @@ -0,0 +1,85 @@ +// 导入配置 +import config from '@/uni_modules/uni-id-pages/config.js' + +const {passwordStrength} = config + +// 密码强度表达式 +const passwordRules = { + // 密码必须包含大小写字母、数字和特殊符号 + super: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/, + // 密码必须包含字母、数字和特殊符号 + strong: /^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/, + // 密码必须为字母、数字和特殊符号任意两种的组合 + medium: /^(?![0-9]+$)(?![a-zA-Z]+$)(?![~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]+$)[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/, + // 密码必须包含字母和数字 + weak: /^(?=.*[0-9])(?=.*[a-zA-Z])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{6,16}$/ +} + +const ERROR = { + normal: { + noPwd: '请输入密码', + noRePwd: '再次输入密码', + rePwdErr: '两次输入密码不一致' + }, + passwordStrengthError: { + super: '密码必须包含大小写字母、数字和特殊符号,密码长度必须在8-16位之间', + strong: '密码必须包含字母、数字和特殊符号,密码长度必须在8-16位之间', + medium: '密码必须为字母、数字和特殊符号任意两种的组合,密码长度必须在8-16位之间', + weak: '密码必须包含字母,密码长度必须在6-16位之间' + } +} + +function validPwd(password) { + //强度校验 + if (passwordStrength && passwordRules[passwordStrength]) { + if (!new RegExp(passwordRules[passwordStrength]).test(password)) { + return ERROR.passwordStrengthError[passwordStrength] + } + } + return true +} + +function getPwdRules(pwdName = 'password', rePwdName = 'password2') { + const rules = {} + rules[pwdName] = { + rules: [{ + required: true, + errorMessage: ERROR.normal.noPwd, + }, + { + validateFunction: function(rule, value, data, callback) { + const checkRes = validPwd(value) + if (checkRes !== true) { + callback(checkRes) + } + return true + } + } + ] + } + + if (rePwdName) { + rules[rePwdName] = { + rules: [{ + required: true, + errorMessage: ERROR.normal.noRePwd, + }, + { + validateFunction: function(rule, value, data, callback) { + if (value != data[pwdName]) { + callback(ERROR.normal.rePwdErr) + } + return true + } + } + ] + } + } + return rules +} + +export default { + ERROR, + validPwd, + getPwdRules +} diff --git a/uni-im示例/uni_modules/uni-id-pages/common/store.js b/uni-im示例/uni_modules/uni-id-pages/common/store.js new file mode 100644 index 0000000..83fdd1e --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/common/store.js @@ -0,0 +1,174 @@ +import pagesJson from '@/pages.json' +import config from '@/uni_modules/uni-id-pages/config.js' + +const uniIdCo = uniCloud.importObject("uni-id-co") +const db = uniCloud.database(); +const usersTable = db.collection('uni-id-users') + +let hostUserInfo = uni.getStorageSync('uni-id-pages-userInfo')||{} +// console.log( hostUserInfo); +const data = { + userInfo: hostUserInfo, + hasLogin: Object.keys(hostUserInfo).length != 0 +} + +// console.log('data', data); +// 定义 mutations, 修改属性 +export const mutations = { + // data不为空,表示传递要更新的值(注意不是覆盖是合并),什么也不传时,直接查库获取更新 + async updateUserInfo(data = false) { + if (data) { + usersTable.where('_id==$env.uid').update(data).then(e => { + // console.log(e); + if (e.result.updated) { + uni.showToast({ + title: "更新成功", + icon: 'none', + duration: 3000 + }); + this.setUserInfo(data) + } else { + uni.showToast({ + title: "没有改变", + icon: 'none', + duration: 3000 + }); + } + }) + + } else { + const uniIdCo = uniCloud.importObject("uni-id-co", { + customUI: true + }) + try { + let res = await usersTable.where("'_id' == $cloudEnv_uid") + .field('mobile,nickname,username,email,avatar_file') + .get() + + const realNameRes = await uniIdCo.getRealNameInfo() + + // console.log('fromDbData',res.result.data); + this.setUserInfo({ + ...res.result.data[0], + realNameAuth: realNameRes + }) + } catch (e) { + this.setUserInfo({},{cover:true}) + console.error(e.message, e.errCode); + } + } + }, + async setUserInfo(data, {cover}={cover:false}) { + // console.log('set-userInfo', data); + let userInfo = cover?data:Object.assign(store.userInfo,data) + store.userInfo = Object.assign({},userInfo) + store.hasLogin = Object.keys(store.userInfo).length != 0 + // console.log('store.userInfo', store.userInfo); + uni.setStorageSync('uni-id-pages-userInfo', store.userInfo) + return data + }, + async logout() { + // 1. 已经过期就不需要调用服务端的注销接口 2.即使调用注销接口失败,不能阻塞客户端 + if(uniCloud.getCurrentUserInfo().tokenExpired > Date.now()){ + try{ + await uniIdCo.logout() + }catch(e){ + console.error(e); + } + } + uni.removeStorageSync('uni_id_token'); + uni.setStorageSync('uni_id_token_expired', 0) + uni.redirectTo({ + url: `/${pagesJson.uniIdRouter && pagesJson.uniIdRouter.loginPage ? pagesJson.uniIdRouter.loginPage: 'uni_modules/uni-id-pages/pages/login/login-withoutpwd'}`, + }); + uni.$emit('uni-id-pages-logout') + this.setUserInfo({},{cover:true}) + }, + + loginBack (e = {}) { + const {uniIdRedirectUrl = ''} = e + let delta = 0; //判断需要返回几层 + let pages = getCurrentPages(); + // console.log(pages); + pages.forEach((page, index) => { + if (pages[pages.length - index - 1].route.split('/')[3] == 'login') { + delta++ + } + }) + // console.log('判断需要返回几层:', delta); + if (uniIdRedirectUrl) { + return uni.redirectTo({ + url: uniIdRedirectUrl, + fail: (err1) => { + uni.switchTab({ + url:uniIdRedirectUrl, + fail: (err2) => { + console.log(err1,err2) + } + }) + } + }) + } + // #ifdef H5 + if (e.loginType == 'weixin') { + // console.log('window.history', window.history); + return window.history.go(-3) + } + // #endif + + if (delta) { + const page = pagesJson.pages[0] + return uni.reLaunch({ + url: `/${page.path}` + }) + } + + uni.navigateBack({ + delta + }) + }, + loginSuccess(e = {}){ + const { + showToast = true, toastText = '登录成功', autoBack = true, uniIdRedirectUrl = '', passwordConfirmed + } = e + // console.log({toastText,autoBack}); + if (showToast) { + uni.showToast({ + title: toastText, + icon: 'none', + duration: 3000 + }); + } + this.updateUserInfo() + + uni.$emit('uni-id-pages-login-success') + + if (config.setPasswordAfterLogin && !passwordConfirmed) { + return uni.redirectTo({ + url: uniIdRedirectUrl ? `/uni_modules/uni-id-pages/pages/userinfo/set-pwd/set-pwd?uniIdRedirectUrl=${uniIdRedirectUrl}&loginType=${e.loginType}`: `/uni_modules/uni-id-pages/pages/userinfo/set-pwd/set-pwd?loginType=${e.loginType}`, + fail: (err) => { + console.log(err) + } + }) + } + + if (autoBack) { + this.loginBack({uniIdRedirectUrl}) + } + } + +} + +// #ifdef VUE2 +import Vue from 'vue' +// 通过Vue.observable创建一个可响应的对象 +export const store = Vue.observable(data) +// #endif + +// #ifdef VUE3 +import { + reactive +} from 'vue' +// 通过Vue.observable创建一个可响应的对象 +export const store = reactive(data) +// #endif diff --git a/uni-im示例/uni_modules/uni-id-pages/components/cloud-image/cloud-image.vue b/uni-im示例/uni_modules/uni-id-pages/components/cloud-image/cloud-image.vue new file mode 100644 index 0000000..d714a3a --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/components/cloud-image/cloud-image.vue @@ -0,0 +1,73 @@ + + + \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-agreements/uni-id-pages-agreements.vue b/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-agreements/uni-id-pages-agreements.vue new file mode 100644 index 0000000..31ab0b7 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-agreements/uni-id-pages-agreements.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-avatar/uni-id-pages-avatar.vue b/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-avatar/uni-id-pages-avatar.vue new file mode 100644 index 0000000..7aa6062 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-avatar/uni-id-pages-avatar.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-bind-mobile/uni-id-pages-bind-mobile.vue b/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-bind-mobile/uni-id-pages-bind-mobile.vue new file mode 100644 index 0000000..56edaec --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-bind-mobile/uni-id-pages-bind-mobile.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-email-form/uni-id-pages-email-form.vue b/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-email-form/uni-id-pages-email-form.vue new file mode 100644 index 0000000..b10d7dc --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-email-form/uni-id-pages-email-form.vue @@ -0,0 +1,248 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-fab-login/uni-id-pages-fab-login.vue b/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-fab-login/uni-id-pages-fab-login.vue new file mode 100644 index 0000000..9688abf --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-fab-login/uni-id-pages-fab-login.vue @@ -0,0 +1,568 @@ + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-sms-form/uni-id-pages-sms-form.vue b/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-sms-form/uni-id-pages-sms-form.vue new file mode 100644 index 0000000..1d38b87 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/components/uni-id-pages-sms-form/uni-id-pages-sms-form.vue @@ -0,0 +1,242 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/config.js b/uni-im示例/uni_modules/uni-id-pages/config.js new file mode 100644 index 0000000..618bcf7 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/config.js @@ -0,0 +1,67 @@ +export default { + // 调试模式 + debug: false, + /* + 登录类型 未列举到的或运行环境不支持的,将被自动隐藏。 + 如果需要在不同平台有不同的配置,直接用条件编译即可 + */ + isAdmin: false, // 区分管理端与用户端 + loginTypes: [ + // "qq", + // "xiaomi", + // "sinaweibo", + // "taobao", + // "facebook", + // "google", + // "alipay", + // "douyin", + + // #ifdef APP + 'univerify', + // #endif + 'weixin', + 'username', + // #ifdef APP + 'apple', + // #endif + 'smsCode' + ], + // 政策协议 + agreements: { + serviceUrl: 'https://xxx', // 用户服务协议链接 + privacyUrl: 'https://xxx', // 隐私政策条款链接 + // 哪些场景下显示,1.注册(包括登录并注册,如:微信登录、苹果登录、短信验证码登录)、2.登录(如:用户名密码登录) + scope: [ + 'register', 'login', 'realNameVerify' + ] + }, + // 提供各类服务接入(如微信登录服务)的应用id + appid: { + weixin: { + // 微信公众号的appid,来源:登录微信公众号(https://mp.weixin.qq.com)-> 设置与开发 -> 基本配置 -> 公众号开发信息 -> AppID + h5: 'xxxxxx', + // 微信开放平台的appid,来源:登录微信开放平台(https://open.weixin.qq.com) -> 管理中心 -> 网站应用 -> 选择对应的应用名称,点击查看 -> AppID + web: 'xxxxxx' + } + }, + /** + * 密码强度 + * super(超强:密码必须包含大小写字母、数字和特殊符号,长度范围:8-16位之间) + * strong(强: 密密码必须包含字母、数字和特殊符号,长度范围:8-16位之间) + * medium (中:密码必须为字母、数字和特殊符号任意两种的组合,长度范围:8-16位之间) + * weak(弱:密码必须包含字母和数字,长度范围:6-16位之间) + * 为空或false则不验证密码强度 + */ + passwordStrength: 'medium', + /** + * 登录后允许用户设置密码(只针对未设置密码得用户) + * 开启此功能将 setPasswordAfterLogin 设置为 true 即可 + * "setPasswordAfterLogin": false + * + * 如果允许用户跳过设置密码 将 allowSkip 设置为 true + * "setPasswordAfterLogin": { + * "allowSkip": true + * } + * */ + setPasswordAfterLogin: false +} diff --git a/uni-im示例/uni_modules/uni-id-pages/init.js b/uni-im示例/uni_modules/uni-id-pages/init.js new file mode 100644 index 0000000..1d9b29d --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/init.js @@ -0,0 +1,95 @@ +// 导入配置 +import config from '@/uni_modules/uni-id-pages/config.js' +// uni-id的云对象 +const uniIdCo = uniCloud.importObject('uni-id-co', { + customUI: true +}) +// 用户配置的登录方式、是否打开调试模式 +const { + loginTypes, + debug +} = config + +export default async function () { + // 有打开调试模式的情况下 + if (debug) { + // 1. 检查本地uni-id-pages中配置的登录方式,服务器端是否已经配置正确。否则提醒并引导去配置 + // 调用云对象,获取服务端已正确配置的登录方式 + const { + supportedLoginType + } = await uniIdCo.getSupportedLoginType() + console.log('supportedLoginType: ' + JSON.stringify(supportedLoginType)) + // 登录方式,服务端和客户端的映射关系 + const data = { + smsCode: 'mobile-code', + univerify: 'univerify', + username: 'username-password', + weixin: 'weixin', + qq: 'qq', + xiaomi: 'xiaomi', + sinaweibo: 'sinaweibo', + taobao: 'taobao', + facebook: 'facebook', + google: 'google', + alipay: 'alipay', + apple: 'apple', + weixinMobile: 'weixin' + } + // 遍历客户端配置的登录方式,与服务端比对。并在错误时抛出错误提示 + const list = loginTypes.filter(type => !supportedLoginType.includes(data[type])) + if (list.length) { + console.error( + `错误:前端启用的登录方式:${list.join(',')};没有在服务端完成配置。配置文件路径:"/uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json"` + ) + } + } + + // #ifdef APP-PLUS + // 如果uni-id-pages配置的登录功能有一键登录,有则执行预登录(异步) + if (loginTypes.includes('univerify')) { + uni.preLogin({ + provider: 'univerify', + complete: e => { + // console.log(e); + } + }) + } + // #endif + + // 3. 绑定clientDB错误事件 + // clientDB对象 + const db = uniCloud.database() + db.on('error', onDBError) + // clientDB的错误提示 + function onDBError ({ + code, // 错误码详见https://uniapp.dcloud.net.cn/uniCloud/clientdb?id=returnvalue + message + }) { + // console.error('onDBError', {code,message}); + } + // 解绑clientDB错误事件 + // db.off('error', onDBError) + + // 4. 同步客户端push_clientid至device表 + if (uniCloud.onRefreshToken) { + uniCloud.onRefreshToken(() => { + // console.log('onRefreshToken'); + if (uni.getPushClientId) { + uni.getPushClientId({ + success: async function (e) { + // console.log(e) + const pushClientId = e.cid + // console.log(pushClientId); + const res = await uniIdCo.setPushCid({ + pushClientId + }) + // console.log('getPushClientId', res); + }, + fail (e) { + // console.log(e) + } + }) + } + }) + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/package.json b/uni-im示例/uni_modules/uni-id-pages/package.json new file mode 100644 index 0000000..567e19a --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/package.json @@ -0,0 +1,102 @@ +{ + "id": "uni-id-pages", + "displayName": "uni-id-pages", + "version": "1.1.14", + "description": "云端一体简单、统一、可扩展的用户中心页面模版", + "keywords": [ + "用户管理", + "用户中心", + "短信验证码", + "login", + "登录" + ], + "repository": "https://gitcode.net/dcloud/hello_uni-id-pages", + "engines": { + "HBuilderX": "^3.4.17" + }, + "dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "", + "type": "unicloud-template-page" + }, + "uni_modules": { + "dependencies": [ + "uni-captcha", + "uni-config-center", + "uni-data-checkbox", + "uni-easyinput", + "uni-forms", + "uni-icons", + "uni-id-common", + "uni-list", + "uni-load-more", + "uni-popup", + "uni-scss", + "uni-transition", + "uni-open-bridge-common", + "uni-cloud-s2s" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "Vue": { + "vue2": "y", + "vue3": "y" + }, + "App": { + "app-vue": "y", + "app-nvue": "u" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "u", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "u", + "百度": "u", + "字节跳动": "u", + "QQ": "u", + "钉钉": "u", + "快手": "u", + "飞书": "u", + "京东": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + } + } + } + }, + "dependencies": { + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/common/webview/webview.vue b/uni-im示例/uni_modules/uni-id-pages/pages/common/webview/webview.vue new file mode 100644 index 0000000..71ff55c --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/common/webview/webview.vue @@ -0,0 +1,35 @@ + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/login/login-smscode.vue b/uni-im示例/uni_modules/uni-id-pages/pages/login/login-smscode.vue new file mode 100644 index 0000000..f847cc5 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/login/login-smscode.vue @@ -0,0 +1,120 @@ + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/login/login-withoutpwd.vue b/uni-im示例/uni_modules/uni-id-pages/pages/login/login-withoutpwd.vue new file mode 100644 index 0000000..5e88727 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/login/login-withoutpwd.vue @@ -0,0 +1,241 @@ + + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/login/login-withpwd.vue b/uni-im示例/uni_modules/uni-id-pages/pages/login/login-withpwd.vue new file mode 100644 index 0000000..de7e977 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/login/login-withpwd.vue @@ -0,0 +1,176 @@ + + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/register/register-admin.vue b/uni-im示例/uni_modules/uni-id-pages/pages/register/register-admin.vue new file mode 100644 index 0000000..fcaf88a --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/register/register-admin.vue @@ -0,0 +1,178 @@ + + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/register/register-by-email.vue b/uni-im示例/uni_modules/uni-id-pages/pages/register/register-by-email.vue new file mode 100644 index 0000000..6f05c8a --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/register/register-by-email.vue @@ -0,0 +1,216 @@ + + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/register/register.vue b/uni-im示例/uni_modules/uni-id-pages/pages/register/register.vue new file mode 100644 index 0000000..20de1ad --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/register/register.vue @@ -0,0 +1,181 @@ + + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/register/validator.js b/uni-im示例/uni_modules/uni-id-pages/pages/register/validator.js new file mode 100644 index 0000000..9173e3c --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/register/validator.js @@ -0,0 +1,56 @@ +import passwordMod from '@/uni_modules/uni-id-pages/common/password.js' +export default { + "username": { + "rules": [{ + required: true, + errorMessage: '请输入用户名', + }, + { + minLength: 3, + maxLength: 32, + errorMessage: '用户名长度在 {minLength} 到 {maxLength} 个字符', + }, + { + validateFunction: function(rule, value, data, callback) { + // console.log(value); + if (/^1\d{10}$/.test(value) || /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/.test(value)) { + callback('用户名不能是:手机号或邮箱') + }; + if (/^\d+$/.test(value)) { + callback('用户名不能为纯数字') + }; + if(/[\u4E00-\u9FA5\uF900-\uFA2D]{1,}/.test(value)){ + callback('用户名不能包含中文') + } + return true + } + } + ], + "label": "用户名" + }, + "nickname": { + "rules": [{ + minLength: 3, + maxLength: 32, + errorMessage: '昵称长度在 {minLength} 到 {maxLength} 个字符', + }, + { + validateFunction: function(rule, value, data, callback) { + // console.log(value); + if (/^1\d{10}$/.test(value) || /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/.test(value)) { + callback('昵称不能是:手机号或邮箱') + }; + if (/^\d+$/.test(value)) { + callback('昵称不能为纯数字') + }; + if(/[\u4E00-\u9FA5\uF900-\uFA2D]{1,}/.test(value)){ + callback('昵称不能包含中文') + } + return true + } + } + ], + "label": "昵称" + }, + ...passwordMod.getPwdRules() +} diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/retrieve/retrieve-by-email.vue b/uni-im示例/uni_modules/uni-id-pages/pages/retrieve/retrieve-by-email.vue new file mode 100644 index 0000000..b6e7704 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/retrieve/retrieve-by-email.vue @@ -0,0 +1,218 @@ + + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/retrieve/retrieve.vue b/uni-im示例/uni_modules/uni-id-pages/pages/retrieve/retrieve.vue new file mode 100644 index 0000000..6e1976b --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/retrieve/retrieve.vue @@ -0,0 +1,241 @@ + + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/bind-mobile/bind-mobile.vue b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/bind-mobile/bind-mobile.vue new file mode 100644 index 0000000..a6b2b1f --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/bind-mobile/bind-mobile.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/change_pwd/change_pwd.vue b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/change_pwd/change_pwd.vue new file mode 100644 index 0000000..2992623 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/change_pwd/change_pwd.vue @@ -0,0 +1,130 @@ + + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/cropImage.vue b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/cropImage.vue new file mode 100644 index 0000000..2317b9b --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/cropImage.vue @@ -0,0 +1,39 @@ + + + + + \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/README.md b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/README.md new file mode 100644 index 0000000..9219f81 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/README.md @@ -0,0 +1,227 @@ +> 插件来源:[https://ext.dcloud.net.cn/plugin?id=3594](https://ext.dcloud.net.cn/plugin?id=3594) +##### 以下是作者写的插件介绍: + +# Clipper 图片裁剪 +> uniapp 图片裁剪,可用于图片头像等裁剪处理 +> [查看更多](http://liangei.gitee.io/limeui/#/clipper)
+> Q群:458377637 + + +## 平台兼容 + +| H5 | 微信小程序 | 支付宝小程序 | 百度小程序 | 头条小程序 | QQ 小程序 | App | +| --- | ---------- | ------------ | ---------- | ---------- | --------- | --- | +| √ | √ | √ | 未测 | √ | √ | √ | + + +## 代码演示 +### 基本用法 +`@success` 事件点击 👉 **确定** 后会返回生成的图片信息,包含 `url`、`width`、`height` + +```html + + + +``` + +```js +// 非uni_modules引入 +import lClipper from '@/components/lime-clipper/' +// uni_modules引入 +import lClipper from '@/uni_modules/lime-clipper/components/lime-clipper/' +export default { + components: {lClipper}, + data() { + return { + show: false, + url: '', + } + } +} +``` + + +### 传入图片 +`image-url`可传入**相对路径**、**临时路径**、**本地路径**、**网络图片**
+ +* **当为网络地址时** +* H5:👉 需要解决跨域问题。
+* 小程序:👉 需要配置 downloadFile 域名
+ + +```html + + + +``` + +```js +export default { + components: {lClipper}, + data() { + return { + imageUrl: 'https://img12.360buyimg.com/pop/s1180x940_jfs/t1/97205/26/1142/87801/5dbac55aEf795d962/48a4d7a63ff80b8b.jpg', + show: false, + url: '', + } + } +} +``` + + +### 确定按钮颜色 +样式变量名:`--l-clipper-confirm-color` +可放到全局样式的 `page` 里或节点的 `style` +```html + +``` +```css +// css 中为组件设置 CSS 变量 +.clipper { + --l-clipper-confirm-color: linear-gradient(to right, #ff6034, #ee0a24) +} +// 全局 +page { + --l-clipper-confirm-color: linear-gradient(to right, #ff6034, #ee0a24) +} +``` + + +### 使用插槽 +共五个插槽 `cancel` 取消按钮、 `photo` 选择图片按钮、 `rotate` 旋转按钮、 `confirm` 确定按钮和默认插槽。 + +```html + + + + 取消 + 选择图片 + 旋转 + 确定 + + + 显示取消按钮 + + + 显示选择图片按钮 + + + 显示旋转按钮 + + + 显示确定按钮 + + + 锁定裁剪框宽度 + + + 锁定裁剪框高度 + + + 锁定裁剪框比例 + + + 限制移动范围 + + + 禁止缩放 + + + 禁止旋转 + + + + + +``` + +```js +export default { + components: {lClipper}, + data() { + return { + show: false, + url: '', + isLockWidth: false, + isLockHeight: false, + isLockRatio: true, + isLimitMove: false, + isDisableScale: false, + isDisableRotate: false, + isShowCancelBtn: true, + isShowPhotoBtn: true, + isShowRotateBtn: true, + isShowConfirmBtn: true + } + } +} +``` + + +## API + +### Props + +| 参数 | 说明 | 类型 | 默认值 | +| ------------- | ------------ | ---------------- | ------------ | +| image-url | 图片路径 | string | | +| quality | 图片的质量,取值范围为 [0, 1],不在范围内时当作1处理 | number | `1` | +| source | `{album: '从相册中选择'}`key为图片来源类型,value为选项说明 | Object | | +| width | 裁剪框宽度,单位为 `rpx` | number | `400` | +| height | 裁剪框高度 | number | `400` | +| min-width | 裁剪框最小宽度 | number | `200` | +| min-height |裁剪框最小高度 | number | `200` | +| max-width | 裁剪框最大宽度 | number | `600` | +| max-height | 裁剪框最大宽度 | number | `600` | +| min-ratio | 图片最小缩放比 | number | `0.5` | +| max-ratio | 图片最大缩放比 | number | `2` | +| rotate-angle | 旋转按钮每次旋转的角度 | number | `90` | +| scale-ratio | 生成图片相对于裁剪框的比例, **比例越高生成图片越清晰** | number | `1` | +| is-lock-width | 是否锁定裁剪框宽度 | boolean | `false` | +| is-lock-height | 是否锁定裁剪框高度上 | boolean | `false` | +| is-lock-ratio | 是否锁定裁剪框比例 | boolean | `true` | +| is-disable-scale | 是否禁止缩放 | boolean | `false` | +| is-disable-rotate | 是否禁止旋转 | boolean | `false` | +| is-limit-move | 是否限制移动范围 | boolean | `false` | +| is-show-photo-btn | 是否显示选择图片按钮 | boolean | `true` | +| is-show-rotate-btn | 是否显示转按钮 | boolean | `true` | +| is-show-confirm-btn | 是否显示确定按钮 | boolean | `true` | +| is-show-cancel-btn | 是否显示关闭按钮 | boolean | `true` | + + + +### 事件 Events + +| 事件名 | 说明 | 回调 | +| ------- | ------------ | -------------- | +| success | 生成图片成功 | {`width`, `height`, `url`} | +| fail | 生成图片失败 | `error` | +| cancel | 关闭 | `false` | +| ready | 图片加载完成 | {`width`, `height`, `path`, `orientation`, `type`} | +| change | 图片大小改变时触发 | {`width`, `height`} | +| rotate | 图片旋转时触发 | `angle` | + +## 常见问题 +> 1、H5端使用网络图片需要解决跨域问题。
+> 2、小程序使用网络图片需要去公众平台增加下载白名单!二级域名也需要配!
+> 3、H5端生成图片是base64,有时显示只有一半可以使用原生标签``
+> 4、IOS APP 请勿使用HBX2.9.3.20201014的版本!这个版本无法生成图片。
+> 5、APP端无成功反馈、也无失败反馈时,请更新基座和HBX。
+ + +## 打赏 +如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
+![输入图片说明](https://images.gitee.com/uploads/images/2020/1122/222521_bb543f96_518581.jpeg "微信图片编辑_20201122220352.jpg") \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/images/photo.svg b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/images/photo.svg new file mode 100644 index 0000000..7b4b590 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/images/photo.svg @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/images/rotate.svg b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/images/rotate.svg new file mode 100644 index 0000000..0143706 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/images/rotate.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/index.css b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/index.css new file mode 100644 index 0000000..ce542bf --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/index.css @@ -0,0 +1,160 @@ +.flex-auto { + flex: auto; +} +.bg-transparent { + background-color: rgba(0,0,0,0.9); + transition-duration: 0.35s; +} +.l-clipper { + width: 100vw; + height: calc(100vh - var(--window-top)); + background-color: rgba(0,0,0,0.9); + position: fixed; + top: var(--window-top); + left: 0; + z-index: 1; +} +.l-clipper-mask { + position: relative; + z-index: 2; + pointer-events: none; +} +.l-clipper__content { + pointer-events: none; + position: absolute; + border: 1rpx solid rgba(255,255,255,0.3); + box-sizing: border-box; + box-shadow: rgba(0,0,0,0.5) 0 0 0 80vh; + background: transparent; +} +.l-clipper__content::before, +.l-clipper__content::after { + content: ''; + position: absolute; + border: 1rpx dashed rgba(255,255,255,0.3); +} +.l-clipper__content::before { + width: 100%; + top: 33.33%; + height: 33.33%; + border-left: none; + border-right: none; +} +.l-clipper__content::after { + width: 33.33%; + left: 33.33%; + height: 100%; + border-top: none; + border-bottom: none; +} +.l-clipper__edge { + position: absolute; + width: 34rpx; + height: 34rpx; + border: 6rpx solid #fff; + pointer-events: auto; +} +.l-clipper__edge::before { + content: ''; + position: absolute; + width: 40rpx; + height: 40rpx; + background-color: transparent; +} +.l-clipper__edge:nth-child(1) { + left: -6rpx; + top: -6rpx; + border-bottom-width: 0 !important; + border-right-width: 0 !important; +} +.l-clipper__edge:nth-child(1):before { + top: -50%; + left: -50%; +} +.l-clipper__edge:nth-child(2) { + right: -6rpx; + top: -6rpx; + border-bottom-width: 0 !important; + border-left-width: 0 !important; +} +.l-clipper__edge:nth-child(2):before { + top: -50%; + left: 50%; +} +.l-clipper__edge:nth-child(3) { + left: -6rpx; + bottom: -6rpx; + border-top-width: 0 !important; + border-right-width: 0 !important; +} +.l-clipper__edge:nth-child(3):before { + bottom: -50%; + left: -50%; +} +.l-clipper__edge:nth-child(4) { + right: -6rpx; + bottom: -6rpx; + border-top-width: 0 !important; + border-left-width: 0 !important; +} +.l-clipper__edge:nth-child(4):before { + bottom: -50%; + left: 50%; +} +.l-clipper-image { + width: 100%; + border-style: none; + position: absolute; + top: 0; + left: 0; + z-index: 1; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + transform-origin: center; +} +.l-clipper-canvas { + position: fixed; + z-index: 10; + left: -200vw; + top: -200vw; + pointer-events: none; +} +.l-clipper-tools { + position: fixed; + left: 0; + bottom: 10px; + width: 100%; + z-index: 99; + color: #fff; +} +.l-clipper-tools__btns { + font-weight: bold; + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 20rpx 40rpx; + box-sizing: border-box; +} +.l-clipper-tools__btns .cancel { + width: 112rpx; + height: 60rpx; + text-align: center; + line-height: 60rpx; +} +.l-clipper-tools__btns .confirm { + width: 112rpx; + height: 60rpx; + line-height: 60rpx; + background-color: #07c160; + border-radius: 6rpx; + text-align: center; +} +.l-clipper-tools__btns image { + display: block; + width: 60rpx; + height: 60rpx; +} +.l-clipper-tools__btns { + flex-direction: row; +} diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/limeClipper.vue b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/limeClipper.vue new file mode 100644 index 0000000..4fc62b3 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/limeClipper.vue @@ -0,0 +1,820 @@ + + + + + \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/utils.js b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/utils.js new file mode 100644 index 0000000..980c439 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/cropImage/limeClipper/utils.js @@ -0,0 +1,244 @@ +/** + * 判断手指触摸位置 + */ +export function determineDirection(clipX, clipY, clipWidth, clipHeight, currentX, currentY) { + /* + * (右下>>1 右上>>2 左上>>3 左下>>4) + */ + let corner; + /** + * 思路:(利用直角坐标系) + * 1.找出裁剪框中心点 + * 2.如点击坐标在上方点与左方点区域内,则点击为左上角 + * 3.如点击坐标在下方点与右方点区域内,则点击为右下角 + * 4.其他角同理 + */ + const mainPoint = [clipX + clipWidth / 2, clipY + clipHeight / 2]; // 中心点 + const currentPoint = [currentX, currentY]; // 触摸点 + + if (currentPoint[0] <= mainPoint[0] && currentPoint[1] <= mainPoint[1]) { + corner = 3; // 左上 + } else if (currentPoint[0] >= mainPoint[0] && currentPoint[1] <= mainPoint[1]) { + corner = 2; // 右上 + } else if (currentPoint[0] <= mainPoint[0] && currentPoint[1] >= mainPoint[1]) { + corner = 4; // 左下 + } else if (currentPoint[0] >= mainPoint[0] && currentPoint[1] >= mainPoint[1]) { + corner = 1; // 右下 + } + + return corner; +} + +/** + * 图片边缘检测检测时,计算图片偏移量 + */ +export function calcImageOffset(data, scale) { + let left = data.imageLeft; + let top = data.imageTop; + scale = scale || data.scale; + + let imageWidth = data.imageWidth; + let imageHeight = data.imageHeight; + if ((data.angle / 90) % 2) { + imageWidth = data.imageHeight; + imageHeight = data.imageWidth; + } + const { + clipX, + clipWidth, + clipY, + clipHeight + } = data; + + // 当前图片宽度/高度 + const currentImageSize = (size) => (size * scale) / 2; + const currentImageWidth = currentImageSize(imageWidth); + const currentImageHeight = currentImageSize(imageHeight); + + left = clipX + currentImageWidth >= left ? left : clipX + currentImageWidth; + left = clipX + clipWidth - currentImageWidth <= left ? left : clipX + clipWidth - currentImageWidth; + top = clipY + currentImageHeight >= top ? top : clipY + currentImageHeight; + top = clipY + clipHeight - currentImageHeight <= top ? top : clipY + clipHeight - currentImageHeight; + return { + left, + top, + scale + }; +} + +/** + * 图片边缘检测时,计算图片缩放比例 + */ +export function calcImageScale(data, scale) { + scale = scale || data.scale; + let { + imageWidth, + imageHeight, + clipWidth, + clipHeight, + angle + } = data + if ((angle / 90) % 2) { + imageWidth = imageHeight; + imageHeight = imageWidth; + } + if (imageWidth * scale < clipWidth) { + scale = clipWidth / imageWidth; + } + if (imageHeight * scale < clipHeight) { + scale = Math.max(scale, clipHeight / imageHeight); + } + return scale; +} + +/** + * 计算图片尺寸 + */ +export function calcImageSize(width, height, data) { + let imageWidth = width, + imageHeight = height; + let { + clipWidth, + clipHeight, + sysinfo, + width: originWidth, + height: originHeight + } = data + if (imageWidth && imageHeight) { + if (imageWidth / imageHeight > (clipWidth || originWidth) / (clipWidth || originHeight)) { + imageHeight = clipHeight || originHeight; + imageWidth = (width / height) * imageHeight; + } else { + imageWidth = clipWidth || originWidth; + imageHeight = (height / width) * imageWidth; + } + } else { + let sys = sysinfo || uni.getSystemInfoSync(); + imageWidth = sys.windowWidth; + imageHeight = 0; + } + return { + imageWidth, + imageHeight + }; +} + +/** + * 勾股定理求斜边 + */ +export function calcPythagoreanTheorem(width, height) { + return Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)); +} + +/** + * 拖动裁剪框时计算 + */ +export function clipTouchMoveOfCalculate(data, event) { + const clientX = event.touches[0].clientX; + const clientY = event.touches[0].clientY; + + let { + clipWidth, + clipHeight, + clipY: oldClipY, + clipX: oldClipX, + clipStart, + isLockRatio, + maxWidth, + minWidth, + maxHeight, + minHeight + } = data; + maxWidth = maxWidth / 2; + minWidth = minWidth / 2; + minHeight = minHeight / 2; + maxHeight = maxHeight / 2; + + let width = clipWidth, + height = clipHeight, + clipY = oldClipY, + clipX = oldClipX, + // 获取裁剪框实际宽度/高度 + // 如果大于最大值则使用最大值 + // 如果小于最小值则使用最小值 + sizecorrect = () => { + width = width <= maxWidth ? (width >= minWidth ? width : minWidth) : maxWidth; + height = height <= maxHeight ? (height >= minHeight ? height : minHeight) : maxHeight; + }, + sizeinspect = () => { + sizecorrect(); + if ((width > maxWidth || width < minWidth || height > maxHeight || height < minHeight) && isLockRatio) { + return false; + } else { + return true; + } + }; + //if (clipStart.corner) { + height = clipStart.height + (clipStart.corner > 1 && clipStart.corner < 4 ? 1 : -1) * (clipStart.y - clientY); + //} + switch (clipStart.corner) { + case 1: + width = clipStart.width - clipStart.x + clientX; + if (isLockRatio) { + height = width / (clipWidth / clipHeight); + } + if (!sizeinspect()) return; + break; + case 2: + width = clipStart.width - clipStart.x + clientX; + if (isLockRatio) { + height = width / (clipWidth / clipHeight); + } + if (!sizeinspect()) { + return; + } else { + clipY = clipStart.clipY - (height - clipStart.height); + } + + break; + case 3: + width = clipStart.width + clipStart.x - clientX; + if (isLockRatio) { + height = width / (clipWidth / clipHeight); + } + if (!sizeinspect()) { + return; + } else { + clipY = clipStart.clipY - (height - clipStart.height); + clipX = clipStart.clipX - (width - clipStart.width); + } + + break; + case 4: + width = clipStart.width + clipStart.x - clientX; + if (isLockRatio) { + height = width / (clipWidth / clipHeight); + } + if (!sizeinspect()) { + return; + } else { + clipX = clipStart.clipX - (width - clipStart.width); + } + break; + default: + break; + } + return { + width, + height, + clipX, + clipY + }; +} + +/** + * 单指拖动图片计算偏移 + */ +export function imageTouchMoveOfCalcOffset(data, clientXForLeft, clientYForLeft) { + let left = clientXForLeft - data.touchRelative[0].x, + top = clientYForLeft - data.touchRelative[0].y; + return { + left, + top + }; +} diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/deactivate/deactivate.vue b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/deactivate/deactivate.vue new file mode 100644 index 0000000..1b666de --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/deactivate/deactivate.vue @@ -0,0 +1,117 @@ + + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/realname-verify/face-verify-icon.svg b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/realname-verify/face-verify-icon.svg new file mode 100644 index 0000000..df30eb4 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/realname-verify/face-verify-icon.svg @@ -0,0 +1 @@ + diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/realname-verify/realname-verify.vue b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/realname-verify/realname-verify.vue new file mode 100644 index 0000000..001fa5a --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/realname-verify/realname-verify.vue @@ -0,0 +1,315 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/set-pwd/set-pwd.vue b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/set-pwd/set-pwd.vue new file mode 100644 index 0000000..94ed02a --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/set-pwd/set-pwd.vue @@ -0,0 +1,171 @@ + + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/userinfo.vue b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/userinfo.vue new file mode 100644 index 0000000..729484a --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/pages/userinfo/userinfo.vue @@ -0,0 +1,272 @@ + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/readme.md b/uni-im示例/uni_modules/uni-id-pages/readme.md new file mode 100644 index 0000000..1650e45 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/readme.md @@ -0,0 +1,15 @@ +# 文档已移至uni-id-pages文档[https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html) + + + +关于插件更新的说明: + +所有uni_modules,在HBuilderX里点右键都可以直接升级。或者在插件市场导入覆盖。 + +覆盖时HBuilderX会弹出代码差异比对,可以决定接受哪些更改、拒绝哪些更改。 + +当拒绝局部修改时,注意可能产生兼容性问题。 + +你需要二次开发uni-id-pages的前端页面, +- 如果改动不大,那么每次更新uni-id-pages时,在HBuilderX的对比界面对比一下就好 +- 如果改动较大,建议复制一套前端页面到自己工程的pages目录下,pages.json里只引用根目录pages下的页面,不引用uni_modules下的页面。然后每次uni-id-pages更新,你对比下比上一版uni-id-pages改了什么,看你是否需要再合并到你自己的pages里。pages.json里不引用uni_modules里的页面的话,打包时不会把这些页面打包进去,不影响发行后的包体积 \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-id-pages/static/app-plus/apple.png b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/apple.png new file mode 100644 index 0000000..556f686 Binary files /dev/null and b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/apple.png differ diff --git a/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/alipay.png b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/alipay.png new file mode 100644 index 0000000..5256d7a Binary files /dev/null and b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/alipay.png differ diff --git a/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/apple.png b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/apple.png new file mode 100644 index 0000000..99082a7 Binary files /dev/null and b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/apple.png differ diff --git a/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/douyin.png b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/douyin.png new file mode 100644 index 0000000..f218f68 Binary files /dev/null and b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/douyin.png differ diff --git a/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/facebook.png b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/facebook.png new file mode 100644 index 0000000..1331b61 Binary files /dev/null and b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/facebook.png differ diff --git a/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/google.png b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/google.png new file mode 100644 index 0000000..9155046 Binary files /dev/null and b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/google.png differ diff --git a/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/qq.png b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/qq.png new file mode 100644 index 0000000..f170691 Binary files /dev/null and b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/qq.png differ diff --git a/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/sinaweibo.png b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/sinaweibo.png new file mode 100644 index 0000000..12cd038 Binary files /dev/null and b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/sinaweibo.png differ diff --git a/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/taobao.png b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/taobao.png new file mode 100644 index 0000000..1837f71 Binary files /dev/null and b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/taobao.png differ diff --git a/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/univerify.png b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/univerify.png new file mode 100644 index 0000000..aa0b9f5 Binary files /dev/null and b/uni-im示例/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/univerify.png differ diff --git a/uni-im示例/uni_modules/uni-id-pages/static/limeClipper/photo.svg b/uni-im示例/uni_modules/uni-id-pages/static/limeClipper/photo.svg new file mode 100644 index 0000000..7b4b590 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/static/limeClipper/photo.svg @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/static/limeClipper/rotate.svg b/uni-im示例/uni_modules/uni-id-pages/static/limeClipper/rotate.svg new file mode 100644 index 0000000..0143706 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/static/limeClipper/rotate.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/uni-im示例/uni_modules/uni-id-pages/static/login/uni-fab-login/sms.png b/uni-im示例/uni_modules/uni-id-pages/static/login/uni-fab-login/sms.png new file mode 100644 index 0000000..5533743 Binary files /dev/null and b/uni-im示例/uni_modules/uni-id-pages/static/login/uni-fab-login/sms.png differ diff --git a/uni-im示例/uni_modules/uni-id-pages/static/login/uni-fab-login/user.png b/uni-im示例/uni_modules/uni-id-pages/static/login/uni-fab-login/user.png new file mode 100644 index 0000000..268420b Binary files /dev/null and b/uni-im示例/uni_modules/uni-id-pages/static/login/uni-fab-login/user.png differ diff --git a/uni-im示例/uni_modules/uni-id-pages/static/login/uni-fab-login/weixin.png b/uni-im示例/uni_modules/uni-id-pages/static/login/uni-fab-login/weixin.png new file mode 100644 index 0000000..af7175b Binary files /dev/null and b/uni-im示例/uni_modules/uni-id-pages/static/login/uni-fab-login/weixin.png differ diff --git a/uni-im示例/uni_modules/uni-id-pages/static/login/weixin.png b/uni-im示例/uni_modules/uni-id-pages/static/login/weixin.png new file mode 100644 index 0000000..df70aac Binary files /dev/null and b/uni-im示例/uni_modules/uni-id-pages/static/login/weixin.png differ diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/constants.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/constants.js new file mode 100644 index 0000000..afce8b8 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/constants.js @@ -0,0 +1,108 @@ +const db = uniCloud.database() +const dbCmd = db.command +const userCollectionName = 'uni-id-users' +const userCollection = db.collection(userCollectionName) +const verifyCollectionName = 'opendb-verify-codes' +const verifyCollection = db.collection(verifyCollectionName) +const deviceCollectionName = 'uni-id-device' +const deviceCollection = db.collection(deviceCollectionName) +const openDataCollectionName = 'opendb-open-data' +const openDataCollection = db.collection(openDataCollectionName) +const frvLogsCollectionName = 'opendb-frv-logs' +const frvLogsCollection = db.collection(frvLogsCollectionName) + +const USER_IDENTIFIER = { + _id: 'uid', + username: 'username', + mobile: 'mobile', + email: 'email', + wx_unionid: 'wechat-account', + 'wx_openid.app': 'wechat-account', + 'wx_openid.mp': 'wechat-account', + 'wx_openid.h5': 'wechat-account', + 'wx_openid.web': 'wechat-account', + qq_unionid: 'qq-account', + 'qq_openid.app': 'qq-account', + 'qq_openid.mp': 'qq-account', + ali_openid: 'alipay-account', + apple_openid: 'alipay-account', + identities: 'idp' +} + +const USER_STATUS = { + NORMAL: 0, + BANNED: 1, + AUDITING: 2, + AUDIT_FAILED: 3, + CLOSED: 4 +} + +const CAPTCHA_SCENE = { + REGISTER: 'register', + LOGIN_BY_PWD: 'login-by-pwd', + LOGIN_BY_SMS: 'login-by-sms', + RESET_PWD_BY_SMS: 'reset-pwd-by-sms', + RESET_PWD_BY_EMAIL: 'reset-pwd-by-email', + SEND_SMS_CODE: 'send-sms-code', + SEND_EMAIL_CODE: 'send-email-code', + BIND_MOBILE_BY_SMS: 'bind-mobile-by-sms', + SET_PWD_BY_SMS: 'set-pwd-by-sms' +} + +const LOG_TYPE = { + LOGOUT: 'logout', + LOGIN: 'login', + REGISTER: 'register', + RESET_PWD_BY_SMS: 'reset-pwd', + RESET_PWD_BY_EMAIL: 'reset-pwd', + BIND_MOBILE: 'bind-mobile', + BIND_WEIXIN: 'bind-weixin', + BIND_QQ: 'bind-qq', + BIND_APPLE: 'bind-apple', + BIND_ALIPAY: 'bind-alipay', + UNBIND_WEIXIN: 'unbind-weixin', + UNBIND_QQ: 'unbind-qq', + UNBIND_ALIPAY: 'unbind-alipay', + UNBIND_APPLE: 'unbind-apple' +} + +const SMS_SCENE = { + LOGIN_BY_SMS: 'login-by-sms', + RESET_PWD_BY_SMS: 'reset-pwd-by-sms', + BIND_MOBILE_BY_SMS: 'bind-mobile-by-sms', + SET_PWD_BY_SMS: 'set-pwd-by-sms' +} + +const EMAIL_SCENE = { + REGISTER: 'register', + LOGIN_BY_EMAIL: 'login-by-email', + RESET_PWD_BY_EMAIL: 'reset-pwd-by-email', + BIND_EMAIL: 'bind-email' +} + +const REAL_NAME_STATUS = { + NOT_CERTIFIED: 0, + WAITING_CERTIFIED: 1, + CERTIFIED: 2, + CERTIFY_FAILED: 3 +} + +const EXTERNAL_DIRECT_CONNECT_PROVIDER = 'externalDirectConnect' + +module.exports = { + db, + dbCmd, + userCollection, + verifyCollection, + deviceCollection, + openDataCollection, + frvLogsCollection, + USER_IDENTIFIER, + USER_STATUS, + CAPTCHA_SCENE, + LOG_TYPE, + SMS_SCENE, + EMAIL_SCENE, + REAL_NAME_STATUS, + EXTERNAL_DIRECT_CONNECT_PROVIDER +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/error.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/error.js new file mode 100644 index 0000000..1e33845 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/error.js @@ -0,0 +1,70 @@ +const ERROR = { + ACCOUNT_EXISTS: 'uni-id-account-exists', + ACCOUNT_NOT_EXISTS: 'uni-id-account-not-exists', + ACCOUNT_NOT_EXISTS_IN_CURRENT_APP: 'uni-id-account-not-exists-in-current-app', + ACCOUNT_CONFLICT: 'uni-id-account-conflict', + ACCOUNT_BANNED: 'uni-id-account-banned', + ACCOUNT_AUDITING: 'uni-id-account-auditing', + ACCOUNT_AUDIT_FAILED: 'uni-id-account-audit-failed', + ACCOUNT_CLOSED: 'uni-id-account-closed', + CAPTCHA_REQUIRED: 'uni-id-captcha-required', + PASSWORD_ERROR: 'uni-id-password-error', + PASSWORD_ERROR_EXCEED_LIMIT: 'uni-id-password-error-exceed-limit', + INVALID_USERNAME: 'uni-id-invalid-username', + INVALID_PASSWORD: 'uni-id-invalid-password', + INVALID_PASSWORD_SUPER: 'uni-id-invalid-password-super', + INVALID_PASSWORD_STRONG: 'uni-id-invalid-password-strong', + INVALID_PASSWORD_MEDIUM: 'uni-id-invalid-password-medium', + INVALID_PASSWORD_WEAK: 'uni-id-invalid-password-weak', + INVALID_MOBILE: 'uni-id-invalid-mobile', + INVALID_EMAIL: 'uni-id-invalid-email', + INVALID_NICKNAME: 'uni-id-invalid-nickname', + INVALID_PARAM: 'uni-id-invalid-param', + PARAM_REQUIRED: 'uni-id-param-required', + GET_THIRD_PARTY_ACCOUNT_FAILED: 'uni-id-get-third-party-account-failed', + GET_THIRD_PARTY_USER_INFO_FAILED: 'uni-id-get-third-party-user-info-failed', + MOBILE_VERIFY_CODE_ERROR: 'uni-id-mobile-verify-code-error', + EMAIL_VERIFY_CODE_ERROR: 'uni-id-email-verify-code-error', + ADMIN_EXISTS: 'uni-id-admin-exists', + PERMISSION_ERROR: 'uni-id-permission-error', + SYSTEM_ERROR: 'uni-id-system-error', + SET_INVITE_CODE_FAILED: 'uni-id-set-invite-code-failed', + INVALID_INVITE_CODE: 'uni-id-invalid-invite-code', + CHANGE_INVITER_FORBIDDEN: 'uni-id-change-inviter-forbidden', + BIND_CONFLICT: 'uni-id-bind-conflict', + UNBIND_FAIL: 'uni-id-unbind-failed', + UNBIND_NOT_SUPPORTED: 'uni-id-unbind-not-supported', + UNBIND_UNIQUE_LOGIN: 'uni-id-unbind-unique-login', + UNBIND_PASSWORD_NOT_EXISTS: 'uni-id-unbind-password-not-exists', + UNBIND_MOBILE_NOT_EXISTS: 'uni-id-unbind-mobile-not-exists', + UNSUPPORTED_REQUEST: 'uni-id-unsupported-request', + ILLEGAL_REQUEST: 'uni-id-illegal-request', + CONFIG_FIELD_REQUIRED: 'uni-id-config-field-required', + CONFIG_FIELD_INVALID: 'uni-id-config-field-invalid', + FRV_FAIL: 'uni-id-frv-fail', + FRV_PROCESSING: 'uni-id-frv-processing', + REAL_NAME_VERIFIED: 'uni-id-realname-verified', + ID_CARD_EXISTS: 'uni-id-idcard-exists', + INVALID_ID_CARD: 'uni-id-invalid-idcard', + INVALID_REAL_NAME: 'uni-id-invalid-realname', + UNKNOWN_ERROR: 'uni-id-unknown-error', + REAL_NAME_VERIFY_UPPER_LIMIT: 'uni-id-realname-verify-upper-limit' +} + +function isUniIdError (errCode) { + return Object.values(ERROR).includes(errCode) +} + +class UniCloudError extends Error { + constructor (options) { + super(options.message) + this.errMsg = options.message || '' + this.errCode = options.code + } +} + +module.exports = { + ERROR, + isUniIdError, + UniCloudError +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/sensitive-aes-cipher.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/sensitive-aes-cipher.js new file mode 100644 index 0000000..b2d6d95 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/sensitive-aes-cipher.js @@ -0,0 +1,64 @@ +const crypto = require('crypto') +const { ERROR } = require('./error') + +function checkSecret (secret) { + if (!secret) { + throw { + errCode: ERROR.CONFIG_FIELD_REQUIRED, + errMsgValue: { + field: 'sensitiveInfoEncryptSecret' + } + } + } + + if (secret.length !== 32) { + throw { + errCode: ERROR.CONFIG_FIELD_INVALID, + errMsgValue: { + field: 'sensitiveInfoEncryptSecret' + } + } + } +} +function encryptData (text = '') { + if (!text) return text + + const encryptSecret = this.config.sensitiveInfoEncryptSecret + + checkSecret(encryptSecret) + + const iv = encryptSecret.slice(-16) + + const cipher = crypto.createCipheriv('aes-256-cbc', encryptSecret, iv) + + const encrypted = Buffer.concat([ + cipher.update(Buffer.from(text, 'utf-8')), + cipher.final() + ]) + + return encrypted.toString('base64') +} + +function decryptData (text = '') { + if (!text) return text + + const encryptSecret = this.config.sensitiveInfoEncryptSecret + + checkSecret(encryptSecret) + + const iv = encryptSecret.slice(-16) + + const cipher = crypto.createDecipheriv('aes-256-cbc', encryptSecret, iv) + + const decrypted = Buffer.concat([ + cipher.update(Buffer.from(text, 'base64')), + cipher.final() + ]) + + return decrypted.toString('utf-8') +} + +module.exports = { + encryptData, + decryptData +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/universal.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/universal.js new file mode 100644 index 0000000..4bf46a0 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/universal.js @@ -0,0 +1,47 @@ +const { ERROR } = require('./error') + +function getHttpClientInfo () { + const requestId = this.getUniCloudRequestId() + const { clientIP, userAgent, source, secretType = 'none' } = this.getClientInfo() + const { clientInfo = {} } = JSON.parse(this.getHttpInfo().body) + + return { + ...clientInfo, + clientIP, + userAgent, + source, + secretType, + requestId + } +} + +function getHttpUniIdToken () { + const { uniIdToken = '' } = JSON.parse(this.getHttpInfo().body) + + return uniIdToken +} + +function verifyHttpMethod () { + const { headers, httpMethod } = this.getHttpInfo() + + if (!/^application\/json/.test(headers['content-type']) || httpMethod.toUpperCase() !== 'POST') { + throw { + errCode: ERROR.UNSUPPORTED_REQUEST, + errMsg: 'unsupported request' + } + } +} + +function universal () { + if (this.getClientInfo().source === 'http') { + verifyHttpMethod.call(this) + this.getParams()[0] = JSON.parse(this.getHttpInfo().body).params + this.getUniversalClientInfo = getHttpClientInfo.bind(this) + this.getUniversalUniIdToken = getHttpUniIdToken.bind(this) + } else { + this.getUniversalClientInfo = this.getClientInfo + this.getUniversalUniIdToken = this.getUniIdToken + } +} + +module.exports = universal diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/utils.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/utils.js new file mode 100644 index 0000000..11dc6e7 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/utils.js @@ -0,0 +1,270 @@ +function batchFindObjctValue (obj = {}, keys = []) { + const values = {} + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + const keyPath = key.split('.') + let currentKey = keyPath.shift() + let result = obj + while (currentKey) { + if (!result) { + break + } + result = result[currentKey] + currentKey = keyPath.shift() + } + values[key] = result + } + return values +} + +function getType (val) { + return Object.prototype.toString.call(val).slice(8, -1).toLowerCase() +} + +function hasOwn (obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key) +} + +function isValidString (val) { + return val && getType(val) === 'string' +} + +function isPlainObject (obj) { + return getType(obj) === 'object' +} + +function isFn (fn) { + // 务必注意AsyncFunction + return typeof fn === 'function' +} + +// 获取文件后缀,只添加几种图片类型供客服消息接口使用 +const mime2ext = { + 'image/png': 'png', + 'image/jpeg': 'jpg', + 'image/gif': 'gif', + 'image/svg+xml': 'svg', + 'image/bmp': 'bmp', + 'image/webp': 'webp' +} + +function getExtension (contentType) { + return mime2ext[contentType] +} + +const isSnakeCase = /_(\w)/g +const isCamelCase = /[A-Z]/g + +function snake2camel (value) { + return value.replace(isSnakeCase, (_, c) => (c ? c.toUpperCase() : '')) +} + +function camel2snake (value) { + return value.replace(isCamelCase, str => '_' + str.toLowerCase()) +} + +function parseObjectKeys (obj, type) { + let parserReg, parser + switch (type) { + case 'snake2camel': + parser = snake2camel + parserReg = isSnakeCase + break + case 'camel2snake': + parser = camel2snake + parserReg = isCamelCase + break + } + for (const key in obj) { + if (hasOwn(obj, key)) { + if (parserReg.test(key)) { + const keyCopy = parser(key) + obj[keyCopy] = obj[key] + delete obj[key] + if (isPlainObject(obj[keyCopy])) { + obj[keyCopy] = parseObjectKeys(obj[keyCopy], type) + } else if (Array.isArray(obj[keyCopy])) { + obj[keyCopy] = obj[keyCopy].map((item) => { + return parseObjectKeys(item, type) + }) + } + } + } + } + return obj +} + +function snake2camelJson (obj) { + return parseObjectKeys(obj, 'snake2camel') +} + +function camel2snakeJson (obj) { + return parseObjectKeys(obj, 'camel2snake') +} + +function getOffsetDate (offset) { + return new Date( + Date.now() + (new Date().getTimezoneOffset() + (offset || 0) * 60) * 60000 + ) +} + +function getDateStr (date, separator = '-') { + date = date || new Date() + const dateArr = [] + dateArr.push(date.getFullYear()) + dateArr.push(('00' + (date.getMonth() + 1)).substr(-2)) + dateArr.push(('00' + date.getDate()).substr(-2)) + return dateArr.join(separator) +} + +function getTimeStr (date, separator = ':') { + date = date || new Date() + const timeArr = [] + timeArr.push(('00' + date.getHours()).substr(-2)) + timeArr.push(('00' + date.getMinutes()).substr(-2)) + timeArr.push(('00' + date.getSeconds()).substr(-2)) + return timeArr.join(separator) +} + +function getFullTimeStr (date) { + date = date || new Date() + return getDateStr(date) + ' ' + getTimeStr(date) +} + +function getDistinctArray (arr) { + return Array.from(new Set(arr)) +} + +/** + * 拼接url + * @param {string} base 基础路径 + * @param {string} path 在基础路径上拼接的路径 + * @returns + */ +function resolveUrl (base, path) { + if (/^https?:/.test(path)) { + return path + } + return base + path +} + +function getVerifyCode (len = 6) { + let code = '' + for (let i = 0; i < len; i++) { + code += Math.floor(Math.random() * 10) + } + return code +} + +function coverMobile (mobile) { + if (typeof mobile !== 'string') { + return mobile + } + return mobile.slice(0, 3) + '****' + mobile.slice(7) +} + +function getNonceStr (length = 16) { + let str = '' + while (str.length < length) { + str += Math.random().toString(32).substring(2) + } + return str.substring(0, length) +} + +try { + require('lodash.merge') +} catch (error) { + console.error('uni-id-co缺少依赖,请在uniCloud/cloudfunctions/uni-id-co目录执行 npm install 安装依赖') + throw error +} + +function isMatchUserApp (userAppList, matchAppList) { + if (userAppList === undefined || userAppList === null) { + return true + } + if (getType(userAppList) !== 'array') { + return false + } + if (userAppList.includes('*')) { + return true + } + if (getType(matchAppList) === 'string') { + matchAppList = [matchAppList] + } + return userAppList.some(item => matchAppList.includes(item)) +} + +function checkIdCard (idCardNumber) { + if (!idCardNumber || typeof idCardNumber !== 'string' || idCardNumber.length !== 18) return false + + const coefficient = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] + const checkCode = [1, 0, 'x', 9, 8, 7, 6, 5, 4, 3, 2] + const code = idCardNumber.substring(17) + + let sum = 0 + for (let i = 0; i < 17; i++) { + sum += Number(idCardNumber.charAt(i)) * coefficient[i] + } + + return checkCode[sum % 11].toString() === code.toLowerCase() +} + +function catchAwait (fn, finallyFn) { + if (!fn) return [new Error('no function')] + + if (Promise.prototype.finally === undefined) { + // eslint-disable-next-line no-extend-native + Promise.prototype.finally = function (finallyFn) { + return this.then( + res => Promise.resolve(finallyFn()).then(() => res), + error => Promise.resolve(finallyFn()).then(() => { throw error }) + ) + } + } + + return fn + .then((data) => [undefined, data]) + .catch((error) => [error]) + .finally(() => typeof finallyFn === 'function' && finallyFn()) +} + +function dataDesensitization (value = '', options = {}) { + const { onlyLast = false } = options + const [firstIndex, middleIndex, lastIndex] = onlyLast ? [0, 0, -1] : [0, 1, -1] + + if (!value) return value + const first = value.slice(firstIndex, middleIndex) + const middle = value.slice(middleIndex, lastIndex) + const last = value.slice(lastIndex) + const star = Array.from(new Array(middle.length), (v) => '*').join('') + + return first + star + last +} + +function getCurrentDateTimestamp (date = Date.now(), targetTimezone = 8) { + const oneHour = 60 * 60 * 1000 + return parseInt((date + targetTimezone * oneHour) / (24 * oneHour)) * (24 * oneHour) - targetTimezone * oneHour +} + +module.exports = { + getType, + isValidString, + batchFindObjctValue, + isPlainObject, + isFn, + getDistinctArray, + getFullTimeStr, + resolveUrl, + getOffsetDate, + camel2snakeJson, + snake2camelJson, + getExtension, + getVerifyCode, + coverMobile, + getNonceStr, + isMatchUserApp, + checkIdCard, + catchAwait, + dataDesensitization, + getCurrentDateTimestamp +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/validator.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/validator.js new file mode 100644 index 0000000..e14600c --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/common/validator.js @@ -0,0 +1,439 @@ +const { + isValidString, + getType +} = require('./utils.js') +const { + ERROR +} = require('./error') + +const baseValidator = Object.create(null) + +baseValidator.username = function (username) { + const errCode = ERROR.INVALID_USERNAME + if (!isValidString(username)) { + return { + errCode + } + } + if (/^\d+$/.test(username)) { + // 用户名不能为纯数字 + return { + errCode + } + }; + if (!/^[a-zA-Z0-9_-]+$/.test(username)) { + // 用户名仅能使用数字、字母、“_”及“-” + return { + errCode + } + } +} + +baseValidator.password = function (password) { + const errCode = ERROR.INVALID_PASSWORD + if (!isValidString(password)) { + return { + errCode + } + } + if (password.length < 6) { + // 密码长度不能小于6 + return { + errCode + } + } +} + +baseValidator.mobile = function (mobile) { + const errCode = ERROR.INVALID_MOBILE + if (getType(mobile) !== 'string') { + return { + errCode + } + } + if (mobile && !/^1\d{10}$/.test(mobile)) { + return { + errCode + } + } +} + +baseValidator.email = function (email) { + const errCode = ERROR.INVALID_EMAIL + if (getType(email) !== 'string') { + return { + errCode + } + } + if (email && !/@/.test(email)) { + return { + errCode + } + } +} + +baseValidator.nickname = function (nickname) { + const errCode = ERROR.INVALID_NICKNAME + if (nickname.indexOf('@') !== -1) { + // 昵称不允许含@ + return { + errCode + } + }; + if (/^\d+$/.test(nickname)) { + // 昵称不能为纯数字 + return { + errCode + } + }; + if (nickname.length > 100) { + // 昵称不可超过100字符 + return { + errCode + } + } +} + +const baseType = ['string', 'boolean', 'number', 'null'] // undefined不会由客户端提交上来 + +baseType.forEach((type) => { + baseValidator[type] = function (val) { + if (getType(val) === type) { + return + } + return { + errCode: ERROR.INVALID_PARAM + } + } +}) + +function tokenize(name) { + let i = 0 + const result = [] + let token = '' + while (i < name.length) { + const char = name[i] + switch (char) { + case '|': + case '<': + case '>': + token && result.push(token) + result.push(char) + token = '' + break + default: + token += char + break + } + i++ + if (i === name.length && token) { + result.push(token) + } + } + return result +} + +/** + * 处理validator名 + * @param {string} name + */ +function parseValidatorName(name) { + const tokenList = tokenize(name) + let i = 0 + let currentToken = tokenList[i] + const result = { + type: 'root', + children: [], + parent: null + } + let lastRealm = result + while (currentToken) { + switch (currentToken) { + case 'array': { + const currentRealm = { + type: 'array', + children: [], + parent: lastRealm + } + lastRealm.children.push(currentRealm) + lastRealm = currentRealm + break + } + case '<': + if (lastRealm.type !== 'array') { + throw new Error('Invalid validator token "<"') + } + break + case '>': + if (lastRealm.type !== 'array') { + throw new Error('Invalid validator token ">"') + } + lastRealm = lastRealm.parent + break + case '|': + if (lastRealm.type !== 'array' && lastRealm.type !== 'root') { + throw new Error('Invalid validator token "|"') + } + break + default: + lastRealm.children.push({ + type: currentToken + }) + break + } + i++ + currentToken = tokenList[i] + } + return result +} + +function getRuleCategory(rule) { + switch (rule.type) { + case 'array': + return 'array' + case 'root': + return 'root' + default: + return 'base' + } +} + +function isMatchUnionType(val, rule) { + if (!rule.children || rule.children.length === 0) { + return true + } + const children = rule.children + for (let i = 0; i < children.length; i++) { + const child = children[i] + const category = getRuleCategory(child) + let pass = false + switch (category) { + case 'base': + pass = isMatchBaseType(val, child) + break + case 'array': + pass = isMatchArrayType(val, child) + break + default: + break + } + if (pass) { + return true + } + } + return false +} + +function isMatchBaseType(val, rule) { + if (typeof baseValidator[rule.type] !== 'function') { + throw new Error(`invalid schema type: ${rule.type}`) + } + const validateRes = baseValidator[rule.type](val) + if (validateRes && validateRes.errCode) { + return false + } + return true +} + +function isMatchArrayType(arr, rule) { + if (getType(arr) !== 'array') { + return false + } + if (rule.children && rule.children.length && arr.some(item => !isMatchUnionType(item, rule))) { + return false + } + return true +} + +// 特殊符号 https://www.ibm.com/support/pages/password-strength-rules ~!@#$%^&*_-+=`|\(){}[]:;"'<>,.?/ +// const specialChar = '~!@#$%^&*_-+=`|\(){}[]:;"\'<>,.?/' +// const specialCharRegExp = /^[~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]$/ +// for (let i = 0, arr = specialChar.split(''); i < arr.length; i++) { +// const char = arr[i] +// if (!specialCharRegExp.test(char)) { +// throw new Error('check special character error: ' + char) +// } +// } + +// 密码强度表达式 +const passwordRules = { + // 密码必须包含大小写字母、数字和特殊符号 + super: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/, + // 密码必须包含字母、数字和特殊符号 + strong: /^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/, + // 密码必须为字母、数字和特殊符号任意两种的组合 + medium: /^(?![0-9]+$)(?![a-zA-Z]+$)(?![~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]+$)[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/, + // 密码必须包含字母和数字 + weak: /^(?=.*[0-9])(?=.*[a-zA-Z])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{6,16}$/, + +} + +function createPasswordVerifier({ + passwordStrength = '' +} = {}) { + return function (password) { + const passwordRegExp = passwordRules[passwordStrength] + if (!passwordRegExp) { + throw new Error('Invalid password strength config: ' + passwordStrength) + } + const errCode = ERROR.INVALID_PASSWORD + if (!isValidString(password)) { + return { + errCode + } + } + if (!passwordRegExp.test(password)) { + return { + errCode: errCode + '-' + passwordStrength + } + } + } +} + +function isEmpty(value) { + return value === undefined || + value === null || + (typeof value === 'string' && value.trim() === '') +} + +class Validator { + constructor({ + passwordStrength = '' + } = {}) { + this.baseValidator = baseValidator + this.customValidator = Object.create(null) + if (passwordStrength) { + this.mixin( + 'password', + createPasswordVerifier({ + passwordStrength + }) + ) + } + } + + mixin(type, handler) { + this.customValidator[type] = handler + } + + getRealBaseValidator(type) { + return this.customValidator[type] || this.baseValidator[type] + } + + get validator() { + return new Proxy({}, { + get: (_, prop) => { + if (typeof prop !== 'string') { + return + } + const realBaseValidator = this.getRealBaseValidator(prop) + if (realBaseValidator) { + return realBaseValidator + } + const rule = parseValidatorName(prop) + return function (val) { + if (!isMatchUnionType(val, rule)) { + return { + errCode: ERROR.INVALID_PARAM + } + } + } + } + }) + } + + validate(value = {}, schema = {}) { + for (const schemaKey in schema) { + let schemaValue = schema[schemaKey] + if (getType(schemaValue) === 'string') { + schemaValue = { + required: true, + type: schemaValue + } + } + const { + required, + type + } = schemaValue + // value内未传入了schemaKey或对应值为undefined + if (isEmpty(value[schemaKey])) { + if (required) { + return { + errCode: ERROR.PARAM_REQUIRED, + errMsgValue: { + param: schemaKey + }, + schemaKey + } + } else { + //delete value[schemaKey] + continue + } + } + const validateMethod = this.validator[type] + if (!validateMethod) { + throw new Error(`invalid schema type: ${type}`) + } + const validateRes = validateMethod(value[schemaKey]) + if (validateRes) { + validateRes.schemaKey = schemaKey + return validateRes + } + } + } +} + +function checkClientInfo(clientInfo) { + const stringNotRequired = { + required: false, + type: 'string' + } + const numberNotRequired = { + required: false, + type: 'number' + } + const numberOrStringNotRequired = { + required: false, + type: 'number|string' + } + const schema = { + uniPlatform: 'string', + appId: 'string', + deviceId: stringNotRequired, + osName: stringNotRequired, + locale: stringNotRequired, + clientIP: stringNotRequired, + appName: stringNotRequired, + appVersion: stringNotRequired, + appVersionCode: numberOrStringNotRequired, + channel: numberOrStringNotRequired, + userAgent: stringNotRequired, + uniIdToken: stringNotRequired, + deviceBrand: stringNotRequired, + deviceModel: stringNotRequired, + osVersion: stringNotRequired, + osLanguage: stringNotRequired, + osTheme: stringNotRequired, + romName: stringNotRequired, + romVersion: stringNotRequired, + devicePixelRatio: numberNotRequired, + windowWidth: numberNotRequired, + windowHeight: numberNotRequired, + screenWidth: numberNotRequired, + screenHeight: numberNotRequired + } + const validateRes = new Validator().validate(clientInfo, schema) + if (validateRes) { + if (validateRes.errCode === ERROR.PARAM_REQUIRED) { + console.warn('- 如果使用HBuilderX运行本地云函数/云对象功能时出现此提示,请改为使用客户端调用本地云函数方式调试,或更新HBuilderX到3.4.12及以上版本。\n- 如果是缺少clientInfo.appId,请检查项目manifest.json内是否配置了DCloud AppId') + throw new Error(`"clientInfo.${validateRes.schemaKey}" is required.`) + } else { + throw new Error(`Invalid client info: clienInfo.${validateRes.schemaKey}`) + } + } +} + +module.exports = { + Validator, + checkClientInfo +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/config/permission.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/config/permission.js new file mode 100644 index 0000000..229a264 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/config/permission.js @@ -0,0 +1,90 @@ +// 各接口权限配置,未配置接口表示允许任何用户访问(包括未登录用户) +module.exports = { + // 管理接口 + addUser: { + // auth: true // 已登录用户方可操作,配置角色或权限时此项可不写 + role: ['admin'] // 允许进行此操作的角色,包含任一角色均可操作。 + // permission: [] // 允许进行此操作的权限,包含任一权限均可操作。 + // 权限角色均配置时,用户拥有任一权限或任一角色均可操作 + }, + updateUser: { + role: ['admin'] + }, + authorizeAppLogin: { + role: ['admin'] + }, + removeAuthorizedApp: { + role: ['admin'] + }, + setAuthorizedApp: { + role: ['admin'] + }, + + // 用户接口 + closeAccount: { + auth: true + }, + updatePwd: { + auth: true + }, + logout: { + auth: true + }, + bindMobileBySms: { + auth: true + }, + bindMobileByUniverify: { + auth: true + }, + bindMobileByMpWeixin: { + auth: true + }, + bindAlipay: { + auth: true + }, + bindApple: { + auth: true + }, + bindQQ: { + auth: true + }, + bindWeixin: { + auth: true + }, + acceptInvite: { + auth: true + }, + getInvitedUser: { + auth: true + }, + setPushCid: { + auth: true + }, + getAccountInfo: { + auth: true + }, + unbindWeixin: { + auth: true + }, + unbindAlipay: { + auth: true + }, + unbindQQ: { + auth: true + }, + unbindApple: { + auth: true + }, + setPwd: { + auth: true + }, + getFrvCertifyId: { + auth: true + }, + getFrvAuthResult: { + auth: true + }, + getRealNameInfo: { + auth: true + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/index.obj.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/index.obj.js new file mode 100644 index 0000000..147432c --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/index.obj.js @@ -0,0 +1,694 @@ +const uniIdCommon = require('uni-id-common') +const uniCaptcha = require('uni-captcha') +const { + getType, + checkIdCard +} = require('./common/utils') +const { + checkClientInfo, + Validator +} = require('./common/validator') +const ConfigUtils = require('./lib/utils/config') +const { + isUniIdError, + ERROR +} = require('./common/error') +const middleware = require('./middleware/index') +const universal = require('./common/universal') + +const { + registerAdmin, + registerUser, + registerUserByEmail +} = require('./module/register/index') +const { + addUser, + updateUser +} = require('./module/admin/index') +const { + login, + loginBySms, + loginByUniverify, + loginByWeixin, + loginByAlipay, + loginByQQ, + loginByApple, + loginByWeixinMobile +} = require('./module/login/index') +const { + logout +} = require('./module/logout/index') +const { + bindMobileBySms, + bindMobileByUniverify, + bindMobileByMpWeixin, + bindAlipay, + bindApple, + bindQQ, + bindWeixin, + unbindWeixin, + unbindAlipay, + unbindQQ, + unbindApple +} = require('./module/relate/index') +const { + setPwd, + updatePwd, + resetPwdBySms, + resetPwdByEmail, + closeAccount, + getAccountInfo, + getRealNameInfo +} = require('./module/account/index') +const { + createCaptcha, + refreshCaptcha, + sendSmsCode, + sendEmailCode +} = require('./module/verify/index') +const { + refreshToken, + setPushCid, + secureNetworkHandshakeByWeixin +} = require('./module/utils/index') +const { + getInvitedUser, + acceptInvite +} = require('./module/fission') +const { + authorizeAppLogin, + removeAuthorizedApp, + setAuthorizedApp +} = require('./module/multi-end') +const { + getSupportedLoginType +} = require('./module/dev/index') +const { + externalRegister, + externalLogin, + updateUserInfoByExternal +} = require('./module/external') +const { + getFrvCertifyId, + getFrvAuthResult +} = require('./module/facial-recognition-verify') + +module.exports = { + async _before () { + // 支持 callFunction 与 URL化 + universal.call(this) + + const clientInfo = this.getUniversalClientInfo() + /** + * 检查clientInfo,无appId和uniPlatform时本云对象无法正常运行 + * 此外需要保证用到的clientInfo字段均经过类型检查 + * clientInfo由客户端上传并非完全可信,clientInfo内除clientIP、userAgent、source外均为客户端上传参数 + * 否则可能会出现一些意料外的情况 + */ + checkClientInfo(clientInfo) + let clientPlatform = clientInfo.uniPlatform + // 统一platform名称 + switch (clientPlatform) { + case 'app': + case 'app-plus': + clientPlatform = 'app' + break + case 'web': + case 'h5': + clientPlatform = 'web' + break + default: + break + } + + this.clientPlatform = clientPlatform + + // 挂载uni-id实例到this上,方便后续调用 + this.uniIdCommon = uniIdCommon.createInstance({ + clientInfo + }) + + // 包含uni-id配置合并等功能的工具集 + this.configUtils = new ConfigUtils({ + context: this + }) + this.config = this.configUtils.getPlatformConfig() + this.hooks = this.configUtils.getHooks() + + this.validator = new Validator({ + passwordStrength: this.config.passwordStrength + }) + + // 扩展 validator 增加 验证身份证号码合法性 + this.validator.mixin('idCard', function (idCard) { + if (!checkIdCard(idCard)) { + return { + errCode: ERROR.INVALID_ID_CARD + } + } + }) + this.validator.mixin('realName', function (realName) { + if ( + typeof realName !== 'string' || + realName.length < 2 || + !/^[\u4e00-\u9fa5]{1,10}(·?[\u4e00-\u9fa5]{1,10}){0,5}$/.test(realName) + ) { + return { + errCode: ERROR.INVALID_REAL_NAME + } + } + }) + /** + * 示例:覆盖密码验证规则 + */ + // this.validator.mixin('password', function (password) { + // if (typeof password !== 'string' || password.length < 10) { + // // 调整为密码长度不能小于10 + // return { + // errCode: ERROR.INVALID_PASSWORD + // } + // } + // }) + /** + * 示例:新增验证规则 + */ + // this.validator.mixin('timestamp', function (timestamp) { + // if (typeof timestamp !== 'number' || timestamp > Date.now()) { + // return { + // errCode: ERROR.INVALID_PARAM + // } + // } + // }) + // // 新增规则同样可以在数组验证规则中使用 + // this.validator.valdate({ + // timestamp: 123456789 + // }, { + // timestamp: 'timestamp' + // }) + // this.validator.valdate({ + // timestampList: [123456789, 123123123123] + // }, { + // timestampList: 'array' + // }) + // // 甚至更复杂的写法 + // this.validator.valdate({ + // timestamp: [123456789, 123123123123] + // }, { + // timestamp: 'timestamp|array' + // }) + + // 挂载uni-captcha到this上,方便后续调用 + this.uniCaptcha = uniCaptcha + Object.defineProperty(this, 'uniOpenBridge', { + get () { + return require('uni-open-bridge-common') + } + }) + + // 挂载中间件 + this.middleware = {} + for (const mwName in middleware) { + this.middleware[mwName] = middleware[mwName].bind(this) + } + + // 国际化 + const messages = require('./lang/index') + const fallbackLocale = 'zh-Hans' + const i18n = uniCloud.initI18n({ + locale: clientInfo.locale, + fallbackLocale, + messages: JSON.parse(JSON.stringify(messages)) + }) + if (!messages[i18n.locale]) { + i18n.setLocale(fallbackLocale) + } + this.t = i18n.t.bind(i18n) + + this.response = {} + + // 请求鉴权验证 + await this.middleware.verifyRequestSign() + + // 通用权限校验模块 + await this.middleware.accessControl() + }, + _after (error, result) { + if (error) { + // 处理中间件内抛出的标准响应对象 + if (error.errCode && getType(error) === 'object') { + const errCode = error.errCode + if (!isUniIdError(errCode)) { + return error + } + return { + errCode, + errMsg: error.errMsg || this.t(errCode, error.errMsgValue) + } + } + throw error + } + return Object.assign(this.response, result) + }, + /** + * 注册管理员 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#register-admin + * @param {Object} params + * @param {String} params.username 用户名 + * @param {String} params.password 密码 + * @param {String} params.nickname 昵称 + * @returns + */ + registerAdmin, + /** + * 新增用户 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#add-user + * @param {Object} params + * @param {String} params.username 用户名 + * @param {String} params.password 密码 + * @param {String} params.nickname 昵称 + * @param {Array} params.authorizedApp 允许登录的AppID列表 + * @param {Array} params.role 用户角色列表 + * @param {String} params.mobile 手机号 + * @param {String} params.email 邮箱 + * @param {Array} params.tags 用户标签 + * @param {Number} params.status 用户状态 + * @returns + */ + addUser, + /** + * 修改用户 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#update-user + * @param {Object} params + * @param {String} params.id 要更新的用户id + * @param {String} params.username 用户名 + * @param {String} params.password 密码 + * @param {String} params.nickname 昵称 + * @param {Array} params.authorizedApp 允许登录的AppID列表 + * @param {Array} params.role 用户角色列表 + * @param {String} params.mobile 手机号 + * @param {String} params.email 邮箱 + * @param {Array} params.tags 用户标签 + * @param {Number} params.status 用户状态 + * @returns + */ + updateUser, + /** + * 授权用户登录应用 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#authorize-app-login + * @param {Object} params + * @param {String} params.uid 用户id + * @param {String} params.appId 授权的应用的AppId + * @returns + */ + authorizeAppLogin, + /** + * 移除用户登录授权 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#remove-authorized-app + * @param {Object} params + * @param {String} params.uid 用户id + * @param {String} params.appId 取消授权的应用的AppId + * @returns + */ + removeAuthorizedApp, + /** + * 设置用户允许登录的应用列表 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-authorized-app + * @param {Object} params + * @param {String} params.uid 用户id + * @param {Array} params.appIdList 允许登录的应用AppId列表 + * @returns + */ + setAuthorizedApp, + /** + * 注册普通用户 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#register-user + * @param {Object} params + * @param {String} params.username 用户名 + * @param {String} params.password 密码 + * @param {String} params.captcha 图形验证码 + * @param {String} params.nickname 昵称 + * @param {String} params.inviteCode 邀请码 + * @returns + */ + registerUser, + /** + * 通过邮箱+验证码注册用户 + * @param {Object} params + * @param {String} params.email 邮箱 + * @param {String} params.password 密码 + * @param {String} params.nickname 昵称 + * @param {String} params.code 邮箱验证码 + * @param {String} params.inviteCode 邀请码 + * @returns + */ + registerUserByEmail, + /** + * 用户名密码登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login + * @param {Object} params + * @param {String} params.username 用户名 + * @param {String} params.mobile 手机号 + * @param {String} params.email 邮箱 + * @param {String} params.password 密码 + * @param {String} params.captcha 图形验证码 + * @returns + */ + login, + /** + * 短信验证码登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-sms + * @param {Object} params + * @param {String} params.mobile 手机号 + * @param {String} params.code 短信验证码 + * @param {String} params.captcha 图形验证码 + * @param {String} params.inviteCode 邀请码 + * @returns + */ + loginBySms, + /** + * App端一键登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-univerify + * @param {Object} params + * @param {String} params.access_token APP端一键登录返回的access_token + * @param {String} params.openid APP端一键登录返回的openid + * @param {String} params.inviteCode 邀请码 + * @returns + */ + loginByUniverify, + /** + * 微信登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-weixin + * @param {Object} params + * @param {String} params.code 微信登录返回的code + * @param {String} params.inviteCode 邀请码 + * @returns + */ + loginByWeixin, + /** + * 支付宝登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-alipay + * @param {Object} params + * @param {String} params.code 支付宝小程序客户端登录返回的code + * @param {String} params.inviteCode 邀请码 + * @returns + */ + loginByAlipay, + /** + * QQ登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-qq + * @param {Object} params + * @param {String} params.code QQ小程序登录返回的code参数 + * @param {String} params.accessToken App端QQ登录返回的accessToken参数 + * @param {String} params.accessTokenExpired accessToken过期时间,由App端QQ登录返回的expires_in参数计算而来,单位:毫秒 + * @param {String} params.inviteCode 邀请码 + * @returns + */ + loginByQQ, + /** + * 苹果登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-apple + * @param {Object} params + * @param {String} params.identityToken 苹果登录返回的identityToken + * @param {String} params.nickname 用户昵称 + * @param {String} params.inviteCode 邀请码 + * @returns + */ + loginByApple, + loginByWeixinMobile, + /** + * 用户退出登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#logout + * @returns + */ + logout, + /** + * 通过短信验证码绑定手机号 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-sms + * @param {Object} params + * @param {String} params.mobile 手机号 + * @param {String} params.code 短信验证码 + * @param {String} params.captcha 图形验证码 + * @returns + */ + bindMobileBySms, + /** + * 通过一键登录绑定手机号 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-univerify + * @param {Object} params + * @param {String} params.openid APP端一键登录返回的openid + * @param {String} params.access_token APP端一键登录返回的access_token + * @returns + */ + bindMobileByUniverify, + /** + * 通过微信绑定手机号 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-mp-weixin + * @param {Object} params + * @param {String} params.encryptedData 微信获取手机号返回的加密信息 + * @param {String} params.iv 微信获取手机号返回的初始向量 + * @returns + */ + bindMobileByMpWeixin, + /** + * 绑定微信 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-weixin + * @param {Object} params + * @param {String} params.code 微信登录返回的code + * @returns + */ + bindWeixin, + /** + * 绑定QQ + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-qq + * @param {Object} params + * @param {String} params.code 小程序端QQ登录返回的code + * @param {String} params.accessToken APP端QQ登录返回的accessToken + * @param {String} params.accessTokenExpired accessToken过期时间,由App端QQ登录返回的expires_in参数计算而来,单位:毫秒 + * @returns + */ + bindQQ, + /** + * 绑定支付宝账号 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-alipay + * @param {Object} params + * @param {String} params.code 支付宝小程序登录返回的code参数 + * @returns + */ + bindAlipay, + /** + * 绑定苹果账号 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-apple + * @param {Object} params + * @param {String} params.identityToken 苹果登录返回identityToken + * @returns + */ + bindApple, + /** + * 更新密码 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#update-pwd + * @param {object} params + * @param {string} params.oldPassword 旧密码 + * @param {string} params.newPassword 新密码 + * @returns {object} + */ + updatePwd, + /** + * 通过短信验证码重置密码 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#reset-pwd-by-sms + * @param {object} params + * @param {string} params.mobile 手机号 + * @param {string} params.mobile 短信验证码 + * @param {string} params.password 密码 + * @param {string} params.captcha 图形验证码 + * @returns {object} + */ + resetPwdBySms, + /** + * 通过邮箱验证码重置密码 + * @param {object} params + * @param {string} params.email 邮箱 + * @param {string} params.code 邮箱验证码 + * @param {string} params.password 密码 + * @param {string} params.captcha 图形验证码 + * @returns {object} + */ + resetPwdByEmail, + /** + * 注销账户 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#close-account + * @returns + */ + closeAccount, + /** + * 获取账户账户简略信息 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-account-info + */ + getAccountInfo, + /** + * 创建图形验证码 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#create-captcha + * @param {Object} params + * @param {String} params.scene 图形验证码使用场景 + * @returns + */ + createCaptcha, + /** + * 刷新图形验证码 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#refresh-captcha + * @param {Object} params + * @param {String} params.scene 图形验证码使用场景 + * @returns + */ + refreshCaptcha, + /** + * 发送短信验证码 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#send-sms-code + * @param {Object} params + * @param {String} params.mobile 手机号 + * @param {String} params.captcha 图形验证码 + * @param {String} params.scene 短信验证码使用场景 + * @returns + */ + sendSmsCode, + /** + * 发送邮箱验证码 + * @tutorial 需自行实现功能 + * @param {Object} params + * @param {String} params.email 邮箱 + * @param {String} params.captcha 图形验证码 + * @param {String} params.scene 短信验证码使用场景 + * @returns + */ + sendEmailCode, + /** + * 刷新token + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#refresh-token + */ + refreshToken, + /** + * 接受邀请 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#accept-invite + * @param {Object} params + * @param {String} params.inviteCode 邀请码 + * @returns + */ + acceptInvite, + /** + * 获取受邀用户 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-invited-user + * @param {Object} params + * @param {Number} params.level 获取受邀用户的级数,1表示直接邀请的用户 + * @param {Number} params.limit 返回数据大小 + * @param {Number} params.offset 返回数据偏移 + * @param {Boolean} params.needTotal 是否需要返回总数 + * @returns + */ + getInvitedUser, + /** + * 更新device表的push_clien_id + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-push-cid + * @param {object} params + * @param {string} params.pushClientId 客户端pushClientId + * @returns + */ + setPushCid, + /** + * 获取支持的登录方式 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-supported-login-type + * @returns + */ + getSupportedLoginType, + + /** + * 解绑微信 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-weixin + * @returns + */ + unbindWeixin, + /** + * 解绑支付宝 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-alipay + * @returns + */ + unbindAlipay, + /** + * 解绑QQ + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-qq + * @returns + */ + unbindQQ, + /** + * 解绑Apple + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-apple + * @returns + */ + unbindApple, + /** + * 安全网络握手,目前仅处理微信小程序安全网络握手 + */ + secureNetworkHandshakeByWeixin, + /** + * 设置密码 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-pwd + * @returns + */ + setPwd, + /** + * 外部注册用户 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-register + * @param {object} params + * @param {string} params.externalUid 业务系统的用户id + * @param {string} params.nickname 昵称 + * @param {string} params.gender 性别 + * @param {string} params.avatar 头像 + * @returns {object} + */ + externalRegister, + /** + * 外部用户登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-login + * @param {object} params + * @param {string} params.userId uni-id体系用户id + * @param {string} params.externalUid 业务系统的用户id + * @returns {object} + */ + externalLogin, + /** + * 使用 userId 或 externalUid 获取用户信息 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-update-userinfo + * @param {object} params + * @param {string} params.userId uni-id体系的用户id + * @param {string} params.externalUid 业务系统的用户id + * @param {string} params.nickname 昵称 + * @param {string} params.gender 性别 + * @param {string} params.avatar 头像 + * @returns {object} + */ + updateUserInfoByExternal, + /** + * 获取认证ID + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-frv-certify-id + * @param {Object} params + * @param {String} params.realName 真实姓名 + * @param {String} params.idCard 身份证号码 + * @returns + */ + getFrvCertifyId, + /** + * 查询认证结果 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-frv-auth-result + * @param {Object} params + * @param {String} params.certifyId 认证ID + * @param {String} params.needAlivePhoto 是否获取认证照片,Y_O (原始图片)、Y_M(虚化,背景马赛克)、N(不返图) + * @returns + */ + getFrvAuthResult, + /** + * 获取实名信息 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-realname-info + * @param {Object} params + * @param {Boolean} params.decryptData 是否解密数据 + * @returns + */ + getRealNameInfo +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lang/en.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lang/en.js new file mode 100644 index 0000000..6825461 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lang/en.js @@ -0,0 +1,62 @@ +const word = { + login: 'login', + 'verify-mobile': 'verify phone number' +} + +const sentence = { + 'uni-id-account-exists': 'Account exists', + 'uni-id-account-not-exists': 'Account does not exists', + 'uni-id-account-not-exists-in-current-app': 'Account does not exists in current app', + 'uni-id-account-conflict': 'User account conflict', + 'uni-id-account-banned': 'Account has been banned', + 'uni-id-account-auditing': 'Account audit in progress', + 'uni-id-account-audit-failed': 'Account audit failed', + 'uni-id-account-closed': 'Account has been closed', + 'uni-id-captcha-required': 'Captcha required', + 'uni-id-password-error': 'Password error', + 'uni-id-password-error-exceed-limit': 'The number of password errors is excessive', + 'uni-id-invalid-username': 'Invalid username', + 'uni-id-invalid-password': 'invalid password', + 'uni-id-invalid-password-super': 'Passwords must have 8-16 characters and contain uppercase letters, lowercase letters, numbers, and symbols.', + 'uni-id-invalid-password-strong': 'Passwords must have 8-16 characters and contain letters, numbers and symbols.', + 'uni-id-invalid-password-medium': 'Passwords must have 8-16 characters and contain at least two of the following: letters, numbers, and symbols.', + 'uni-id-invalid-password-weak': 'Passwords must have 6-16 characters and contain letters and numbers.', + 'uni-id-invalid-mobile': 'Invalid mobile phone number', + 'uni-id-invalid-email': 'Invalid email address', + 'uni-id-invalid-nickname': 'Invalid nickname', + 'uni-id-invalid-param': 'Invalid parameter', + 'uni-id-param-required': 'Parameter required: {param}', + 'uni-id-get-third-party-account-failed': 'Get third party account failed', + 'uni-id-get-third-party-user-info-failed': 'Get third party user info failed', + 'uni-id-mobile-verify-code-error': 'Verify code error or expired', + 'uni-id-email-verify-code-error': 'Verify code error or expired', + 'uni-id-admin-exists': 'Administrator exists', + 'uni-id-permission-error': 'Permission denied', + 'uni-id-system-error': 'System error', + 'uni-id-set-invite-code-failed': 'Set invite code failed', + 'uni-id-invalid-invite-code': 'Invalid invite code', + 'uni-id-change-inviter-forbidden': 'Change inviter is not allowed', + 'uni-id-bind-conflict': 'This account has been bound', + 'uni-id-admin-exist-in-other-apps': 'Administrator is registered in other consoles', + 'uni-id-unbind-failed': 'Please bind first and then unbind', + 'uni-id-unbind-not-supported': 'Unbinding is not supported', + 'uni-id-unbind-mobile-not-exists': 'This is the only way to login at the moment, please bind your phone number and then try to unbind', + 'uni-id-unbind-password-not-exists': 'Please set a password first', + 'uni-id-unsupported-request': 'Unsupported request', + 'uni-id-illegal-request': 'Illegal request', + 'uni-id-config-field-required': 'Config field required: {field}', + 'uni-id-config-field-invalid': 'Config field: {field} is invalid', + 'uni-id-frv-fail': 'Real name certify failed', + 'uni-id-frv-processing': 'Waiting for face recognition', + 'uni-id-realname-verified': 'This account has been verified', + 'uni-id-idcard-exists': 'The ID number has been bound to the account', + 'uni-id-invalid-idcard': 'ID number is invalid', + 'uni-id-invalid-realname': 'The name can only be Chinese characters', + 'uni-id-unknown-error': 'unknown error', + 'uni-id-realname-verify-upper-limit': 'The number of real-name certify on the day has reached the upper limit' +} + +module.exports = { + ...word, + ...sentence +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lang/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lang/index.js new file mode 100644 index 0000000..1f22998 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lang/index.js @@ -0,0 +1,22 @@ +let lang = { + 'zh-Hans': require('./zh-hans'), + en: require('./en') +} + +function mergeLanguage (lang1, lang2) { + const localeList = Object.keys(lang1) + localeList.push(...Object.keys(lang2)) + const result = {} + for (let i = 0; i < localeList.length; i++) { + const locale = localeList[i] + result[locale] = Object.assign({}, lang1[locale], lang2[locale]) + } + return result +} + +try { + const langPath = require.resolve('uni-config-center/uni-id/lang/index.js') + lang = mergeLanguage(lang, require(langPath)) +} catch (error) { } + +module.exports = lang diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lang/zh-hans.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lang/zh-hans.js new file mode 100644 index 0000000..911ce20 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lang/zh-hans.js @@ -0,0 +1,64 @@ +const word = { + login: '登录', + 'verify-mobile': '验证手机号' +} + +const sentence = { + 'uni-id-token-expired': '登录状态失效,token已过期', + 'uni-id-check-token-failed': 'token校验未通过', + 'uni-id-account-exists': '此账号已注册', + 'uni-id-account-not-exists': '此账号未注册', + 'uni-id-account-not-exists-in-current-app': '此账号未在该应用注册', + 'uni-id-account-conflict': '用户账号冲突', + 'uni-id-account-banned': '此账号已封禁', + 'uni-id-account-auditing': '此账号正在审核中', + 'uni-id-account-audit-failed': '此账号审核失败', + 'uni-id-account-closed': '此账号已注销', + 'uni-id-captcha-required': '请输入图形验证码', + 'uni-id-password-error': '密码错误', + 'uni-id-password-error-exceed-limit': '密码错误次数过多,请稍后再试', + 'uni-id-invalid-username': '用户名不合法', + 'uni-id-invalid-password': '密码不合法', + 'uni-id-invalid-password-super': '密码必须包含大小写字母、数字和特殊符号,长度8-16位', + 'uni-id-invalid-password-strong': '密码必须包含字母、数字和特殊符号,长度8-16位不合法', + 'uni-id-invalid-password-medium': '密码必须为字母、数字和特殊符号任意两种的组合,长度8-16位', + 'uni-id-invalid-password-weak': '密码必须包含字母和数字,长度6-16位', + 'uni-id-invalid-mobile': '手机号码不合法', + 'uni-id-invalid-email': '邮箱不合法', + 'uni-id-invalid-nickname': '昵称不合法', + 'uni-id-invalid-param': '参数错误', + 'uni-id-param-required': '缺少参数: {param}', + 'uni-id-get-third-party-account-failed': '获取第三方账号失败', + 'uni-id-get-third-party-user-info-failed': '获取用户信息失败', + 'uni-id-mobile-verify-code-error': '手机验证码错误或已过期', + 'uni-id-email-verify-code-error': '邮箱验证码错误或已过期', + 'uni-id-admin-exists': '超级管理员已存在', + 'uni-id-permission-error': '权限错误', + 'uni-id-system-error': '系统错误', + 'uni-id-set-invite-code-failed': '设置邀请码失败', + 'uni-id-invalid-invite-code': '邀请码不可用', + 'uni-id-change-inviter-forbidden': '禁止修改邀请人', + 'uni-id-bind-conflict': '此账号已被绑定', + 'uni-id-admin-exist-in-other-apps': '超级管理员已在其他控制台注册', + 'uni-id-unbind-failed': '请先绑定后再解绑', + 'uni-id-unbind-not-supported': '不支持解绑', + 'uni-id-unbind-mobile-not-exists': '这是当前唯一登录方式,请绑定手机号后再尝试解绑', + 'uni-id-unbind-password-not-exists': '请先设置密码在尝试解绑', + 'uni-id-unsupported-request': '不支持的请求方式', + 'uni-id-illegal-request': '非法请求', + 'uni-id-frv-fail': '实名认证失败', + 'uni-id-frv-processing': '等待人脸识别', + 'uni-id-realname-verified': '该账号已实名认证', + 'uni-id-idcard-exists': '该证件号码已绑定账号', + 'uni-id-invalid-idcard': '身份证号码不合法', + 'uni-id-invalid-realname': '姓名只能是汉字', + 'uni-id-unknown-error': '未知错误', + 'uni-id-realname-verify-upper-limit': '当日实名认证次数已达上限', + 'uni-id-config-field-required': '缺少配置项: {field}', + 'uni-id-config-field-invalid': '配置项: {field}无效' +} + +module.exports = { + ...word, + ...sentence +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/README.md b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/README.md new file mode 100644 index 0000000..47d8c4c --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/README.md @@ -0,0 +1,3 @@ +# 说明 + +此目录内为uni-id-co基础能力,不建议直接修改。如果你发现有些逻辑加入会更好,或者此部分代码有Bug可以向我们提交PR,仓库地址:[]()。如果有特殊的需求也可以在[论坛](https://ask.dcloud.net.cn/)提出,我们可以讨论下如何实现。 \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/alipay/account/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/alipay/account/index.js new file mode 100644 index 0000000..dbec081 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/alipay/account/index.js @@ -0,0 +1,16 @@ +const AlipayBase = require('../alipayBase') +const protocols = require('./protocols') +module.exports = class Auth extends AlipayBase { + constructor (options) { + super(options) + this._protocols = protocols + } + + async code2Session (code) { + const result = await this._exec('alipay.system.oauth.token', { + grantType: 'authorization_code', + code + }) + return result + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/alipay/account/protocols.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/alipay/account/protocols.js new file mode 100644 index 0000000..cff351d --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/alipay/account/protocols.js @@ -0,0 +1,10 @@ +module.exports = { + code2Session: { + // args (fromArgs) { + // return fromArgs + // }, + returnValue: { + openid: 'userId' + } + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/alipay/alipayBase.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/alipay/alipayBase.js new file mode 100644 index 0000000..1462b04 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/alipay/alipayBase.js @@ -0,0 +1,231 @@ +const { + camel2snakeJson, + snake2camelJson, + getOffsetDate, + getFullTimeStr +} = require('../../../common/utils') +const crypto = require('crypto') + +const ALIPAY_ALGORITHM_MAPPING = { + RSA: 'RSA-SHA1', + RSA2: 'RSA-SHA256' +} + +module.exports = class AlipayBase { + constructor (options = {}) { + if (!options.appId) throw new Error('appId required') + if (!options.privateKey) throw new Error('privateKey required') + const defaultOptions = { + gateway: 'https://openapi.alipay.com/gateway.do', + timeout: 5000, + charset: 'utf-8', + version: '1.0', + signType: 'RSA2', + timeOffset: -new Date().getTimezoneOffset() / 60, + keyType: 'PKCS8' + } + + if (options.sandbox) { + options.gateway = 'https://openapi.alipaydev.com/gateway.do' + } + + this.options = Object.assign({}, defaultOptions, options) + + const privateKeyType = + this.options.keyType === 'PKCS8' ? 'PRIVATE KEY' : 'RSA PRIVATE KEY' + + this.options.privateKey = this._formatKey( + this.options.privateKey, + privateKeyType + ) + if (this.options.alipayPublicKey) { + this.options.alipayPublicKey = this._formatKey( + this.options.alipayPublicKey, + 'PUBLIC KEY' + ) + } + } + + _formatKey (key, type) { + return `-----BEGIN ${type}-----\n${key}\n-----END ${type}-----` + } + + _formatUrl (url, params) { + let requestUrl = url + // 需要放在 url 中的参数列表 + const urlArgs = [ + 'app_id', + 'method', + 'format', + 'charset', + 'sign_type', + 'sign', + 'timestamp', + 'version', + 'notify_url', + 'return_url', + 'auth_token', + 'app_auth_token' + ] + + for (const key in params) { + if (urlArgs.indexOf(key) > -1) { + const val = encodeURIComponent(params[key]) + requestUrl = `${requestUrl}${requestUrl.includes('?') ? '&' : '?' + }${key}=${val}` + // 删除 postData 中对应的数据 + delete params[key] + } + } + + return { execParams: params, url: requestUrl } + } + + _getSign (method, params) { + const bizContent = params.bizContent || null + delete params.bizContent + + const signParams = Object.assign({ + method, + appId: this.options.appId, + charset: this.options.charset, + version: this.options.version, + signType: this.options.signType, + timestamp: getFullTimeStr(getOffsetDate(this.options.timeOffset)) + }, params) + + if (bizContent) { + signParams.bizContent = JSON.stringify(camel2snakeJson(bizContent)) + } + + // params key 驼峰转下划线 + const decamelizeParams = camel2snakeJson(signParams) + + // 排序 + const signStr = Object.keys(decamelizeParams) + .sort() + .map((key) => { + let data = decamelizeParams[key] + if (Array.prototype.toString.call(data) !== '[object String]') { + data = JSON.stringify(data) + } + return `${key}=${data}` + }) + .join('&') + + // 计算签名 + const sign = crypto + .createSign(ALIPAY_ALGORITHM_MAPPING[this.options.signType]) + .update(signStr, 'utf8') + .sign(this.options.privateKey, 'base64') + + return Object.assign(decamelizeParams, { sign }) + } + + async _exec (method, params = {}, option = {}) { + // 计算签名 + const signData = this._getSign(method, params) + const { url, execParams } = this._formatUrl(this.options.gateway, signData) + const { status, data } = await uniCloud.httpclient.request(url, { + method: 'POST', + data: execParams, + // 按 text 返回(为了验签) + dataType: 'text', + timeout: this.options.timeout + }) + if (status !== 200) throw new Error('request fail') + /** + * 示例响应格式 + * {"alipay_trade_precreate_response": + * {"code": "10000","msg": "Success","out_trade_no": "111111","qr_code": "https:\/\/"}, + * "sign": "abcde=" + * } + * 或者 + * {"error_response": + * {"code":"40002","msg":"Invalid Arguments","sub_code":"isv.code-invalid","sub_msg":"授权码code无效"}, + * } + */ + const result = JSON.parse(data) + const responseKey = `${method.replace(/\./g, '_')}_response` + const response = result[responseKey] + const errorResponse = result.error_response + if (response) { + // 按字符串验签 + const validateSuccess = option.validateSign ? this._checkResponseSign(data, responseKey) : true + if (validateSuccess) { + if (!response.code || response.code === '10000') { + const errCode = 0 + const errMsg = response.msg || '' + return { + errCode, + errMsg, + ...snake2camelJson(response) + } + } + const msg = response.sub_code ? `${response.sub_code} ${response.sub_msg}` : `${response.msg || 'unkonwn error'}` + throw new Error(msg) + } else { + throw new Error('check sign error') + } + } else if (errorResponse) { + throw new Error(errorResponse.sub_msg || errorResponse.msg || 'request fail') + } + + throw new Error('request fail') + } + + _checkResponseSign (signStr, responseKey) { + if (!this.options.alipayPublicKey || this.options.alipayPublicKey === '') { + console.warn('options.alipayPublicKey is empty') + // 支付宝公钥不存在时不做验签 + return true + } + + // 带验签的参数不存在时返回失败 + if (!signStr) { return false } + + // 根据服务端返回的结果截取需要验签的目标字符串 + const validateStr = this._getSignStr(signStr, responseKey) + // 服务端返回的签名 + const serverSign = JSON.parse(signStr).sign + + // 参数存在,并且是正常的结果(不包含 sub_code)时才验签 + const verifier = crypto.createVerify(ALIPAY_ALGORITHM_MAPPING[this.options.signType]) + verifier.update(validateStr, 'utf8') + return verifier.verify(this.options.alipayPublicKey, serverSign, 'base64') + } + + _getSignStr (originStr, responseKey) { + // 待签名的字符串 + let validateStr = originStr.trim() + // 找到 xxx_response 开始的位置 + const startIndex = originStr.indexOf(`${responseKey}"`) + // 找到最后一个 “"sign"” 字符串的位置(避免) + const lastIndex = originStr.lastIndexOf('"sign"') + + /** + * 删除 xxx_response 及之前的字符串 + * 假设原始字符串为 + * {"xxx_response":{"code":"10000"},"sign":"jumSvxTKwn24G5sAIN"} + * 删除后变为 + * :{"code":"10000"},"sign":"jumSvxTKwn24G5sAIN"} + */ + validateStr = validateStr.substr(startIndex + responseKey.length + 1) + + /** + * 删除最后一个 "sign" 及之后的字符串 + * 删除后变为 + * :{"code":"10000"}, + * {} 之间就是待验签的字符串 + */ + validateStr = validateStr.substr(0, lastIndex) + + // 删除第一个 { 之前的任何字符 + validateStr = validateStr.replace(/^[^{]*{/g, '{') + + // 删除最后一个 } 之后的任何字符 + validateStr = validateStr.replace(/\}([^}]*)$/g, '}') + + return validateStr + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/apple/account/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/apple/account/index.js new file mode 100644 index 0000000..0e51b4d --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/apple/account/index.js @@ -0,0 +1,76 @@ +const rsaPublicKeyPem = require('../rsa-public-key-pem') +let authKeysCache = null + +module.exports = class Auth { + constructor (options) { + this.options = Object.assign({ + baseUrl: 'https://appleid.apple.com', + timeout: 10000 + }, options) + } + + async _fetch (url, options) { + const { baseUrl } = this.options + return uniCloud.httpclient.request(baseUrl + url, options) + } + + async verifyIdentityToken (identityToken) { + // 解密出kid,拿取key + const jwtHeader = identityToken.split('.')[0] + const { kid } = JSON.parse(Buffer.from(jwtHeader, 'base64').toString()) + let authKeys + if (authKeysCache) { + authKeys = authKeysCache + } else { + authKeys = await this.getAuthKeys() + authKeysCache = authKeys + } + const usedKey = authKeys.find(item => item.kid === kid) + + /** + * identityToken 格式 + * + * { + * iss: 'https://appleid.apple.com', + * aud: 'io.dcloud.hellouniapp', + * exp: 1610626724, + * iat: 1610540324, + * sub: '000628.30119d332d9b45a3be4a297f9391fd5c.0403', + * c_hash: 'oFfgewoG36cJX00KUbj45A', + * email: 'x2awmap99s@privaterelay.appleid.com', + * email_verified: 'true', + * is_private_email: 'true', + * auth_time: 1610540324, + * nonce_supported: true + * } + */ + const payload = require('jsonwebtoken').verify( + identityToken, + rsaPublicKeyPem(usedKey.n, usedKey.e), + { + algorithms: usedKey.alg + } + ) + + if (payload.iss !== 'https://appleid.apple.com' || payload.aud !== this.options.bundleId) { + throw new Error('Invalid identity token') + } + + return { + openid: payload.sub, + email: payload.email, + emailVerified: payload.email_verified === 'true', + isPrivateEmail: payload.is_private_email === 'true' + } + } + + async getAuthKeys () { + const { status, data } = await this._fetch('/auth/keys', { + method: 'GET', + dataType: 'json', + timeout: this.options.timeout + }) + if (status !== 200) throw new Error('request https://appleid.apple.com/auth/keys fail') + return data.keys + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/apple/rsa-public-key-pem.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/apple/rsa-public-key-pem.js new file mode 100644 index 0000000..e1dbb31 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/apple/rsa-public-key-pem.js @@ -0,0 +1,64 @@ +// http://stackoverflow.com/questions/18835132/xml-to-pem-in-node-js +/* eslint-disable camelcase */ +function rsaPublicKeyPem (modulus_b64, exponent_b64) { + const modulus = Buffer.from(modulus_b64, 'base64') + const exponent = Buffer.from(exponent_b64, 'base64') + + let modulus_hex = modulus.toString('hex') + let exponent_hex = exponent.toString('hex') + + modulus_hex = prepadSigned(modulus_hex) + exponent_hex = prepadSigned(exponent_hex) + + const modlen = modulus_hex.length / 2 + const explen = exponent_hex.length / 2 + + const encoded_modlen = encodeLengthHex(modlen) + const encoded_explen = encodeLengthHex(explen) + const encoded_pubkey = '30' + + encodeLengthHex( + modlen + + explen + + encoded_modlen.length / 2 + + encoded_explen.length / 2 + 2 + ) + + '02' + encoded_modlen + modulus_hex + + '02' + encoded_explen + exponent_hex + + const der_b64 = Buffer.from(encoded_pubkey, 'hex').toString('base64') + + const pem = '-----BEGIN RSA PUBLIC KEY-----\n' + + der_b64.match(/.{1,64}/g).join('\n') + + '\n-----END RSA PUBLIC KEY-----\n' + + return pem +} + +function prepadSigned (hexStr) { + const msb = hexStr[0] + if (msb < '0' || msb > '7') { + return '00' + hexStr + } else { + return hexStr + } +} + +function toHex (number) { + const nstr = number.toString(16) + if (nstr.length % 2) return '0' + nstr + return nstr +} + +// encode ASN.1 DER length field +// if <=127, short form +// if >=128, long form +function encodeLengthHex (n) { + if (n <= 127) return toHex(n) + else { + const n_hex = toHex(n) + const length_of_length_byte = 128 + n_hex.length / 2 // 0x80+numbytes + return toHex(length_of_length_byte) + n_hex + } +} + +module.exports = rsaPublicKeyPem diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/index.js new file mode 100644 index 0000000..149c7de --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/index.js @@ -0,0 +1,36 @@ +const WxAccount = require('./weixin/account/index') +const QQAccount = require('./qq/account/index') +const AliAccount = require('./alipay/account/index') +const AppleAccount = require('./apple/account/index') + +const createApi = require('./share/create-api') + +module.exports = { + initWeixin: function () { + const oauthConfig = this.configUtils.getOauthConfig({ provider: 'weixin' }) + return createApi(WxAccount, { + appId: oauthConfig.appid, + secret: oauthConfig.appsecret + }) + }, + initQQ: function () { + const oauthConfig = this.configUtils.getOauthConfig({ provider: 'qq' }) + return createApi(QQAccount, { + appId: oauthConfig.appid, + secret: oauthConfig.appsecret + }) + }, + initAlipay: function () { + const oauthConfig = this.configUtils.getOauthConfig({ provider: 'alipay' }) + return createApi(AliAccount, { + appId: oauthConfig.appid, + privateKey: oauthConfig.privateKey + }) + }, + initApple: function () { + const oauthConfig = this.configUtils.getOauthConfig({ provider: 'apple' }) + return createApi(AppleAccount, { + bundleId: oauthConfig.bundleId + }) + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/qq/account/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/qq/account/index.js new file mode 100644 index 0000000..9b4879a --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/qq/account/index.js @@ -0,0 +1,97 @@ +const { + UniCloudError +} = require('../../../../common/error') +const { + resolveUrl +} = require('../../../../common/utils') +const { + callQQOpenApi +} = require('../normalize') + +module.exports = class Auth { + constructor (options) { + this.options = Object.assign({ + baseUrl: 'https://graph.qq.com', + timeout: 5000 + }, options) + } + + async _requestQQOpenapi ({ name, url, data, options }) { + const defaultOptions = { + method: 'GET', + dataType: 'json', + dataAsQueryString: true, + timeout: this.options.timeout + } + const result = await callQQOpenApi({ + name: `auth.${name}`, + url: resolveUrl(this.options.baseUrl, url), + data, + options, + defaultOptions + }) + return result + } + + async getUserInfo ({ + accessToken, + openid + } = {}) { + const url = '/user/get_user_info' + const result = await this._requestQQOpenapi({ + name: 'getUserInfo', + url, + data: { + oauthConsumerKey: this.options.appId, + accessToken, + openid + } + }) + return { + nickname: result.nickname, + avatar: result.figureurl_qq_1 + } + } + + async getOpenidByToken ({ + accessToken + } = {}) { + const url = '/oauth2.0/me' + const result = await this._requestQQOpenapi({ + name: 'getOpenidByToken', + url, + data: { + accessToken, + unionid: 1, + fmt: 'json' + } + }) + if (result.clientId !== this.options.appId) { + throw new UniCloudError({ + code: 'APPID_NOT_MATCH', + message: 'appid not match' + }) + } + return { + openid: result.openid, + unionid: result.unionid + } + } + + async code2Session ({ + code + } = {}) { + const url = 'https://api.q.qq.com/sns/jscode2session' + const result = await this._requestQQOpenapi({ + name: 'getOpenidByToken', + url, + data: { + grant_type: 'authorization_code', + appid: this.options.appId, + secret: this.options.secret, + js_code: code + } + }) + return result + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/qq/account/protocol.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/qq/account/protocol.js new file mode 100644 index 0000000..e69de29 diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/qq/normalize.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/qq/normalize.js new file mode 100644 index 0000000..fcfdc1e --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/qq/normalize.js @@ -0,0 +1,85 @@ +const { + UniCloudError +} = require('../../../common/error') +const { + camel2snakeJson, + snake2camelJson +} = require('../../../common/utils') + +function generateApiResult (apiName, data) { + if (data.ret || data.error) { + // 这三种都是qq的错误码规范 + const code = data.ret || data.error || data.errcode || -2 + const message = data.msg || data.error_description || data.errmsg || `${apiName} fail` + throw new UniCloudError({ + code, + message + }) + } else { + delete data.ret + delete data.msg + delete data.error + delete data.error_description + delete data.errcode + delete data.errmsg + return { + ...data, + errMsg: `${apiName} ok`, + errCode: 0 + } + } +} + +function nomalizeError (apiName, error) { + throw new UniCloudError({ + code: error.code || -2, + message: error.message || `${apiName} fail` + }) +} + +async function callQQOpenApi ({ + name, + url, + data, + options, + defaultOptions +}) { + options = Object.assign({}, defaultOptions, options, { data: camel2snakeJson(Object.assign({}, data)) }) + let result + try { + result = await uniCloud.httpclient.request(url, options) + } catch (e) { + return nomalizeError(name, e) + } + let resData = result.data + const contentType = result.headers['content-type'] + if ( + Buffer.isBuffer(resData) && + (contentType.indexOf('text/plain') === 0 || + contentType.indexOf('application/json') === 0) + ) { + try { + resData = JSON.parse(resData.toString()) + } catch (e) { + resData = resData.toString() + } + } else if (Buffer.isBuffer(resData)) { + resData = { + buffer: resData, + contentType + } + } + return snake2camelJson( + generateApiResult( + name, + resData || { + errCode: -2, + errMsg: 'Request failed' + } + ) + ) +} + +module.exports = { + callQQOpenApi +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/share/create-api.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/share/create-api.js new file mode 100644 index 0000000..c58f1e8 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/share/create-api.js @@ -0,0 +1,73 @@ +const { + isFn, + isPlainObject +} = require('../../../common/utils') + +// 注意:不进行递归处理 +function parseParams (params = {}, rule) { + if (!rule || !params) { + return params + } + const internalKeys = ['_pre', '_purify', '_post'] + // 转换之前的处理 + if (rule._pre) { + params = rule._pre(params) + } + // 净化参数 + let purify = { shouldDelete: new Set([]) } + if (rule._purify) { + const _purify = rule._purify + for (const purifyKey in _purify) { + _purify[purifyKey] = new Set(_purify[purifyKey]) + } + purify = Object.assign(purify, _purify) + } + if (isPlainObject(rule)) { + for (const key in rule) { + const parser = rule[key] + if (isFn(parser) && internalKeys.indexOf(key) === -1) { + params[key] = parser(params) + } else if (typeof parser === 'string' && internalKeys.indexOf(key) === -1) { + // 直接转换属性名称的删除旧属性名 + params[key] = params[parser] + purify.shouldDelete.add(parser) + } + } + } else if (isFn(rule)) { + params = rule(params) + } + + if (purify.shouldDelete) { + for (const item of purify.shouldDelete) { + delete params[item] + } + } + + // 转换之后的处理 + if (rule._post) { + params = rule._post(params) + } + + return params +} + +function createApi (ApiClass, options) { + const apiInstance = new ApiClass(options) + return new Proxy(apiInstance, { + get: function (obj, prop) { + if (typeof obj[prop] === 'function' && prop.indexOf('_') !== 0 && obj._protocols && obj._protocols[prop]) { + const protocol = obj._protocols[prop] + return async function (params) { + params = parseParams(params, protocol.args) + let result = await obj[prop](params) + result = parseParams(result, protocol.returnValue) + return result + } + } else { + return obj[prop] + } + } + }) +} + +module.exports = createApi diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/weixin/account/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/weixin/account/index.js new file mode 100644 index 0000000..7ecea84 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/weixin/account/index.js @@ -0,0 +1,111 @@ +const { + callWxOpenApi, + buildUrl +} = require('../normalize') + +module.exports = class Auth { + constructor (options) { + this.options = Object.assign({ + baseUrl: 'https://api.weixin.qq.com', + timeout: 5000 + }, options) + } + + async _requestWxOpenapi ({ name, url, data, options }) { + const defaultOptions = { + method: 'GET', + dataType: 'json', + dataAsQueryString: true, + timeout: this.options.timeout + } + const result = await callWxOpenApi({ + name: `auth.${name}`, + url: `${this.options.baseUrl}${buildUrl(url, data)}`, + data, + options, + defaultOptions + }) + return result + } + + async code2Session (code) { + const url = '/sns/jscode2session' + const result = await this._requestWxOpenapi({ + name: 'code2Session', + url, + data: { + grant_type: 'authorization_code', + appid: this.options.appId, + secret: this.options.secret, + js_code: code + } + }) + return result + } + + async getOauthAccessToken (code) { + const url = '/sns/oauth2/access_token' + const result = await this._requestWxOpenapi({ + name: 'getOauthAccessToken', + url, + data: { + grant_type: 'authorization_code', + appid: this.options.appId, + secret: this.options.secret, + code + } + }) + if (result.expiresIn) { + result.expired = Date.now() + result.expiresIn * 1000 + // delete result.expiresIn + } + return result + } + + async getUserInfo ({ + accessToken, + openid + } = {}) { + const url = '/sns/userinfo' + const { + nickname, + headimgurl: avatar + } = await this._requestWxOpenapi({ + name: 'getUserInfo', + url, + data: { + accessToken, + openid, + appid: this.options.appId, + secret: this.options.secret, + scope: 'snsapi_userinfo' + } + }) + return { + nickname, + avatar + } + } + + async getPhoneNumber (accessToken, code) { + const url = `/wxa/business/getuserphonenumber?access_token=${accessToken}` + const { phoneInfo } = await this._requestWxOpenapi({ + name: 'getPhoneNumber', + url, + data: { + code + }, + options: { + method: 'POST', + dataAsQueryString: false, + headers: { + 'content-type': 'application/json' + } + } + }) + + return { + purePhoneNumber: phoneInfo.purePhoneNumber + } + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/weixin/normalize.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/weixin/normalize.js new file mode 100644 index 0000000..908d916 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/weixin/normalize.js @@ -0,0 +1,95 @@ +const { + UniCloudError +} = require('../../../common/error') +const { + camel2snakeJson, snake2camelJson +} = require('../../../common/utils') + +function generateApiResult (apiName, data) { + if (data.errcode) { + throw new UniCloudError({ + code: data.errcode || -2, + message: data.errmsg || `${apiName} fail` + }) + } else { + delete data.errcode + delete data.errmsg + return { + ...data, + errMsg: `${apiName} ok`, + errCode: 0 + } + } +} + +function nomalizeError (apiName, error) { + throw new UniCloudError({ + code: error.code || -2, + message: error.message || `${apiName} fail` + }) +} + +// 微信openapi接口接收蛇形(snake case)参数返回蛇形参数,这里进行转化,如果是formdata里面的参数需要在对应api实现时就转为蛇形 +async function callWxOpenApi ({ + name, + url, + data, + options, + defaultOptions +}) { + let result = {} + // 获取二维码的接口wxacode.get和wxacode.getUnlimited不可以传入access_token(可能有其他接口也不可以),否则会返回data format error + const dataCopy = camel2snakeJson(Object.assign({}, data)) + if (dataCopy && dataCopy.access_token) { + delete dataCopy.access_token + } + try { + options = Object.assign({}, defaultOptions, options, { data: dataCopy }) + result = await uniCloud.httpclient.request(url, options) + } catch (e) { + return nomalizeError(name, e) + } + + // 有几个接口成功返回buffer失败返回json,对这些接口统一成返回buffer,然后分别解析 + let resData = result.data + const contentType = result.headers['content-type'] + if ( + Buffer.isBuffer(resData) && + (contentType.indexOf('text/plain') === 0 || + contentType.indexOf('application/json') === 0) + ) { + try { + resData = JSON.parse(resData.toString()) + } catch (e) { + resData = resData.toString() + } + } else if (Buffer.isBuffer(resData)) { + resData = { + buffer: resData, + contentType + } + } + return snake2camelJson( + generateApiResult( + name, + resData || { + errCode: -2, + errMsg: 'Request failed' + } + ) + ) +} + +function buildUrl (url, data) { + let query = '' + if (data && data.accessToken) { + const divider = url.indexOf('?') > -1 ? '&' : '?' + query = `${divider}access_token=${data.accessToken}` + } + return `${url}${query}` +} + +module.exports = { + callWxOpenApi, + buildUrl +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/weixin/utils.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/weixin/utils.js new file mode 100644 index 0000000..c141016 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/third-party/weixin/utils.js @@ -0,0 +1,87 @@ +const crypto = require('crypto') +const { + isPlainObject +} = require('../../../common/utils') + +// 退款通知解密key=md5(key) +function decryptData (encryptedData, key, iv = '') { + // 解密 + const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv) + // 设置自动 padding 为 true,删除填充补位 + decipher.setAutoPadding(true) + let decoded = decipher.update(encryptedData, 'base64', 'utf8') + decoded += decipher.final('utf8') + return decoded +} + +function md5 (str, encoding = 'utf8') { + return crypto + .createHash('md5') + .update(str, encoding) + .digest('hex') +} + +function sha256 (str, key, encoding = 'utf8') { + return crypto + .createHmac('sha256', key) + .update(str, encoding) + .digest('hex') +} + +function getSignStr (obj) { + return Object.keys(obj) + .filter(key => key !== 'sign' && obj[key] !== undefined && obj[key] !== '') + .sort() + .map(key => key + '=' + obj[key]) + .join('&') +} + +function getNonceStr (length = 16) { + let str = '' + while (str.length < length) { + str += Math.random().toString(32).substring(2) + } + return str.substring(0, length) +} + +// 简易版Object转XML,只可在微信支付时使用,不支持嵌套 +function buildXML (obj, rootName = 'xml') { + const content = Object.keys(obj).map(item => { + if (isPlainObject(obj[item])) { + return `<${item}>` + } else { + return `<${item}>` + } + }) + return `<${rootName}>${content.join('')}` +} + +function isXML (str) { + const reg = /^(<\?xml.*\?>)?(\r?\n)*(.|\r?\n)*<\/xml>$/i + return reg.test(str.trim()) +}; + +// 简易版XML转Object,只可在微信支付时使用,不支持嵌套 +function parseXML (xml) { + const xmlReg = /<(?:xml|root).*?>([\s|\S]*)<\/(?:xml|root)>/ + const str = xmlReg.exec(xml)[1] + const obj = {} + const nodeReg = /<(.*?)>(?:){0,1}<\/.*?>/g + let matches = null + // eslint-disable-next-line no-cond-assign + while ((matches = nodeReg.exec(str))) { + obj[matches[1]] = matches[2] + } + return obj +} + +module.exports = { + decryptData, + md5, + sha256, + getSignStr, + getNonceStr, + buildXML, + parseXML, + isXML +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/account.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/account.js new file mode 100644 index 0000000..1fd25f0 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/account.js @@ -0,0 +1,98 @@ +const { + dbCmd, + userCollection +} = require('../../common/constants') +const { + USER_IDENTIFIER +} = require('../../common/constants') +const { + batchFindObjctValue, + getType, + isMatchUserApp +} = require('../../common/utils') + +/** + * 查询满足条件的用户 + * @param {Object} params + * @param {Object} params.userQuery 用户唯一标识组成的查询条件 + * @param {Object} params.authorizedApp 用户允许登录的应用 + * @returns userMatched 满足条件的用户列表 + */ +async function findUser (params = {}) { + const { + userQuery, + authorizedApp = [] + } = params + const condition = getUserQueryCondition(userQuery) + if (condition.length === 0) { + throw new Error('Invalid user query') + } + const authorizedAppType = getType(authorizedApp) + if (authorizedAppType !== 'string' && authorizedAppType !== 'array') { + throw new Error('Invalid authorized app') + } + + let finalQuery + + if (condition.length === 1) { + finalQuery = condition[0] + } else { + finalQuery = dbCmd.or(condition) + } + const userQueryRes = await userCollection.where(finalQuery).get() + return { + total: userQueryRes.data.length, + userMatched: userQueryRes.data.filter(item => { + return isMatchUserApp(item.dcloud_appid, authorizedApp) + }) + } +} + +function getUserIdentifier (userRecord = {}) { + const keys = Object.keys(USER_IDENTIFIER) + return batchFindObjctValue(userRecord, keys) +} + +function getUserQueryCondition (userRecord = {}) { + const userIdentifier = getUserIdentifier(userRecord) + const condition = [] + for (const key in userIdentifier) { + const value = userIdentifier[key] + if (!value) { + // 过滤所有value为假值的条件,在查询用户时没有意义 + continue + } + const queryItem = { + [key]: value + } + // 为兼容用户老数据用户名及邮箱需要同时查小写及原始大小写数据 + if (key === 'mobile') { + queryItem.mobile_confirmed = 1 + } else if (key === 'email') { + queryItem.email_confirmed = 1 + const email = userIdentifier.email + if (email.toLowerCase() !== email) { + condition.push({ + email: email.toLowerCase(), + email_confirmed: 1 + }) + } + } else if (key === 'username') { + const username = userIdentifier.username + if (username.toLowerCase() !== username) { + condition.push({ + username: username.toLowerCase() + }) + } + } else if (key === 'identities') { + queryItem.identities = dbCmd.elemMatch(value) + } + condition.push(queryItem) + } + return condition +} + +module.exports = { + findUser, + getUserIdentifier +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/captcha.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/captcha.js new file mode 100644 index 0000000..0dd620e --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/captcha.js @@ -0,0 +1,76 @@ +const { + ERROR +} = require('../../common/error') + +async function getNeedCaptcha ({ + uid, + username, + mobile, + email, + type = 'login', + limitDuration = 7200000, // 两小时 + limitTimes = 3 // 记录次数 +} = {}) { + const db = uniCloud.database() + const dbCmd = db.command + // 当用户最近“2小时内(limitDuration)”登录失败达到3次(limitTimes)时。要求用户提交验证码 + const now = Date.now() + const uniIdLogCollection = db.collection('uni-id-log') + const userIdentifier = { + user_id: uid, + username, + mobile, + email + } + + let totalKey = 0; let deleteKey = 0 + for (const key in userIdentifier) { + totalKey++ + if (!userIdentifier[key] || typeof userIdentifier[key] !== 'string') { + deleteKey++ + delete userIdentifier[key] + } + } + + if (deleteKey === totalKey) { + throw new Error('System error') // 正常情况下不会进入此条件,但是考虑到后续会有其他开发者修改此云对象,在此处做一个判断 + } + + const { + data: recentRecord + } = await uniIdLogCollection.where({ + ip: this.getUniversalClientInfo().clientIP, + ...userIdentifier, + type, + create_date: dbCmd.gt(now - limitDuration) + }) + .orderBy('create_date', 'desc') + .limit(limitTimes) + .get() + return recentRecord.length === limitTimes && recentRecord.every(item => item.state === 0) +} + +async function verifyCaptcha (params = {}) { + const { + captcha, + scene + } = params + if (!captcha) { + throw { + errCode: ERROR.CAPTCHA_REQUIRED + } + } + const payload = await this.uniCaptcha.verify({ + deviceId: this.getUniversalClientInfo().deviceId, + captcha, + scene + }) + if (payload.errCode) { + throw payload + } +} + +module.exports = { + getNeedCaptcha, + verifyCaptcha +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/config.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/config.js new file mode 100644 index 0000000..48d5688 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/config.js @@ -0,0 +1,135 @@ +const { + getWeixinPlatform +} = require('./weixin') +const createConfig = require('uni-config-center') + +const requiredConfig = { + 'web.weixin-h5': ['appid', 'appsecret'], + 'web.weixin-web': ['appid', 'appsecret'], + 'app.weixin': ['appid', 'appsecret'], + 'mp-weixin.weixin': ['appid', 'appsecret'], + 'app.qq': ['appid', 'appsecret'], + 'mp-alipay.alipay': ['appid', 'privateKey'], + 'app.apple': ['bundleId'] +} + +const uniIdConfig = createConfig({ + pluginId: 'uni-id' +}) + +class ConfigUtils { + constructor ({ + context + } = {}) { + this.context = context + this.clientInfo = context.getUniversalClientInfo() + const { + appId, + uniPlatform + } = this.clientInfo + this.appId = appId + switch (uniPlatform) { + case 'app': + case 'app-plus': + this.platform = 'app' + break + case 'web': + case 'h5': + this.platform = 'web' + break + default: + this.platform = uniPlatform + break + } + } + + getConfigArray () { + let configContent + try { + configContent = require('uni-config-center/uni-id/config.json') + } catch (error) { + throw new Error('Invalid config file\n' + error.message) + } + if (configContent[0]) { + return Object.values(configContent) + } + configContent.isDefaultConfig = true + return [configContent] + } + + getAppConfig () { + const configArray = this.getConfigArray() + return configArray.find(item => item.dcloudAppid === this.appId) || configArray.find(item => item.isDefaultConfig) + } + + getPlatformConfig () { + const appConfig = this.getAppConfig() + if (!appConfig) { + throw new Error( + `Config for current app (${this.appId}) was not found, please check your config file or client appId`) + } + const platform = this.platform + if ( + (this.platform === 'app' && appConfig['app-plus']) || + (this.platform === 'web' && appConfig.h5) + ) { + throw new Error( + `Client platform is ${this.platform}, but ${this.platform === 'web' ? 'h5' : 'app-plus'} was found in config. Please refer to: https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary?id=m-to-co` + ) + } + + const defaultConfig = { + tokenExpiresIn: 7200, + tokenExpiresThreshold: 1200, + passwordErrorLimit: 6, + passwordErrorRetryTime: 3600 + } + return Object.assign(defaultConfig, appConfig, appConfig[platform]) + } + + getOauthProvider ({ + provider + } = {}) { + const clientPlatform = this.platform + let oatuhProivder = provider + if (provider === 'weixin' && clientPlatform === 'web') { + const weixinPlatform = getWeixinPlatform.call(this.context) + if (weixinPlatform === 'h5' || weixinPlatform === 'web') { + oatuhProivder = 'weixin-' + weixinPlatform // weixin-h5 公众号,weixin-web pc端 + } + } + return oatuhProivder + } + + getOauthConfig ({ + provider + } = {}) { + const config = this.getPlatformConfig() + const clientPlatform = this.platform + const oatuhProivder = this.getOauthProvider({ + provider + }) + const requireConfigKey = requiredConfig[`${clientPlatform}.${oatuhProivder}`] || [] + if (!config.oauth || !config.oauth[oatuhProivder]) { + throw new Error(`Config param required: ${clientPlatform}.oauth.${oatuhProivder}`) + } + const oauthConfig = config.oauth[oatuhProivder] + requireConfigKey.forEach((item) => { + if (!oauthConfig[item]) { + throw new Error(`Config param required: ${clientPlatform}.oauth.${oatuhProivder}.${item}`) + } + }) + return oauthConfig + } + + getHooks () { + if (uniIdConfig.hasFile('hooks/index.js')) { + return require( + uniIdConfig.resolve('hooks/index.js') + ) + } + return {} + } +} + +module.exports = ConfigUtils diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/fission.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/fission.js new file mode 100644 index 0000000..84233c3 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/fission.js @@ -0,0 +1,192 @@ +const { + dbCmd, + userCollection +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') +/** + * 获取随机邀请码,邀请码由大写字母加数字组成,由于存在手动输入邀请码的场景,从可选字符中去除 0、1、I、O + * @param {number} len 邀请码长度,默认6位 + * @returns {string} 随机邀请码 + */ +function getRandomInviteCode (len = 6) { + const charArr = ['2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'] + let code = '' + for (let i = 0; i < len; i++) { + code += charArr[Math.floor(Math.random() * charArr.length)] + } + return code +} + +/** + * 获取可用的邀请码,至多尝试十次以获取可用邀请码。从10亿可选值中随机,碰撞概率较低 + * 也有其他方案可以尝试,比如在数据库内设置一个从0开始计数的数字,每次调用此方法时使用updateAndReturn使数字加1并返回加1后的值,根据这个值生成对应的邀请码,比如(22222A + 1 == 22222B),此方式性能理论更好,但是不适用于旧项目 + * @param {object} param + * @param {string} param.inviteCode 初始随机邀请码 + */ +async function getValidInviteCode () { + let retry = 10 + let code + let codeValid = false + while (retry > 0 && !codeValid) { + retry-- + code = getRandomInviteCode() + const getUserRes = await userCollection.where({ + my_invite_code: code + }).limit(1).get() + if (getUserRes.data.length === 0) { + codeValid = true + break + } + } + if (!codeValid) { + throw { + errCode: ERROR.SET_INVITE_CODE_FAILED + } + } + return code +} + +/** + * 根据邀请码查询邀请人 + * @param {object} param + * @param {string} param.inviteCode 邀请码 + * @param {string} param.queryUid 受邀人id,非空时校验不可被下家或自己邀请 + * @returns + */ +async function findUserByInviteCode ({ + inviteCode, + queryUid +} = {}) { + if (typeof inviteCode !== 'string') { + throw { + errCode: ERROR.SYSTEM_ERROR + } + } + // 根据邀请码查询邀请人 + let getInviterRes + if (queryUid) { + getInviterRes = await userCollection.where({ + _id: dbCmd.neq(queryUid), + inviter_uid: dbCmd.not(dbCmd.all([queryUid])), + my_invite_code: inviteCode + }).get() + } else { + getInviterRes = await userCollection.where({ + my_invite_code: inviteCode + }).get() + } + if (getInviterRes.data.length > 1) { + // 正常情况下不可能进入此条件,以防用户自行修改数据库出错,在此做出判断 + throw { + errCode: ERROR.SYSTEM_ERROR + } + } + const inviterRecord = getInviterRes.data[0] + if (!inviterRecord) { + throw { + errCode: ERROR.INVALID_INVITE_CODE + } + } + return inviterRecord +} + +/** + * 根据邀请码生成邀请信息 + * @param {object} param + * @param {string} param.inviteCode 邀请码 + * @param {string} param.queryUid 受邀人id,非空时校验不可被下家或自己邀请 + * @returns + */ +async function generateInviteInfo ({ + inviteCode, + queryUid +} = {}) { + const inviterRecord = await findUserByInviteCode({ + inviteCode, + queryUid + }) + // 倒叙拼接当前用户邀请链 + const inviterUid = inviterRecord.inviter_uid || [] + inviterUid.unshift(inviterRecord._id) + return { + inviterUid, + inviteTime: Date.now() + } +} + +/** + * 检查当前用户是否可以接受邀请,如果可以返回用户记录 + * @param {string} uid + */ +async function checkInviteInfo (uid) { + // 检查当前用户是否已有邀请人 + const getUserRes = await userCollection.doc(uid).field({ + my_invite_code: true, + inviter_uid: true + }).get() + const userRecord = getUserRes.data[0] + if (!userRecord) { + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS + } + } + if (userRecord.inviter_uid && userRecord.inviter_uid.length > 0) { + throw { + errCode: ERROR.CHANGE_INVITER_FORBIDDEN + } + } + return userRecord +} + +/** + * 指定用户接受邀请码邀请 + * @param {object} param + * @param {string} param.uid 用户uid + * @param {string} param.inviteCode 邀请人的邀请码 + * @returns + */ +async function acceptInvite ({ + uid, + inviteCode +} = {}) { + await checkInviteInfo(uid) + const { + inviterUid, + inviteTime + } = await generateInviteInfo({ + inviteCode, + queryUid: uid + }) + + if (inviterUid === uid) { + throw { + errCode: ERROR.INVALID_INVITE_CODE + } + } + + // 更新当前用户的邀请人信息 + await userCollection.doc(uid).update({ + inviter_uid: inviterUid, + invite_time: inviteTime + }) + + // 更新当前用户邀请的用户的邀请人信息,这步可能较为耗时 + await userCollection.where({ + inviter_uid: uid + }).update({ + inviter_uid: dbCmd.push(inviterUid) + }) + + return { + errCode: 0, + errMsg: '' + } +} + +module.exports = { + acceptInvite, + generateInviteInfo, + getValidInviteCode +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/login.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/login.js new file mode 100644 index 0000000..ea8532d --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/login.js @@ -0,0 +1,246 @@ +const { + findUser +} = require('./account') +const { + userCollection, + LOG_TYPE +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') +const { + logout +} = require('./logout') +const PasswordUtils = require('./password') + +async function realPreLogin (params = {}) { + const { + user + } = params + const appId = this.getUniversalClientInfo().appId + const { + total, + userMatched + } = await findUser({ + userQuery: user, + authorizedApp: appId + }) + if (userMatched.length === 0) { + if (total > 0) { + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS_IN_CURRENT_APP + } + } + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS + } + } else if (userMatched.length > 1) { + throw { + errCode: ERROR.ACCOUNT_CONFLICT + } + } + const userRecord = userMatched[0] + checkLoginUserRecord(userRecord) + return userRecord +} + +async function preLogin (params = {}) { + const { + user + } = params + try { + const user = await realPreLogin.call(this, params) + return user + } catch (error) { + await this.middleware.uniIdLog({ + success: false, + data: user, + type: LOG_TYPE.LOGIN + }) + throw error + } +} + +async function preLoginWithPassword (params = {}) { + const { + user, + password + } = params + try { + const userRecord = await realPreLogin.call(this, params) + const { + passwordErrorLimit, + passwordErrorRetryTime + } = this.config + const { + clientIP + } = this.getUniversalClientInfo() + // 根据ip地址,密码错误次数过多,锁定登录 + let loginIPLimit = userRecord.login_ip_limit || [] + // 清理无用记录 + loginIPLimit = loginIPLimit.filter(item => item.last_error_time > Date.now() - passwordErrorRetryTime * 1000) + let currentIPLimit = loginIPLimit.find(item => item.ip === clientIP) + if (currentIPLimit && currentIPLimit.error_times >= passwordErrorLimit) { + throw { + errCode: ERROR.PASSWORD_ERROR_EXCEED_LIMIT + } + } + const passwordUtils = new PasswordUtils({ + userRecord, + clientInfo: this.getUniversalClientInfo(), + passwordSecret: this.config.passwordSecret + }) + + const { + success: checkPasswordSuccess, + refreshPasswordInfo + } = passwordUtils.checkUserPassword({ + password + }) + if (!checkPasswordSuccess) { + // 更新用户ip对应的密码错误记录 + if (!currentIPLimit) { + currentIPLimit = { + ip: clientIP, + error_times: 1, + last_error_time: Date.now() + } + loginIPLimit.push(currentIPLimit) + } else { + currentIPLimit.error_times++ + currentIPLimit.last_error_time = Date.now() + } + await userCollection.doc(userRecord._id).update({ + login_ip_limit: loginIPLimit + }) + throw { + errCode: ERROR.PASSWORD_ERROR + } + } + const extraData = {} + if (refreshPasswordInfo) { + extraData.password = refreshPasswordInfo.passwordHash + extraData.password_secret_version = refreshPasswordInfo.version + } + + const currentIPLimitIndex = loginIPLimit.indexOf(currentIPLimit) + if (currentIPLimitIndex > -1) { + loginIPLimit.splice(currentIPLimitIndex, 1) + } + extraData.login_ip_limit = loginIPLimit + return { + user: userRecord, + extraData + } + } catch (error) { + await this.middleware.uniIdLog({ + success: false, + data: user, + type: LOG_TYPE.LOGIN + }) + throw error + } +} + +function checkLoginUserRecord (user) { + switch (user.status) { + case undefined: + case 0: + break + case 1: + throw { + errCode: ERROR.ACCOUNT_BANNED + } + case 2: + throw { + errCode: ERROR.ACCOUNT_AUDITING + } + case 3: + throw { + errCode: ERROR.ACCOUNT_AUDIT_FAILED + } + case 4: + throw { + errCode: ERROR.ACCOUNT_CLOSED + } + default: + break + } +} + +async function thirdPartyLogin (params = {}) { + const { + user + } = params + return { + mobileConfirmed: !!user.mobile_confirmed, + emailConfirmed: !!user.email_confirmed + } +} + +async function postLogin (params = {}) { + const { + user, + extraData, + isThirdParty = false + } = params + const { + clientIP + } = this.getUniversalClientInfo() + const uniIdToken = this.getUniversalUniIdToken() + const uid = user._id + const updateData = { + last_login_date: Date.now(), + last_login_ip: clientIP, + ...extraData + } + const createTokenRes = await this.uniIdCommon.createToken({ + uid + }) + + const { + errCode, + token, + tokenExpired + } = createTokenRes + if (errCode) { + throw createTokenRes + } + + if (uniIdToken) { + try { + await logout.call(this) + } catch (error) {} + } + + await userCollection.doc(uid).update(updateData) + await this.middleware.uniIdLog({ + data: { + user_id: uid + }, + type: LOG_TYPE.LOGIN + }) + return { + errCode: 0, + newToken: { + token, + tokenExpired + }, + uid, + ...( + isThirdParty + ? thirdPartyLogin({ + user + }) + : {} + ), + passwordConfirmed: !!user.password + } +} + +module.exports = { + preLogin, + postLogin, + checkLoginUserRecord, + preLoginWithPassword +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/logout.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/logout.js new file mode 100644 index 0000000..ddcbb97 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/logout.js @@ -0,0 +1,49 @@ +const { + dbCmd, + LOG_TYPE, + deviceCollection, + userCollection +} = require('../../common/constants') + +async function logout () { + const { + deviceId + } = this.getUniversalClientInfo() + const uniIdToken = this.getUniversalUniIdToken() + const payload = await this.uniIdCommon.checkToken( + uniIdToken, + { + autoRefresh: false + } + ) + if (payload.errCode) { + throw payload + } + const uid = payload.uid + + // 删除token + await userCollection.doc(uid).update({ + token: dbCmd.pull(uniIdToken) + }) + + // 仅当device表的device_id和user_id均对应时才进行更新 + await deviceCollection.where({ + device_id: deviceId, + user_id: uid + }).update({ + token_expired: 0 + }) + await this.middleware.uniIdLog({ + data: { + user_id: uid + }, + type: LOG_TYPE.LOGOUT + }) + return { + errCode: 0 + } +} + +module.exports = { + logout +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/password.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/password.js new file mode 100644 index 0000000..0e46757 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/password.js @@ -0,0 +1,261 @@ +const { + getType +} = require('../../common/utils') +const crypto = require('crypto') +const createConfig = require('uni-config-center') +const shareConfig = createConfig({ + pluginId: 'uni-id' +}) +let customPassword = {} +if (shareConfig.hasFile('custom-password.js')) { + customPassword = shareConfig.requireFile('custom-password.js') || {} +} + +const passwordAlgorithmMap = { + UNI_ID_HMAC_SHA1: 'hmac-sha1', + UNI_ID_HMAC_SHA256: 'hmac-sha256', + UNI_ID_CUSTOM: 'custom' +} + +const passwordAlgorithmKeyMap = Object.keys(passwordAlgorithmMap).reduce((res, item) => { + res[passwordAlgorithmMap[item]] = item + return res +}, {}) + +const passwordExtMethod = { + [passwordAlgorithmMap.UNI_ID_HMAC_SHA1]: { + verify ({ password }) { + const { password_secret_version: passwordSecretVersion } = this.userRecord + + const passwordSecret = this._getSecretByVersion({ + version: passwordSecretVersion + }) + + const { passwordHash } = this.encrypt({ + password, + passwordSecret + }) + + return passwordHash === this.userRecord.password + }, + encrypt ({ password, passwordSecret }) { + const { value: secret, version } = passwordSecret + const hmac = crypto.createHmac('sha1', secret.toString('ascii')) + + hmac.update(password) + + return { + passwordHash: hmac.digest('hex'), + version + } + } + }, + [passwordAlgorithmMap.UNI_ID_HMAC_SHA256]: { + verify ({ password }) { + const parse = this._parsePassword() + const passwordHash = crypto.createHmac(parse.algorithm, parse.salt).update(password).digest('hex') + + return passwordHash === parse.hash + }, + encrypt ({ password, passwordSecret }) { + const { version } = passwordSecret + + // 默认使用 sha256 加密算法 + const salt = crypto.randomBytes(10).toString('hex') + const sha256Hash = crypto.createHmac(passwordAlgorithmMap.UNI_ID_HMAC_SHA256.substring(5), salt).update(password).digest('hex') + const algorithm = passwordAlgorithmKeyMap[passwordAlgorithmMap.UNI_ID_HMAC_SHA256] + // B 为固定值,对应 PasswordMethodMaps 中的 sha256算法 + // hash 格式 $[PasswordMethodFlagMapsKey]$[salt size]$[salt][Hash] + const passwordHash = `$${algorithm}$${salt.length}$${salt}${sha256Hash}` + + return { + passwordHash, + version + } + } + }, + [passwordAlgorithmMap.UNI_ID_CUSTOM]: { + verify ({ password, passwordSecret }) { + if (!customPassword.verifyPassword) throw new Error('verifyPassword method not found in custom password file') + + // return true or false + return customPassword.verifyPassword({ + password, + passwordSecret, + userRecord: this.userRecord, + clientInfo: this.clientInfo + }) + }, + encrypt ({ password, passwordSecret }) { + if (!customPassword.encryptPassword) throw new Error('encryptPassword method not found in custom password file') + + // return object<{passwordHash: string, version: number}> + return customPassword.encryptPassword({ + password, + passwordSecret, + clientInfo: this.clientInfo + }) + } + } +} + +class PasswordUtils { + constructor ({ + userRecord = {}, + clientInfo, + passwordSecret + } = {}) { + if (!clientInfo) throw new Error('Invalid clientInfo') + if (!passwordSecret) throw new Error('Invalid password secret') + + this.clientInfo = clientInfo + this.userRecord = userRecord + this.passwordSecret = this.prePasswordSecret(passwordSecret) + } + + /** + * passwordSecret 预处理 + * @param passwordSecret + * @return {*[]} + */ + prePasswordSecret (passwordSecret) { + const newPasswordSecret = [] + if (getType(passwordSecret) === 'string') { + newPasswordSecret.push({ + value: passwordSecret, + type: passwordAlgorithmMap.UNI_ID_HMAC_SHA1 + }) + } else if (getType(passwordSecret) === 'array') { + for (const secret of passwordSecret.sort((a, b) => a.version - b.version)) { + newPasswordSecret.push({ + ...secret, + // 没有 type 设置默认 type hmac-sha1 + type: secret.type || passwordAlgorithmMap.UNI_ID_HMAC_SHA1 + }) + } + } else { + throw new Error('Invalid password secret') + } + + return newPasswordSecret + } + + /** + * 获取最新加密密钥 + * @return {*} + * @private + */ + _getLastestSecret () { + return this.passwordSecret[this.passwordSecret.length - 1] + } + + _getOldestSecret () { + return this.passwordSecret[0] + } + + _getSecretByVersion ({ version } = {}) { + if (!version && version !== 0) { + return this._getOldestSecret() + } + if (this.passwordSecret.length === 1) { + return this.passwordSecret[0] + } + return this.passwordSecret.find(item => item.version === version) + } + + /** + * 获取密码的验证/加密方法 + * @param passwordSecret + * @return {*[]} + * @private + */ + _getPasswordExt (passwordSecret) { + const ext = passwordExtMethod[passwordSecret.type] + if (!ext) { + throw new Error(`暂不支持 ${passwordSecret.type} 类型的加密算法`) + } + + const passwordExt = Object.create(null) + + for (const key in ext) { + passwordExt[key] = ext[key].bind(Object.assign(this, Object.keys(ext).reduce((res, item) => { + if (item !== key) { + res[item] = ext[item].bind(this) + } + return res + }, {}))) + } + + return passwordExt + } + + _parsePassword () { + const [algorithmKey = '', cost = 0, hashStr = ''] = this.userRecord.password.split('$').filter(key => key) + const algorithm = passwordAlgorithmMap[algorithmKey] ? passwordAlgorithmMap[algorithmKey].substring(5) : null + const salt = hashStr.substring(0, Number(cost)) + const hash = hashStr.substring(Number(cost)) + + return { + algorithm, + salt, + hash + } + } + + /** + * 生成加密后的密码 + * @param {String} password 密码 + */ + generatePasswordHash ({ password }) { + if (!password) throw new Error('Invalid password') + + const passwordSecret = this._getLastestSecret() + const ext = this._getPasswordExt(passwordSecret) + + const { passwordHash, version } = ext.encrypt({ + password, + passwordSecret + }) + + return { + passwordHash, + version + } + } + + /** + * 密码校验 + * @param {String} password + * @param {Boolean} autoRefresh + * @return {{refreshPasswordInfo: {version: *, passwordHash: *}, success: boolean}|{success: boolean}} + */ + checkUserPassword ({ password, autoRefresh = true }) { + if (!password) throw new Error('Invalid password') + + const { password_secret_version: passwordSecretVersion } = this.userRecord + const passwordSecret = this._getSecretByVersion({ + version: passwordSecretVersion + }) + const ext = this._getPasswordExt(passwordSecret) + + const success = ext.verify({ password, passwordSecret }) + + if (!success) { + return { + success: false + } + } + + let refreshPasswordInfo + if (autoRefresh && passwordSecretVersion !== this._getLastestSecret().version) { + refreshPasswordInfo = this.generatePasswordHash({ password }) + } + + return { + success: true, + refreshPasswordInfo + } + } +} + +module.exports = PasswordUtils diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/qq.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/qq.js new file mode 100644 index 0000000..7ea612d --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/qq.js @@ -0,0 +1,152 @@ +const { + userCollection +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') + +function getQQPlatform () { + const platform = this.clientPlatform + switch (platform) { + case 'app': + case 'app-plus': + return 'app' + case 'mp-qq': + return 'mp' + default: + throw new Error('Unsupported qq platform') + } +} + +async function saveQQUserKey ({ + openid, + sessionKey, // QQ小程序用户sessionKey + accessToken, // App端QQ用户accessToken + accessTokenExpired // App端QQ用户accessToken过期时间 +} = {}) { + // 微信公众平台、开放平台refreshToken有效期均为30天(微信没有在网络请求里面返回30天这个值,务必注意未来可能出现调整,需及时更新此处逻辑)。 + // 此前QQ开放平台有调整过accessToken的过期时间:[access_token有效期由90天缩短至30天](https://wiki.connect.qq.com/%E3%80%90qq%E4%BA%92%E8%81%94%E3%80%91access_token%E6%9C%89%E6%95%88%E6%9C%9F%E8%B0%83%E6%95%B4) + const appId = this.getUniversalClientInfo().appId + const qqPlatform = getQQPlatform.call(this) + const keyObj = { + dcloudAppid: appId, + openid, + platform: 'qq-' + qqPlatform + } + switch (qqPlatform) { + case 'mp': + await this.uniOpenBridge.setSessionKey(keyObj, { + session_key: sessionKey + }, 30 * 24 * 60 * 60) + break + case 'app': + case 'h5': + case 'web': + await this.uniOpenBridge.setUserAccessToken(keyObj, { + access_token: accessToken, + access_token_expired: accessTokenExpired + }, accessTokenExpired + ? Math.floor((accessTokenExpired - Date.now()) / 1000) + : 30 * 24 * 60 * 60 + ) + break + default: + break + } +} + +function generateQQCache ({ + sessionKey, // QQ小程序用户sessionKey + accessToken, // App端QQ用户accessToken + accessTokenExpired // App端QQ用户accessToken过期时间 +} = {}) { + const platform = getQQPlatform.call(this) + let cache + switch (platform) { + case 'app': + cache = { + access_token: accessToken, + access_token_expired: accessTokenExpired + } + break + case 'mp': + cache = { + session_key: sessionKey + } + break + default: + throw new Error('Unsupported qq platform') + } + return { + third_party: { + [`${platform}_qq`]: cache + } + } +} + +function getQQOpenid ({ + userRecord +} = {}) { + const qqPlatform = getQQPlatform.call(this) + const appId = this.getUniversalClientInfo().appId + const qqOpenidObj = userRecord.qq_openid + if (!qqOpenidObj) { + return + } + return qqOpenidObj[`${qqPlatform}_${appId}`] || qqOpenidObj[qqPlatform] +} + +async function getQQCacheFallback ({ + userRecord, + key +} = {}) { + const platform = getQQPlatform.call(this) + const thirdParty = userRecord && userRecord.third_party + if (!thirdParty) { + return + } + const qqCache = thirdParty[`${platform}_qq`] + return qqCache && qqCache[key] +} + +async function getQQCache ({ + uid, + userRecord, + key +} = {}) { + const qqPlatform = getQQPlatform.call(this) + const appId = this.getUniversalClientInfo().appId + + if (!userRecord) { + const getUserRes = await userCollection.doc(uid).get() + userRecord = getUserRes.data[0] + } + if (!userRecord) { + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS + } + } + const openid = getQQOpenid.call(this, { + userRecord + }) + const getCacheMethod = qqPlatform === 'mp' ? 'getSessionKey' : 'getUserAccessToken' + const userKey = await this.uniOpenBridge[getCacheMethod]({ + dcloudAppid: appId, + platform: 'qq-' + qqPlatform, + openid + }) + if (userKey) { + return userKey[key] + } + return getQQCacheFallback({ + userRecord, + key + }) +} + +module.exports = { + getQQPlatform, + generateQQCache, + getQQCache, + saveQQUserKey +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/register.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/register.js new file mode 100644 index 0000000..d937610 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/register.js @@ -0,0 +1,229 @@ +const { + userCollection, + LOG_TYPE +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') +const { + findUser +} = require('./account') +const { + getValidInviteCode, + generateInviteInfo +} = require('./fission') +const { + logout +} = require('./logout') +const PasswordUtils = require('./password') +const merge = require('lodash.merge') + +async function realPreRegister (params = {}) { + const { + user + } = params + const { + userMatched + } = await findUser({ + userQuery: user, + authorizedApp: this.getUniversalClientInfo().appId + }) + if (userMatched.length > 0) { + throw { + errCode: ERROR.ACCOUNT_EXISTS + } + } +} + +async function preRegister (params = {}) { + try { + await realPreRegister.call(this, params) + } catch (error) { + await this.middleware.uniIdLog({ + success: false, + type: LOG_TYPE.REGISTER + }) + throw error + } +} + +async function preRegisterWithPassword (params = {}) { + const { + user, + password + } = params + await preRegister.call(this, { + user + }) + const passwordUtils = new PasswordUtils({ + clientInfo: this.getUniversalClientInfo(), + passwordSecret: this.config.passwordSecret + }) + const { + passwordHash, + version + } = passwordUtils.generatePasswordHash({ + password + }) + const extraData = { + password: passwordHash, + password_secret_version: version + } + return { + user, + extraData + } +} + +async function thirdPartyRegister ({ + user = {} +} = {}) { + return { + mobileConfirmed: !!(user.mobile && user.mobile_confirmed) || false, + emailConfirmed: !!(user.email && user.email_confirmed) || false + } +} + +async function postRegister (params = {}) { + const { + user, + extraData = {}, + isThirdParty = false, + inviteCode + } = params + const { + appId, + appName, + appVersion, + appVersionCode, + channel, + scene, + clientIP, + osName + } = this.getUniversalClientInfo() + const uniIdToken = this.getUniversalUniIdToken() + + merge(user, extraData) + + const registerChannel = channel || scene + user.register_env = { + appid: appId || '', + uni_platform: this.clientPlatform || '', + os_name: osName || '', + app_name: appName || '', + app_version: appVersion || '', + app_version_code: appVersionCode || '', + channel: registerChannel ? registerChannel + '' : '', // channel可能为数字,统一存为字符串 + client_ip: clientIP || '' + } + + user.register_date = Date.now() + user.dcloud_appid = [appId] + + if (user.username) { + user.username = user.username.toLowerCase() + } + if (user.email) { + user.email = user.email.toLowerCase() + } + + const { + autoSetInviteCode, // 注册时自动设置邀请码 + forceInviteCode, // 必须有邀请码才允许注册,注意此逻辑不可对admin生效 + userRegisterDefaultRole // 用户注册时配置的默认角色 + } = this.config + if (autoSetInviteCode) { + user.my_invite_code = await getValidInviteCode() + } + + // 如果用户注册默认角色配置存在且不为空数组 + if (userRegisterDefaultRole && userRegisterDefaultRole.length) { + // 将用户已有的角色和配置的默认角色合并成一个数组,并去重 + user.role = Array.from(new Set([...(user.role || []), ...userRegisterDefaultRole])) + } + + const isAdmin = user.role && user.role.includes('admin') + + if (forceInviteCode && !isAdmin && !inviteCode) { + throw { + errCode: ERROR.INVALID_INVITE_CODE + } + } + + if (inviteCode) { + const { + inviterUid, + inviteTime + } = await generateInviteInfo({ + inviteCode + }) + user.inviter_uid = inviterUid + user.invite_time = inviteTime + } + + if (uniIdToken) { + try { + await logout.call(this) + } catch (error) { } + } + + const beforeRegister = this.hooks.beforeRegister + let userRecord = user + if (beforeRegister) { + userRecord = await beforeRegister({ + userRecord, + clientInfo: this.getUniversalClientInfo() + }) + } + + const { + id: uid + } = await userCollection.add(userRecord) + + const createTokenRes = await this.uniIdCommon.createToken({ + uid + }) + + const { + errCode, + token, + tokenExpired + } = createTokenRes + + if (errCode) { + throw createTokenRes + } + + await this.middleware.uniIdLog({ + data: { + user_id: uid + }, + type: LOG_TYPE.REGISTER + }) + + return { + errCode: 0, + uid, + newToken: { + token, + tokenExpired + }, + ...( + isThirdParty + ? thirdPartyRegister({ + user: { + ...userRecord, + _id: uid + } + }) + : {} + ), + passwordConfirmed: !!userRecord.password + } +} + +module.exports = { + preRegister, + preRegisterWithPassword, + postRegister +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/relate.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/relate.js new file mode 100644 index 0000000..8431551 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/relate.js @@ -0,0 +1,164 @@ +const { + findUser +} = require('./account') +const { + ERROR +} = require('../../common/error') +const { + userCollection, dbCmd, USER_IDENTIFIER +} = require('../../common/constants') +const { + getUserIdentifier +} = require('../../lib/utils/account') + +const { + batchFindObjctValue +} = require('../../common/utils') +const merge = require('lodash.merge') + +/** + * + * @param {object} param + * @param {string} param.uid 用户id + * @param {string} param.bindAccount 要绑定的三方账户、手机号或邮箱 + */ +async function preBind ({ + uid, + bindAccount, + logType +} = {}) { + const { + userMatched + } = await findUser({ + userQuery: bindAccount, + authorizedApp: this.getUniversalClientInfo().appId + }) + if (userMatched.length > 0) { + await this.middleware.uniIdLog({ + data: { + user_id: uid + }, + type: logType, + success: false + }) + throw { + errCode: ERROR.BIND_CONFLICT + } + } +} + +async function postBind ({ + uid, + extraData = {}, + bindAccount, + logType +} = {}) { + await userCollection.doc(uid).update(merge(bindAccount, extraData)) + await this.middleware.uniIdLog({ + data: { + user_id: uid + }, + type: logType + }) + return { + errCode: 0 + } +} + +async function preUnBind ({ + uid, + unBindAccount, + logType +}) { + const notUnBind = ['username', 'mobile', 'email'] + const userIdentifier = getUserIdentifier(unBindAccount) + const condition = Object.keys(userIdentifier).reduce((res, key) => { + if (userIdentifier[key]) { + if (notUnBind.includes(key)) { + throw { + errCode: ERROR.UNBIND_NOT_SUPPORTED + } + } + + res.push({ + [key]: userIdentifier[key] + }) + } + + return res + }, []) + const currentUnBindAccount = Object.keys(userIdentifier).reduce((res, key) => { + if (userIdentifier[key]) { + res.push(key) + } + return res + }, []) + const { data: users } = await userCollection.where(dbCmd.and( + { _id: uid }, + dbCmd.or(condition) + )).get() + + if (users.length <= 0) { + await this.middleware.uniIdLog({ + data: { + user_id: uid + }, + type: logType, + success: false + }) + throw { + errCode: ERROR.UNBIND_FAIL + } + } + + const [user] = users + const otherAccounts = batchFindObjctValue(user, Object.keys(USER_IDENTIFIER).filter(key => !notUnBind.includes(key) && !currentUnBindAccount.includes(key))) + let hasOtherAccountBind = false + + for (const key in otherAccounts) { + if (otherAccounts[key]) { + hasOtherAccountBind = true + break + } + } + + // 如果没有其他第三方登录方式 + if (!hasOtherAccountBind) { + // 存在用户名或者邮箱但是没有设置过没密码就提示设置密码 + if ((user.username || user.email) && !user.password) { + throw { + errCode: ERROR.UNBIND_PASSWORD_NOT_EXISTS + } + } + // 账号任何登录方式都没有就优先绑定手机号 + if (!user.mobile) { + throw { + errCode: ERROR.UNBIND_MOBILE_NOT_EXISTS + } + } + } +} + +async function postUnBind ({ + uid, + unBindAccount, + logType +}) { + await userCollection.doc(uid).update(unBindAccount) + await this.middleware.uniIdLog({ + data: { + user_id: uid + }, + type: logType + }) + return { + errCode: 0 + } +} + +module.exports = { + preBind, + postBind, + preUnBind, + postUnBind +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/sms.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/sms.js new file mode 100644 index 0000000..21c70e9 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/sms.js @@ -0,0 +1,81 @@ +const { + setMobileVerifyCode +} = require('./verify-code') +const { + getVerifyCode +} = require('../../common/utils') + +/** + * 发送短信 + * @param {object} param + * @param {string} param.mobile 手机号 + * @param {object} param.code 可选,验证码 + * @param {object} param.scene 短信场景 + * @param {object} param.templateId 可选,短信模板id + * @returns + */ +async function sendSmsCode ({ + mobile, + code, + scene, + templateId +} = {}) { + const requiredParams = [ + 'name', + 'smsKey', + 'smsSecret', + 'codeExpiresIn' + ] + const smsConfig = (this.config.service && this.config.service.sms) || {} + for (let i = 0; i < requiredParams.length; i++) { + const key = requiredParams[i] + if (!smsConfig[key]) { + throw new Error(`Missing config param: service.sms.${key}`) + } + } + if (!code) { + code = getVerifyCode() + } + let action + switch (scene) { + case 'login-by-sms': + action = this.t('login') + break + default: + action = this.t('verify-mobile') + break + } + const sceneConfig = (smsConfig.scene || {})[scene] || {} + if (!templateId) { + templateId = sceneConfig.templateId + } + if (!templateId) { + throw new Error('"templateId" is required') + } + const codeExpiresIn = sceneConfig.codeExpiresIn || smsConfig.codeExpiresIn + await setMobileVerifyCode.call(this, { + mobile, + code, + expiresIn: codeExpiresIn, + scene + }) + await uniCloud.sendSms({ + smsKey: smsConfig.smsKey, + smsSecret: smsConfig.smsSecret, + phone: mobile, + templateId, + data: { + name: smsConfig.name, + code, + action, + expMinute: '' + Math.round(codeExpiresIn / 60) + } + }) + return { + errCode: 0 + } +} + +module.exports = { + sendSmsCode +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/unified-login.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/unified-login.js new file mode 100644 index 0000000..eac7a51 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/unified-login.js @@ -0,0 +1,106 @@ +const { + checkLoginUserRecord, + postLogin +} = require('./login') +const { + postRegister +} = require('./register') +const { + findUser +} = require('./account') +const { + ERROR +} = require('../../common/error') + +async function realPreUnifiedLogin (params = {}) { + const { + user, + type + } = params + const appId = this.getUniversalClientInfo().appId + const { + total, + userMatched + } = await findUser({ + userQuery: user, + authorizedApp: appId + }) + if (userMatched.length === 0) { + if (type === 'login') { + if (total > 0) { + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS_IN_CURRENT_APP + } + } + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS + } + } + return { + type: 'register', + user + } + } if (userMatched.length === 1) { + if (type === 'register') { + throw { + errCode: ERROR.ACCOUNT_EXISTS + } + } + const userRecord = userMatched[0] + checkLoginUserRecord(userRecord) + return { + type: 'login', + user: userRecord + } + } else if (userMatched.length > 1) { + throw { + errCode: ERROR.ACCOUNT_CONFLICT + } + } +} + +async function preUnifiedLogin (params = {}) { + try { + const result = await realPreUnifiedLogin.call(this, params) + return result + } catch (error) { + await this.middleware.uniIdLog({ + success: false + }) + throw error + } +} + +async function postUnifiedLogin (params = {}) { + const { + user, + extraData = {}, + isThirdParty = false, + type, + inviteCode + } = params + let result + if (type === 'login') { + result = await postLogin.call(this, { + user, + extraData, + isThirdParty + }) + } else if (type === 'register') { + result = await postRegister.call(this, { + user, + extraData, + isThirdParty, + inviteCode + }) + } + return { + ...result, + type + } +} + +module.exports = { + preUnifiedLogin, + postUnifiedLogin +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/univerify.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/univerify.js new file mode 100644 index 0000000..33c17c0 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/univerify.js @@ -0,0 +1,27 @@ +async function getPhoneNumber ({ + // eslint-disable-next-line camelcase + access_token, + openid +} = {}) { + const requiredParams = ['apiKey', 'apiSecret'] + const univerifyConfig = (this.config.service && this.config.service.univerify) || {} + for (let i = 0; i < requiredParams.length; i++) { + const key = requiredParams[i] + if (!univerifyConfig[key]) { + throw new Error(`Missing config param: service.univerify.${key}`) + } + } + return uniCloud.getPhoneNumber({ + provider: 'univerify', + appid: this.getUniversalClientInfo().appId, + apiKey: univerifyConfig.apiKey, + apiSecret: univerifyConfig.apiSecret, + // eslint-disable-next-line camelcase + access_token, + openid + }) +} + +module.exports = { + getPhoneNumber +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/update-user-info.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/update-user-info.js new file mode 100644 index 0000000..ced33b9 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/update-user-info.js @@ -0,0 +1,25 @@ +const { + userCollection +} = require('../../common/constants') +const { + USER_STATUS +} = require('../../common/constants') +async function setUserStatus (uid, status) { + const updateData = { + status + } + if (status !== USER_STATUS.NORMAL) { + updateData.valid_token_date = Date.now() + } + await userCollection.doc(uid).update({ + status + }) + // TODO 此接口尚不完善,例如注销后其他客户端可能存在有效token,支持Redis后此处会补充额外逻辑 + return { + errCode: 0 + } +} + +module.exports = { + setUserStatus +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/utils.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/utils.js new file mode 100644 index 0000000..7d3e0f3 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/utils.js @@ -0,0 +1,18 @@ +let redisEnable = null +function getRedisEnable() { + // 未用到的时候不调用redis接口,节省一些连接数 + if (redisEnable !== null) { + return redisEnable + } + try { + uniCloud.redis() + redisEnable = true + } catch (error) { + redisEnable = false + } + return redisEnable +} + +module.exports = { + getRedisEnable +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/verify-code.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/verify-code.js new file mode 100644 index 0000000..b11bc02 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/verify-code.js @@ -0,0 +1,152 @@ +const { + dbCmd, + verifyCollection +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') +const { + getVerifyCode +} = require('../../common/utils') + +async function setVerifyCode ({ + mobile, + email, + code, + expiresIn, + scene +} = {}) { + const now = Date.now() + const record = { + mobile, + email, + scene, + code: code || getVerifyCode(), + state: 0, + ip: this.getUniversalClientInfo().clientIP, + created_date: now, + expired_date: now + expiresIn * 1000 + } + await verifyCollection.add(record) + return { + errCode: 0 + } +} + +async function setEmailVerifyCode ({ + email, + code, + expiresIn, + scene +} = {}) { + email = email && email.trim() + if (!email) { + throw { + errCode: ERROR.INVALID_EMAIL + } + } + email = email.toLowerCase() + return setVerifyCode.call(this, { + email, + code, + expiresIn, + scene + }) +} + +async function setMobileVerifyCode ({ + mobile, + code, + expiresIn, + scene +} = {}) { + mobile = mobile && mobile.trim() + if (!mobile) { + throw { + errCode: ERROR.INVALID_MOBILE + } + } + return setVerifyCode.call(this, { + mobile, + code, + expiresIn, + scene + }) +} + +async function verifyEmailCode ({ + email, + code, + scene +} = {}) { + email = email && email.trim() + if (!email) { + throw { + errCode: ERROR.INVALID_EMAIL + } + } + email = email.toLowerCase() + const { + data: codeRecord + } = await verifyCollection.where({ + email, + scene, + code, + state: 0, + expired_date: dbCmd.gt(Date.now()) + }).limit(1).get() + + if (codeRecord.length === 0) { + throw { + errCode: ERROR.EMAIL_VERIFY_CODE_ERROR + } + } + await verifyCollection.doc(codeRecord[0]._id).update({ + state: 1 + }) + return { + errCode: 0 + } +} + +async function verifyMobileCode ({ + mobile, + code, + scene +} = {}) { + mobile = mobile && mobile.trim() + if (!mobile) { + throw { + errCode: ERROR.INVALID_MOBILE + } + } + const { + data: codeRecord + } = await verifyCollection.where({ + mobile, + scene, + code, + state: 0, + expired_date: dbCmd.gt(Date.now()) + }).limit(1).get() + + if (codeRecord.length === 0) { + throw { + errCode: ERROR.MOBILE_VERIFY_CODE_ERROR + } + } + + await verifyCollection.doc(codeRecord[0]._id).update({ + state: 1 + }) + return { + errCode: 0 + } +} + +module.exports = { + verifyEmailCode, + verifyMobileCode, + setEmailVerifyCode, + setMobileVerifyCode +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/weixin.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/weixin.js new file mode 100644 index 0000000..98eb370 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/lib/utils/weixin.js @@ -0,0 +1,234 @@ +const crypto = require('crypto') +const { + userCollection +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') +const { + getRedisEnable +} = require('./utils') +const { + openDataCollection +} = require('../../common/constants') + +function decryptWeixinData ({ + encryptedData, + sessionKey, + iv +} = {}) { + const oauthConfig = this.configUtils.getOauthConfig({ + provider: 'weixin' + }) + const decipher = crypto.createDecipheriv( + 'aes-128-cbc', + Buffer.from(sessionKey, 'base64'), + Buffer.from(iv, 'base64') + ) + // 设置自动 padding 为 true,删除填充补位 + decipher.setAutoPadding(true) + let decoded + decoded = decipher.update(encryptedData, 'base64', 'utf8') + decoded += decipher.final('utf8') + decoded = JSON.parse(decoded) + if (decoded.watermark.appid !== oauthConfig.appid) { + throw new Error('Invalid wechat appid in decode content') + } + return decoded +} + +function getWeixinPlatform () { + const platform = this.clientPlatform + const userAgent = this.getUniversalClientInfo().userAgent + switch (platform) { + case 'app': + case 'app-plus': + return 'app' + case 'mp-weixin': + return 'mp' + case 'h5': + case 'web': + return userAgent.indexOf('MicroMessenger') > -1 ? 'h5' : 'web' + default: + throw new Error('Unsupported weixin platform') + } +} + +async function saveWeixinUserKey ({ + openid, + sessionKey, // 微信小程序用户sessionKey + accessToken, // App端微信用户accessToken + refreshToken, // App端微信用户refreshToken + accessTokenExpired // App端微信用户accessToken过期时间 +} = {}) { + // 微信公众平台、开放平台refreshToken有效期均为30天(微信没有在网络请求里面返回30天这个值,务必注意未来可能出现调整,需及时更新此处逻辑)。 + // 此前QQ开放平台有调整过accessToken的过期时间:[access_token有效期由90天缩短至30天](https://wiki.connect.qq.com/%E3%80%90qq%E4%BA%92%E8%81%94%E3%80%91access_token%E6%9C%89%E6%95%88%E6%9C%9F%E8%B0%83%E6%95%B4) + + const appId = this.getUniversalClientInfo().appId + const weixinPlatform = getWeixinPlatform.call(this) + const keyObj = { + dcloudAppid: appId, + openid, + platform: 'weixin-' + weixinPlatform + } + switch (weixinPlatform) { + case 'mp': + await this.uniOpenBridge.setSessionKey(keyObj, { + session_key: sessionKey + }, 30 * 24 * 60 * 60) + break + case 'app': + case 'h5': + case 'web': + await this.uniOpenBridge.setUserAccessToken(keyObj, { + access_token: accessToken, + refresh_token: refreshToken, + access_token_expired: accessTokenExpired + }, 30 * 24 * 60 * 60) + break + default: + break + } +} + +async function saveSecureNetworkCache ({ + code, + openid, + unionid, + sessionKey +}) { + const { + appId + } = this.getUniversalClientInfo() + const key = `uni-id:${appId}:weixin-mp:code:${code}:secure-network-cache` + const value = JSON.stringify({ + openid, + unionid, + session_key: sessionKey + }) + // 此处存储的是code的缓存,设置有效期和token一致 + const expiredSeconds = this.config.tokenExpiresIn || 3 * 24 * 60 * 60 + + await openDataCollection.doc(key).set({ + value, + expired: Date.now() + expiredSeconds * 1000 + }) + const isRedisEnable = getRedisEnable() + if (isRedisEnable) { + const redis = uniCloud.redis() + await redis.set(key, value, 'EX', expiredSeconds) + } +} + +function generateWeixinCache ({ + sessionKey, // 微信小程序用户sessionKey + accessToken, // App端微信用户accessToken + refreshToken, // App端微信用户refreshToken + accessTokenExpired // App端微信用户accessToken过期时间 +} = {}) { + const platform = getWeixinPlatform.call(this) + let cache + switch (platform) { + case 'app': + case 'h5': + case 'web': + cache = { + access_token: accessToken, + refresh_token: refreshToken, + access_token_expired: accessTokenExpired + } + break + case 'mp': + cache = { + session_key: sessionKey + } + break + default: + throw new Error('Unsupported weixin platform') + } + return { + third_party: { + [`${platform}_weixin`]: cache + } + } +} + +function getWeixinOpenid ({ + userRecord +} = {}) { + const weixinPlatform = getWeixinPlatform.call(this) + const appId = this.getUniversalClientInfo().appId + const wxOpenidObj = userRecord.wx_openid + if (!wxOpenidObj) { + return + } + return wxOpenidObj[`${weixinPlatform}_${appId}`] || wxOpenidObj[weixinPlatform] +} + +async function getWeixinCacheFallback ({ + userRecord, + key +} = {}) { + const platform = getWeixinPlatform.call(this) + const thirdParty = userRecord && userRecord.third_party + if (!thirdParty) { + return + } + const weixinCache = thirdParty[`${platform}_weixin`] + return weixinCache && weixinCache[key] +} + +async function getWeixinCache ({ + uid, + userRecord, + key +} = {}) { + const weixinPlatform = getWeixinPlatform.call(this) + const appId = this.getUniversalClientInfo().appId + if (!userRecord) { + const getUserRes = await userCollection.doc(uid).get() + userRecord = getUserRes.data[0] + } + if (!userRecord) { + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS + } + } + const openid = getWeixinOpenid.call(this, { + userRecord + }) + const getCacheMethod = weixinPlatform === 'mp' ? 'getSessionKey' : 'getUserAccessToken' + const userKey = await this.uniOpenBridge[getCacheMethod]({ + dcloudAppid: appId, + platform: 'weixin-' + weixinPlatform, + openid + }) + if (userKey) { + return userKey[key] + } + return getWeixinCacheFallback({ + userRecord, + key + }) +} + +async function getWeixinAccessToken () { + const weixinPlatform = getWeixinPlatform.call(this) + const appId = this.getUniversalClientInfo().appId + + const cache = await this.uniOpenBridge.getAccessToken({ + dcloudAppid: appId, + platform: 'weixin-' + weixinPlatform + }) + + return cache.access_token +} +module.exports = { + decryptWeixinData, + getWeixinPlatform, + generateWeixinCache, + getWeixinCache, + saveWeixinUserKey, + getWeixinAccessToken, + saveSecureNetworkCache +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/access-control.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/access-control.js new file mode 100644 index 0000000..e333fe0 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/access-control.js @@ -0,0 +1,59 @@ +const methodPermission = require('../config/permission') +const { + ERROR +} = require('../common/error') + +function isAccessAllowed (user, setting) { + const { + role: userRole = [], + permission: userPermission = [] + } = user + const { + role: settingRole = [], + permission: settingPermission = [] + } = setting + if (userRole.includes('admin')) { + return + } + if ( + settingRole.length > 0 && + settingRole.every(item => !userRole.includes(item)) + ) { + throw { + errCode: ERROR.PERMISSION_ERROR + } + } + if ( + settingPermission.length > 0 && + settingPermission.every(item => !userPermission.includes(item)) + ) { + throw { + errCode: ERROR.PERMISSION_ERROR + } + } +} + +module.exports = async function () { + const methodName = this.getMethodName() + if (!(methodName in methodPermission)) { + return + } + const { + auth, + role, + permission + } = methodPermission[methodName] + if (auth || role || permission) { + await this.middleware.auth() + } + if (role && role.length === 0) { + throw new Error('[AccessControl]Empty role array is not supported') + } + if (permission && permission.length === 0) { + throw new Error('[AccessControl]Empty permission array is not supported') + } + return isAccessAllowed(this.authInfo, { + role, + permission + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/auth.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/auth.js new file mode 100644 index 0000000..1915335 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/auth.js @@ -0,0 +1,17 @@ +module.exports = async function () { + if (this.authInfo) { // 多次执行auth时如果第一次成功后续不再执行 + return + } + const token = this.getUniversalUniIdToken() + const payload = await this.uniIdCommon.checkToken(token) + if (payload.errCode) { + throw payload + } + this.authInfo = payload + if (payload.token) { + this.response.newToken = { + token: payload.token, + tokenExpired: payload.tokenExpired + } + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/index.js new file mode 100644 index 0000000..9f7c958 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/index.js @@ -0,0 +1,8 @@ +module.exports = { + auth: require('./auth'), + uniIdLog: require('./uni-id-log'), + validate: require('./validate'), + accessControl: require('./access-control'), + verifyRequestSign: require('./verify-request-sign'), + ...require('./rbac') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/rbac.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/rbac.js new file mode 100644 index 0000000..f42ef8d --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/rbac.js @@ -0,0 +1,39 @@ +const { + ERROR +} = require('../common/error') + +function hasRole (...roleList) { + const userRole = this.authInfo.role || [] + if (userRole.includes('admin')) { + return + } + const isMatch = roleList.every(roleItem => { + return userRole.includes(roleItem) + }) + if (!isMatch) { + throw { + errCode: ERROR.PERMISSION_ERROR + } + } +} + +function hasPermission (...permissionList) { + const userRole = this.authInfo.role || [] + const userPermission = this.authInfo.permission || [] + if (userRole.includes('admin')) { + return + } + const isMatch = permissionList.every(permissionItem => { + return userPermission.includes(permissionItem) + }) + if (!isMatch) { + throw { + errCode: ERROR.PERMISSION_ERROR + } + } +} + +module.exports = { + hasRole, + hasPermission +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/uni-id-log.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/uni-id-log.js new file mode 100644 index 0000000..ca6927d --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/uni-id-log.js @@ -0,0 +1,39 @@ +const db = uniCloud.database() +module.exports = async function ({ + data = {}, + success = true, + type = 'login' +} = {}) { + const now = Date.now() + const uniIdLogCollection = db.collection('uni-id-log') + const requiredDataKeyList = ['user_id', 'username', 'email', 'mobile'] + const dataCopy = {} + for (let i = 0; i < requiredDataKeyList.length; i++) { + const key = requiredDataKeyList[i] + if (key in data && typeof data[key] === 'string') { + dataCopy[key] = data[key] + } + } + const { + appId, + clientIP, + deviceId, + userAgent + } = this.getUniversalClientInfo() + const logData = { + appid: appId, + device_id: deviceId, + ip: clientIP, + type, + ua: userAgent, + create_date: now, + ...dataCopy + } + + if (success) { + logData.state = 1 + } else { + logData.state = 0 + } + return uniIdLogCollection.add(logData) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/validate.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/validate.js new file mode 100644 index 0000000..52ff047 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/validate.js @@ -0,0 +1,7 @@ +module.exports = function (value = {}, schema = {}) { + const validateRes = this.validator.validate(value, schema) + if (validateRes) { + delete validateRes.schemaKey + throw validateRes + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/verify-request-sign.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/verify-request-sign.js new file mode 100644 index 0000000..1c12edc --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/middleware/verify-request-sign.js @@ -0,0 +1,82 @@ +const crypto = require('crypto') +const createConfig = require('uni-config-center') +const { verifyHttpInfo } = require('uni-cloud-s2s') + +const { ERROR } = require('../common/error') +const s2sConfig = createConfig({ + pluginId: 'uni-cloud-s2s' +}) +const needSignFunctions = new Set([ + 'externalRegister', + 'externalLogin', + 'updateUserInfoByExternal' +]) + +module.exports = function () { + const methodName = this.getMethodName() + const { source } = this.getUniversalClientInfo() + + // 指定接口需要鉴权 + if (!needSignFunctions.has(methodName)) return + + // 非 HTTP 方式请求拒绝访问 + if (source !== 'http') { + throw { + errCode: ERROR.ILLEGAL_REQUEST + } + } + + // 支持 uni-cloud-s2s 验证请求 + if (s2sConfig.hasFile('config.json')) { + try { + if (!verifyHttpInfo(this.getHttpInfo())) { + throw { + errCode: ERROR.ILLEGAL_REQUEST + } + } + } catch (e) { + if (e.errSubject === 'uni-cloud-s2s') { + throw { + errCode: ERROR.ILLEGAL_REQUEST, + errMsg: e.errMsg + } + } + throw e + } + + return + } + + if (!this.config.requestAuthSecret || typeof this.config.requestAuthSecret !== 'string') { + throw { + errCode: ERROR.CONFIG_FIELD_REQUIRED, + errMsgValue: { + field: 'requestAuthSecret' + } + } + } + + const timeout = 20 * 1000 // 请求超过20秒不能再请求,防止重放攻击 + const { headers, body: _body } = this.getHttpInfo() + const { 'uni-id-nonce': nonce, 'uni-id-timestamp': timestamp, 'uni-id-signature': signature } = headers + const body = JSON.parse(_body).params || {} + const bodyStr = Object.keys(body) + .sort() + .filter(item => typeof body[item] !== 'object') + .map(item => `${item}=${body[item]}`) + .join('&') + + if (isNaN(Number(timestamp)) || (Number(timestamp) + timeout) < Date.now()) { + throw { + errCode: ERROR.ILLEGAL_REQUEST + } + } + + const reSignature = crypto.createHmac('sha256', `${this.config.requestAuthSecret + nonce}`).update(`${timestamp}${bodyStr}`).digest('hex') + + if (signature !== reSignature.toUpperCase()) { + throw { + errCode: ERROR.ILLEGAL_REQUEST + } + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/close-account.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/close-account.js new file mode 100644 index 0000000..f1bdf96 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/close-account.js @@ -0,0 +1,16 @@ +const { + setUserStatus +} = require('../../lib/utils/update-user-info') +const { + USER_STATUS +} = require('../../common/constants') + +/** + * 注销账户 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#close-account + * @returns + */ +module.exports = async function () { + const { uid } = this.authInfo + return setUserStatus(uid, USER_STATUS.CLOSED) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/get-account-info.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/get-account-info.js new file mode 100644 index 0000000..7b8599a --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/get-account-info.js @@ -0,0 +1,69 @@ +const { + userCollection +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') + +function isUsernameSet (userRecord) { + return !!userRecord.username +} +function isNicknameSet (userRecord) { + return !!userRecord.nickname +} +function isPasswordSet (userRecord) { + return !!userRecord.password +} +function isMobileBound (userRecord) { + return !!(userRecord.mobile && userRecord.mobile_confirmed) +} +function isEmailBound (userRecord) { + return !!(userRecord.email && userRecord.email_confirmed) +} +function isWeixinBound (userRecord) { + return !!( + userRecord.wx_unionid || + Object.keys(userRecord.wx_openid || {}).length + ) +} +function isQQBound (userRecord) { + return !!( + userRecord.qq_unionid || + Object.keys(userRecord.qq_openid || {}).length + ) +} +function isAlipayBound (userRecord) { + return !!userRecord.ali_openid +} +function isAppleBound (userRecord) { + return !!userRecord.apple_openid +} + +/** + * 获取账户账户简略信息 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-account-info + */ +module.exports = async function () { + const { + uid + } = this.authInfo + const getUserRes = await userCollection.doc(uid).get() + const userRecord = getUserRes && getUserRes.data && getUserRes.data[0] + if (!userRecord) { + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS + } + } + return { + errCode: 0, + isUsernameSet: isUsernameSet(userRecord), + isNicknameSet: isNicknameSet(userRecord), + isPasswordSet: isPasswordSet(userRecord), + isMobileBound: isMobileBound(userRecord), + isEmailBound: isEmailBound(userRecord), + isWeixinBound: isWeixinBound(userRecord), + isQQBound: isQQBound(userRecord), + isAlipayBound: isAlipayBound(userRecord), + isAppleBound: isAppleBound(userRecord) + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/get-realname-info.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/get-realname-info.js new file mode 100644 index 0000000..0ea8f05 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/get-realname-info.js @@ -0,0 +1,45 @@ +const { userCollection } = require('../../common/constants') +const { ERROR } = require('../../common/error') +const { decryptData } = require('../../common/sensitive-aes-cipher') +const { dataDesensitization } = require('../../common/utils') + +/** + * 获取实名信息 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-realname-info + * @param {Object} params + * @param {Boolean} params.decryptData 是否解密数据 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + decryptData: { + required: false, + type: 'boolean' + } + } + + this.middleware.validate(params, schema) + + const { decryptData: isDecryptData = true } = params + + const { + uid + } = this.authInfo + const getUserRes = await userCollection.doc(uid).get() + const userRecord = getUserRes && getUserRes.data && getUserRes.data[0] + if (!userRecord) { + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS + } + } + + const { realname_auth: realNameAuth = {} } = userRecord + + return { + errCode: 0, + type: realNameAuth.type, + authStatus: realNameAuth.auth_status, + realName: isDecryptData ? dataDesensitization(decryptData.call(this, realNameAuth.real_name), { onlyLast: true }) : realNameAuth.real_name, + identity: isDecryptData ? dataDesensitization(decryptData.call(this, realNameAuth.identity)) : realNameAuth.identity + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/index.js new file mode 100644 index 0000000..0e55385 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/index.js @@ -0,0 +1,9 @@ +module.exports = { + setPwd: require('./set-pwd'), + updatePwd: require('./update-pwd'), + resetPwdBySms: require('./reset-pwd-by-sms'), + resetPwdByEmail: require('./reset-pwd-by-email'), + closeAccount: require('./close-account'), + getAccountInfo: require('./get-account-info'), + getRealNameInfo: require('./get-realname-info') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/reset-pwd-by-email.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/reset-pwd-by-email.js new file mode 100644 index 0000000..20c6219 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/reset-pwd-by-email.js @@ -0,0 +1,128 @@ +const { + ERROR +} = require('../../common/error') +const { + getNeedCaptcha, + verifyCaptcha +} = require('../../lib/utils/captcha') +const { + verifyEmailCode +} = require('../../lib/utils/verify-code') +const { + userCollection, + EMAIL_SCENE, + CAPTCHA_SCENE, + LOG_TYPE +} = require('../../common/constants') +const { + findUser +} = require('../../lib/utils/account') +const PasswordUtils = require('../../lib/utils/password') + +/** + * 通过邮箱验证码重置密码 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#reset-pwd-by-email + * @param {object} params + * @param {string} params.email 邮箱 + * @param {string} params.code 邮箱验证码 + * @param {string} params.password 密码 + * @param {string} params.captcha 图形验证码 + * @returns {object} + */ +module.exports = async function (params = {}) { + const schema = { + email: 'email', + code: 'string', + password: 'password', + captcha: { + required: false, + type: 'string' + } + } + this.middleware.validate(params, schema) + const { + email, + code, + password, + captcha + } = params + + const needCaptcha = await getNeedCaptcha.call(this, { + email, + type: LOG_TYPE.RESET_PWD_BY_EMAIL + }) + if (needCaptcha) { + await verifyCaptcha.call(this, { + captcha, + scene: CAPTCHA_SCENE.RESET_PWD_BY_EMAIL + }) + } + try { + // 验证手机号验证码,验证不通过时写入失败日志 + await verifyEmailCode({ + email, + code, + scene: EMAIL_SCENE.RESET_PWD_BY_EMAIL + }) + } catch (error) { + await this.middleware.uniIdLog({ + data: { + email + }, + type: LOG_TYPE.RESET_PWD_BY_EMAIL, + success: false + }) + throw error + } + // 根据手机号查找匹配的用户 + const { + total, + userMatched + } = await findUser.call(this, { + userQuery: { + email + }, + authorizedApp: [this.getUniversalClientInfo().appId] + }) + if (userMatched.length === 0) { + if (total > 0) { + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS_IN_CURRENT_APP + } + } + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS + } + } else if (userMatched.length > 1) { + throw { + errCode: ERROR.ACCOUNT_CONFLICT + } + } + const { _id: uid } = userMatched[0] + const { + passwordHash, + version + } = new PasswordUtils({ + clientInfo: this.getUniversalClientInfo(), + passwordSecret: this.config.passwordSecret + }).generatePasswordHash({ + password + }) + // 更新用户密码 + await userCollection.doc(uid).update({ + password: passwordHash, + password_secret_version: version, + valid_token_date: Date.now() + }) + + // 写入成功日志 + await this.middleware.uniIdLog({ + data: { + email + }, + type: LOG_TYPE.RESET_PWD_BY_SMS + }) + return { + errCode: 0 + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/reset-pwd-by-sms.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/reset-pwd-by-sms.js new file mode 100644 index 0000000..bc10dc8 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/reset-pwd-by-sms.js @@ -0,0 +1,128 @@ +const { + ERROR +} = require('../../common/error') +const { + getNeedCaptcha, + verifyCaptcha +} = require('../../lib/utils/captcha') +const { + verifyMobileCode +} = require('../../lib/utils/verify-code') +const { + userCollection, + SMS_SCENE, + CAPTCHA_SCENE, + LOG_TYPE +} = require('../../common/constants') +const { + findUser +} = require('../../lib/utils/account') +const PasswordUtils = require('../../lib/utils/password') + +/** + * 通过短信验证码重置密码 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#reset-pwd-by-sms + * @param {object} params + * @param {string} params.mobile 手机号 + * @param {string} params.mobile 短信验证码 + * @param {string} params.password 密码 + * @param {string} params.captcha 图形验证码 + * @returns {object} + */ +module.exports = async function (params = {}) { + const schema = { + mobile: 'mobile', + code: 'string', + password: 'password', + captcha: { + required: false, + type: 'string' + } + } + this.middleware.validate(params, schema) + const { + mobile, + code, + password, + captcha + } = params + + const needCaptcha = await getNeedCaptcha.call(this, { + mobile, + type: LOG_TYPE.RESET_PWD_BY_SMS + }) + if (needCaptcha) { + await verifyCaptcha.call(this, { + captcha, + scene: CAPTCHA_SCENE.RESET_PWD_BY_SMS + }) + } + try { + // 验证手机号验证码,验证不通过时写入失败日志 + await verifyMobileCode({ + mobile, + code, + scene: SMS_SCENE.RESET_PWD_BY_SMS + }) + } catch (error) { + await this.middleware.uniIdLog({ + data: { + mobile + }, + type: LOG_TYPE.RESET_PWD_BY_SMS, + success: false + }) + throw error + } + // 根据手机号查找匹配的用户 + const { + total, + userMatched + } = await findUser.call(this, { + userQuery: { + mobile + }, + authorizedApp: [this.getUniversalClientInfo().appId] + }) + if (userMatched.length === 0) { + if (total > 0) { + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS_IN_CURRENT_APP + } + } + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS + } + } else if (userMatched.length > 1) { + throw { + errCode: ERROR.ACCOUNT_CONFLICT + } + } + const { _id: uid } = userMatched[0] + const { + passwordHash, + version + } = new PasswordUtils({ + clientInfo: this.getUniversalClientInfo(), + passwordSecret: this.config.passwordSecret + }).generatePasswordHash({ + password + }) + // 更新用户密码 + await userCollection.doc(uid).update({ + password: passwordHash, + password_secret_version: version, + valid_token_date: Date.now() + }) + + // 写入成功日志 + await this.middleware.uniIdLog({ + data: { + mobile + }, + type: LOG_TYPE.RESET_PWD_BY_SMS + }) + return { + errCode: 0 + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/set-pwd.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/set-pwd.js new file mode 100644 index 0000000..f33c6f4 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/set-pwd.js @@ -0,0 +1,83 @@ +const { userCollection, SMS_SCENE, LOG_TYPE, CAPTCHA_SCENE } = require('../../common/constants') +const { ERROR } = require('../../common/error') +const { verifyMobileCode } = require('../../lib/utils/verify-code') +const PasswordUtils = require('../../lib/utils/password') +const { getNeedCaptcha, verifyCaptcha } = require('../../lib/utils/captcha') + +module.exports = async function (params = {}) { + const schema = { + password: 'password', + code: 'string', + captcha: { + required: false, + type: 'string' + } + } + this.middleware.validate(params, schema) + + const { password, code, captcha } = params + const uid = this.authInfo.uid + const getUserRes = await userCollection.doc(uid).get() + const userRecord = getUserRes.data[0] + if (!userRecord) { + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS + } + } + + const needCaptcha = await getNeedCaptcha.call(this, { + mobile: userRecord.mobile + }) + + if (needCaptcha) { + await verifyCaptcha.call(this, { + captcha, + scene: CAPTCHA_SCENE.SET_PWD_BY_SMS + }) + } + + try { + // 验证手机号验证码,验证不通过时写入失败日志 + await verifyMobileCode({ + mobile: userRecord.mobile, + code, + scene: SMS_SCENE.SET_PWD_BY_SMS + }) + } catch (error) { + await this.middleware.uniIdLog({ + data: { + mobile: userRecord.mobile + }, + type: LOG_TYPE.SET_PWD_BY_SMS, + success: false + }) + throw error + } + + const { + passwordHash, + version + } = new PasswordUtils({ + clientInfo: this.getUniversalClientInfo(), + passwordSecret: this.config.passwordSecret + }).generatePasswordHash({ + password + }) + + // 更新用户密码 + await userCollection.doc(uid).update({ + password: passwordHash, + password_secret_version: version + }) + + await this.middleware.uniIdLog({ + data: { + mobile: userRecord.mobile + }, + type: LOG_TYPE.SET_PWD_BY_SMS + }) + + return { + errCode: 0 + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/update-pwd.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/update-pwd.js new file mode 100644 index 0000000..97fd1be --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/account/update-pwd.js @@ -0,0 +1,69 @@ +const { + userCollection +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') +const PasswordUtils = require('../../lib/utils/password') +/** + * 更新密码 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#update-pwd + * @param {object} params + * @param {string} params.oldPassword 旧密码 + * @param {string} params.newPassword 新密码 + * @returns {object} + */ +module.exports = async function (params = {}) { + const schema = { + oldPassword: 'string', // 防止密码规则调整导致旧密码无法更新 + newPassword: 'password' + } + this.middleware.validate(params, schema) + const uid = this.authInfo.uid + const getUserRes = await userCollection.doc(uid).get() + const userRecord = getUserRes.data[0] + if (!userRecord) { + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS + } + } + const { + oldPassword, + newPassword + } = params + const passwordUtils = new PasswordUtils({ + userRecord, + clientInfo: this.getUniversalClientInfo(), + passwordSecret: this.config.passwordSecret + }) + + const { + success: checkPasswordSuccess + } = passwordUtils.checkUserPassword({ + password: oldPassword, + autoRefresh: false + }) + + if (!checkPasswordSuccess) { + throw { + errCode: ERROR.PASSWORD_ERROR + } + } + + const { + passwordHash, + version + } = passwordUtils.generatePasswordHash({ + password: newPassword + }) + + await userCollection.doc(uid).update({ + password: passwordHash, + password_secret_version: version, + valid_token_date: Date.now() // refreshToken时会校验,如果创建token时间在此时间点之前,则拒绝下发新token,返回token失效错误码 + }) + // 执行更新密码操作后客户端应将用户退出重新登录 + return { + errCode: 0 + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/admin/add-user.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/admin/add-user.js new file mode 100644 index 0000000..330fd37 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/admin/add-user.js @@ -0,0 +1,131 @@ +const { + findUser +} = require('../../lib/utils/account') +const { + ERROR +} = require('../../common/error') +const { + userCollection +} = require('../../common/constants') +const PasswordUtils = require('../../lib/utils/password') + +/** + * 新增用户 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#add-user + * @param {Object} params + * @param {String} params.username 用户名 + * @param {String} params.password 密码 + * @param {String} params.nickname 昵称 + * @param {Array} params.authorizedApp 允许登录的AppID列表 + * @param {Array} params.role 用户角色列表 + * @param {String} params.mobile 手机号 + * @param {String} params.email 邮箱 + * @param {Array} params.tags 用户标签 + * @param {Number} params.status 用户状态 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + username: 'username', + password: 'password', + authorizedApp: { + required: false, + type: 'array' + }, // 指定允许登录的app,传空数组或不传时表示可以不可以在任何端登录 + nickname: { + required: false, + type: 'nickname' + }, + role: { + require: false, + type: 'array' + }, + mobile: { + required: false, + type: 'mobile' + }, + email: { + required: false, + type: 'email' + }, + tags: { + required: false, + type: 'array' + }, + status: { + required: false, + type: 'number' + } + } + this.middleware.validate(params, schema) + const { + username, + password, + authorizedApp, + nickname, + role, + mobile, + email, + tags, + status + } = params + const { + userMatched + } = await findUser({ + userQuery: { + username, + mobile, + email + }, + authorizedApp + }) + if (userMatched.length) { + throw { + errCode: ERROR.ACCOUNT_EXISTS + } + } + const passwordUtils = new PasswordUtils({ + clientInfo: this.getUniversalClientInfo(), + passwordSecret: this.config.passwordSecret + }) + const { + passwordHash, + version + } = passwordUtils.generatePasswordHash({ + password + }) + const data = { + username, + password: passwordHash, + password_secret_version: version, + dcloud_appid: authorizedApp || [], + nickname, + role: role || [], + mobile, + email, + tags: tags || [], + status + } + if (email) { + data.email_confirmed = 1 + } + if (mobile) { + data.mobile_confirmed = 1 + } + + // 触发 beforeRegister 钩子 + const beforeRegister = this.hooks.beforeRegister + let userRecord = data + if (beforeRegister) { + userRecord = await beforeRegister({ + userRecord, + clientInfo: this.getUniversalClientInfo() + }) + } + + await userCollection.add(userRecord) + return { + errCode: 0, + errMsg: '' + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/admin/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/admin/index.js new file mode 100644 index 0000000..c8830f5 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/admin/index.js @@ -0,0 +1,4 @@ +module.exports = { + addUser: require('./add-user'), + updateUser: require('./update-user') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/admin/update-user.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/admin/update-user.js new file mode 100644 index 0000000..ed2f7b6 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/admin/update-user.js @@ -0,0 +1,138 @@ +const { + findUser +} = require('../../lib/utils/account') +const { + ERROR +} = require('../../common/error') +const { + userCollection +} = require('../../common/constants') +const PasswordUtils = require('../../lib/utils/password') + +/** + * 修改用户 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#update-user + * @param {Object} params + * @param {String} params.uid 要更新的用户id + * @param {String} params.username 用户名 + * @param {String} params.password 密码 + * @param {String} params.nickname 昵称 + * @param {Array} params.authorizedApp 允许登录的AppID列表 + * @param {Array} params.role 用户角色列表 + * @param {String} params.mobile 手机号 + * @param {String} params.email 邮箱 + * @param {Array} params.tags 用户标签 + * @param {Number} params.status 用户状态 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + uid: 'string', + username: 'username', + password: { + required: false, + type: 'password' + }, + authorizedApp: { + required: false, + type: 'array' + }, // 指定允许登录的app,传空数组或不传时表示可以不可以在任何端登录 + nickname: { + required: false, + type: 'nickname' + }, + role: { + require: false, + type: 'array' + }, + mobile: { + required: false, + type: 'mobile' + }, + email: { + required: false, + type: 'email' + }, + tags: { + required: false, + type: 'array' + }, + status: { + required: false, + type: 'number' + } + } + + this.middleware.validate(params, schema) + + const { + uid, + username, + password, + authorizedApp, + nickname, + role, + mobile, + email, + tags, + status + } = params + + // 更新的用户数据字段 + const data = { + username, + dcloud_appid: authorizedApp, + nickname, + role, + mobile, + email, + tags, + status + } + + const realData = Object.keys(data).reduce((res, key) => { + const item = data[key] + if (item !== undefined) { + res[key] = item + } + return res + }, {}) + + // 更新用户名时验证用户名是否重新 + if (username) { + const { + userMatched + } = await findUser({ + userQuery: { + username + }, + authorizedApp + }) + if (userMatched.filter(user => user._id !== uid).length) { + throw { + errCode: ERROR.ACCOUNT_EXISTS + } + } + } + if (password) { + const passwordUtils = new PasswordUtils({ + clientInfo: this.getUniversalClientInfo(), + passwordSecret: this.config.passwordSecret + }) + const { + passwordHash, + version + } = passwordUtils.generatePasswordHash({ + password + }) + + realData.password = passwordHash + realData.password_secret_version = version + } + + await userCollection.doc(uid).update(realData) + + return { + errCode: 0 + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/dev/get-supported-login-type.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/dev/get-supported-login-type.js new file mode 100644 index 0000000..476e234 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/dev/get-supported-login-type.js @@ -0,0 +1,71 @@ +function isMobileCodeSupported () { + const config = this.config + return !!(config.service && config.service.sms && config.service.sms.smsKey) +} + +function isUniverifySupport () { + const config = this.config + return !!(config.service && config.service.univerify && config.service.univerify.apiKey) +} + +function isWeixinSupported () { + this.configUtils.getOauthConfig({ + provider: 'weixin' + }) + return true +} + +function isQQSupported () { + this.configUtils.getOauthConfig({ + provider: 'qq' + }) + return true +} + +function isAppleSupported () { + this.configUtils.getOauthConfig({ + provider: 'apple' + }) + return true +} + +function isAlipaySupported () { + this.configUtils.getOauthConfig({ + provider: 'alipay' + }) + return true +} + +const loginTypeTester = { + 'mobile-code': isMobileCodeSupported, + univerify: isUniverifySupport, + weixin: isWeixinSupported, + qq: isQQSupported, + apple: isAppleSupported, + alipay: isAlipaySupported +} + +/** + * 获取支持的登录方式 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-supported-login-type + * @returns + */ +module.exports = async function () { + const supportedLoginType = [ + 'username-password', + 'mobile-password', + 'email-password' + ] + for (const type in loginTypeTester) { + try { + if (loginTypeTester[type].call(this)) { + supportedLoginType.push(type) + } + } catch (error) { } + } + return { + errCode: 0, + errMsg: '', + supportedLoginType + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/dev/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/dev/index.js new file mode 100644 index 0000000..e22f9f2 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/dev/index.js @@ -0,0 +1,3 @@ +module.exports = { + getSupportedLoginType: require('./get-supported-login-type') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/external/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/external/index.js new file mode 100644 index 0000000..6fa597f --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/external/index.js @@ -0,0 +1,5 @@ +module.exports = { + externalRegister: require('./register'), + externalLogin: require('./login'), + updateUserInfoByExternal: require('./update-user-info') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/external/login.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/external/login.js new file mode 100644 index 0000000..af13013 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/external/login.js @@ -0,0 +1,68 @@ +const { preLogin, postLogin } = require('../../lib/utils/login') +const { EXTERNAL_DIRECT_CONNECT_PROVIDER } = require('../../common/constants') +const { ERROR } = require('../../common/error') + +/** + * 外部用户登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-login + * @param {object} params + * @param {string} params.uid uni-id体系用户id + * @param {string} params.externalUid 业务系统的用户id + * @returns {object} + */ +module.exports = async function (params = {}) { + const schema = { + uid: { + required: false, + type: 'string' + }, + externalUid: { + required: false, + type: 'string' + } + } + + this.middleware.validate(params, schema) + + const { + uid, + externalUid + } = params + + if (!uid && !externalUid) { + throw { + errCode: ERROR.PARAM_REQUIRED, + errMsgValue: { + param: 'uid or externalUid' + } + } + } + + let query + if (uid) { + query = { + _id: uid + } + } else { + query = { + identities: { + provider: EXTERNAL_DIRECT_CONNECT_PROVIDER, + uid: externalUid + } + } + } + + const user = await preLogin.call(this, { + user: query + }) + + const result = await postLogin.call(this, { + user + }) + + return { + errCode: result.errCode, + newToken: result.newToken, + uid: result.uid + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/external/register.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/external/register.js new file mode 100644 index 0000000..1b2279c --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/external/register.js @@ -0,0 +1,93 @@ +const url = require('url') +const { preRegister, postRegister } = require('../../lib/utils/register') +const { EXTERNAL_DIRECT_CONNECT_PROVIDER } = require('../../common/constants') + +/** + * 外部注册用户 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-register + * @param {object} params + * @param {string} params.externalUid 业务系统的用户id + * @param {string} params.nickname 昵称 + * @param {number} params.gender 性别 + * @param {string} params.avatar 头像 + * @returns {object} + */ +module.exports = async function (params = {}) { + const schema = { + externalUid: 'string', + nickname: { + required: false, + type: 'nickname' + }, + gender: { + required: false, + type: 'number' + }, + avatar: { + required: false, + type: 'string' + } + } + + this.middleware.validate(params, schema) + + const { + externalUid, + avatar, + gender, + nickname + } = params + + await preRegister.call(this, { + user: { + identities: { + provider: EXTERNAL_DIRECT_CONNECT_PROVIDER, + uid: externalUid + } + } + }) + + const extraData = {} + + if (avatar) { + // eslint-disable-next-line n/no-deprecated-api + const avatarPath = url.parse(avatar).pathname + const extName = avatarPath.indexOf('.') > -1 ? avatarPath.split('.').pop() : '' + + extraData.avatar_file = { + name: avatarPath, + extname: extName, + url: avatar + } + } + + const result = await postRegister.call(this, { + user: { + avatar, + gender, + nickname, + identities: [ + { + provider: EXTERNAL_DIRECT_CONNECT_PROVIDER, + userInfo: { + avatar, + gender, + nickname + }, + uid: externalUid + } + ] + }, + extraData + }) + + return { + errCode: result.errCode, + newToken: result.newToken, + externalUid, + avatar, + gender, + nickname, + uid: result.uid + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/external/update-user-info.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/external/update-user-info.js new file mode 100644 index 0000000..a91fe9f --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/external/update-user-info.js @@ -0,0 +1,208 @@ +const url = require('url') +const { userCollection, EXTERNAL_DIRECT_CONNECT_PROVIDER } = require('../../common/constants') +const { ERROR } = require('../../common/error') +const { findUser } = require('../../lib/utils/account') +const PasswordUtils = require('../../lib/utils/password') + +/** + * 使用 uid 或 externalUid 获取用户信息 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-update-userinfo + * @param {object} params + * @param {string} params.uid uni-id体系的用户id + * @param {string} params.externalUid 业务系统的用户id + * @param {string} params.nickname 昵称 + * @param {string} params.gender 性别 + * @param {string} params.avatar 头像 + * @returns {object} + */ +module.exports = async function (params = {}) { + const schema = { + uid: { + required: false, + type: 'string' + }, + externalUid: { + required: false, + type: 'string' + }, + username: { + required: false, + type: 'string' + }, + password: { + required: false, + type: 'password' + }, + authorizedApp: { + required: false, + type: 'array' + }, // 指定允许登录的app,传空数组或不传时表示可以不可以在任何端登录 + nickname: { + required: false, + type: 'nickname' + }, + role: { + require: false, + type: 'array' + }, + mobile: { + required: false, + type: 'mobile' + }, + email: { + required: false, + type: 'email' + }, + tags: { + required: false, + type: 'array' + }, + status: { + required: false, + type: 'number' + }, + gender: { + required: false, + type: 'number' + }, + avatar: { + required: false, + type: 'string' + } + } + + this.middleware.validate(params, schema) + + const { + uid, + externalUid, + username, + password, + authorizedApp, + nickname, + role, + mobile, + email, + tags, + status, + avatar, + gender + } = params + + if (!uid && !externalUid) { + throw { + errCode: ERROR.PARAM_REQUIRED, + errMsgValue: { + param: 'uid or externalUid' + } + } + } + + let query + if (uid) { + query = { + _id: uid + } + } else { + query = { + identities: { + provider: EXTERNAL_DIRECT_CONNECT_PROVIDER, + uid: externalUid + } + } + } + + const users = await userCollection.where(query).get() + const user = users.data && users.data[0] + if (!user) { + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS + } + } + + // 更新的用户数据字段 + const data = { + username, + dcloud_appid: authorizedApp, + nickname, + role, + mobile, + email, + tags, + status, + avatar, + gender + } + + const realData = Object.keys(data).reduce((res, key) => { + const item = data[key] + if (item !== undefined) { + res[key] = item + } + return res + }, {}) + + // 更新用户名时验证用户名是否重新 + if (username) { + const { + userMatched + } = await findUser({ + userQuery: { + username + }, + authorizedApp + }) + if (userMatched.filter(user => user._id !== uid).length) { + throw { + errCode: ERROR.ACCOUNT_EXISTS + } + } + } + if (password) { + const passwordUtils = new PasswordUtils({ + clientInfo: this.getUniversalClientInfo(), + passwordSecret: this.config.passwordSecret + }) + const { + passwordHash, + version + } = passwordUtils.generatePasswordHash({ + password + }) + + realData.password = passwordHash + realData.password_secret_version = version + } + + if (avatar) { + // eslint-disable-next-line n/no-deprecated-api + const avatarPath = url.parse(avatar).pathname + const extName = avatarPath.indexOf('.') > -1 ? avatarPath.split('.').pop() : '' + + realData.avatar_file = { + name: avatarPath, + extname: extName, + url: avatar + } + } + + if (user.identities.length) { + const identity = user.identities.find(item => item.provider === EXTERNAL_DIRECT_CONNECT_PROVIDER) + + if (identity) { + identity.userInfo = { + avatar, + gender, + nickname + } + } + + realData.identities = user.identities + } + + await userCollection.where(query).update(realData) + + return { + errCode: 0 + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/facial-recognition-verify/get-auth-result.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/facial-recognition-verify/get-auth-result.js new file mode 100644 index 0000000..0b37b4e --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/facial-recognition-verify/get-auth-result.js @@ -0,0 +1,135 @@ +const { userCollection, REAL_NAME_STATUS, frvLogsCollection } = require('../../common/constants') +const { dataDesensitization, catchAwait } = require('../../common/utils') +const { encryptData, decryptData } = require('../../common/sensitive-aes-cipher') +const { ERROR } = require('../../common/error') + +/** + * 查询认证结果 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-frv-auth-result + * @param {Object} params + * @param {String} params.certifyId 认证ID + * @returns + */ +module.exports = async function (params) { + const schema = { + certifyId: 'string' + } + + this.middleware.validate(params, schema) + + const { uid } = this.authInfo // 从authInfo中取出uid属性 + const { certifyId } = params // 从params中取出certifyId属性 + + const user = await userCollection.doc(uid).get() // 根据uid查询用户信息 + const userInfo = user.data && user.data[0] // 从查询结果中获取userInfo对象 + + // 如果用户不存在,抛出账户不存在的错误 + if (!userInfo) { + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS + } + } + + const { realname_auth: realNameAuth = {} } = userInfo + + // 如果用户已经实名认证,抛出已实名认证的错误 + if (realNameAuth.auth_status === REAL_NAME_STATUS.CERTIFIED) { + throw { + errCode: ERROR.REAL_NAME_VERIFIED + } + } + + // 初始化实人认证服务 + const frvManager = uniCloud.getFacialRecognitionVerifyManager({ + requestId: this.getUniCloudRequestId() + }) + + // 调用frvManager的getAuthResult方法,获取认证结果 + const [error, res] = await catchAwait(frvManager.getAuthResult({ + certifyId + })) + + // 如果出现错误,抛出未知错误并打印日志 + if (error) { + console.log(ERROR.UNKNOWN_ERROR, 'error: ', error) + throw error + } + + // 如果认证状态为“PROCESSING”,抛出认证正在处理中的错误 + if (res.authState === 'PROCESSING') { + throw { + errCode: ERROR.FRV_PROCESSING + } + } + + // 如果认证状态为“FAIL”,更新认证日志的状态并抛出认证失败的错误 + if (res.authState === 'FAIL') { + await frvLogsCollection.where({ + certify_id: certifyId + }).update({ + status: REAL_NAME_STATUS.CERTIFY_FAILED + }) + + console.log(ERROR.FRV_FAIL, 'error: ', res) + throw { + errCode: ERROR.FRV_FAIL + } + } + + // 如果认证状态不为“SUCCESS”,抛出未知错误并打印日志 + if (res.authState !== 'SUCCESS') { + console.log(ERROR.UNKNOWN_ERROR, 'source res: ', res) + throw { + errCode: ERROR.UNKNOWN_ERROR + } + } + + // 根据certifyId查询认证记录 + const frvLogs = await frvLogsCollection.where({ + certify_id: certifyId + }).get() + + const log = frvLogs.data && frvLogs.data[0] + + const updateData = { + realname_auth: { + auth_status: REAL_NAME_STATUS.CERTIFIED, + real_name: log.real_name, + identity: log.identity, + auth_date: Date.now(), + type: 0 + } + } + + // 如果获取到了认证照片的地址,则会对其进行下载,并使用uniCloud.uploadFile方法将其上传到云存储,并将上传后的fileID保存起来。 + if (res.pictureUrl) { + const pictureRes = await uniCloud.httpclient.request(res.pictureUrl) + if (pictureRes.status < 400) { + const { + fileID + } = await uniCloud.uploadFile({ + cloudPath: `user/id-card/${uid}.b64`, + fileContent: Buffer.from(encryptData.call(this, pictureRes.data.toString('base64'))) + }) + updateData.realname_auth.in_hand = fileID + } + } + + await Promise.all([ + // 更新用户认证状态 + userCollection.doc(uid).update(updateData), + // 更新实人认证记录状态 + frvLogsCollection.where({ + certify_id: certifyId + }).update({ + status: REAL_NAME_STATUS.CERTIFIED + }) + ]) + + return { + errCode: 0, + authStatus: REAL_NAME_STATUS.CERTIFIED, + realName: dataDesensitization(decryptData.call(this, log.real_name), { onlyLast: true }), // 对姓名进行脱敏处理 + identity: dataDesensitization(decryptData.call(this, log.identity)) // 对身份证号进行脱敏处理 + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/facial-recognition-verify/get-certify-id.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/facial-recognition-verify/get-certify-id.js new file mode 100644 index 0000000..cb8b48b --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/facial-recognition-verify/get-certify-id.js @@ -0,0 +1,99 @@ +const { userCollection, REAL_NAME_STATUS, frvLogsCollection, dbCmd } = require('../../common/constants') +const { ERROR } = require('../../common/error') +const { encryptData } = require('../../common/sensitive-aes-cipher') +const { getCurrentDateTimestamp } = require('../../common/utils') + +// const CertifyIdExpired = 25 * 60 * 1000 // certifyId 过期时间为30分钟,在25分时置为过期 + +/** + * 获取认证ID + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-frv-certify-id + * @param {Object} params + * @param {String} params.realName 真实姓名 + * @param {String} params.idCard 身份证号码 + * @param {String} params.metaInfo 客户端初始化时返回的metaInfo + * @returns + */ +module.exports = async function (params) { + const schema = { + realName: 'realName', + idCard: 'idCard', + metaInfo: 'string' + } + + this.middleware.validate(params, schema) + + const { realName: originalRealName, idCard: originalIdCard, metaInfo } = params // 解构出传入参数的真实姓名、身份证号码、其他元数据 + const realName = encryptData.call(this, originalRealName) // 对真实姓名进行加密处理 + const idCard = encryptData.call(this, originalIdCard) // 对身份证号码进行加密处理 + + const { uid } = this.authInfo // 获取当前用户的 ID + const idCardCertifyLimit = this.config.idCardCertifyLimit || 1 // 获取身份证认证限制次数,默认为1次 + const realNameCertifyLimit = this.config.realNameCertifyLimit || 5 // 获取实名认证限制次数,默认为5次 + const frvNeedAlivePhoto = this.config.frvNeedAlivePhoto || false // 是否需要拍摄活体照片,默认为 false + + const user = await userCollection.doc(uid).get() // 获取用户信息 + const userInfo = user.data && user.data[0] // 获取用户信息对象中的实名认证信息 + const { realname_auth: realNameAuth = {} } = userInfo // 解构出实名认证信息中的认证状态对象,默认为空对象 + + // 如果用户已经实名认证过,不能再次认证 + if (realNameAuth.auth_status === REAL_NAME_STATUS.CERTIFIED) { + throw { + errCode: ERROR.REAL_NAME_VERIFIED + } + } + + // 查询已经使用同一个身份证认证的账号数量,如果超过限制则不能认证 + const idCardAccount = await userCollection.where({ + realname_auth: { + type: 0, // 用户认证状态是个人 + auth_status: REAL_NAME_STATUS.CERTIFIED, // 认证状态为已认证 + identity: idCard // 身份证号码和传入参数的身份证号码相同 + } + }).get() + if (idCardAccount.data.length >= idCardCertifyLimit) { + throw { + errCode: ERROR.ID_CARD_EXISTS + } + } + + // 查询用户今天已经进行的实名认证次数,如果超过限制则不能认证 + const userFrvLogs = await frvLogsCollection.where({ + user_id: uid, + created_date: dbCmd.gt(getCurrentDateTimestamp()) // 查询今天的认证记录 + }).get() + + // 限制用户每日认证次数 + if (userFrvLogs.data && userFrvLogs.data.length >= realNameCertifyLimit) { + throw { + errCode: ERROR.REAL_NAME_VERIFY_UPPER_LIMIT + } + } + + // 初始化实人认证服务 + const frvManager = uniCloud.getFacialRecognitionVerifyManager({ + requestId: this.getUniCloudRequestId() // 获取当前 + }) + // 调用实人认证服务,获取认证 ID + const res = await frvManager.getCertifyId({ + realName: originalRealName, + idCard: originalIdCard, + needPicture: frvNeedAlivePhoto, + metaInfo + }) + + // 将认证记录插入到实名认证日志中 + await frvLogsCollection.add({ + user_id: uid, + certify_id: res.certifyId, + real_name: realName, + identity: idCard, + status: REAL_NAME_STATUS.WAITING_CERTIFIED, + created_date: Date.now() + }) + + // 返回认证ID + return { + certifyId: res.certifyId + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/facial-recognition-verify/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/facial-recognition-verify/index.js new file mode 100644 index 0000000..63f6b1f --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/facial-recognition-verify/index.js @@ -0,0 +1,4 @@ +module.exports = { + getFrvCertifyId: require('./get-certify-id'), + getFrvAuthResult: require('./get-auth-result') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/fission/accept-invite.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/fission/accept-invite.js new file mode 100644 index 0000000..2461e06 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/fission/accept-invite.js @@ -0,0 +1,25 @@ +const { + acceptInvite +} = require('../../lib/utils/fission') + +/** + * 接受邀请 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#accept-invite + * @param {Object} params + * @param {String} params.inviteCode 邀请码 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + inviteCode: 'string' + } + this.middleware.validate(params, schema) + const { + inviteCode + } = params + const uid = this.authInfo.uid + return acceptInvite({ + uid, + inviteCode + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/fission/get-invited-user.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/fission/get-invited-user.js new file mode 100644 index 0000000..93d4671 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/fission/get-invited-user.js @@ -0,0 +1,80 @@ +const { + userCollection +} = require('../../common/constants') +const { + coverMobile +} = require('../../common/utils') + +/** + * 获取受邀用户 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-invited-user + * @param {Object} params + * @param {Number} params.level 获取受邀用户的级数,1表示直接邀请的用户 + * @param {Number} params.limit 返回数据大小 + * @param {Number} params.offset 返回数据偏移 + * @param {Boolean} params.needTotal 是否需要返回总数 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + level: 'number', + limit: { + required: false, + type: 'number' + }, + offset: { + required: false, + type: 'number' + }, + needTotal: { + required: false, + type: 'boolean' + } + } + this.middleware.validate(params, schema) + const { + level, + limit = 20, + offset = 0, + needTotal = false + } = params + const uid = this.authInfo.uid + const query = { + [`inviter_uid.${level - 1}`]: uid + } + const getUserRes = await userCollection.where(query) + .field({ + _id: true, + avatar: true, + avatar_file: true, + username: true, + nickname: true, + mobile: true, + invite_time: true + }) + .orderBy('invite_time', 'desc') + .skip(offset) + .limit(limit) + .get() + + const invitedUser = getUserRes.data.map(item => { + return { + uid: item._id, + username: item.username, + nickname: item.nickname, + mobile: coverMobile(item.mobile), + inviteTime: item.invite_time, + avatar: item.avatar, + avatarFile: item.avatar_file + } + }) + const result = { + errCode: 0, + invitedUser + } + if (needTotal) { + const getTotalRes = await userCollection.where(query).count() + result.total = getTotalRes.total + } + return result +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/fission/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/fission/index.js new file mode 100644 index 0000000..4a9bee1 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/fission/index.js @@ -0,0 +1,4 @@ +module.exports = { + acceptInvite: require('./accept-invite'), + getInvitedUser: require('./get-invited-user') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/index.js new file mode 100644 index 0000000..f65f58b --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/index.js @@ -0,0 +1,20 @@ +module.exports = { + login: require('./login'), + loginBySms: require('./login-by-sms'), + loginByUniverify: require('./login-by-univerify'), + loginByWeixin: require('./login-by-weixin'), + loginByAlipay: require('./login-by-alipay'), + loginByQQ: require('./login-by-qq'), + loginByApple: require('./login-by-apple'), + loginByBaidu: require('./login-by-baidu'), + loginByDingtalk: require('./login-by-dingtalk'), + loginByToutiao: require('./login-by-toutiao'), + loginByDouyin: require('./login-by-douyin'), + loginByWeibo: require('./login-by-weibo'), + loginByTaobao: require('./login-by-taobao'), + loginByEmailLink: require('./login-by-email-link'), + loginByEmailCode: require('./login-by-email-code'), + loginByFacebook: require('./login-by-facebook'), + loginByGoogle: require('./login-by-google'), + loginByWeixinMobile: require('./login-by-weixin-mobile') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-alipay.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-alipay.js new file mode 100644 index 0000000..d5d4631 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-alipay.js @@ -0,0 +1,70 @@ +const { + initAlipay +} = require('../../lib/third-party/index') +const { + ERROR +} = require('../../common/error') +const { + preUnifiedLogin, + postUnifiedLogin +} = require('../../lib/utils/unified-login') +const { + LOG_TYPE +} = require('../../common/constants') + +/** + * 支付宝登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-alipay + * @param {Object} params + * @param {String} params.code 支付宝小程序客户端登录返回的code + * @param {String} params.inviteCode 邀请码 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + code: 'string', + inviteCode: { + type: 'string', + required: false + } + } + this.middleware.validate(params, schema) + const { + code, + inviteCode + } = params + const alipayApi = initAlipay.call(this) + let getAlipayAccountResult + try { + getAlipayAccountResult = await alipayApi.code2Session(code) + } catch (error) { + console.error(error) + await this.middleware.uniIdLog({ + success: false, + type: LOG_TYPE.LOGIN + }) + throw { + errCode: ERROR.GET_THIRD_PARTY_ACCOUNT_FAILED + } + } + + const { + openid + } = getAlipayAccountResult + + const { + type, + user + } = await preUnifiedLogin.call(this, { + user: { + ali_openid: openid + } + }) + return postUnifiedLogin.call(this, { + user, + extraData: {}, + isThirdParty: true, + type, + inviteCode + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-apple.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-apple.js new file mode 100644 index 0000000..5f39e62 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-apple.js @@ -0,0 +1,77 @@ +const { + initApple +} = require('../../lib/third-party/index') +const { + ERROR +} = require('../../common/error') +const { + preUnifiedLogin, + postUnifiedLogin +} = require('../../lib/utils/unified-login') +const { + LOG_TYPE +} = require('../../common/constants') + +/** + * 苹果登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-apple + * @param {Object} params + * @param {String} params.identityToken 苹果登录返回的identityToken + * @param {String} params.nickname 用户昵称 + * @param {String} params.inviteCode 邀请码 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + identityToken: 'string', + nickname: { + required: false, + type: 'nickname' + }, + inviteCode: { + required: false, + type: 'string' + } + } + this.middleware.validate(params, schema) + const { + identityToken, + nickname, + inviteCode + } = params + const appleApi = initApple.call(this) + let verifyResult + try { + verifyResult = await appleApi.verifyIdentityToken(identityToken) + } catch (error) { + console.error(error) + await this.middleware.uniIdLog({ + success: false, + type: LOG_TYPE.LOGIN + }) + throw { + errCode: ERROR.GET_THIRD_PARTY_ACCOUNT_FAILED + } + } + const { + openid + } = verifyResult + + const { + type, + user + } = await preUnifiedLogin.call(this, { + user: { + apple_openid: openid + } + }) + return postUnifiedLogin.call(this, { + user, + extraData: { + nickname + }, + isThirdParty: true, + type, + inviteCode + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-baidu.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-baidu.js new file mode 100644 index 0000000..856449d --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-baidu.js @@ -0,0 +1,9 @@ +/** + * 百度登录 + * @param {Object} params + * @returns + */ +module.exports = async function (params = {}) { + // 此接口暂未实现,欢迎向我们提交pr + throw new Error('api[loginByBaidu] is not yet implemented') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-dingtalk.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-dingtalk.js new file mode 100644 index 0000000..afe1f01 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-dingtalk.js @@ -0,0 +1,9 @@ +/** + * 钉钉登录 + * @param {Object} params + * @returns + */ +module.exports = async function (params = {}) { + // 此接口暂未实现,欢迎向我们提交pr + throw new Error('api[loginByDingtalk] is not yet implemented') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-douyin.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-douyin.js new file mode 100644 index 0000000..8cd4ab5 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-douyin.js @@ -0,0 +1,9 @@ +/** + * 抖音登录 + * @param {Object} params + * @returns + */ +module.exports = async function (params = {}) { + // 此接口暂未实现,欢迎向我们提交pr + throw new Error('api[loginByDouyin] is not yet implemented') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-email-code.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-email-code.js new file mode 100644 index 0000000..c3af08f --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-email-code.js @@ -0,0 +1,9 @@ +/** + * 邮箱验证码登录 + * @param {Object} params + * @returns + */ +module.exports = async function (params = {}) { + // 此接口暂未实现,欢迎向我们提交pr + throw new Error('api[loginByEmailCode] is not yet implemented') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-email-link.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-email-link.js new file mode 100644 index 0000000..0ebbf3a --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-email-link.js @@ -0,0 +1,9 @@ +/** + * 邮箱点击链接登录 + * @param {Object} params + * @returns + */ +module.exports = async function (params = {}) { + // 此接口暂未实现,欢迎向我们提交pr + throw new Error('api[loginByEmailLink] is not yet implemented') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-facebook.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-facebook.js new file mode 100644 index 0000000..5c93bd4 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-facebook.js @@ -0,0 +1,9 @@ +/** + * Facebook登录 + * @param {Object} params + * @returns + */ +module.exports = async function (params = {}) { + // 此接口暂未实现,欢迎向我们提交pr + throw new Error('api[loginByFacebook] is not yet implemented') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-google.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-google.js new file mode 100644 index 0000000..8054ece --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-google.js @@ -0,0 +1,9 @@ +/** + * Google登录 + * @param {Object} params + * @returns + */ +module.exports = async function (params = {}) { + // 此接口暂未实现,欢迎向我们提交pr + throw new Error('api[loginByGoogle] is not yet implemented') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-qq.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-qq.js new file mode 100644 index 0000000..7d2f588 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-qq.js @@ -0,0 +1,165 @@ +const { + initQQ +} = require('../../lib/third-party/index') +const { + ERROR +} = require('../../common/error') +const { + preUnifiedLogin, + postUnifiedLogin +} = require('../../lib/utils/unified-login') +const { + LOG_TYPE +} = require('../../common/constants') +const { + getQQPlatform, + generateQQCache, + saveQQUserKey +} = require('../../lib/utils/qq') +const url = require('url') + +/** + * QQ登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-qq + * @param {Object} params + * @param {String} params.code QQ小程序登录返回的code参数 + * @param {String} params.accessToken App端QQ登录返回的accessToken参数 + * @param {String} params.accessTokenExpired accessToken过期时间,由App端QQ登录返回的expires_in参数计算而来,单位:毫秒 + * @param {String} params.inviteCode 邀请码 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + code: { + type: 'string', + required: false + }, + accessToken: { + type: 'string', + required: false + }, + accessTokenExpired: { + type: 'number', + required: false + }, + inviteCode: { + type: 'string', + required: false + } + } + this.middleware.validate(params, schema) + const { + code, + accessToken, + accessTokenExpired, + inviteCode + } = params + const { + appId + } = this.getUniversalClientInfo() + const qqApi = initQQ.call(this) + const qqPlatform = getQQPlatform.call(this) + let apiName + switch (qqPlatform) { + case 'mp': + apiName = 'code2Session' + break + case 'app': + apiName = 'getOpenidByToken' + break + default: + throw new Error('Unsupported qq platform') + } + let getQQAccountResult + try { + getQQAccountResult = await qqApi[apiName]({ + code, + accessToken + }) + } catch (error) { + console.error(error) + await this.middleware.uniIdLog({ + success: false, + type: LOG_TYPE.LOGIN + }) + throw { + errCode: ERROR.GET_THIRD_PARTY_ACCOUNT_FAILED + } + } + + const { + openid, + unionid, + // 保存下面的字段 + sessionKey // QQ小程序用户sessionKey + } = getQQAccountResult + + const { + type, + user + } = await preUnifiedLogin.call(this, { + user: { + qq_openid: { + [qqPlatform]: openid + }, + qq_unionid: unionid + } + }) + const extraData = { + qq_openid: { + [`${qqPlatform}_${appId}`]: openid + }, + qq_unionid: unionid + } + if (type === 'register' && qqPlatform !== 'mp') { + const { + nickname, + avatar + } = await qqApi.getUserInfo({ + accessToken, + openid + }) + // eslint-disable-next-line n/no-deprecated-api + const extName = url.parse(avatar).pathname.split('.').pop() + const cloudPath = `user/avatar/${openid.slice(-8) + Date.now()}-avatar.${extName}` + const getAvatarRes = await uniCloud.httpclient.request(avatar) + if (getAvatarRes.status >= 400) { + throw { + errCode: ERROR.GET_THIRD_PARTY_USER_INFO_FAILED + } + } + const { + fileID + } = await uniCloud.uploadFile({ + cloudPath, + fileContent: getAvatarRes.data + }) + extraData.nickname = nickname + extraData.avatar_file = { + name: cloudPath, + extname: extName, + url: fileID + } + } + await saveQQUserKey.call(this, { + openid, + sessionKey, + accessToken, + accessTokenExpired + }) + return postUnifiedLogin.call(this, { + user, + extraData: { + ...extraData, + ...generateQQCache.call(this, { + openid, + sessionKey, // QQ小程序用户sessionKey + accessToken, // App端QQ用户accessToken + accessTokenExpired // App端QQ用户accessToken过期时间 + }) + }, + isThirdParty: true, + type, + inviteCode + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-sms.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-sms.js new file mode 100644 index 0000000..915e9b6 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-sms.js @@ -0,0 +1,99 @@ +const { + getNeedCaptcha, + verifyCaptcha +} = require('../../lib/utils/captcha') +const { + verifyMobileCode +} = require('../../lib/utils/verify-code') +const { + preUnifiedLogin, + postUnifiedLogin +} = require('../../lib/utils/unified-login') +const { + CAPTCHA_SCENE, + SMS_SCENE, + LOG_TYPE +} = require('../../common/constants') + +/** + * 短信验证码登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-sms + * @param {Object} params + * @param {String} params.mobile 手机号 + * @param {String} params.code 短信验证码 + * @param {String} params.captcha 图形验证码 + * @param {String} params.inviteCode 邀请码 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + mobile: 'mobile', + code: 'string', + captcha: { + required: false, + type: 'string' + }, + inviteCode: { + required: false, + type: 'string' + } + } + this.middleware.validate(params, schema) + const { + mobile, + code, + captcha, + inviteCode + } = params + + const needCaptcha = await getNeedCaptcha.call(this, { + mobile + }) + + if (needCaptcha) { + await verifyCaptcha.call(this, { + captcha, + scene: CAPTCHA_SCENE.LOGIN_BY_SMS + }) + } + + try { + await verifyMobileCode({ + mobile, + code, + scene: SMS_SCENE.LOGIN_BY_SMS + }) + } catch (error) { + console.log(error, { + mobile, + code, + type: SMS_SCENE.LOGIN_BY_SMS + }) + await this.middleware.uniIdLog({ + success: false, + data: { + mobile + }, + type: LOG_TYPE.LOGIN + }) + throw error + } + + const { + type, + user + } = await preUnifiedLogin.call(this, { + user: { + mobile + } + }) + return postUnifiedLogin.call(this, { + user, + extraData: { + mobile_confirmed: 1 + }, + isThirdParty: false, + type, + inviteCode + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-taobao.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-taobao.js new file mode 100644 index 0000000..6a6d599 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-taobao.js @@ -0,0 +1,9 @@ +/** + * 淘宝登录 + * @param {Object} params + * @returns + */ +module.exports = async function (params = {}) { + // 此接口暂未实现,欢迎向我们提交pr + throw new Error('api[loginByTaobao] is not yet implemented') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-toutiao.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-toutiao.js new file mode 100644 index 0000000..133aadb --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-toutiao.js @@ -0,0 +1,9 @@ +/** + * 头条登录 + * @param {Object} params + * @returns + */ +module.exports = async function (params = {}) { + // 此接口暂未实现,欢迎向我们提交pr + throw new Error('api[loginByToutiao] is not yet implemented') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-univerify.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-univerify.js new file mode 100644 index 0000000..53e681c --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-univerify.js @@ -0,0 +1,69 @@ +const { + getPhoneNumber +} = require('../../lib/utils/univerify') +const { + preUnifiedLogin, + postUnifiedLogin +} = require('../../lib/utils/unified-login') +const { + LOG_TYPE +} = require('../../common/constants') + +/** + * App端一键登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-univerify + * @param {Object} params + * @param {String} params.access_token APP端一键登录返回的access_token + * @param {String} params.openid APP端一键登录返回的openid + * @param {String} params.inviteCode 邀请码 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + access_token: 'string', + openid: 'string', + inviteCode: { + required: false, + type: 'string' + } + } + this.middleware.validate(params, schema) + const { + // eslint-disable-next-line camelcase + access_token, + openid, + inviteCode + } = params + + let mobile + try { + const phoneInfo = await getPhoneNumber.call(this, { + // eslint-disable-next-line camelcase + access_token, + openid + }) + mobile = phoneInfo.phoneNumber + } catch (error) { + await this.middleware.uniIdLog({ + success: false, + type: LOG_TYPE.LOGIN + }) + throw error + } + const { + user, + type + } = await preUnifiedLogin.call(this, { + user: { + mobile + } + }) + return postUnifiedLogin.call(this, { + user, + extraData: { + mobile_confirmed: 1 + }, + type, + inviteCode + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-weibo.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-weibo.js new file mode 100644 index 0000000..496cdb4 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-weibo.js @@ -0,0 +1,9 @@ +/** + * 微博登录 + * @param {Object} params + * @returns + */ +module.exports = async function (params = {}) { + // 此接口暂未实现,欢迎向我们提交pr + throw new Error('api[loginByWeibo] is not yet implemented') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-weixin-mobile.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-weixin-mobile.js new file mode 100644 index 0000000..c27c2b2 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-weixin-mobile.js @@ -0,0 +1,106 @@ +const { + initWeixin +} = require('../../lib/third-party/index') +const { + getWeixinAccessToken +} = require('../../lib/utils/weixin') +const { + ERROR +} = require('../../common/error') +const { + preUnifiedLogin, + postUnifiedLogin +} = require('../../lib/utils/unified-login') +const { + LOG_TYPE +} = require('../../common/constants') +const { + preBind, + postBind +} = require('../../lib/utils/relate') + +/** + * 微信授权手机号登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-weixin-mobile + * @param {Object} params + * @param {String} params.phoneCode 微信手机号返回的code + * @param {String} params.inviteCode 邀请码 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + phoneCode: 'string', + inviteCode: { + type: 'string', + required: false + } + } + + this.middleware.validate(params, schema) + + const { phoneCode, inviteCode } = params + + const weixinApi = initWeixin.call(this) + let mobile + + try { + const accessToken = await getWeixinAccessToken.call(this) + const mobileRes = await weixinApi.getPhoneNumber(accessToken, phoneCode) + mobile = mobileRes.purePhoneNumber + } catch (error) { + console.error(error) + await this.middleware.uniIdLog({ + success: false, + type: LOG_TYPE.LOGIN + }) + throw { + errCode: ERROR.GET_THIRD_PARTY_ACCOUNT_FAILED + } + } + + const { type, user } = await preUnifiedLogin.call(this, { + user: { + mobile + } + }) + + let extraData = { + mobile_confirmed: 1 + } + + if (type === 'login') { + // 绑定手机号 + if (!user.mobile_confirmed) { + const bindAccount = { + mobile + } + await preBind.call(this, { + uid: user._id, + bindAccount, + logType: LOG_TYPE.BIND_MOBILE + }) + await postBind.call(this, { + uid: user._id, + bindAccount, + extraData: { + mobile_confirmed: 1 + }, + logType: LOG_TYPE.BIND_MOBILE + }) + extraData = { + ...extraData, + ...bindAccount + } + } + } + + return postUnifiedLogin.call(this, { + user, + extraData: { + ...extraData + }, + isThirdParty: false, + type, + inviteCode + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-weixin.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-weixin.js new file mode 100644 index 0000000..b50f9a5 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login-by-weixin.js @@ -0,0 +1,176 @@ +const { + initWeixin +} = require('../../lib/third-party/index') +const { + ERROR +} = require('../../common/error') +const { + preUnifiedLogin, + postUnifiedLogin +} = require('../../lib/utils/unified-login') +const { + generateWeixinCache, + getWeixinPlatform, + saveWeixinUserKey, + saveSecureNetworkCache +} = require('../../lib/utils/weixin') +const { + LOG_TYPE +} = require('../../common/constants') +const url = require('url') + +/** + * 微信登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-weixin + * @param {Object} params + * @param {String} params.code 微信登录返回的code + * @param {String} params.inviteCode 邀请码 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + code: 'string', + inviteCode: { + type: 'string', + required: false + } + } + this.middleware.validate(params, schema) + const { + code, + inviteCode, + // 内部参数,暂不暴露 + secureNetworkCache = false + } = params + const { + appId + } = this.getUniversalClientInfo() + const weixinApi = initWeixin.call(this) + const weixinPlatform = getWeixinPlatform.call(this) + let apiName + switch (weixinPlatform) { + case 'mp': + apiName = 'code2Session' + break + case 'app': + case 'h5': + case 'web': + apiName = 'getOauthAccessToken' + break + default: + throw new Error('Unsupported weixin platform') + } + let getWeixinAccountResult + try { + getWeixinAccountResult = await weixinApi[apiName](code) + } catch (error) { + console.error(error) + await this.middleware.uniIdLog({ + success: false, + type: LOG_TYPE.LOGIN + }) + throw { + errCode: ERROR.GET_THIRD_PARTY_ACCOUNT_FAILED + } + } + + const { + openid, + unionid, + // 保存下面四个字段 + sessionKey, // 微信小程序用户sessionKey + accessToken, // App端微信用户accessToken + refreshToken, // App端微信用户refreshToken + expired: accessTokenExpired // App端微信用户accessToken过期时间 + } = getWeixinAccountResult + + if (secureNetworkCache) { + if (weixinPlatform !== 'mp') { + throw new Error('Unsupported weixin platform, expect mp-weixin') + } + await saveSecureNetworkCache.call(this, { + code, + openid, + unionid, + sessionKey + }) + } + + const { + type, + user + } = await preUnifiedLogin.call(this, { + user: { + wx_openid: { + [weixinPlatform]: openid + }, + wx_unionid: unionid + } + }) + const extraData = { + wx_openid: { + [`${weixinPlatform}_${appId}`]: openid + }, + wx_unionid: unionid + } + if (type === 'register' && weixinPlatform !== 'mp') { + const { + nickname, + avatar + } = await weixinApi.getUserInfo({ + accessToken, + openid + }) + + if (avatar) { + // eslint-disable-next-line n/no-deprecated-api + const avatarPath = url.parse(avatar).pathname + const extName = avatarPath.indexOf('.') > -1 ? url.parse(avatar).pathname.split('.').pop() : 'jpg' + const cloudPath = `user/avatar/${openid.slice(-8) + Date.now()}-avatar.${extName}` + const getAvatarRes = await uniCloud.httpclient.request(avatar) + if (getAvatarRes.status >= 400) { + throw { + errCode: ERROR.GET_THIRD_PARTY_USER_INFO_FAILED + } + } + + const { + fileID + } = await uniCloud.uploadFile({ + cloudPath, + fileContent: getAvatarRes.data + }) + + extraData.avatar_file = { + name: cloudPath, + extname: extName, + url: fileID + } + } + + extraData.nickname = nickname + } + await saveWeixinUserKey.call(this, { + openid, + sessionKey, + accessToken, + refreshToken, + accessTokenExpired + }) + return postUnifiedLogin.call(this, { + user, + extraData: { + ...extraData, + ...generateWeixinCache.call(this, { + openid, + sessionKey, // 微信小程序用户sessionKey + accessToken, // App端微信用户accessToken + refreshToken, // App端微信用户refreshToken + accessTokenExpired // App端微信用户accessToken过期时间 + }) + }, + isThirdParty: true, + type, + inviteCode + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login.js new file mode 100644 index 0000000..97e9cfe --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/login/login.js @@ -0,0 +1,94 @@ +const { + preLoginWithPassword, + postLogin +} = require('../../lib/utils/login') +const { + getNeedCaptcha, + verifyCaptcha +} = require('../../lib/utils/captcha') +const { + CAPTCHA_SCENE +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') + +/** + * 用户名密码登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login + * @param {Object} params + * @param {String} params.username 用户名 + * @param {String} params.mobile 手机号 + * @param {String} params.email 邮箱 + * @param {String} params.password 密码 + * @param {String} params.captcha 图形验证码 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + username: { + required: false, + type: 'username' + }, + mobile: { + required: false, + type: 'mobile' + }, + email: { + required: false, + type: 'email' + }, + password: 'password', + captcha: { + required: false, + type: 'string' + } + } + this.middleware.validate(params, schema) + const { + username, + mobile, + email, + password, + captcha + } = params + if (!username && !mobile && !email) { + throw { + errCode: ERROR.INVALID_USERNAME + } + } else if ( + (username && email) || + (username && mobile) || + (email && mobile) + ) { + throw { + errCode: ERROR.INVALID_PARAM + } + } + const needCaptcha = await getNeedCaptcha.call(this, { + username, + mobile, + email + }) + if (needCaptcha) { + await verifyCaptcha.call(this, { + captcha, + scene: CAPTCHA_SCENE.LOGIN_BY_PWD + }) + } + const { + user, + extraData + } = await preLoginWithPassword.call(this, { + user: { + username, + mobile, + email + }, + password + }) + return postLogin.call(this, { + user, + extraData + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/logout/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/logout/index.js new file mode 100644 index 0000000..544be2b --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/logout/index.js @@ -0,0 +1,3 @@ +module.exports = { + logout: require('./logout') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/logout/logout.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/logout/logout.js new file mode 100644 index 0000000..7d491c6 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/logout/logout.js @@ -0,0 +1,15 @@ +const { + logout +} = require('../../lib/utils/logout') + +/** + * 用户退出登录 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#logout + * @returns + */ +module.exports = async function () { + await logout.call(this) + return { + errCode: 0 + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/multi-end/authorize-app-login.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/multi-end/authorize-app-login.js new file mode 100644 index 0000000..8f8a167 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/multi-end/authorize-app-login.js @@ -0,0 +1,37 @@ +const { + isAuthorizeApproved +} = require('./utils') +const { + dbCmd, + userCollection +} = require('../../common/constants') + +/** + * 授权用户登录应用 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#authorize-app-login + * @param {Object} params + * @param {String} params.uid 用户id + * @param {String} params.appId 授权的应用的AppId + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + uid: 'string', + appId: 'string' + } + this.middleware.validate(params, schema) + const { + uid, + appId + } = params + await isAuthorizeApproved({ + uid, + appIdList: [appId] + }) + await userCollection.doc(uid).update({ + dcloud_appid: dbCmd.push(appId) + }) + return { + errCode: 0 + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/multi-end/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/multi-end/index.js new file mode 100644 index 0000000..ce9cc7b --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/multi-end/index.js @@ -0,0 +1,5 @@ +module.exports = { + authorizeAppLogin: require('./authorize-app-login'), + removeAuthorizedApp: require('./remove-authorized-app'), + setAuthorizedApp: require('./set-authorized-app') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/multi-end/remove-authorized-app.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/multi-end/remove-authorized-app.js new file mode 100644 index 0000000..df82184 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/multi-end/remove-authorized-app.js @@ -0,0 +1,30 @@ +const { + dbCmd, + userCollection +} = require('../../common/constants') + +/** + * 移除用户登录授权 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#remove-authorized-app + * @param {Object} params + * @param {String} params.uid 用户id + * @param {String} params.appId 取消授权的应用的AppId + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + uid: 'string', + appId: 'string' + } + this.middleware.validate(params, schema) + const { + uid, + appId + } = params + await userCollection.doc(uid).update({ + dcloud_appid: dbCmd.pull(appId) + }) + return { + errCode: 0 + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/multi-end/set-authorized-app.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/multi-end/set-authorized-app.js new file mode 100644 index 0000000..a438ef9 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/multi-end/set-authorized-app.js @@ -0,0 +1,36 @@ +const { + isAuthorizeApproved +} = require('./utils') +const { + userCollection +} = require('../../common/constants') + +/** + * 设置用户允许登录的应用列表 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-authorized-app + * @param {Object} params + * @param {String} params.uid 用户id + * @param {Array} params.appIdList 允许登录的应用AppId列表 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + uid: 'string', + appIdList: 'array' + } + this.middleware.validate(params, schema) + const { + uid, + appIdList + } = params + await isAuthorizeApproved({ + uid, + appIdList + }) + await userCollection.doc(uid).update({ + dcloud_appid: appIdList + }) + return { + errCode: 0 + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/multi-end/utils.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/multi-end/utils.js new file mode 100644 index 0000000..4ee4e26 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/multi-end/utils.js @@ -0,0 +1,38 @@ +const { + userCollection +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') +const { + findUser +} = require('../../lib/utils/account') + +async function isAuthorizeApproved ({ + uid, + appIdList +} = {}) { + const getUserRes = await userCollection.doc(uid).get() + const userRecord = getUserRes.data[0] + if (!userRecord) { + throw { + errCode: ERROR.ACCOUNT_NOT_EXISTS + } + } + const { + userMatched + } = await findUser({ + userQuery: userRecord, + authorizedApp: appIdList + }) + + if (userMatched.some(item => item._id !== uid)) { + throw { + errCode: ERROR.ACCOUNT_CONFLICT + } + } +} + +module.exports = { + isAuthorizeApproved +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/register/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/register/index.js new file mode 100644 index 0000000..64ff603 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/register/index.js @@ -0,0 +1,5 @@ +module.exports = { + registerUser: require('./register-user'), + registerAdmin: require('./register-admin'), + registerUserByEmail: require('./register-user-by-email') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/register/register-admin.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/register/register-admin.js new file mode 100644 index 0000000..5e122ab --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/register/register-admin.js @@ -0,0 +1,72 @@ +const { + userCollection +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') +const { + preRegisterWithPassword, + postRegister +} = require('../../lib/utils/register') + +/** + * 注册管理员 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#register-admin + * @param {Object} params + * @param {String} params.username 用户名 + * @param {String} params.password 密码 + * @param {String} params.nickname 昵称 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + username: 'username', + password: 'password', + nickname: { + type: 'nickname', + required: false + } + } + this.middleware.validate(params, schema) + const { + username, + password, + nickname + } = params + const getAdminRes = await userCollection.where({ + role: 'admin' + }).limit(1).get() + if (getAdminRes.data.length > 0) { + const [admin] = getAdminRes.data + const appId = this.getUniversalClientInfo().appId + + if (!admin.dcloud_appid || (admin.dcloud_appid && admin.dcloud_appid.includes(appId))) { + return { + errCode: ERROR.ADMIN_EXISTS, + errMsg: this.t('uni-id-admin-exists') + } + } else { + return { + errCode: ERROR.ADMIN_EXISTS, + errMsg: this.t('uni-id-admin-exist-in-other-apps') + } + } + } + const { + user, + extraData + } = await preRegisterWithPassword.call(this, { + user: { + username + }, + password + }) + return postRegister.call(this, { + user, + extraData: { + ...extraData, + nickname, + role: ['admin'] + } + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/register/register-user-by-email.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/register/register-user-by-email.js new file mode 100644 index 0000000..b52c1d2 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/register/register-user-by-email.js @@ -0,0 +1,87 @@ +const { + postRegister, + preRegisterWithPassword +} = require('../../lib/utils/register') +const { + verifyCaptcha +} = require('../../lib/utils/captcha') +const { + CAPTCHA_SCENE, + EMAIL_SCENE, + LOG_TYPE +} = require('../../common/constants') +const { + verifyEmailCode +} = require('../../lib/utils/verify-code') + +/** + * 通过邮箱+验证码注册普通用户 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#register-user-by-email + * @param {Object} params + * @param {String} params.email 邮箱 + * @param {String} params.password 密码 + * @param {String} params.nickname 昵称 + * @param {String} params.code 邮箱验证码 + * @param {String} params.inviteCode 邀请码 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + email: 'email', + password: 'password', + nickname: { + required: false, + type: 'nickname' + }, + code: 'string', + inviteCode: { + required: false, + type: 'string' + } + } + this.middleware.validate(params, schema) + const { + email, + password, + nickname, + code, + inviteCode + } = params + + try { + // 验证邮箱验证码,验证不通过时写入失败日志 + await verifyEmailCode({ + email, + code, + scene: EMAIL_SCENE.REGISTER + }) + } catch (error) { + await this.middleware.uniIdLog({ + data: { + email + }, + type: LOG_TYPE.REGISTER, + success: false + }) + throw error + } + + const { + user, + extraData + } = await preRegisterWithPassword.call(this, { + user: { + email + }, + password + }) + return postRegister.call(this, { + user, + extraData: { + ...extraData, + nickname, + email_confirmed: 1 + }, + inviteCode + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/register/register-user.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/register/register-user.js new file mode 100644 index 0000000..130dece --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/register/register-user.js @@ -0,0 +1,68 @@ +const { + postRegister, + preRegisterWithPassword +} = require('../../lib/utils/register') +const { + verifyCaptcha +} = require('../../lib/utils/captcha') +const { + CAPTCHA_SCENE +} = require('../../common/constants') + +/** + * 注册普通用户 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#register-user + * @param {Object} params + * @param {String} params.username 用户名 + * @param {String} params.password 密码 + * @param {String} params.captcha 图形验证码 + * @param {String} params.nickname 昵称 + * @param {String} params.inviteCode 邀请码 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + username: 'username', + password: 'password', + captcha: 'string', + nickname: { + required: false, + type: 'nickname' + }, + inviteCode: { + required: false, + type: 'string' + } + } + this.middleware.validate(params, schema) + const { + username, + password, + nickname, + captcha, + inviteCode + } = params + + await verifyCaptcha.call(this, { + captcha, + scene: CAPTCHA_SCENE.REGISTER + }) + + const { + user, + extraData + } = await preRegisterWithPassword.call(this, { + user: { + username + }, + password + }) + return postRegister.call(this, { + user, + extraData: { + ...extraData, + nickname + }, + inviteCode + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-alipay.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-alipay.js new file mode 100644 index 0000000..bdb451b --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-alipay.js @@ -0,0 +1,63 @@ +const { + preBind, + postBind +} = require('../../lib/utils/relate') +const { + LOG_TYPE +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') +const { + initAlipay +} = require('../../lib/third-party/index') + +/** + * 绑定支付宝账号 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-alipay + * @param {Object} params + * @param {String} params.code 支付宝小程序登录返回的code参数 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + code: 'string' + } + this.middleware.validate(params, schema) + const uid = this.authInfo.uid + const { + code + } = params + const alipayApi = initAlipay.call(this) + let getAlipayAccountResult + try { + getAlipayAccountResult = await alipayApi().code2Session(code) + } catch (error) { + await this.middleware.uniIdLog({ + success: false, + type: LOG_TYPE.BIND_ALIPAY + }) + throw { + errCode: ERROR.GET_THIRD_PARTY_ACCOUNT_FAILED + } + } + + const { + openid + } = getAlipayAccountResult + + const bindAccount = { + ali_openid: openid + } + await preBind.call(this, { + uid, + bindAccount, + logType: LOG_TYPE.BIND_APPLE + }) + return postBind.call(this, { + uid, + bindAccount, + extraData: {}, + logType: LOG_TYPE.BIND_APPLE + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-apple.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-apple.js new file mode 100644 index 0000000..eb87f8b --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-apple.js @@ -0,0 +1,62 @@ +const { + preBind, + postBind +} = require('../../lib/utils/relate') +const { + LOG_TYPE +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') +const { + initApple +} = require('../../lib/third-party/index') + +/** + * 绑定苹果账号 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-apple + * @param {Object} params + * @param {String} params.identityToken 苹果登录返回identityToken + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + identityToken: 'string' + } + this.middleware.validate(params, schema) + const uid = this.authInfo.uid + const { + identityToken + } = params + const appleApi = initApple.call(this) + let verifyResult + try { + verifyResult = await appleApi.verifyIdentityToken(identityToken) + } catch (error) { + await this.middleware.uniIdLog({ + success: false, + type: LOG_TYPE.BIND_APPLE + }) + throw { + errCode: ERROR.GET_THIRD_PARTY_ACCOUNT_FAILED + } + } + const { + openid + } = verifyResult + + const bindAccount = { + apple_openid: openid + } + await preBind.call(this, { + uid, + bindAccount, + logType: LOG_TYPE.BIND_APPLE + }) + return postBind.call(this, { + uid, + bindAccount, + extraData: {}, + logType: LOG_TYPE.BIND_APPLE + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-mobile-by-mp-weixin.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-mobile-by-mp-weixin.js new file mode 100644 index 0000000..f4c2bd0 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-mobile-by-mp-weixin.js @@ -0,0 +1,104 @@ +const { + preBind, + postBind +} = require('../../lib/utils/relate') +const { + LOG_TYPE +} = require('../../common/constants') +const { + decryptWeixinData, + getWeixinCache, getWeixinAccessToken +} = require('../../lib/utils/weixin') +const { initWeixin } = require('../../lib/third-party') +const { ERROR } = require('../../common/error') + +/** + * 通过微信绑定手机号 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-mp-weixin + * @param {Object} params + * @param {String} params.encryptedData 微信获取手机号返回的加密信息 + * @param {String} params.iv 微信获取手机号返回的初始向量 + * @param {String} params.code 微信获取手机号返回的code + * @returns + */ +module.exports = async function (params = {}) { + /** + * 微信小程序的规则是客户端应先使用checkSession接口检测上次获取的sessionKey是否仍有效 + * 如果有效则直接使用上次存储的sessionKey即可 + * 如果无效应重新调用login接口再次刷新sessionKey + * 因此此接口不应直接使用客户端login获取的code,只能使用缓存的sessionKey + */ + const schema = { + encryptedData: { + required: false, + type: 'string' + }, + iv: { + required: false, + type: 'string' + }, + code: { + required: false, + type: 'string' + } + } + const { + encryptedData, + iv, + code + } = params + this.middleware.validate(params, schema) + + if ((!encryptedData && !iv) && !code) { + return { + errCode: ERROR.INVALID_PARAM + } + } + + const uid = this.authInfo.uid + + let mobile + if (code) { + // 区分客户端类型 小程序还是App + const accessToken = await getWeixinAccessToken.call(this) + const weixinApi = initWeixin.call(this) + const res = await weixinApi.getPhoneNumber(accessToken, code) + + mobile = res.purePhoneNumber + } else { + const sessionKey = await getWeixinCache.call(this, { + uid, + key: 'session_key' + }) + if (!sessionKey) { + throw new Error('Session key not found') + } + const res = decryptWeixinData.call(this, { + encryptedData, + sessionKey, + iv + }) + + mobile = res.purePhoneNumber + } + + const bindAccount = { + mobile + } + await preBind.call(this, { + uid, + bindAccount, + logType: LOG_TYPE.BIND_MOBILE + }) + await postBind.call(this, { + uid, + bindAccount, + extraData: { + mobile_confirmed: 1 + }, + logType: LOG_TYPE.BIND_MOBILE + }) + return { + errCode: 0 + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-mobile-by-sms.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-mobile-by-sms.js new file mode 100644 index 0000000..1640c2d --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-mobile-by-sms.js @@ -0,0 +1,92 @@ +const { + getNeedCaptcha, + verifyCaptcha +} = require('../../lib/utils/captcha') +const { + LOG_TYPE, + SMS_SCENE, + CAPTCHA_SCENE +} = require('../../common/constants') +const { + verifyMobileCode +} = require('../../lib/utils/verify-code') +const { + preBind, + postBind +} = require('../../lib/utils/relate') + +/** + * 通过短信验证码绑定手机号 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-sms + * @param {Object} params + * @param {String} params.mobile 手机号 + * @param {String} params.code 短信验证码 + * @param {String} params.captcha 图形验证码 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + mobile: 'mobile', + code: 'string', + captcha: { + type: 'string', + required: false + } + } + const { + mobile, + code, + captcha + } = params + this.middleware.validate(params, schema) + const uid = this.authInfo.uid + + // 判断是否需要验证码 + const needCaptcha = await getNeedCaptcha.call(this, { + uid, + type: LOG_TYPE.BIND_MOBILE + }) + if (needCaptcha) { + await verifyCaptcha.call(this, { + captcha, + scene: CAPTCHA_SCENE.BIND_MOBILE_BY_SMS + }) + } + + try { + // 验证手机号验证码,验证不通过时写入失败日志 + await verifyMobileCode({ + mobile, + code, + scene: SMS_SCENE.BIND_MOBILE_BY_SMS + }) + } catch (error) { + await this.middleware.uniIdLog({ + data: { + user_id: uid + }, + type: LOG_TYPE.BIND_MOBILE, + success: false + }) + throw error + } + const bindAccount = { + mobile + } + await preBind.call(this, { + uid, + bindAccount, + logType: LOG_TYPE.BIND_MOBILE + }) + await postBind.call(this, { + uid, + bindAccount, + extraData: { + mobile_confirmed: 1 + }, + logType: LOG_TYPE.BIND_MOBILE + }) + return { + errCode: 0 + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-mobile-by-univerify.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-mobile-by-univerify.js new file mode 100644 index 0000000..2970c61 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-mobile-by-univerify.js @@ -0,0 +1,70 @@ +const { + getPhoneNumber +} = require('../../lib/utils/univerify') +const { + LOG_TYPE +} = require('../../common/constants') +const { + preBind, + postBind +} = require('../../lib/utils/relate') + +/** + * 通过一键登录绑定手机号 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-univerify + * @param {Object} params + * @param {String} params.openid APP端一键登录返回的openid + * @param {String} params.access_token APP端一键登录返回的access_token + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + openid: 'string', + access_token: 'string' + } + const { + openid, + // eslint-disable-next-line camelcase + access_token + } = params + this.middleware.validate(params, schema) + const uid = this.authInfo.uid + let mobile + try { + const phoneInfo = await getPhoneNumber.call(this, { + // eslint-disable-next-line camelcase + access_token, + openid + }) + mobile = phoneInfo.phoneNumber + } catch (error) { + await this.middleware.uniIdLog({ + success: false, + data: { + user_id: uid + }, + type: LOG_TYPE.BIND_MOBILE + }) + throw error + } + + const bindAccount = { + mobile + } + await preBind.call(this, { + uid, + bindAccount, + logType: LOG_TYPE.BIND_MOBILE + }) + await postBind.call(this, { + uid, + bindAccount, + extraData: { + mobile_confirmed: 1 + }, + logType: LOG_TYPE.BIND_MOBILE + }) + return { + errCode: 0 + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-qq.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-qq.js new file mode 100644 index 0000000..574f917 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-qq.js @@ -0,0 +1,110 @@ +const { + preBind, + postBind +} = require('../../lib/utils/relate') +const { + LOG_TYPE +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') +const { + initQQ +} = require('../../lib/third-party/index') +const { + generateQQCache, + getQQPlatform, + saveQQUserKey +} = require('../../lib/utils/qq') + +/** + * 绑定QQ + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-qq + * @param {Object} params + * @param {String} params.code 小程序端QQ登录返回的code + * @param {String} params.accessToken APP端QQ登录返回的accessToken + * @param {String} params.accessTokenExpired accessToken过期时间,由App端QQ登录返回的expires_in参数计算而来,单位:毫秒 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + code: { + type: 'string', + required: false + }, + accessToken: { + type: 'string', + required: false + }, + accessTokenExpired: { + type: 'number', + required: false + } + } + this.middleware.validate(params, schema) + const uid = this.authInfo.uid + const { + code, + accessToken, + accessTokenExpired + } = params + const qqPlatform = getQQPlatform.call(this) + const appId = this.getUniversalClientInfo().appId + const qqApi = initQQ.call(this) + const clientPlatform = this.clientPlatform + const apiName = clientPlatform === 'mp-qq' ? 'code2Session' : 'getOpenidByToken' + let getQQAccountResult + try { + getQQAccountResult = await qqApi[apiName]({ + code, + accessToken + }) + } catch (error) { + await this.middleware.uniIdLog({ + success: false, + type: LOG_TYPE.BIND_QQ + }) + throw { + errCode: ERROR.GET_THIRD_PARTY_ACCOUNT_FAILED + } + } + + const { + openid, + unionid, + // 保存下面四个字段 + sessionKey // 微信小程序用户sessionKey + } = getQQAccountResult + + const bindAccount = { + qq_openid: { + [qqPlatform]: openid + }, + qq_unionid: unionid + } + await preBind.call(this, { + uid, + bindAccount, + logType: LOG_TYPE.BIND_QQ + }) + await saveQQUserKey.call(this, { + openid, + sessionKey, + accessToken, + accessTokenExpired + }) + return postBind.call(this, { + uid, + bindAccount, + extraData: { + qq_openid: { + [`${qqPlatform}_${appId}`]: openid + }, + ...generateQQCache.call(this, { + openid, + sessionKey + }) + }, + logType: LOG_TYPE.BIND_QQ + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-weixin.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-weixin.js new file mode 100644 index 0000000..d649478 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/bind-weixin.js @@ -0,0 +1,100 @@ +const { + preBind, + postBind +} = require('../../lib/utils/relate') +const { + LOG_TYPE +} = require('../../common/constants') +const { + generateWeixinCache, + saveWeixinUserKey, + getWeixinPlatform +} = require('../../lib/utils/weixin') +const { + initWeixin +} = require('../../lib/third-party/index') +const { + ERROR +} = require('../../common/error') + +/** + * 绑定微信 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-weixin + * @param {Object} params + * @param {String} params.code 微信登录返回的code + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + code: 'string' + } + this.middleware.validate(params, schema) + const uid = this.authInfo.uid + const { + code + } = params + const weixinPlatform = getWeixinPlatform.call(this) + const appId = this.getUniversalClientInfo().appId + + const weixinApi = initWeixin.call(this) + const clientPlatform = this.clientPlatform + const apiName = clientPlatform === 'mp-weixin' ? 'code2Session' : 'getOauthAccessToken' + let getWeixinAccountResult + try { + getWeixinAccountResult = await weixinApi[apiName](code) + } catch (error) { + await this.middleware.uniIdLog({ + success: false, + type: LOG_TYPE.BIND_WEIXIN + }) + throw { + errCode: ERROR.GET_THIRD_PARTY_ACCOUNT_FAILED + } + } + + const { + openid, + unionid, + // 保存下面四个字段 + sessionKey, // 微信小程序用户sessionKey + accessToken, // App端微信用户accessToken + refreshToken, // App端微信用户refreshToken + expired: accessTokenExpired // App端微信用户accessToken过期时间 + } = getWeixinAccountResult + + const bindAccount = { + wx_openid: { + [weixinPlatform]: openid + }, + wx_unionid: unionid + } + await preBind.call(this, { + uid, + bindAccount, + logType: LOG_TYPE.BIND_WEIXIN + }) + await saveWeixinUserKey.call(this, { + openid, + sessionKey, + accessToken, + refreshToken, + accessTokenExpired + }) + return postBind.call(this, { + uid, + bindAccount, + extraData: { + wx_openid: { + [`${weixinPlatform}_${appId}`]: openid + }, + ...generateWeixinCache.call(this, { + openid, + sessionKey, // 微信小程序用户sessionKey + accessToken, // App端微信用户accessToken + refreshToken, // App端微信用户refreshToken + accessTokenExpired // App端微信用户accessToken过期时间 + }) + }, + logType: LOG_TYPE.BIND_WEIXIN + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/index.js new file mode 100644 index 0000000..4d99c02 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/index.js @@ -0,0 +1,13 @@ +module.exports = { + bindMobileBySms: require('./bind-mobile-by-sms'), + bindMobileByUniverify: require('./bind-mobile-by-univerify'), + bindMobileByMpWeixin: require('./bind-mobile-by-mp-weixin'), + bindAlipay: require('./bind-alipay'), + bindApple: require('./bind-apple'), + bindQQ: require('./bind-qq'), + bindWeixin: require('./bind-weixin'), + unbindWeixin: require('./unbind-weixin'), + unbindAlipay: require('./unbind-alipay'), + unbindQQ: require('./unbind-qq'), + unbindApple: require('./unbind-apple') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/unbind-alipay.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/unbind-alipay.js new file mode 100644 index 0000000..67bb43b --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/unbind-alipay.js @@ -0,0 +1,32 @@ +const { + preUnBind, + postUnBind +} = require('../../lib/utils/relate') +const { + LOG_TYPE, dbCmd +} = require('../../common/constants') + +/** + * 解绑支付宝 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-alipay + * @returns + */ +module.exports = async function () { + const { uid } = this.authInfo + + await preUnBind.call(this, { + uid, + unBindAccount: { + ali_openid: dbCmd.exists(true) + }, + logType: LOG_TYPE.UNBIND_ALIPAY + }) + + return await postUnBind.call(this, { + uid, + unBindAccount: { + ali_openid: dbCmd.remove() + }, + logType: LOG_TYPE.UNBIND_ALIPAY + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/unbind-apple.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/unbind-apple.js new file mode 100644 index 0000000..111c1bf --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/unbind-apple.js @@ -0,0 +1,32 @@ +const { + preUnBind, + postUnBind +} = require('../../lib/utils/relate') +const { + LOG_TYPE, dbCmd +} = require('../../common/constants') + +/** + * 解绑apple + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-apple + * @returns + */ +module.exports = async function () { + const { uid } = this.authInfo + + await preUnBind.call(this, { + uid, + unBindAccount: { + apple_openid: dbCmd.exists(true) + }, + logType: LOG_TYPE.UNBIND_APPLE + }) + + return await postUnBind.call(this, { + uid, + unBindAccount: { + apple_openid: dbCmd.remove() + }, + logType: LOG_TYPE.UNBIND_APPLE + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/unbind-qq.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/unbind-qq.js new file mode 100644 index 0000000..0c9704c --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/unbind-qq.js @@ -0,0 +1,33 @@ +const { + preUnBind, + postUnBind +} = require('../../lib/utils/relate') +const { + LOG_TYPE, dbCmd +} = require('../../common/constants') +/** + * 解绑QQ + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-qq + * @returns + */ +module.exports = async function () { + const { uid } = this.authInfo + + await preUnBind.call(this, { + uid, + unBindAccount: { + qq_openid: dbCmd.exists(true), + qq_unionid: dbCmd.exists(true) + }, + logType: LOG_TYPE.UNBIND_QQ + }) + + return await postUnBind.call(this, { + uid, + unBindAccount: { + qq_openid: dbCmd.remove(), + qq_unionid: dbCmd.remove() + }, + logType: LOG_TYPE.UNBIND_QQ + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/unbind-weixin.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/unbind-weixin.js new file mode 100644 index 0000000..2248327 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/relate/unbind-weixin.js @@ -0,0 +1,38 @@ +const { + preUnBind, + postUnBind +} = require('../../lib/utils/relate') +const { + LOG_TYPE, dbCmd +} = require('../../common/constants') +const { + getWeixinPlatform +} = require('../../lib/utils/weixin') + +/** + * 解绑微信 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-weixin + * @returns + */ +module.exports = async function () { + const { uid } = this.authInfo + // const weixinPlatform = getWeixinPlatform.call(this) + + await preUnBind.call(this, { + uid, + unBindAccount: { + wx_openid: dbCmd.exists(true), + wx_unionid: dbCmd.exists(true) + }, + logType: LOG_TYPE.UNBIND_WEIXIN + }) + + return await postUnBind.call(this, { + uid, + unBindAccount: { + wx_openid: dbCmd.remove(), + wx_unionid: dbCmd.remove() + }, + logType: LOG_TYPE.UNBIND_WEIXIN + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/utils/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/utils/index.js new file mode 100644 index 0000000..0ec67a5 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/utils/index.js @@ -0,0 +1,5 @@ +module.exports = { + refreshToken: require('./refresh-token'), + setPushCid: require('./set-push-cid'), + secureNetworkHandshakeByWeixin: require('./secure-network-handshake-by-weixin') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/utils/refresh-token.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/utils/refresh-token.js new file mode 100644 index 0000000..b12f1f0 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/utils/refresh-token.js @@ -0,0 +1,24 @@ +/** + * 刷新token + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#refresh-token + */ +module.exports = async function () { + const refreshTokenRes = await this.uniIdCommon.refreshToken({ + token: this.getUniversalUniIdToken() + }) + const { + errCode, + token, + tokenExpired + } = refreshTokenRes + if (errCode) { + throw refreshTokenRes + } + return { + errCode: 0, + newToken: { + token, + tokenExpired + } + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/utils/secure-network-handshake-by-weixin.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/utils/secure-network-handshake-by-weixin.js new file mode 100644 index 0000000..82ea0b3 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/utils/secure-network-handshake-by-weixin.js @@ -0,0 +1,73 @@ +const { + ERROR +} = require('../../common/error') +const { + initWeixin +} = require('../../lib/third-party/index') +const { + saveWeixinUserKey, + saveSecureNetworkCache +} = require('../../lib/utils/weixin') +const loginByWeixin = require('../login/login-by-weixin') +/** + * 微信安全网络握手 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-push-cid + * @param {object} params + * @param {string} params.code 微信登录返回的code + * @param {boolean} params.callLoginByWeixin 是否同时调用一次微信登录 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + code: 'string', + callLoginByWeixin: { + type: 'boolean', + required: false + } + } + this.middleware.validate(params, schema) + let platform = this.clientPlatform + if (platform !== 'mp-weixin') { + throw new Error(`[secureNetworkHandshake] platform ${platform} is not supported`) + } + const { + code, + callLoginByWeixin = false + } = params + if (callLoginByWeixin) { + return loginByWeixin.call(this, { + code, + secureNetworkCache: true + }) + } + + const weixinApi = initWeixin.call(this) + let getWeixinAccountResult + try { + getWeixinAccountResult = await weixinApi.code2Session(code) + } catch (error) { + console.error(error) + throw { + errCode: ERROR.GET_THIRD_PARTY_ACCOUNT_FAILED + } + } + const { + openid, + unionid, + sessionKey // 微信小程序用户sessionKey + } = getWeixinAccountResult + await saveSecureNetworkCache.call(this, { + code, + openid, + unionid, + sessionKey + }) + await saveWeixinUserKey.call(this, { + openid, + sessionKey + }) + + return { + errCode: 0 + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/utils/set-push-cid.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/utils/set-push-cid.js new file mode 100644 index 0000000..9e08183 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/utils/set-push-cid.js @@ -0,0 +1,132 @@ +const { + deviceCollection +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') + +async function setOpendbDevice ({ + pushClientId +} = {}) { + // 仅新增,如果存在进行更新操作 + const { + appId, + deviceId, + deviceBrand, + deviceModel, + osName, + osVersion, + osLanguage, + osTheme, + devicePixelRatio, + windowWidth, + windowHeight, + screenWidth, + screenHeight, + romName, + romVersion + } = this.getUniversalClientInfo() + const platform = this.clientPlatform + const now = Date.now() + + const db = uniCloud.database() + const opendbDeviceCollection = db.collection('opendb-device') + const getDeviceRes = await opendbDeviceCollection.where({ + device_id: deviceId + }).get() + const data = { + appid: appId, + device_id: deviceId, + vendor: deviceBrand, + model: deviceModel, + uni_platform: platform, + os_name: osName, + os_version: osVersion, + os_language: osLanguage, + os_theme: osTheme, + pixel_ratio: devicePixelRatio, + window_width: windowWidth, + window_height: windowHeight, + screen_width: screenWidth, + screen_height: screenHeight, + rom_name: romName, + rom_version: romVersion, + last_update_date: now, + push_clientid: pushClientId + } + if (getDeviceRes.data.length > 0) { + await opendbDeviceCollection.where({ + device_id: deviceId + }).update(data) + return + } + data.create_date = now + await opendbDeviceCollection.add(data) +} + +/** + * 更新device表的push_clien_id + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-push-cid + * @param {object} params + * @param {string} params.pushClientId 客户端pushClientId + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + pushClientId: 'string' + } + this.middleware.validate(params, schema) + const { + deviceId, + appId, + osName + } = this.getUniversalClientInfo() + let platform = this.clientPlatform + if (platform === 'app') { + platform += osName + } + + const { + uid, + exp + } = this.authInfo + const { pushClientId } = params + const tokenExpired = exp * 1000 + const getDeviceRes = await deviceCollection.where({ + device_id: deviceId + }).get() + // console.log(getDeviceRes) + if (getDeviceRes.data.length > 1) { + return { + errCode: ERROR.SYSTEM_ERROR + } + } + const deviceRecord = getDeviceRes.data[0] + await setOpendbDevice.call(this, { + pushClientId + }) + if (!deviceRecord) { + await deviceCollection.add({ + user_id: uid, + device_id: deviceId, + token_expired: tokenExpired, + push_clientid: pushClientId, + appid: appId + }) + return { + errCode: 0 + } + } + + await deviceCollection.where({ + device_id: deviceId + }).update({ + user_id: uid, + token_expired: tokenExpired, + push_clientid: pushClientId, + appid: appId + }) + return { + errCode: 0 + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/create-captcha.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/create-captcha.js new file mode 100644 index 0000000..c3f7d81 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/create-captcha.js @@ -0,0 +1,35 @@ +const { + CAPTCHA_SCENE +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') + +/** + * 创建图形验证码 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#create-captcha + * @param {Object} params + * @param {String} params.scene 图形验证码使用场景 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + scene: 'string' + } + this.middleware.validate(params, schema) + + const { deviceId, platform } = this.getUniversalClientInfo() + const { + scene + } = params + if (!(Object.values(CAPTCHA_SCENE).includes(scene))) { + throw { + errCode: ERROR.INVALID_PARAM + } + } + return this.uniCaptcha.create({ + deviceId, + scene, + uniPlatform: platform + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/index.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/index.js new file mode 100644 index 0000000..fba3524 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/index.js @@ -0,0 +1,7 @@ +module.exports = { + createCaptcha: require('./create-captcha'), + refreshCaptcha: require('./refresh-captcha'), + sendSmsCode: require('./send-sms-code'), + sendEmailLink: require('./send-email-link'), + sendEmailCode: require('./send-email-code') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/refresh-captcha.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/refresh-captcha.js new file mode 100644 index 0000000..fafdc6b --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/refresh-captcha.js @@ -0,0 +1,36 @@ +const { + CAPTCHA_SCENE +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') + +/** + * 刷新图形验证码 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#refresh-captcha + * @param {Object} params + * @param {String} params.scene 图形验证码使用场景 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + scene: 'string' + } + this.middleware.validate(params, schema) + + const { deviceId, platform } = this.getUniversalClientInfo() + + const { + scene + } = params + if (!(Object.values(CAPTCHA_SCENE).includes(scene))) { + throw { + errCode: ERROR.INVALID_PARAM + } + } + return this.uniCaptcha.refresh({ + deviceId, + scene, + uniPlatform: platform + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/send-email-code.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/send-email-code.js new file mode 100644 index 0000000..1a6304d --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/send-email-code.js @@ -0,0 +1,60 @@ +const { + verifyCaptcha +} = require('../../lib/utils/captcha') +const { + EMAIL_SCENE +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') +/** + * 发送邮箱验证码,可用于登录、注册、绑定邮箱、修改密码等操作 + * @tutorial + * @param {Object} params + * @param {String} params.email 邮箱 + * @param {String} params.captcha 图形验证码 + * @param {String} params.scene 使用场景 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + email: 'email', + captcha: 'string', + scene: 'string' + } + this.middleware.validate(params, schema) + + const { + email, + captcha, + scene + } = params + + if (!(Object.values(EMAIL_SCENE).includes(scene))) { + throw { + errCode: ERROR.INVALID_PARAM + } + } + + await verifyCaptcha.call(this, { + scene: 'send-email-code', + captcha + }) + + // -- 测试代码 + await require('../../lib/utils/verify-code') + .setEmailVerifyCode.call(this, { + email, + code: '123456', + expiresIn: 180, + scene + }) + return { + errCode: 'uni-id-invalid-mail-template', + errMsg: `已启动测试模式,直接使用:123456作为邮箱验证码即可。\n如果是正式项目,需自行实现发送邮件的相关功能` + } + // -- 测试代码 + + + //发送邮件--需自行实现 +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/send-email-link.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/send-email-link.js new file mode 100644 index 0000000..f643434 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/send-email-link.js @@ -0,0 +1,12 @@ +/** + * 发送邮箱链接,可用于登录、注册、绑定邮箱、修改密码等操作 + * @tutorial + * @param {Object} params + * @param {String} params.email 邮箱 + * @param {String} params.scene 使用场景 + * @returns + */ +module.exports = async function (params = {}) { + // 此接口暂未实现,欢迎向我们提交pr + throw new Error('api[sendEmailLink] is not yet implemented') +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/send-sms-code.js b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/send-sms-code.js new file mode 100644 index 0000000..b392e7e --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/module/verify/send-sms-code.js @@ -0,0 +1,71 @@ +const { + sendSmsCode +} = require('../../lib/utils/sms') +const { + verifyCaptcha +} = require('../../lib/utils/captcha') +const { + SMS_SCENE +} = require('../../common/constants') +const { + ERROR +} = require('../../common/error') + +/** + * 发送短信验证码 + * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#send-sms-code + * @param {Object} params + * @param {String} params.mobile 手机号 + * @param {String} params.captcha 图形验证码 + * @param {String} params.scene 短信验证码使用场景 + * @returns + */ +module.exports = async function (params = {}) { + const schema = { + mobile: 'mobile', + captcha: 'string', + scene: 'string' + } + this.middleware.validate(params, schema) + const { + mobile, + captcha, + scene + } = params + if (!(Object.values(SMS_SCENE).includes(scene))) { + throw { + errCode: ERROR.INVALID_PARAM + } + } + await verifyCaptcha.call(this, { + scene: 'send-sms-code', + captcha + }) + + // -- 测试代码 + const { + templateId + } = (this.config.service && + this.config.service.sms && + this.config.service.sms.scene && + this.config.service.sms.scene[scene]) || {} + if (!templateId) { + await require('../../lib/utils/verify-code') + .setMobileVerifyCode.call(this, { + mobile: params.mobile, + code: '123456', + expiresIn: 180, + scene + }) + return { + errCode: 'uni-id-invalid-sms-template-id', + errMsg: `未找到scene=${scene},的短信模版templateId。\n已启动测试模式,直接使用:123456作为短信验证码即可。\n如果是正式项目,请在路径:/common/uni-config-center/uni-id/config.json中service->sms中配置密钥等信息\n更多详情:https://uniapp.dcloud.io/uniCloud/uni-id.html#config` + } + } + // -- 测试代码 + + return sendSmsCode.call(this, { + mobile, + scene + }) +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/package.json b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/package.json new file mode 100644 index 0000000..66d1a8b --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/cloudfunctions/uni-id-co/package.json @@ -0,0 +1,22 @@ +{ + "name": "uni-id-co", + "version": "1.1.14", + "description": "", + "main": "index.js", + "keywords": [], + "author": "DCloud", + "dependencies": { + "jsonwebtoken": "8.5.1", + "lodash.merge": "^4.6.2", + "uni-captcha": "file:../../../../uni-captcha/uniCloud/cloudfunctions/common/uni-captcha", + "uni-config-center": "file:../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center", + "uni-id-common": "file:../../../../uni-id-common/uniCloud/cloudfunctions/common/uni-id-common", + "uni-open-bridge-common": "file:../../../../uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common", + "uni-cloud-s2s": "file:../../../../uni-cloud-s2s/uniCloud/cloudfunctions/common/uni-cloud-s2s" + }, + "extensions": { + "uni-cloud-redis": {}, + "uni-cloud-sms": {}, + "uni-cloud-verify": {} + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/opendb-device.schema.json b/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/opendb-device.schema.json new file mode 100644 index 0000000..c3591cc --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/opendb-device.schema.json @@ -0,0 +1,142 @@ +{ + "bsonType": "object", + "required": [], + "permission": { + "read": false, + "create": true, + "update": false, + "delete": false + }, + "properties": { + "_id": { + "description": "ID,系统自动生成" + }, + "appid": { + "bsonType": "string", + "description": "DCloud appid" + }, + "device_id": { + "bsonType": "string", + "description": "设备唯一标识" + }, + "vendor": { + "bsonType": "string", + "description": "设备厂商" + }, + "push_clientid": { + "bsonType": "string", + "description": "推送设备客户端标识" + }, + "imei": { + "bsonType": "string", + "description": "国际移动设备识别码IMEI(International Mobile Equipment Identity)" + }, + "oaid": { + "bsonType": "string", + "description": "移动智能设备标识公共服务平台提供的匿名设备标识符(OAID)" + }, + "idfa": { + "bsonType": "string", + "description": "iOS平台配置应用使用广告标识(IDFA)" + }, + "imsi": { + "bsonType": "string", + "description": "国际移动用户识别码(International Mobile Subscriber Identification Number)" + }, + "model": { + "bsonType": "string", + "description": "设备型号" + }, + "platform": { + "bsonType": "string", + "description": "平台类型" + }, + "uni_platform": { + "bsonType": "string", + "description": "uni-app 运行平台,与条件编译平台相同。" + }, + "os_name": { + "bsonType": "string", + "description": "ios|android|windows|mac|linux " + }, + "os_version": { + "bsonType": "string", + "description": "操作系统版本号 " + }, + "os_language": { + "bsonType": "string", + "description": "操作系统语言 " + }, + "os_theme": { + "bsonType": "string", + "description": "操作系统主题 light|dark" + }, + "pixel_ratio": { + "bsonType": "string", + "description": "设备像素比 " + }, + "network_model": { + "bsonType": "string", + "description": "设备网络型号wifi\/3G\/4G\/" + }, + "window_width": { + "bsonType": "string", + "description": "设备窗口宽度 " + }, + "window_height": { + "bsonType": "string", + "description": "设备窗口高度" + }, + "screen_width": { + "bsonType": "string", + "description": "设备屏幕宽度" + }, + "screen_height": { + "bsonType": "string", + "description": "设备屏幕高度" + }, + "rom_name": { + "bsonType": "string", + "description": "rom 名称" + }, + "rom_version": { + "bsonType": "string", + "description": "rom 版本" + }, + "location_latitude": { + "bsonType": "double", + "description": "纬度" + }, + "location_longitude": { + "bsonType": "double", + "description": "经度" + }, + "location_country": { + "bsonType": "string", + "description": "国家" + }, + "location_province": { + "bsonType": "string", + "description": "省份" + }, + "location_city": { + "bsonType": "string", + "description": "城市" + }, + "create_date": { + "bsonType": "timestamp", + "description": "创建时间", + "forceDefaultValue": { + "$env": "now" + } + }, + "last_update_date": { + "bsonType": "timestamp", + "description": "最后一次修改时间", + "forceDefaultValue": { + "$env": "now" + } + } + }, + "version": "0.0.1" +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/opendb-frv-logs.schema.json b/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/opendb-frv-logs.schema.json new file mode 100644 index 0000000..239fd82 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/opendb-frv-logs.schema.json @@ -0,0 +1,44 @@ +{ + "bsonType": "object", + "permission": { + "read": "doc._id == auth.uid || 'CREATE_UNI_ID_USERS' in auth.permission", + "create": "'CREATE_UNI_ID_USERS' in auth.permission", + "update": "doc._id == auth.uid || 'UPDATE_UNI_ID_USERS' in auth.permission", + "delete": "'DELETE_UNI_ID_USERS' in auth.permission" + }, + "properties": { + "_id": { + "description": "存储文档 ID(用户 ID),系统自动生成" + }, + "certify_id": { + "bsonType": "string", + "description": "认证id" + }, + "user_id": { + "bsonType": "string", + "description": "用户id" + }, + "real_name": { + "bsonType": "string", + "description": "姓名" + }, + "identity": { + "bsonType": "string", + "description": "身份证号码" + }, + "status": { + "bsonType": "int", + "description": "认证状态:0 未认证 1 等待认证 2 认证通过 3 认证失败", + "maximum": 3, + "minimum": 0 + }, + "created_date": { + "bsonType": "timestamp", + "description": "创建时间", + "forceDefaultValue": { + "$env": "now" + } + } + }, + "required": [] +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/uni-id-device.schema.json b/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/uni-id-device.schema.json new file mode 100644 index 0000000..4981d75 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/uni-id-device.schema.json @@ -0,0 +1,83 @@ +{ + "bsonType": "object", + "required": [ + "user_id" + ], + "properties": { + "_id": { + "description": "ID,系统自动生成" + }, + "user_id": { + "bsonType": "string", + "description": "用户id,参考uni-id-users表" + }, + "ua": { + "bsonType": "string", + "description": "userAgent" + }, + "uuid": { + "bsonType": "string", + "description": "设备唯一标识(需要加密存储)" + }, + "os_name": { + "bsonType": "string", + "description": "ios|android|windows|mac|linux " + }, + "os_version": { + "bsonType": "string", + "description": "操作系统版本号 " + }, + "os_language": { + "bsonType": "string", + "description": "操作系统语言 " + }, + "os_theme": { + "bsonType": "string", + "description": "操作系统主题 light|dark" + }, + "vendor": { + "bsonType": "string", + "description": "设备厂商" + }, + "push_clientid": { + "bsonType": "string", + "description": "推送设备客户端标识" + }, + "imei": { + "bsonType": "string", + "description": "国际移动设备识别码IMEI(International Mobile Equipment Identity)" + }, + "oaid": { + "bsonType": "string", + "description": "移动智能设备标识公共服务平台提供的匿名设备标识符(OAID)" + }, + "idfa": { + "bsonType": "string", + "description": "iOS平台配置应用使用广告标识(IDFA)" + }, + "model": { + "bsonType": "string", + "description": "设备型号" + }, + "platform": { + "bsonType": "string", + "description": "平台类型" + }, + "create_date": { + "bsonType": "timestamp", + "description": "创建时间", + "forceDefaultValue": { + "$env": "now" + } + }, + "last_active_date": { + "bsonType": "timestamp", + "description": "最后登录时间" + }, + "last_active_ip": { + "bsonType": "string", + "description": "最后登录IP" + } + }, + "version": "0.0.1" +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/uni-id-log.schema.json b/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/uni-id-log.schema.json new file mode 100644 index 0000000..ff4f797 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/uni-id-log.schema.json @@ -0,0 +1,71 @@ +{ + "bsonType": "object", + "required": ["user_id"], + "permission": { + "read": "'READ_UNI_ID_LOG' in auth.permission" + }, + "properties": { + "_id": { + "description": "ID,系统自动生成" + }, + "create_date": { + "bsonType": "timestamp", + "description": "创建时间", + "forceDefaultValue": { + "$env": "now" + } + }, + "device_uuid": { + "bsonType": "string", + "description": "设备唯一标识" + }, + "ip": { + "bsonType": "string", + "description": "ip地址" + }, + "state": { + "bsonType": "int", + "description": "结果:0 失败、1 成功" + }, + "type": { + "bsonType": "string", + "description": "操作类型", + "enum": [ + "logout", + "login", + "register", + "reset-pwd", + "bind-mobile", + "bind-weixin", + "bind-qq", + "bind-apple", + "bind-alipay" + ] + }, + "ua": { + "bsonType": "string", + "description": "userAgent" + }, + "user_id": { + "bsonType": "string", + "foreignKey": "uni-id-users._id", + "description": "用户id,参考uni-id-users表" + }, + "username": { + "bsonType": "string", + "description": "用户名" + }, + "email": { + "bsonType": "string", + "description": "邮箱" + }, + "mobile": { + "bsonType": "string", + "description": "手机号" + }, + "appid": { + "bsonType": "string", + "description": "客户端DCloud AppId" + } + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/uni-id-permissions.schema.json b/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/uni-id-permissions.schema.json new file mode 100644 index 0000000..25209cb --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/uni-id-permissions.schema.json @@ -0,0 +1,52 @@ +{ + "bsonType": "object", + "required": ["permission_id", "permission_name"], + "permission": { + "read": "'READ_UNI_ID_PERMISSIONS' in auth.permission", + "create": "'CREATE_UNI_ID_PERMISSIONS' in auth.permission", + "update": "'UPDATE_UNI_ID_PERMISSIONS' in auth.permission", + "delete": "'DELETE_UNI_ID_PERMISSIONS' in auth.permission" + }, + "properties": { + "_id": { + "description": "存储文档 ID,系统自动生成" + }, + "comment": { + "bsonType": "string", + "component": { + "name": "textarea" + }, + "description": "备注", + "label": "备注", + "title": "备注", + "trim": "both" + }, + "create_date": { + "bsonType": "timestamp", + "description": "创建时间", + "forceDefaultValue": { + "$env": "now" + } + }, + "permission_id": { + "bsonType": "string", + "component": { + "name": "input" + }, + "description": "权限唯一标识,不可修改,不允许重复", + "label": "权限标识", + "title": "权限ID", + "trim": "both" + }, + "permission_name": { + "bsonType": "string", + "component": { + "name": "input" + }, + "description": "权限名称", + "label": "权限名称", + "title": "权限名称", + "trim": "both" + } + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/uni-id-roles.schema.json b/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/uni-id-roles.schema.json new file mode 100644 index 0000000..e2fe322 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/uni-id-roles.schema.json @@ -0,0 +1,50 @@ +{ + "bsonType": "object", + "required": ["role_id", "role_name"], + "permission": { + "read": "'READ_UNI_ID_ROLES' in auth.permission", + "create": "'CREATE_UNI_ID_ROLES' in auth.permission", + "update": "'UPDATE_UNI_ID_ROLES' in auth.permission", + "delete": "'DELETE_UNI_ID_ROLES' in auth.permission" + }, + "properties": { + "_id": { + "description": "存储文档 ID,系统自动生成" + }, + "comment": { + "title": "备注", + "bsonType": "string", + "description": "备注", + "trim": "both" + }, + "create_date": { + "bsonType": "timestamp", + "description": "创建时间", + "forceDefaultValue": { + "$env": "now" + } + }, + "permission": { + "title": "权限", + "bsonType": "array", + "foreignKey": "uni-id-permissions.permission_id", + "description": "角色拥有的权限列表", + "enum": { + "collection": "uni-id-permissions", + "field": "permission_name as text, permission_id as value" + } + }, + "role_id": { + "title": "唯一ID", + "bsonType": "string", + "description": "角色唯一标识,不可修改,不允许重复", + "trim": "both" + }, + "role_name": { + "title": "名称", + "bsonType": "string", + "description": "角色名称", + "trim": "both" + } + } +} diff --git a/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/uni-id-users.schema.json b/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/uni-id-users.schema.json new file mode 100644 index 0000000..b5aea02 --- /dev/null +++ b/uni-im示例/uni_modules/uni-id-pages/uniCloud/database/uni-id-users.schema.json @@ -0,0 +1,473 @@ +{ + "bsonType": "object", + "permission": { + "read": true, + "create": "'CREATE_UNI_ID_USERS' in auth.permission", + "update": "doc._id == auth.uid || 'UPDATE_UNI_ID_USERS' in auth.permission", + "delete": "'DELETE_UNI_ID_USERS' in auth.permission" + }, + "properties": { + "_id": { + "description": "存储文档 ID(用户 ID),系统自动生成" + }, + "ali_openid": { + "bsonType": "string", + "description": "支付宝平台openid", + "permission": { + "read": "'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "apple_openid": { + "bsonType": "string", + "description": "苹果登录openid", + "permission": { + "read": "'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "avatar": { + "bsonType": "string", + "description": "头像地址", + "title": "头像地址", + "trim": "both", + "permission": { + "read": true, + "write": "doc._id == auth.uid || 'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "avatar_file": { + "bsonType": "file", + "description": "用file类型方便使用uni-file-picker组件", + "title": "头像文件", + "permission": { + "read": true, + "write": "doc._id == auth.uid || 'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "comment": { + "bsonType": "string", + "description": "备注", + "title": "备注", + "trim": "both", + "permission": { + "read": "'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "dcloud_appid": { + "bsonType": "array", + "description": "允许登录的客户端的appid列表", + "foreignKey": "opendb-app-list.appid", + "permission": { + "read": "'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "department_id": { + "bsonType": "array", + "description": "部门ID", + "enum": { + "collection": "opendb-department", + "field": "_id as value, name as text", + "orderby": "name asc" + }, + "enumType": "tree", + "title": "部门", + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "email": { + "bsonType": "string", + "description": "邮箱地址", + "format": "email", + "title": "邮箱", + "trim": "both", + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "email_confirmed": { + "bsonType": "int", + "defaultValue": 0, + "description": "邮箱验证状态:0 未验证 1 已验证", + "enum": [{ + "text": "未验证", + "value": 0 + }, + { + "text": "已验证", + "value": 1 + } + ], + "title": "邮箱验证状态", + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "gender": { + "bsonType": "int", + "defaultValue": 0, + "description": "用户性别:0 未知 1 男性 2 女性", + "enum": [{ + "text": "未知", + "value": 0 + }, + { + "text": "男", + "value": 1 + }, + { + "text": "女", + "value": 2 + } + ], + "title": "性别", + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "invite_time": { + "bsonType": "timestamp", + "description": "受邀时间", + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "inviter_uid": { + "bsonType": "array", + "description": "用户全部上级邀请者", + "trim": "both", + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "last_login_date": { + "bsonType": "timestamp", + "description": "最后登录时间", + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "last_login_ip": { + "bsonType": "string", + "description": "最后登录时 IP 地址", + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "mobile": { + "bsonType": "string", + "description": "手机号码", + "pattern": "^\\+?[0-9-]{3,20}$", + "title": "手机号码", + "trim": "both", + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "mobile_confirmed": { + "bsonType": "int", + "defaultValue": 0, + "description": "手机号验证状态:0 未验证 1 已验证", + "enum": [{ + "text": "未验证", + "value": 0 + }, + { + "text": "已验证", + "value": 1 + } + ], + "title": "手机号验证状态", + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "my_invite_code": { + "bsonType": "string", + "description": "用户自身邀请码", + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "nickname": { + "bsonType": "string", + "description": "用户昵称", + "title": "昵称", + "trim": "both", + "permission": { + "read": true, + "write": "doc._id == auth.uid || 'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "password": { + "bsonType": "password", + "description": "密码,加密存储", + "title": "密码", + "trim": "both" + }, + "password_secret_version": { + "bsonType": "int", + "description": "密码使用的passwordSecret版本", + "title": "passwordSecret", + "permission": { + "read": false, + "write": false + } + }, + "realname_auth": { + "bsonType": "object", + "description": "实名认证信息", + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + }, + "properties": { + "auth_date": { + "bsonType": "timestamp", + "description": "认证通过时间" + }, + "auth_status": { + "bsonType": "int", + "description": "认证状态:0 未认证 1 等待认证 2 认证通过 3 认证失败", + "maximum": 3, + "minimum": 0 + }, + "contact_email": { + "bsonType": "string", + "description": "联系人邮箱" + }, + "contact_mobile": { + "bsonType": "string", + "description": "联系人手机号码" + }, + "contact_person": { + "bsonType": "string", + "description": "联系人姓名" + }, + "id_card_back": { + "bsonType": "string", + "description": "身份证反面照 URL" + }, + "id_card_front": { + "bsonType": "string", + "description": "身份证正面照 URL" + }, + "identity": { + "bsonType": "string", + "description": "身份证号码/营业执照号码" + }, + "in_hand": { + "bsonType": "string", + "description": "手持身份证照片 URL" + }, + "license": { + "bsonType": "string", + "description": "营业执照 URL" + }, + "real_name": { + "bsonType": "string", + "description": "真实姓名/企业名称" + }, + "type": { + "bsonType": "int", + "description": "用户类型:0 个人用户 1 企业用户", + "maximum": 1, + "minimum": 0 + } + }, + "required": [ + "type", + "auth_status" + ] + }, + "register_date": { + "bsonType": "timestamp", + "description": "注册时间", + "forceDefaultValue": { + "$env": "now" + }, + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "register_ip": { + "bsonType": "string", + "description": "注册时 IP 地址", + "forceDefaultValue": { + "$env": "clientIP" + }, + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "role": { + "bsonType": "array", + "description": "用户角色", + "enum": { + "collection": "uni-id-roles", + "field": "role_id as value, role_name as text" + }, + "foreignKey": "uni-id-roles.role_id", + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + }, + "title": "角色" + }, + "tags":{ + "bsonType": "array", + "description": "用户标签", + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + }, + "title": "标签" + }, + "score": { + "bsonType": "int", + "description": "用户积分,积分变更记录可参考:uni-id-scores表定义", + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "status": { + "bsonType": "int", + "defaultValue": 0, + "description": "用户状态:0 正常 1 禁用 2 审核中 3 审核拒绝", + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + }, + "enum": [{ + "text": "正常", + "value": 0 + }, + { + "text": "禁用", + "value": 1 + }, + { + "text": "审核中", + "value": 2 + }, + { + "text": "审核拒绝", + "value": 3 + } + ], + "title": "用户状态" + }, + "token": { + "bsonType": "array", + "description": "用户token", + "permission": { + "read": false, + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "username": { + "bsonType": "string", + "description": "用户名,不允许重复", + "title": "用户名", + "trim": "both", + "permission": { + "read": "doc._id == auth.uid || 'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "wx_openid": { + "bsonType": "object", + "description": "微信各个平台openid", + "properties": { + "app": { + "bsonType": "string", + "description": "app平台微信openid" + }, + "mp": { + "bsonType": "string", + "description": "微信小程序平台openid" + }, + "h5": { + "bsonType": "string", + "description": "微信公众号登录openid" + }, + "web": { + "bsonType": "string", + "description": "PC页面扫码登录openid" + } + }, + "permission": { + "read": "'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "wx_unionid": { + "bsonType": "string", + "description": "微信unionid", + "permission": { + "read": "'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "qq_openid": { + "bsonType": "object", + "description": "QQ各个平台openid", + "properties": { + "app": { + "bsonType": "string", + "description": "app平台QQ openid" + }, + "mp": { + "bsonType": "string", + "description": "QQ小程序平台openid" + } + }, + "permission": { + "read": "'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "qq_unionid": { + "bsonType": "string", + "description": "QQ unionid", + "permission": { + "read": "'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + }, + "third_party": { + "bsonType": "object", + "description": "三方平台凭证", + "permission": { + "read": false, + "write": false + } + }, + "identities": { + "bsonType": "array", + "description": "三方平台身份信息;一个对象代表一个身份,参数支持: provider 身份源, userInfo 三方用户信息, openid 三方openid, unionid 三方unionid, uid 三方uid", + "permission": { + "read": "'READ_UNI_ID_USERS' in auth.permission", + "write": "'CREATE_UNI_ID_USERS' in auth.permission || 'UPDATE_UNI_ID_USERS' in auth.permission" + } + } + }, + "required": [] +} diff --git a/uni-im示例/uni_modules/uni-im/changelog.md b/uni-im示例/uni_modules/uni-im/changelog.md new file mode 100644 index 0000000..1e7063d --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/changelog.md @@ -0,0 +1,140 @@ +## 2.0.14(2023-07-05) +- 修复 uni-id-users表的触发器,不支持通过getOne查询的问题 +## 2.0.13(2023-07-03) +- 更新 修改uni-id-users表的触发器,使其在某些情况下部分逻辑无法生效时,只输出日志提示,而不抛出异常。 +## 2.0.12(2023-06-02) +- 修复 web端Vue3下 消息发送后状态不更新的问题 +## 2.0.11(2023-05-29) +- 新增 web-pc 消息输入框工具栏按钮 鼠标上移后的提示 +- 新增 消息发送按钮 +- 新增 点击回复类型的消息,跳到关联的消息后高亮1.5秒的效果 +- 优化 群信息页面 兼容没有昵称的用户显示 防止布局塌陷 +- 优化 回复类型消息的引用文本溢出自动省略显示 +- 修复 因@keyup.shift无效引起的消息发送失败问题 +- 修复 部分情况下,查询到的本地数据顺序错误的问题 +- 修复 同一个浏览器多标签页打开im收到的消息重复的问题 +- 修复 群聊回复消息,非干系人收到有人@我的提醒 +## 2.0.10(2023-05-25) +- 修复 Vue3-web-pc端 敲完回车会先执行换行再发送消息的问题 +## 2.0.9(2023-05-25) +- 修复 因为`2.0.8`优化sqlite,引起的web端报`ReferenceError: sqlite is not defined`的问题 +## 2.0.8(2023-05-24) +- 修复 app-android端 部分情况下发送消息会卡在发送中,再次点击雪花图标后才能发送的问题 +## 2.0.7(2023-05-23) +- 修复 当聊天对话输入框,文字内容超过一行时,切换到语音输入模式;录音按钮位置不正确的问题。 +- 修复 不选择任何好友直接创建群聊,客户端不显示创建者的加群记录的问题 +- 优化 当会话为群聊时,标题栏显示群人数 +- 优化 代码浏览功能的tab-size为4 +## 2.0.6(2023-05-22) +- 修复 Vue3下报 `ReferenceError: Cannot access 'getCloudMsgIng' before initialization`的问题 +## 2.0.5(2023-05-19) +- 修复 iOS端 应用切换到后台之后收到消息,再打开应用,部分情况下会丢消息的问题 +- 修复 微信小程序端 播放语音报错的问题 +- 修复 微信小程序端 发送视频,显示为文件格式,没有用video组件显示的问题 +- 修复 微信小程序端 打开对话窗口 偶尔不能自动滚动到最新一条消息的问题 +- 修复 微信小程序端 在非tabbar界面收到消息或系统通知后tabbar的角标不更新的问题 +- 重构 app-nvue代码浏览模块 +- 修复 云存储临时链接过期后播放视频语音等报错的问题 +- 修复 部分情况下 tabbar角标不更新的问题 +## 2.0.4(2023-04-24) +- 修复 web端 部分情况下 收到新消息需要延迟滚动到最新消息的问题 +## 2.0.3(2023-04-20) +- 新增 限制只能撤回2分钟内的消息(群主不受任何限制) +- 修复 微信小程序端发送图片报错的问题 +- 修复 Vue2 H5端dom加载慢时,showLast报错 +- 修复 新发送的消息 时间不刷新的问题 +## 2.0.2(2023-04-18) +- 修复 Vue2模式 聊天时间当消息过长会消失的问题 +## 2.0.1(2023-04-17) +1. 修复 当用户接收到消息后关闭im,消息发送者再撤回消息。且在push指令离线消息时效过期后,用户再打开im,撤回无效的问题。 +2. 修复 微信小程序端滑动快的时候会抖动的问题 +3. 修复 部分情况下,群聊消息发不出去的问题 +4. 修复 Vue2模式下 消息不满一屏插入消息无效 +5. 修复 创建群聊时,如果没有选择任何用户。报res 不存在的错误 +6. 修复 部分情况下群聊消息必须刷新后才能撤回的问题 +## 2.0.0(2023-04-14) +【重要】v2版正式发布 +## 1.6.3(2023-03-06) +- 新增 移动APP端,应用桌面角标数,动态同步未读消息数 +- 修复 同一个账号同时在多台设备登录,其中一台设备发送消息,其他设备未同步消息的问题 +- 修复 当应用被切换到后台时,应用进程未被关闭,但socket进程被关闭的情况下。切回到前台,期间的消息丢失的问题 +## 1.6.2(2023-03-03) +- 修复 当项目一启动且token无效时,直达与某个用户对话。跳转至登录页面后返回会话页面报`无效的conversation_id`的问题 +## 1.6.1(2023-02-27) +- 修复 因版本号1.5.9引起的微信小程序端拿不到globalData的问题 +## 1.5.9(2023-02-24) +- 修复 群聊消息时间不显示的问题 +- 修复 部分情况下 加好友不显示昵称的问题 +- 修复 部分情况下 web手机端创建群聊后不会自动返回的问题 +## 1.5.8(2023-02-23) +- 修复 部分情况下 非uniCloud项目接入uni-im 联登成功后报找不到uniIdCo的问题 +## 1.5.7(2023-02-22) +- 更新 优化会话表查询性能,防止数据量大时慢查询 +## 1.5.6(2023-02-20) +- 修复 部分情况下 群聊功能,提示有新用户进群的消息样式不正确的问题 +## 1.5.5(2023-02-17) +- 修复 Vue2下不支持“可选链操作符”导致的报错问题 +## 1.5.4(2023-02-16) +- 修复 pc端 当消息不满一页时,来回切换同一个用户 会一直提示正在加载历史消息的问题 +## 1.5.3(2023-02-15) +- 修复 在safari浏览器下的兼容问题 +- 修复 快速滚动消息列表 偶发加载不了更多消息的问题 +## 1.5.2(2023-02-15) +- 修复 iOS端 部分情况下不会自动滚动到最后一条消息的问题 +## 1.5.1(2023-02-14) +- 修复 部分情况下会话列表页面 最新一条消息不刷新,未读消息数不递增的问题 +## 1.5.0(2023-02-11) +- 更新示例项目 演示分包加载 uni-im +- 更新 抽离聊天对话页面的消息列表,为独立组件; 分层简化代码 更清晰方便二开 +- 修复 因iOS端 微信小程序平台 键盘弹出后 引起的输入框偶尔位置不正确的问题 +## 1.4.4(2023-02-03) +- 更新示例项目 采用分包使用uni-id-pages +- 更新 默认不启用代码浏览模块 +## 1.4.3(2023-01-29) +- 注释多余的`console.log`代码 +## 1.4.2(2023-01-29) +- 优化 微信小程序平台 部分全面屏挡住UI操作不方便的问题 +- 修复 因iOS端 微信小程序平台 键盘弹出后 调用 pageScrollTo 偶尔会导致 textarea 组件的 adjust-position=false 失效,而引起的 输入框错位的问题(兼容方案,后续微信小程序官方修复后可移除相关代码) +## 1.4.1(2023-01-28) +- 优化 切换发送消息类型的性能(软键盘不再频繁收起和弹出) +- 修复 iOS端 微信小程序平台 键盘弹出后 连续发送消息输入框跟随移动的问题 +- 修复 iOS端 部分机型 发送语音功能 蒙版显示不完整的问题 +- 修复 uni-im-co 某些情况下调用this.uniIdCommon 报错的问题 +## 1.4.0(2023-01-18) +- 【重要】新增 群聊功能 +- 【重要】新增 好友关系管理功能 +**注意:** 这是一个不兼容的更新,需要执行jql修改相关字段,详情查看:[升级旧项目为 uni-im 1.4.0(群聊版) 注意事项](https://uniapp.dcloud.net.cn/uniCloud/uni-im.html#%E5%8D%87%E7%BA%A7%E6%97%A7%E9%A1%B9%E7%9B%AE%E4%B8%BA-uni-im-1-4-0-%E7%BE%A4%E8%81%8A%E7%89%88-%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9) +## 1.3.3(2022-12-05) +- 新增 移动端支持emoji表情 +## 1.3.2(2022-12-05) +- 修复 因vue2与vue3下 :key 的位置要求不同 引起的chat页面报错问题 +## 1.3.1(2022-12-05) +- 修复 1.3.0版引起的截图粘贴发送失败的问题 +- 修复 集成到 uni-admin 中样式设置失败的问题 +## 1.3.0(2022-12-02) +- 修复 APP端当消息未满半屏时,弹出的键盘会将消息顶出界面外 +- 新增 消息类型支持代码模式 +- 新增 支持超长文本(仅代码模式支持,后续会支持普通消息模式) +- 新增 多媒体消息(含:语音、图片、视频、任意文件),App和小程序端支持使用第三方程序打开文件 +## 1.2.1(2022-11-25) +- 修复 某些情况下 非uniCloud 开发的项目 接入uni-im 登录后会话列表不更新的问题 +## 1.2.0(2022-11-23) +- 【重要】全端支持Vue3 +- 修复 当历史消息超长时,APP端键盘弹起,不能滚动到最后一条消息 +- 修复 键盘收起时,会自动滚动到最后一条消息的问题 +- 修复 部分情况下,切换登录的账号,会话列表没有更新的问题 +## 1.1.2(2022-11-21) +修复 某些情况下 iOS端 输入框内容发生变化时 页面重新排版,导致输入框被键盘挡住的问题 +## 1.1.1(2022-11-18) +修复 向长时间未登录的用户(push_clientid已过期)发送消息,引起的报错问题。将数据写入云数据库,当用户再次登录时从服务端拉取 +## 1.1.0(2022-11-18) +- 新增 支持 非uniCloud(比如:应用服务端的开发语言是php、java、go、c#、python等)或 不基于uni-id-pages 开发的项目 接入uni-im +- 简化部署流程 app.vue 页面仅需init uni-im即可(更加模块化,内部:监听应用生命周期onShow、onHidden实现相关功能、初始化依赖的globalData等) +## 1.0.3(2022-11-15) +降低uni-im使用的HBuilderX版本为`3.6.4`。 注意**APP端**:仅支持Vue2,且HBuilderX的版本为3.6.9+,否则chat页面存在滚动锚定问题(后续会修复此问题) +## 1.0.2(2022-11-14) +使用 1.2.3 版的 uni-list-chat 解决部署在腾讯云版uniCloud的uni-im项目 头像不能显示的问题 +## 1.0.1(2022-11-14) +修复 因nvue下行间样式无法覆盖导致的 样式错误 +## 0.0.1(2022-11-04) +init \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/common/appEvent.js b/uni-im示例/uni_modules/uni-im/common/appEvent.js new file mode 100644 index 0000000..02b2651 --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/common/appEvent.js @@ -0,0 +1,76 @@ +// #ifdef VUE3 +import { + onShow +} from '@dcloudio/uni-app' +import { + onHide +} from '@dcloudio/uni-app' +// #endif + +let onAppShowCallback = [], + onAppHideCallback = [] +// 监听应用生命周期 +const appEvent = { + appShowIndex: 0, + appHideIndex: 0, + onAppShow(callback) { + this.appShowIndex++ + if (typeof callback == 'function') { + onAppShowCallback.push(callback) + } + onAppShowCallback.forEach(fun => fun()) + }, + onAppHide(callback) { + this.appHideIndex++ + if (typeof callback == 'function') { + onAppHideCallback.push(callback) + } + onAppHideCallback.forEach(fun => fun()) + } +} + +setTimeout(() => { + // #ifdef APP + // #ifdef VUE2 + getApp().$vm.$on('hook:onShow', function() { + appEvent.onAppShow() + }) + getApp().$vm.$on('hook:onHide', function() { + appEvent.onAppHide() + }) + // #endif + + // #ifdef VUE3 + onShow(function() { + appEvent.onAppShow() + }, getApp().$vm.$) + onHide(function() { + appEvent.onAppHide() + }, getApp().$vm.$) + // #endif + // #endif + + // #ifdef H5 + document.addEventListener("visibilitychange", () => { + if (document.hidden) { + // 页面被挂起 + appEvent.onAppHide() + } else { + // 页面呼出 + appEvent.onAppShow() + } + }) + // #endif + + // #ifdef MP + uni.onAppShow(function() { + appEvent.onAppShow() + }) + uni.onAppHide(function() { + appEvent.onAppHide() + }) + // #endif + +}, 0) + +export default appEvent diff --git a/uni-im示例/uni_modules/uni-im/common/emojiCodes.js b/uni-im示例/uni_modules/uni-im/common/emojiCodes.js new file mode 100644 index 0000000..87333d1 --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/common/emojiCodes.js @@ -0,0 +1 @@ +export default `😀,😁,😂,🤣,😃,😄,😅,😆,😉,😊,😋,😎,😍,😘,😗,😙,😚,☺️,🙂,🤗,🤩,🤔,🤨,😐,😑,😶,🙄,😏,😣,😥,😮,🤐,😯,😪,😫,😴,😌,😛,😜,😝,🤤,😒,😓,😔,😕,🙃,🤑,😲,☹️,🙁,😖,😞,😟,😤,😢,😭,😦,😧,😨,😩,🤯,😬,😰,😱,😳,🤪,😵,😡,😠,🤬,😷,🤒,🤕,🤢,🤮,🤧,😇,🤠,🤡,🤥,🤫,🤭,🧐,🤓,😈,👿,👹,👺,💀,☠️,👻,👽,🤖,😺,😸,😹,😻,😼,😽,🙀,😿,😾,🙈,🙉,🙊,👶,🧒,👦,👧,🧑,👨,👩,🧓,👴,👵,👨‍⚕️,👩‍⚕️,👨‍🎓,👩‍🎓,👨‍🏫,👩‍🏫,👨‍⚖️,👩‍⚖️,👨‍🌾,👩‍🌾,👨‍🍳,👩‍🍳,👨‍🔧,👩‍🔧,👨‍🏭,👩‍🏭,👨‍💼,👩‍💼,👨‍🔬,👩‍🔬,👨‍💻,👩‍💻,👨‍🎤,👩‍🎤,👨‍🎨,👩‍🎨,👨‍✈️,👩‍✈️,👨‍🚀,👩‍🚀,👨‍🚒,👩‍🚒,👮,👮‍♂️,👮‍♀️,🕵️,🕵️‍♂️,🕵️‍♀️,💂,💂‍♂️,💂‍♀️,👷,👷‍♂️,👷‍♀️,🤴,👸,👳,👳‍♂️,👳‍♀️,👲,🧕,🧔,👱,👱‍♂️,👱‍♀️,🤵,👰,🤰,🤱,👼,🎅,🤶,🧙,🧙‍♀️,🧙‍♂️,🧚,🧚‍♀️,🧚‍♂️,🧛,🧛‍♀️,🧛‍♂️,🧜,🧜‍♀️,🧜‍♂️,🧝,🧝‍♀️,🧝‍♂️,🧞,🧞‍♀️,🧟,🧟‍♀️,🙍,🙍‍♂️,🙍‍♀️,🙎,🙎‍♂️,🙎‍♀️,🙅,🙅‍♂️,🙅‍♀️,🙆,🙆‍♂️,🙆‍♀️,💁,💁‍♂️,💁‍♀️,🙋,🙋‍♂️,🙋‍♀️,🙇,🙇‍♂️,🙇‍♀️,🤦,🤦‍♂️,🤦‍♀️,🤷,🤷‍♂️,🤷‍♀️,💆,💆‍♂️,💆‍♀️,💇,💇‍♂️,💇‍♀️,🚶,🚶‍♂️,🚶‍♀️,🏃,🏃‍♂️,🏃‍♀️,💃,🕺,👯,👯‍♂️,👯‍♀️,🧖,🧖‍♀️,🧖‍♂️,🧗,🧗‍♀️,🧗‍♂️,🧘,🧘‍♀️,🧘‍♂️,🕴️,👤,👥,👫,👬,👭,💏,👨‍❤️‍💋‍👨,👩‍❤️‍💋‍👩,💑,👨‍❤️‍👨,👩‍❤️‍👩,👪,👨‍👩‍👧,👨‍👩‍👧‍👦,👨‍👩‍👦‍👦,👨‍👩‍👧‍👧,👨‍👨‍👦,👨‍👨‍👧,👨‍👨‍👧‍👦,👨‍👨‍👦‍👦,👨‍👨‍👧‍👧,👩‍👩‍👦,👩‍👩‍👧,👩‍👩‍👧‍👦,👩‍👩‍👦‍👦,👩‍👩‍👧‍👧,👨‍👦,👨‍👧,👨‍👧‍👦,👨‍👧‍👧,👩‍👦‍👦,👩‍👧,👩‍👧‍👦,🤳,👃,👅,👄,💋,💘,❤️,💓,💔,💕,💖,💗,💙,💚,💛,🧡,💜,🖤,💝,💞,❣️,💌,💬,🌬️,☃️,⛄,🎎,🗿,👾,💩,🛀,🛌,💅,👂,👣,👀,👁️,🧠,💭,👓,👔,👕,👖,🧣,🧤,🧥,🧦,👗,👘,👙,👚,👛,👜,👝,🎒,👞,👟,👠,👡,👢,👑,👒,🎩,🎓,🧢,📿,💄,💍,💎,🥄,🔪,🏺,🗺️,🗾,🎠,🎡,🎢,💈,🎪,🛰️,🚀,🛸,🛎️,⌛,⏳,⌚,⏰,🕰️,🌡️,🌂,☂️,☔,⛱️,⚡,🎃,🎄,🎆,🎇,🎈,🎉,🎊,🎏,🎐,🎑,🎀,🎁,🎗️,🎟️,🎫,🔮,🎮,🕹️,🎰,🃏,🎴,🎭,🖼️,🎨,🔇,🔈,🔉,🔊,📢,📣,📯,🔔,🔕,🎼,🎵,🎶,🎙️,🎚️,🎛️,🎤,🎧,📻,🎷,🎸,🎹,🎺,🎻,🥁,📱,📲,☎️,📞,📟,📠,🔋,🔌,💻,🖥️,🖨️,⌨️,🖱️,🖲️,💽,💾,💿,📀,🎥,🎞️,📽️,🎬,📺,📷,📸,📹,📼,🔍,🔎,💡,🔦,🏮,📔,📕,📖,📗,📘,📙,📚,📓,📒,📃,📜,📄,📰,📑,🔖,💰,💴,💵,💶,💷,💸,💳,✉️,📧,📨,📩,📤,📥,📦,📫,📪,📬,📭,📮,✏️,✒️,📝,💼,📁,📂,📅,📆,📇,📈,📉,📊,📋,📌,📍,📎,📏,📐,✂️,🔒,🔓,🔏,🔐,🔑,🔨,🔫,🔧,🔩,🔬,🔭,📡,💉,💊,🚪,🚽,🚿,🛁,🛒,🚬,🔅,🔆,⚜️,🔱,📛,🚂,🚃,🚄,🚅,🚆,🚇,🚈,🚉,🚊,🚝,🚞,🚋,🚌,🚍,🚎,🚐,🚑,🚒,🚓,🚔,🚕,🚖,🚗,🚘,🚙,🚚,🚛,🚜,🚲,🛴,🛵,🚏,🛣️,🛤️,🛢️,⛽,🚨,🚥,🚦,🛑,🚧,⛵,🛶,🚤,🛳️,⛴️,🛥️,🚢,✈️,🛩️,🛫,🛬,💺,🚁,🚟,🚠,🚡,⚠️,⛔,🦗,🍇,🍈,🍉,🍊,🍋,🍌,🍍,🍎,🍏,🍐,🍑,🍒,🍓,🥝,🍅,🥥,🥑,🍆,🥔,🥕,🌽,🌶️,🥒,🥦,🥜,🍞,🥐,🥖,🥨,🥞,🧀,🍖,🍗,🥩,🥓,🍔,🍟,🍕,🌭,🥪,🌮,🌯,🥙,🥚,🍳,🥘,🍲,🥣,🥗,🍿,🥫,🍱,🍘,🍙,🍚,🍛,🍜,🍝,🍠,🍢,🍣,🍤,🍥,🍡,🥟,🥠,🥡,🍦,🍧,🍨,🍩,🍪,🎂,🍰,🥧,🍫,🍬,🍭,🍮,🍯,🍼,🥛,☕,🍵,🍶,🍾,🍷,🍸,🍹,🍺,🍻,🥂,🥃,🥤,🥢,🍽️,🍴`.split(',') diff --git a/uni-im示例/uni_modules/uni-im/common/initIndexDB.js b/uni-im示例/uni_modules/uni-im/common/initIndexDB.js new file mode 100644 index 0000000..0902a58 --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/common/initIndexDB.js @@ -0,0 +1,44 @@ +export default function init(callback = ()=>{}){ + // #ifdef H5 + if (!window.indexedDB) { + return uni.showModal({ + content: "Your browser doesn't support a stable version of IndexedDB. Such and such feature will not be available.", + showCancel: false + }); + } + let indexedDB = window.indexedDB + let request = indexedDB.open("uni-im", 1) + request.onsuccess = function(event) { + let indexDB = event.target.result // 数据库对象 + // console.log('数据库打开成功') + callback(event) + } + request.onerror = function(event) { + console.error("Database error: " + event.target.errorCode); + callback(event) + } + + request.onupgradeneeded = function(event) { + // 数据库创建或升级的时候会触发 + // console.log('onupgradeneeded') + let indexDB = event.target.result // 数据库对象 + if (!indexDB.objectStoreNames.contains("uni-im-msg")) { + let uniImMsgStore = indexDB.createObjectStore("uni-im-msg", { + keyPath: 'unique_id', + // autoIncrement:true + }) // 创建表 + uniImMsgStore.createIndex('from_uid', 'from_uid', { unique: false }) // 创建索引 可以让你搜索任意字段 + uniImMsgStore.createIndex('to_uid', 'to_uid', { unique: false }) + uniImMsgStore.createIndex('unique_id', 'unique_id', { unique: true }) + uniImMsgStore.createIndex('conversation_id', 'conversation_id', { unique: false }) + uniImMsgStore.createIndex('group_id', 'group_id', { unique: false }) + uniImMsgStore.createIndex('body', 'body', { unique: false }) + uniImMsgStore.createIndex('is_read', 'is_read', { unique: false }) + uniImMsgStore.createIndex('type', 'type', { unique: false }) + uniImMsgStore.createIndex('create_time', 'create_time', { unique: false }) + uniImMsgStore.createIndex('client_create_time', 'client_create_time', { unique: false }) + uniImMsgStore.createIndex('conversation_id-create_time', ['conversation_id','create_time'], { unique: false }) + } + } + // #endif +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/common/md5.js b/uni-im示例/uni_modules/uni-im/common/md5.js new file mode 100644 index 0000000..8f5f70d --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/common/md5.js @@ -0,0 +1,193 @@ +export default function (sMessage) { + function RotateLeft(lValue, iShiftBits) { + return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits)); + } + + function AddUnsigned(lX, lY) { + var lX4, lY4, lX8, lY8, lResult; + lX8 = (lX & 0x80000000); + lY8 = (lY & 0x80000000); + lX4 = (lX & 0x40000000); + lY4 = (lY & 0x40000000); + lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF); + if (lX4 & lY4) return (lResult ^ 0x80000000 ^ lX8 ^ lY8); + if (lX4 | lY4) { + if (lResult & 0x40000000) return (lResult ^ 0xC0000000 ^ lX8 ^ lY8); + else return (lResult ^ 0x40000000 ^ lX8 ^ lY8); + } else return (lResult ^ lX8 ^ lY8); + } + + function F(x, y, z) { + return (x & y) | ((~x) & z); + } + + function G(x, y, z) { + return (x & z) | (y & (~z)); + } + + function H(x, y, z) { + return (x ^ y ^ z); + } + + function I(x, y, z) { + return (y ^ (x | (~z))); + } + + function FF(a, b, c, d, x, s, ac) { + a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac)); + return AddUnsigned(RotateLeft(a, s), b); + } + + function GG(a, b, c, d, x, s, ac) { + a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac)); + return AddUnsigned(RotateLeft(a, s), b); + } + + function HH(a, b, c, d, x, s, ac) { + a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac)); + return AddUnsigned(RotateLeft(a, s), b); + } + + function II(a, b, c, d, x, s, ac) { + a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac)); + return AddUnsigned(RotateLeft(a, s), b); + } + + function ConvertToWordArray(sMessage) { + var lWordCount; + var lMessageLength = sMessage.length; + var lNumberOfWords_temp1 = lMessageLength + 8; + var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64; + var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16; + var lWordArray = Array(lNumberOfWords - 1); + var lBytePosition = 0; + var lByteCount = 0; + while (lByteCount < lMessageLength) { + lWordCount = (lByteCount - (lByteCount % 4)) / 4; + lBytePosition = (lByteCount % 4) * 8; + lWordArray[lWordCount] = (lWordArray[lWordCount] | (sMessage.charCodeAt(lByteCount) << lBytePosition)); + lByteCount++; + } + lWordCount = (lByteCount - (lByteCount % 4)) / 4; + lBytePosition = (lByteCount % 4) * 8; + lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition); + lWordArray[lNumberOfWords - 2] = lMessageLength << 3; + lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29; + return lWordArray; + } + + function WordToHex(lValue) { + var WordToHexValue = "", + WordToHexValue_temp = "", + lByte, lCount; + for (lCount = 0; lCount <= 3; lCount++) { + lByte = (lValue >>> (lCount * 8)) & 255; + WordToHexValue_temp = "0" + lByte.toString(16); + WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length - 2, 2); + } + return WordToHexValue; + } + var x = Array(); + var k, AA, BB, CC, DD, a, b, c, d + var S11 = 7, + S12 = 12, + S13 = 17, + S14 = 22; + var S21 = 5, + S22 = 9, + S23 = 14, + S24 = 20; + var S31 = 4, + S32 = 11, + S33 = 16, + S34 = 23; + var S41 = 6, + S42 = 10, + S43 = 15, + S44 = 21; + x = ConvertToWordArray(sMessage); + a = 0x67452301; + b = 0xEFCDAB89; + c = 0x98BADCFE; + d = 0x10325476; + for (k = 0; k < x.length; k += 16) { + AA = a; + BB = b; + CC = c; + DD = d; + a = FF(a, b, c, d, x[k + 0], S11, 0xD76AA478); + d = FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756); + c = FF(c, d, a, b, x[k + 2], S13, 0x242070DB); + b = FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE); + a = FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF); + d = FF(d, a, b, c, x[k + 5], S12, 0x4787C62A); + c = FF(c, d, a, b, x[k + 6], S13, 0xA8304613); + b = FF(b, c, d, a, x[k + 7], S14, 0xFD469501); + a = FF(a, b, c, d, x[k + 8], S11, 0x698098D8); + d = FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF); + c = FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1); + b = FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE); + a = FF(a, b, c, d, x[k + 12], S11, 0x6B901122); + d = FF(d, a, b, c, x[k + 13], S12, 0xFD987193); + c = FF(c, d, a, b, x[k + 14], S13, 0xA679438E); + b = FF(b, c, d, a, x[k + 15], S14, 0x49B40821); + + a = GG(a, b, c, d, x[k + 1], S21, 0xF61E2562); + d = GG(d, a, b, c, x[k + 6], S22, 0xC040B340); + c = GG(c, d, a, b, x[k + 11], S23, 0x265E5A51); + b = GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA); + a = GG(a, b, c, d, x[k + 5], S21, 0xD62F105D); + d = GG(d, a, b, c, x[k + 10], S22, 0x2441453); + c = GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681); + b = GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8); + a = GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6); + d = GG(d, a, b, c, x[k + 14], S22, 0xC33707D6); + c = GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87); + b = GG(b, c, d, a, x[k + 8], S24, 0x455A14ED); + a = GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905); + d = GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8); + c = GG(c, d, a, b, x[k + 7], S23, 0x676F02D9); + b = GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A); + + a = HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942); + d = HH(d, a, b, c, x[k + 8], S32, 0x8771F681); + c = HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122); + b = HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C); + a = HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44); + d = HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9); + c = HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60); + b = HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70); + a = HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6); + d = HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA); + c = HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085); + b = HH(b, c, d, a, x[k + 6], S34, 0x4881D05); + a = HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039); + d = HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5); + c = HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8); + b = HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665); + + a = II(a, b, c, d, x[k + 0], S41, 0xF4292244); + d = II(d, a, b, c, x[k + 7], S42, 0x432AFF97); + c = II(c, d, a, b, x[k + 14], S43, 0xAB9423A7); + b = II(b, c, d, a, x[k + 5], S44, 0xFC93A039); + a = II(a, b, c, d, x[k + 12], S41, 0x655B59C3); + d = II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92); + c = II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D); + b = II(b, c, d, a, x[k + 1], S44, 0x85845DD1); + a = II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F); + d = II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0); + c = II(c, d, a, b, x[k + 6], S43, 0xA3014314); + b = II(b, c, d, a, x[k + 13], S44, 0x4E0811A1); + a = II(a, b, c, d, x[k + 4], S41, 0xF7537E82); + d = II(d, a, b, c, x[k + 11], S42, 0xBD3AF235); + c = II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB); + b = II(b, c, d, a, x[k + 9], S44, 0xEB86D391); + + a = AddUnsigned(a, AA); + b = AddUnsigned(b, BB); + c = AddUnsigned(c, CC); + d = AddUnsigned(d, DD); + } + var temp = WordToHex(a) + WordToHex(b) + WordToHex(c) + WordToHex(d); + return temp.toLowerCase(); +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/common/sqlite.js b/uni-im示例/uni_modules/uni-im/common/sqlite.js new file mode 100644 index 0000000..a0af5f9 --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/common/sqlite.js @@ -0,0 +1,135 @@ +let options = { + name: "uni-im", + path: '_doc/uni-im.db' +} +import uniIm from '@/uni_modules/uni-im/lib/main.js'; +export default { + async init(callback = ()=>{}){ + callback() + }, + async checkDataBaseIsOpen(){ + if(uniIm.dataBaseIsOpen){ + return true + } + let dataBaseIsOpen = plus.sqlite.isOpenDatabase(options) + uniIm.dataBaseIsOpen = dataBaseIsOpen + // console.log('uniIm.dataBaseIsOpen',uniIm.dataBaseIsOpen); + if (!dataBaseIsOpen) { + let res = await new Promise((resolve, reject) => { + plus.sqlite.openDatabase({ + ...options, + success: function(e) { + // console.log(e, 'openDatabase success!') + resolve(e) + }, + fail: function(e) { + console.error(e, 'openDatabase failed: ' + JSON.stringify(e)) + reject(e) + } + }); + }) + + let sql = `create table if not exists msg( + "_id" CHAR(32), + "body" TEXT, + "type" CHAR(32), + "from_uid" CHAR(32), + "to_uid" CHAR(32), + "is_read" BOOLEAN, + "friendly_time" DATETIME, + "create_time" DATETIME, + "conversation_id" CHAR(32), + "group_id" CHAR(32), + "client_create_time" DATETIME, + "unique_id" CHAR(32), + "appid" CHAR(32), + "state" INT, + "is_revoke" BOOLEAN, + "is_delete" BOOLEAN, + "action" TEXT + )` + this.executeSql(sql) + + return res + } + }, + async clearMsgTable(){ + let dd = await this.executeSql('drop table msg') + console.log('clearMsgTable',dd); + }, + async executeSql(sql) { //执行executeSql + await this.checkDataBaseIsOpen() + // console.log('sql',sql); + return await new Promise((resolve, reject) => { + // console.log('执行executeSql',{ + // "name": options.name, + // "sql": sql + // }); + try{ + plus.sqlite.executeSql({ + name: options.name, + sql: sql, + success: function(e) { + // console.log(e, 'executeSql success!') + resolve(e) + }, + fail: function(e) { + console.error(e) + console.error({sql}) + console.error('executeSql failed: ' + JSON.stringify(e)) + console.error('executeSql failed: ' + JSON.stringify(sql)) + reject(e) + } + }); + }catch(e){ + reject(e) + } + }) + }, + async selectSql(sql) { //执行selectSql + await this.checkDataBaseIsOpen() + + return await new Promise( async(resolve, reject) => { + // console.log('执行selectSql',{ + // "name": options.name, + // "sql": sql + // }); + try{ + plus.sqlite.selectSql({ + name: options.name, + sql: sql, + success: function(e) { + // console.log(e, 'selectSql success!') + resolve(e) + }, + fail: function(e) { + console.error('sql:'+sql,'selectSql failed: ' + JSON.stringify(e)) + reject(e) + } + }); + }catch(e){ + reject(e) + } + }) + } +} + +// await this.clearMsgTable() +// try{ +// await new Promise((resolve, reject) => { +// plus.sqlite.closeDatabase({ +// ...options, +// success: function(e) { +// console.log(e, 'closeDatabase success!') +// resolve(e) +// }, +// fail: function(e) { +// console.error(e, 'closeDatabase failed: ' + JSON.stringify(e)) +// reject(e) +// } +// }) +// }) +// }catch(e){ +// console.error(e) +// //TODO handle the exception +// } \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/common/toFriendlyTime.js b/uni-im示例/uni_modules/uni-im/common/toFriendlyTime.js new file mode 100644 index 0000000..f6cbdfe --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/common/toFriendlyTime.js @@ -0,0 +1,70 @@ +export default function toFriendlyTime(timestamp) { + const now = new Date(); + const date = new Date(timestamp); + const secondsAgo = Math.floor((now - date) / 1000); + + // 判断是否在今天 + if (date.getDate() === now.getDate() && date.getMonth() === now.getMonth() && date.getFullYear() === now.getFullYear()) { + // 小于1分钟,显示刚刚 + if (secondsAgo < 60) { + return '刚刚'; + } + + // 大于1分钟小于60分钟,显示具体几分钟前 + if (secondsAgo < 60 * 60) { + const minutes = Math.floor(secondsAgo / 60); + return `${minutes}分钟前`; + } + + // 大于60分钟小于2小时,显示具体几小时+分钟前 + if (secondsAgo < 60 * 60 * 2) { + const hours = Math.floor(secondsAgo / (60 * 60)); + const minutes = Math.floor((secondsAgo - hours * 60 * 60) / 60); + return `${hours}小时 ${minutes}分钟前`; + } + + // 超过2小时,显示具体的几点几分 + const ampm = date.getHours() >= 12 ? '下午' : '上午'; + const hour = date.getHours() % 12 || 12; + const minute = date.getMinutes().toString().padStart(2, '0'); + return `${ampm} ${hour}:${minute}`; + } + + // 不在今天,判断是否在两天内 + const oneDayMs = 24 * 60 * 60 * 1000; + if (now - date < oneDayMs * 2) { + if (date.getDate() === now.getDate() - 1 && date.getMonth() === now.getMonth() && date.getFullYear() === now.getFullYear()) { + // 昨天 + const ampm = date.getHours() >= 12 ? '下午' : '上午'; + const hour = date.getHours() % 12 || 12; + const minute = date.getMinutes().toString().padStart(2, '0'); + return `昨天 ${ampm} ${hour}:${minute}`; + } else { + // 前天 + const ampm = date.getHours() >= 12 ? '下午' : '上午'; + const hour = date.getHours() % 12 || 12; + const minute = date.getMinutes().toString().padStart(2, '0'); + return `前天 ${ampm} ${hour}:${minute}`; + } + } + + // 不在两天内,判断是否在同一周 + const oneWeekMs = oneDayMs * 7; + const diffMs = now - date; + if (diffMs < oneWeekMs) { + const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; + const ampm = date.getHours() >= 12 ? '下午' : '上午'; + const hour = date.getHours() % 12 || 12; + const minute = date.getMinutes().toString().padStart(2, '0'); + return `${days[date.getDay()]} ${ampm} ${hour}:${minute}`; + } + + // 不在同一周,显示具体年月日+时间 + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const day = date.getDate().toString().padStart(2, '0'); + const ampm = date.getHours() >= 12 ? '下午' : '上午'; + const hour = date.getHours() % 12 || 12; + const minute = date.getMinutes().toString().padStart(2, '0'); + return `${year}-${month}-${day} ${ampm} ${hour}:${minute}`; +} diff --git a/uni-im示例/uni_modules/uni-im/common/utils.js b/uni-im示例/uni_modules/uni-im/common/utils.js new file mode 100644 index 0000000..6d7974c --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/common/utils.js @@ -0,0 +1,699 @@ +// #ifdef VUE2 +import Vue from 'vue' +// #endif + +import md5 from '@/uni_modules/uni-im/common/md5' +import toFriendlyTime from '@/uni_modules/uni-im/common/toFriendlyTime.js'; +import appEvent from '@/uni_modules/uni-im/common/appEvent.js'; +import { + store as uniIdStore, + mutations as uniIdMutations +} from '@/uni_modules/uni-id-pages/common/store.js'; +import uniIm from '@/uni_modules/uni-im/lib/main.js'; + +// #ifdef H5 +import initIndexDB from '@/uni_modules/uni-im/common/initIndexDB.js'; +initIndexDB(event => { + // console.log('event.target.result',event.target.result); + uniIm.indexDB = event.target.result +}) +// #endif + +// #ifdef APP +import sqlite from '@/uni_modules/uni-im/common/sqlite.js'; +sqlite.init() +// #endif + +const uniIdCo = uniCloud.importObject("uni-id-co", { + customUI: true +}) +const db = uniCloud.database(); +let appIsShow = true; +let getCloudMsgIng = false +let socketIsClose = true +export default { + init() { + // #ifdef APP + getApp().globalData.sqlite = sqlite + // #endif + + uniIm.socketOpenIndex = 0 + uni.onSocketClose(function(res) { + socketIsClose = true + console.log('WebSocket 已关闭!'); + }); + uni.onSocketOpen(function(res) { + console.log('WebSocket连接已打开!'); + socketIsClose = false + // 记录socket连接次数 + uniIm.socketOpenIndex++ + if (uniIm.socketOpenIndex > 1) { + // 获取socket断开后丢失的数据 + getCloudMsg() + } + }); + + appEvent.onAppShow(async () => { + // 获取socket断开后丢失的数据 + getCloudMsg() + }) + + // 获得云端数据,适用于:socket突然断开丢失,或者应用iOS切到后台拿不到透传等场景使用 获取丢失的数据 + function getCloudMsg(){ + if(getCloudMsgIng){ + return // 防止重复发起,比如即被切换到后台,socket又断开的场景 + } + getCloudMsgIng = true + // 下一个事件循环执行 + setTimeout(async () => { + // 根据本地会话的最大更新时间,查询云端数据 + let maxConversation = (await uniIm.conversation.get())[0] + // console.log({maxConversation}); + if (!maxConversation) { + getCloudMsgIng = false + return + } + let res = await db.collection('uni-im-msg') + .where({ + to_uid: uniCloud.getCurrentUserInfo().uid, + create_time: db.command.gt(maxConversation.update_time) + }) + .get() + console.log('getCloudMsg res',maxConversation.update_time,res.result.data); + let clodMsgData = {} + res.result.data.forEach(item => { + if (clodMsgData[item.conversation_id]) { + clodMsgData[item.conversation_id].push(item) + } else { + clodMsgData[item.conversation_id] = [item] + } + }) + + for (let conversation_id in clodMsgData) { + let conversation = await uniIm.conversation.get(conversation_id) + let msg = clodMsgData[conversation_id] + if (msg.length) { + conversation.msgList.push(...msg) + conversation.msgManager.localMsg.add(msg) + conversation.unread_count += msg.length + } + } + getCloudMsgIng = false + // console.log('获取切到后台后socket离线丢失的数据 res',data); + },0); + } + + //监听im消息 + this.listenImMsg() + + //时间戳心跳(定时器)用于刷新:消息或会话与当前的时间差 + setInterval(() => { + uniIm.heartbeat = Date.now() + }, 1000) + + // 监听窗口变化 + // #ifdef H5 + window.addEventListener('resize', () => { + setIsWidescreen() + }) + + function setIsWidescreen() { + let oldState = uniIm.isWidescreen + uniIm.isWidescreen = window.innerWidth > 960 + if (oldState == false && uniIm.isWidescreen == true) { + // if (!window.location.href.includes('/uni_modules/uni-im/pages/index/index')) { + // console.log('uni-im检测到窗口由小屏切为大屏。') + // uni.showModal({ + // content: '检测到你的窗口已改成pc模式,是否切换显示模式', + // cancelText: "不用了", + // confirmText: "立即切换", + // complete(e) { + // if (e.confirm) { + // uni.switchTab({ + // url: "/uni_modules/uni-im/pages/index/index", + // success() { + // window.location.reload() + // } + // }) + // } + // } + // }); + // } + } + } + setIsWidescreen() + // #endif + + const audioContext = uni.createInnerAudioContext() + let _audioContext = {} + Object.defineProperty(_audioContext,'src',{ + set(url) { + audioContext.src = url + }, + get() { + return audioContext.src + } + }) + uniIm.audioContext = new Proxy(_audioContext,{ + get(target,propKey,receiver){ + return audioContext[propKey] + } + }) + + uniIm.systemInfo = uni.getSystemInfoSync() + + // 初始化uniIm依赖的全局变量 + function initData() { + uniIm.conversation.dataList = [] + + // let storageConversation = uni.getStorageSync('uni-im-conversation' + '_uid:' + uniCloud.getCurrentUserInfo().uid) + // if(storageConversation && storageConversation.dataList.length){ + // uniIm.conversation.add(storageConversation.dataList) + // } + + uniIm.notification.dataList = [] + uniIm.notification.loadMore() + + uniIm.friend.dataList = [] + uniIm.friend.loadMore() + + uniIm.group.dataList = [] + uniIm.group.loadMore() + + let userInfo = {} + userInfo[uniIdStore.userInfo._id] = uniIdStore.userInfo + uniIm.mergeUsersInfo(userInfo) + } + // 如果已经登录就直接初始化数据 + if (uniCloud.getCurrentUserInfo().tokenExpired > Date.now()) { + setTimeout(initData, 0) + } + // 登录成功后 初始化数据 + uni.$on('uni-id-pages-login-success', async () => { + initData() + }) + + uni.onPushMessage(async res => { + if (res.data.payload.type == 'uni-im-notification') { + console.log('uni-im-notification-res.data', res.data); + res.data.create_time = Date.now() + if (typeof res.data.is_read == 'undefined') { + res.data.is_read = false + } + console.log('res.data notification.add', res.data) + res.data._id = res.data.payload.notificationId + const notificationData = res.data + delete res.data.payload.notificationId + delete res.data.unipush_version + uniIm.notification.add(res.data) + } + }); + + // 通过拦截器监听路由变化,解决在非tabbar页面,无法设置TabBarBadge的问题 + ['navigateTo', 'redirectTo', 'reLaunch', 'switchTab', 'navigateBack'].forEach((item) => { + uni.addInterceptor(item, { + success: event => { + // 更新底部选项卡角标值 + updateTabBarBadge() + } + }) + }) + + // #ifdef MP-WEIXIN + // 微信的隐式API 监听路由变化 + wx.onAppRoute((res) => { + // console.log('跳转', res) + // 更新底部选项卡角标值 + updateTabBarBadge() + }) + // #endif + + // 更新底部选项卡角标值 + function updateTabBarBadge(){ + setTimeout(() => { + // console.log('updateTabBarBadge'); + /** + * 默认每次路由发生变化都会更新updateTabBarBadge的值 + * 你可以根据自己的项目情况优化,比如首页就是tabbar的可以判断getCurrentPages().length 的长度决定是否继续 + */ + + let unread_count = uniIm.notification.unreadCount() + // console.log({unread_count}); + set(2,unread_count) + + // 获取未读会话消息总数 + unread_count = uniIm.conversation.unreadCount() + // console.log({unread_count}); + set(0,unread_count) + + // 设置底部选项卡角标值 + function set(index,number){ + if (number == 0) { + uni.removeTabBarBadge({ + index, + complete: (e) => { + // console.log(e) + } + }) + } else { + uni.setTabBarBadge({ + index, + text: number + '', + complete: (e) => { + // console.log(e) + } + }) + } + } + }, 300); + } + + uni.$on('uni-id-pages-logout', () => { + uniIm.conversation.dataList = [] + uniIm.conversation.hasMore = true + + uniIm.notification.dataList = [] + uniIm.notification.hasMore = true + + uniIm.friend.dataList = [] + uniIm.friend.hasMore = true + + uniIm.group.dataList = [] + uniIm.group.hasMore = true + + uniIm.currentConversationId = false + }) + + // #ifdef H5 + uni.addInterceptor('switchTab', { + invoke: (e) => { + if (e.url.includes('/uni_modules/uni-im/pages/index/index')) { + if (uniIm.matches) { + let param = getUrlParam(e.url) + // console.log('param----',param); + uni.$emit('uni-im-toChat', param) + } + } + } + }) + + function getUrlParam(url) { + let u = url.split("?"); + if (typeof(u[1]) == "string") { + u = u[1].split("&"); + let get = {}; + for (let i in u) { + let j = u[i].split("="); + get[j[0]] = j[1]; + } + return get; + } else { + return {}; + } + }; + // #endif + + + appEvent.onAppHide(async () => { + appIsShow = false + }) + appEvent.onAppShow(async () => { + appIsShow = true + // #ifdef APP + //清理系统通知栏消息和app角标 + this.clearPushNotify() + // #endif + }) + }, + getConversationId(id, type = 'single') { //single,group + if (type == 'single') { + let current_uid = uniCloud.getCurrentUserInfo().uid + if (!current_uid) { + console.error('错误current_uid不能为空', current_uid); + } + let param = [id, current_uid] + return 'single_' + md5(param.sort().toString()) + } else { + return 'group_' + id + } + }, + listenImMsg() { + uni.onPushMessage(async res => { + console.log('收到消息', res); + // console.log('收到消息 onPushMessage===================',res.type, res.data,uniIm.currentConversationId ); + const { + payload + } = res.data + if (payload.type == "uni-im") { + const msg = payload.data + // console.log({msg}); + // 超长文本传输时的id + if (msg.LongMsg) { + const db = uniCloud.database(); + let res = await db.collection('uni-im-msg') + .where({ + "_id": msg._id, + "conversation_id": msg.conversation_id // conversation_id 必传否则会被触发器拦截 + }) + .get() + // console.log(res); + if (res.result.code == 0) { + payload.data.body = res.result.data[0].body + } else { + console.error('超长文本类型消息查库失败', msg._id); + } + } + // console.log('payload------', payload.device_id, uni.getSystemInfoSync().deviceId); + if (payload.device_id == uni.getSystemInfoSync().deviceId) { + return console.log('当前设备发的消息,不用接收;忽略'); + } + + if (res.type == 'receive') { + // console.log(777); + const { + conversation_id, + group_id + } = msg + // console.log('msgmsgmsgmsgmsg.msg',msg); + // #ifdef APP + let currentPages = getCurrentPages() + let topViewRoute = currentPages[currentPages.length - 1].route + // console.log('topViewRoute',topViewRoute); + let pathList = [ + 'uni_modules/uni-im/pages/chat/chat', + 'uni_modules/uni-im/pages/index/index', + 'uni_modules/uni-im/pages/userList/userList', + 'uni_modules/uni-im/pages/contacts/contacts' + ] + if (!appIsShow || !pathList.includes(topViewRoute)) { + // console.log('payload',payload); + let { + content, + data, + title, + avatar_file + } = payload + let url = avatar_file ? avatar_file.url : '' + let icon = '_www/uni_modules/uni-im/static/avatarUrl.png' + //安卓才有头像功能,再执行下载 + if (uni.getSystemInfoSync().platform == "android") { + if (avatar_file) { + let downloadFileRes = await uni.downloadFile({ + url: avatar_file.url + }); + icon = downloadFileRes[1]?.tempFilePath + } + } + plus.push.createMessage(content, payload, { + title, + icon + }) + } else if (conversation_id != uniIm.currentConversationId) { + // uni.showToast({ + // title: '收到新消息请注意查看', + // icon: 'none' + // }); + } + // #endif + let conversation = await uniIm.conversation.get(conversation_id) + let msgList = conversation.msgList + /** + * 排除会话中已包含此消息的情况 + */ + let lastMsg = [...msgList].pop() + if(lastMsg && lastMsg._id != msg._id){ + msgList.push(msg) + conversation.unread_count++ + } + // 如果socket已经关闭的情况下收到消息,说明消息来源浏览器页签之间通讯 不需要重复存库 + if(!socketIsClose){ + conversation.msgManager.localMsg.add(msg) + } + // console.log(29292929,msg) + //限制群聊才有回复提示 + if (msg.group_id && msg.about_msg_id) { + let current_uid = uniCloud.getCurrentUserInfo().uid + let aboutMsg = msgList.find(i => i._id == msg.about_msg_id) + if(aboutMsg && aboutMsg.from_uid == current_uid){ + conversation.call_list.push(msg._id) + console.log('conversation.call_list', conversation.call_list); + } + } + + // console.log({ + // conversation_id, + // action:'push', + // data:msg + // }); + + if (msg.action == "join-group-notice") { + console.log('"join-group-notice"', msg); + let conversation = await uniIm.conversation.get(msg.conversation_id) + console.log('"join-group-notice"conversation', conversation); + if (conversation) { + let userList = msg.body.user_list + if (userList && Object.keys(conversation.group_member)) { + for (let i = 0; i < userList.length; i++) { + // #ifdef VUE3 + conversation.group_member[userList[i]._id] = userList[i] + // #endif + + // #ifdef VUE2 + Vue.set(conversation.group_member, userList[i]._id, userList[i]) + // #endif + } + console.log('add user to group_member', conversation.group_member) + } + // 记录用户数据到内存 + // console.log('conversation.group_member',conversation.group_member); + uniIm.mergeUsersInfo(conversation.group_member) + + // 如果我的群列表没有这个群,则加上 + let hasIsGroup = uniIm.group.dataList.find(i => i.group_info._id == group_id) + if (!hasIsGroup) { + await uniIm.group.loadMore({ + group_id + }) + } + } + } + + + } else { + let currentPages = getCurrentPages() + let topViewRoute = currentPages[currentPages.length - 1].route + // console.log('topViewRoute',topViewRoute); + if (topViewRoute == 'uni_modules/uni-im/pages/chat/chat') { + uni.redirectTo({ + url: '/uni_modules/uni-im/pages/chat/chat?conversation_id=' + msg.conversation_id, + complete(e) { + console.log(e); + } + }) + } else { + uni.navigateTo({ + url: '/uni_modules/uni-im/pages/chat/chat?conversation_id=' + msg.conversation_id, + complete(e) { + console.log(e); + } + }) + } + } + } else if (payload.type == "uni-im-group-exit" || payload.type == "uni-im-group-expel" || + payload.subType == 'uni-im-group-expel') { + // 用户退群 + // 群聊天记录加上 xxx 退群 + let { + timestamp, + group_id + } = payload.data + let conversation_id = 'group_' + group_id + + let noticeBody = res.data.content + let conversation = await uniIm.conversation.get(conversation_id) + let msg = { + conversation_id, + group_id, + client_create_time: Date.now(), + create_time: Date.now(), + type: 'system', + body: noticeBody + } + conversation.msgList.push(msg) + // 如果socket已经关闭的情况下收到消息,说明消息来源浏览器页签之间通讯 不需要重复存库 + if(!socketIsClose){ + conversation.msgManager.localMsg.add(msg) + } + + // 如果是当前用户退群,就将群会话从列表移除 + if (payload.data.user_id == uniCloud.getCurrentUserInfo().uid) { + let currentConversationId = uniIm.currentConversationId + //如果已经打开此群聊,或在此群聊的设置页面 + let topPageInfo = getTopPageInfo() + + // #ifdef VUE2 + let { + route, + options + } = topPageInfo + // #endif + + // #ifdef VUE3 + let { + route, + options + } = topPageInfo.$page + // #endif + if (route == "uni_modules/uni-im/pages/group/info") { + currentConversationId = options.conversation_id + } + if (currentConversationId == ('group_' + payload.data.group_id)) { + uni.navigateBack({ + delta: 2 + }) + } + setTimeout(() => { + uniIm.conversation.remove(conversation_id) + uniIm.group.remove({ + group_id: payload.data.group_id + }) + }, 1000); + } else { + let data = await uniIm.conversation.get(conversation_id) + // console.error(11111,data) + // #ifdef VUE3 + delete data.group_member[payload.data.user_id] + // #endif + + // #ifdef VUE2 + Vue.delete(data.group_member, payload.data.user_id) + // #endif + } + } else if (payload.type == "uni-im-group-join-request") { + console.log('有用户申请加入群聊'); + // uni.showToast({ + // title: '有用户申请加入群聊', + // icon: 'none' + // }); + } else if (payload.type == "uni-im-notification" && payload.subType == + 'uni-im-group-cancellation') { + // 群解散 + let { + group_id + } = payload.data + let conversationId = 'group_' + group_id + //如果已经打开此群聊,或在此群聊的设置页面 + let currentConversationId = uniIm.currentConversationId + //如果已经打开此群聊,或在此群聊的设置页面 + let topPageInfo = getTopPageInfo() + + // #ifdef VUE2 + let { + route, + options + } = topPageInfo + // #endif + + // #ifdef VUE3 + let { + route, + options + } = topPageInfo.$page + // #endif + + if (route == "uni_modules/uni-im/pages/group/info") { + currentConversationId = options.conversation_id + } + if (currentConversationId == conversationId) { + uni.navigateBack({ + delta: 2 + }) + } + setTimeout(() => { + uniIm.conversation.remove(conversationId) + uniIm.group.remove({ + group_id + }) + }, 1000); + } else if (payload.type == "uni-im-notification" && payload.subType == "uni-im-friend-add") { + // console.log('加好友的申请通过'); + let { + from_uid, + to_uid + } = payload.data; + let friend_uid = from_uid == uniCloud.getCurrentUserInfo().uid ? to_uid : from_uid + await uniIm.conversation.get({ + friend_uid + }) + uniIm.friend.loadMore({ + friend_uid + }) + + } else if (payload.type == "uni-im-notification" && payload.subType == "uni-im-friend-delete") { + let { + from_uid, + to_uid + } = payload.data; + let friend_uid = from_uid == uniCloud.getCurrentUserInfo().uid ? to_uid : from_uid + uniIm.conversation.remove(payload.data.conversationId) + uniIm.friend.remove(friend_uid) + } else if (payload.type == "uni-im-revoke-msg") { + let conversation = await uniIm.conversation.revokeMsg(payload.data) + uni.setStorageSync('uni-im-lastTaskTime', payload.data.taskCreateTime) + } + }) + }, + toFriendlyTime(timestamp) { + // 加上一个*0的数,用于刷新视图中的时间 并无运算意义 + if (timestamp - Date.now() < 3600 * 1000 * 2) { + timestamp += uniIm.heartbeat * 0 + } + if (!timestamp) { + return ''; + } + // return timestamp + + return toFriendlyTime(timestamp) + }, + // #ifdef APP + clearPushNotify() { + plus.push.clear() + plus.runtime.setBadgeNumber(0) + }, + // #endif + async login({ + token, + tokenExpired + }) { + uni.setStorage({ + key: "uni_id_token_expired", + data: tokenExpired + }) + uni.setStorage({ + key: "uni_id_token", + data: token + }) + + uni.getPushClientId({ + success: async function(e) { + // console.log(e) + let pushClientId = e.cid + // console.log(pushClientId); + let res = await uniIdCo.setPushCid({ + pushClientId + }) + // console.log('getPushClientId', res); + }, + fail(e) { + console.log(e) + } + }) + await uniIdMutations.updateUserInfo() + uni.$emit('uni-id-pages-login-success') + } +} + +function getTopPageInfo() { + let pages = getCurrentPages(); + return pages[pages.length - 1]; +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/components/uni-im-code-view/uni-im-code-view.vue b/uni-im示例/uni_modules/uni-im/components/uni-im-code-view/uni-im-code-view.vue new file mode 100644 index 0000000..e302cc1 --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/components/uni-im-code-view/uni-im-code-view.vue @@ -0,0 +1,130 @@ + + + + + \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/components/uni-im-control/uni-im-control.vue b/uni-im示例/uni_modules/uni-im/components/uni-im-control/uni-im-control.vue new file mode 100644 index 0000000..f2609a5 --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/components/uni-im-control/uni-im-control.vue @@ -0,0 +1,283 @@ + + + + + \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/components/uni-im-icons/uni-im-icons.ttf b/uni-im示例/uni_modules/uni-im/components/uni-im-icons/uni-im-icons.ttf new file mode 100644 index 0000000..990da61 Binary files /dev/null and b/uni-im示例/uni_modules/uni-im/components/uni-im-icons/uni-im-icons.ttf differ diff --git a/uni-im示例/uni_modules/uni-im/components/uni-im-icons/uni-im-icons.vue b/uni-im示例/uni_modules/uni-im/components/uni-im-icons/uni-im-icons.vue new file mode 100644 index 0000000..50ae9a3 --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/components/uni-im-icons/uni-im-icons.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-im/components/uni-im-msg-list/components/uni-im-list-item/uni-im-list-item.vue b/uni-im示例/uni_modules/uni-im/components/uni-im-msg-list/components/uni-im-list-item/uni-im-list-item.vue new file mode 100644 index 0000000..c5e1f4f --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/components/uni-im-msg-list/components/uni-im-list-item/uni-im-list-item.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-im/components/uni-im-msg-list/components/uni-im-list/uni-im-list.vue b/uni-im示例/uni_modules/uni-im/components/uni-im-msg-list/components/uni-im-list/uni-im-list.vue new file mode 100644 index 0000000..13b0743 --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/components/uni-im-msg-list/components/uni-im-list/uni-im-list.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-im/components/uni-im-msg-list/uni-im-msg-list.vue b/uni-im示例/uni_modules/uni-im/components/uni-im-msg-list/uni-im-msg-list.vue new file mode 100644 index 0000000..81859b7 --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/components/uni-im-msg-list/uni-im-msg-list.vue @@ -0,0 +1,489 @@ + + + + + \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/components/uni-im-msg-system/uni-im-msg-system.vue b/uni-im示例/uni_modules/uni-im/components/uni-im-msg-system/uni-im-msg-system.vue new file mode 100644 index 0000000..8d21d0e --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/components/uni-im-msg-system/uni-im-msg-system.vue @@ -0,0 +1,52 @@ + + + + + \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/components/uni-im-msg/uni-im-msg.vue b/uni-im示例/uni_modules/uni-im/components/uni-im-msg/uni-im-msg.vue new file mode 100644 index 0000000..49657df --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/components/uni-im-msg/uni-im-msg.vue @@ -0,0 +1,771 @@ + + + + \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/components/uni-im-sound/uni-im-sound.vue b/uni-im示例/uni_modules/uni-im/components/uni-im-sound/uni-im-sound.vue new file mode 100644 index 0000000..ecde2c0 --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/components/uni-im-sound/uni-im-sound.vue @@ -0,0 +1,244 @@ + + + + + diff --git a/uni-im示例/uni_modules/uni-im/lib/MsgManager.js b/uni-im示例/uni_modules/uni-im/lib/MsgManager.js new file mode 100644 index 0000000..2c936b3 --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/lib/MsgManager.js @@ -0,0 +1,524 @@ + import uniIm from '@/uni_modules/uni-im/lib/main.js'; + import md5 from '@/uni_modules/uni-im/common/md5' + const db = uniCloud.database(); + const dbCmd = db.command + +export default class Message { + constructor(currentConversation) { + // #ifdef APP + this.sqlite = getApp().globalData.sqlite + // #endif + // console.log('currentConversation',currentConversation); + // currentConversation = currentConversation//await uniIm.conversation.get(conversation_id) + this.conversation_id = currentConversation.id + + Object.defineProperty(this, 'msgList', { + get() { + // 未init当[],防止本地发送失败的数据 影响查询线上数据结果 + if(currentConversation.isInit){ + return currentConversation.msgList + }else{ + return [] + } + }, + set(data) { + currentConversation.msgList = data + } + }) + + + // #ifdef H5 + Object.defineProperty(this, 'indexDB', { + get() { + return uniIm.indexDB + } + }) + // #endif + + // 初始化 storage中的maxTime + // #ifdef MP + this.localMsg.key = 'uni-im-msg-' + this.conversation_id + // #endif + } + indexDB = false + isInit = false + msgList = [] + async sleep(t) { + return await new Promise((resolve, rejece) => { + setTimeout(resolve, t) + }) + } + async localMsgMaxTime() { + // 拿到【本地】数据库中,当前会话聊天记录的最大值 + if (this.localMsg.maxTime === false) { + let lastLocalDatas = await this.localMsg.get({limit:1,orderBy:{"create_time":"desc"}}) + // console.log('lastLocalDatas------',lastLocalDatas); + let [lastLocalData] =lastLocalDatas + if (lastLocalData) { + this.localMsg.maxTime = lastLocalData.create_time + } else { + this.localMsg.maxTime = 0 + } + // console.log('init localMsgMaxTime',this.localMsg.maxTime); + } + return this.localMsg.maxTime + } + msgListMinTime(){ + let item = this.msgList[0] + if(item){ + return item.create_time + }else{ + return 0 + } + } + getMore = async () => { + // console.log('getMore'); + if (this.cloudMsg.hasAfterStorage) { + let minTime = await this.localMsgMaxTime(), + maxTime = this.msgListMinTime() + // 1. 拉取云端最新数据(大于本地storage的部分)直到没有 + let data = await this.cloudMsg.get({minTime,maxTime}) + + if (data.length) { + // console.error('【+++】cloudMsg:请求到时间大于storeage中的云端数据', minTime, data.length, data); + return data + } else { + this.cloudMsg.hasAfterStorage = false + // console.error('cloudMsg已无:时间大于storeage中的云端数据'); + return this.getMore() + } + + } else if (this.localMsg.hasBeforeList) { + // 2. 拉取storage中的数据(小于已经拉取的数据)直到没有 + let maxTime = this.msgListMinTime() + let data = await this.localMsg.get({ + maxTime + }) + if (data.length) { + // console.error('【+++】localMsg:请求到时间小于列表的本地数据', data.length, data, maxTime); + return data + } else { + this.localMsg.hasBeforeList = false + // console.error('localMsg:已无时间小于列表的本地数据', maxTime); + return this.getMore() + } + } + // 3. 拉取云端(小于已经拉取的数据)的数据直到没有 + if (this.cloudMsg.hasBeforeStorage) { + // await this.sleep(3000) + let maxTime = this.msgList[0] ? this.msgList[0].create_time : false + // console.log('*--**--*', maxTime, JSON.stringify(this.msgList)); + let data = await this.cloudMsg.get({ + maxTime + }) + if (data.length) { + // console.error('【+++】cloudMsg:请求到时间小于列表的云端数据', data.length, data, maxTime); + return data + } else { + this.cloudMsg.hasBeforeStorage = false + // console.error('cloudMsg已无:时间小于列表的云端数据', maxTime); + return [] + } + } + } + cloudMsg = { + hasAfterStorage: true, + hasBeforeStorage: true, + get: async ({ + minTime = 0, + maxTime = false, + limit = 30 + } = {}) => { + // console.log(1111,minTime,maxTime); + //console.log('this',this); + // let where = `"conversation_id" == "${this.conversation_id}"` + let where = { + "conversation_id": this.conversation_id + } + if (minTime && maxTime) { + where.create_time = dbCmd.and([ + dbCmd.gt(minTime), + dbCmd.lt(maxTime) + ]) + } else { + if (minTime) { + // where += `&& "create_time" > ${minTime}` + where.create_time = dbCmd.gt(minTime) + } + if (maxTime) { + // where += `&& "create_time" < ${maxTime}` + where.create_time = dbCmd.lt(maxTime) + } + } + + const msgTable = db.collection('uni-im-msg') + let data; + try { + let res = await msgTable.where(where) + .limit(limit) + .orderBy('create_time', 'desc') + .get() + data = res.result.data.reverse() + // console.error('where', where,{minTime,maxTime},data); + } catch (e) { + // console.error(e); + // 如果断网的话,会请求不到直接返回空即可 + data = [] + } + // console.error('where', where, data); + if (data.length) { + //存到本地 + this.localMsg.add(data, minTime === 0 ? 'unshift' : 'push') + //console.error(996666699955,[...data], minTime != 0); + } + return data + } + } + localMsg = { + maxTime: false, + hasBeforeList: true, + get: async ({ + minTime = 0, + maxTime = false, + limit = 30, + orderBy = {"create_time":"asc"} //asc 升序,desc 降序 + } = {}) => { + // #ifdef APP + let sql = `select * from msg WHERE conversation_id = "${this.conversation_id}" ` //注意结尾要留空格,下一段语句连接 + if(maxTime || minTime){ + if(maxTime){ + sql += `AND create_time < ${maxTime} ` + } + if(minTime){ + sql += `AND create_time > ${minTime} ` + } + } + // 注意传入的orderBy是查询结果不是过程,im场景下都是从大到小查询 + sql += `ORDER BY "create_time" DESC ` + + if(limit){ + sql += `LIMIT ${limit} ` + } + + let datas = [] + try{ + datas = await this.sqlite.selectSql(sql) + }catch(e){ + // 错误时,仍然返回空数组,提高 高可用性 + console.error(e) + } + + // console.error('sql:',sql,datas); + return datas.map(data=>{ + try{ + let mapData = { + """:'"', + "'":"'", + "<":'<', + ">":'>', + "&":'&' + } + + Object.keys(mapData).forEach(key=>{ + data.body = data.body.replace(new RegExp(key,'g'), mapData[key]); + }) + // console.log('data.body',data.body) + data.body = JSON.parse(data.body) + }catch(e){ + console.error(e) + } + return data + }).sort((a,b)=>{ + if(orderBy.create_time == 'asc'){ + return a.create_time - b.create_time + }else{ + return b.create_time - a.create_time + } + }) + // #endif + + // #ifdef H5 + let datas = await new Promise((resolve, reject) => { + let datas = [],index = 0 + if (!maxTime) { + maxTime = Date.now() + } + try{ + // 设置查询索引 + // console.log('kkkkkkkk',[this.conversation_id, minTime], [this.conversation_id,maxTime]); + let range = IDBKeyRange.bound([this.conversation_id, minTime], [this.conversation_id,maxTime]) + // 传入的 prev 表示是降序遍历游标,默认是next表示升序; + // indexDB 在im的场景下,查始终是降序遍历游标 orderBy指的是查询结果的排序方式 + let sort = "prev" + // console.log('sortsortsortsortsortsortsort',sort); + let task = this.indexDB.transaction("uni-im-msg") + .objectStore("uni-im-msg") + .index("conversation_id-create_time") + .openCursor(range, sort) + + task.onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + // console.log('cursor',cursor.value); + // 排除边界值 + if (![minTime, maxTime].includes(cursor.value.create_time)) { + datas.push(cursor.value) + } + index ++ + if(limit && index === limit){ + resolve(datas) + }else{ + cursor.continue(); + } + } else { + resolve(datas) + } + } + + task.onerror = function(err) { + console.error(err); + resolve([]) + } + }catch(e){ + console.error(e); + resolve([]) + //TODO handle the exception + } + }) + return datas.sort((a,b)=>{ + if(orderBy.create_time == 'asc'){ + return a.create_time - b.create_time + }else{ + return b.create_time - a.create_time + } + }) + // #endif + + // #ifdef MP + let data = uni.getStorageSync(this.localMsg.key) || [] + // console.error(111,data,minTime,maxTime) + data = data.filter(item => { + if (minTime && (item.create_time < minTime || item.create_time == minTime)) { + return false + } + if (maxTime && (item.create_time > maxTime || item.create_time == maxTime)) { + return false + } + return true + }) + // console.error(222,data) + data = data.sort((a,b)=>{ + if(orderBy.create_time == 'asc'){ + return a.create_time - b.create_time + }else{ + return b.create_time - a.create_time + } + }) + if(limit){ + data = data.slice(0,limit) + } + return data + // #endif + }, + add: async (datas, action = 'push') => { + // console.log('localMsg.add',action,datas); + if(!Array.isArray(datas)){ + datas = [datas] + } + datas.forEach(async data=>{ + data.unique_id = md5(JSON.stringify(data) + Math.random()) + }) + // #ifdef APP + let sql = [] + datas.forEach(async data=>{ + let keys = Object.keys(data) + let str = keys.reduce((sum,key)=>{ + if(key == 'body'){ + let body = JSON.stringify(data.body) + try{ + body = escapeHtml(body) + // console.log('bodybodybody',body) + }catch(e){ + console.error(e) + } + sum += `"${body}",` + }else if(typeof data[key] == 'string'){ + sum += `"${data[key]}",` + } else if(typeof data[key] == 'undefined'){ + sum += `${null},` + }else{ + sum += `${data[key]},` + } + return sum + },'').slice(0,-1); + sql.push(`insert into msg("${keys.join('","')}") values (${str})`) + }) + if(sql.length){ + try{ + await this.sqlite.executeSql(sql) + }catch(e){ + console.log(e) + } + } + // console.log('executeSql:',sql); + // #endif + + // #ifdef H5 + let res = await new Promise((resolve, reject) => { + // console.log('datas', datas[0]); + // 事务对象 指定表格名称和操作模式("只读"或"读写") + let transaction = this.indexDB.transaction('uni-im-msg', 'readwrite') + let objectStore = transaction.objectStore('uni-im-msg') // 仓库对象 + let index = 0 + let length = datas.length + datas.forEach(data => { + let res = objectStore.add(data) + res.onsuccess = (e) => { + // console.log('resolve',e, index, length); + index++ + if (index == length) { + resolve() + } + } + res.onerror = (e) => { + // console.error('add - failed', e); + // console.log('reject', e); + reject() + } + }) + }) + // #endif + + // #ifdef MP + // mp 端把所有storage查出来 + let _datas = await this.localMsg.get() + if(_datas.length > 20){ + let tipText = "提示:当前会话的离线(存到storage)的聊天记录已经超过20条," + if(action == 'push'){ + console.log(tipText + '将“自动删除”旧数据后再添加新数据。如果你有其他策略可以自己修改此逻辑') + _datas = _datas.slice(-1*datas.length) + }else{ + return console.log(tipText + '不再存储更多') + } + } + _datas[action](...datas) + uni.setStorageSync(this.localMsg.key,_datas) + // #endif + + // 更新最大值 + let maxTime = datas.map(i=>i.create_time).sort((a,b)=>b-a)[0] + let localMsgMaxTime = await this.localMsgMaxTime() + if(maxTime > await localMsgMaxTime){ + // console.error('更新最大值 maxTime----------------',maxTime); + this.localMsg.maxTime = maxTime + } + }, + update: async(unique_id,data)=> { + // console.log('localMsg update',data); + data = Object.assign({},data) + // #ifdef APP + let dataStr = '' + for (let key in data) { + dataStr += `"${key}" = ` + if(key == 'body'){ + let body = JSON.stringify(data.body) + try{ + body = escapeHtml(body) + // console.log('bodybodybody',body) + }catch(e){ + console.error(e) + } + dataStr += `"${body}",` + }else if(typeof data[key] == 'string'){ + dataStr += `"${data[key]}",` + }else if(typeof data[key] == 'undefined'){ + dataStr += `${null},` + }else{ + dataStr += `${data[key]},` + } + } + let sql = `UPDATE msg SET ${dataStr.slice(0,-1)} WHERE unique_id = "${unique_id}"` + try{ + await this.sqlite.executeSql(sql) + }catch(e){ + console.log(e) + } + // console.log('executeSql:',sql); + // #endif + + // #ifdef H5 + let datas = await new Promise((resolve, reject) => { + let request = this.indexDB.transaction(['uni-im-msg'], 'readwrite') + .objectStore("uni-im-msg") + .put(data) + request.onsuccess = function(event) { + // console.log('event---',event); + resolve(event) + } + request.onerror = function(event) { + console.error(event); + reject(event) + }; + }) + // #endif + + // #ifdef MP + // mp 端把所有storage查出来 + let _datas = await this.localMsg.get() + + let index = _datas.findIndex(i=>i.unique_id == unique_id) + _datas[index] = data + uni.setStorageSync(this.localMsg.key,_datas) + // #endif + } + } +} + +// #ifdef APP +var matchHtmlRegExp = /["'&<>]/ +function escapeHtml (string) { + var str = '' + string + var match = matchHtmlRegExp.exec(str) + + if (!match) { + return str + } + + var escape + var html = '' + var index = 0 + var lastIndex = 0 + + for (index = match.index; index < str.length; index++) { + switch (str.charCodeAt(index)) { + case 34: // " + escape = '"' + break + case 38: // & + escape = '&' + break + case 39: // ' + escape = ''' + break + case 60: // < + escape = '<' + break + case 62: // > + escape = '>' + break + default: + continue + } + if (lastIndex !== index) { + html += str.substring(lastIndex, index) + } + + lastIndex = index + 1 + html += escape + } + + return lastIndex !== index + ? html + str.substring(lastIndex, index) + : html +} +// #endif \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/lib/createObservable.js b/uni-im示例/uni_modules/uni-im/lib/createObservable.js new file mode 100644 index 0000000..64d77d5 --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/lib/createObservable.js @@ -0,0 +1,24 @@ +// #ifdef VUE2 +import Vue from 'vue' +// #endif + +// #ifdef VUE3 +import {reactive} from 'vue' +// #endif + + +export default function(data,name = 'imObservableData'){ + if(typeof uni[name] == 'undefined'){ + // #ifdef VUE2 + // 通过Vue.observable创建一个可响应的对象 + data = Vue.observable(data) + // #endif + + // #ifdef VUE3 + // 通过Vue.observable创建一个可响应的对象 + data = reactive(data) + // #endif + uni[name] = data + } + return uni[name] +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/lib/highlight/github-dark.min.css b/uni-im示例/uni_modules/uni-im/lib/highlight/github-dark.min.css new file mode 100644 index 0000000..03b6da8 --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/lib/highlight/github-dark.min.css @@ -0,0 +1,10 @@ +pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*! + Theme: GitHub Dark + Description: Dark theme as seen on github.com + Author: github.com + Maintainer: @Hirse + Updated: 2021-05-15 + + Outdated base version: https://github.com/primer/github-syntax-dark + Current colors taken from GitHub's CSS +*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/lib/highlight/highlight-uni.min.js b/uni-im示例/uni_modules/uni-im/lib/highlight/highlight-uni.min.js new file mode 100644 index 0000000..091aec5 --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/lib/highlight/highlight-uni.min.js @@ -0,0 +1,5256 @@ +// 本文件是由 Highlight.js 经兼容性修改后的文件,请勿直接升级。否则会造成uni-app-vue3-Android下有兼容问题 +/*! + Highlight.js v11.7.0 (git: 82688fad18) + (c) 2006-2022 undefined and other contributors + License: BSD-3-Clause + */ +var e = { + exports: {} +}; + +function n(e) { + return e instanceof Map ? e.clear = e.delete = e.set = () => { + throw Error("map is read-only") + } : e instanceof Set && (e.add = e.clear = e.delete = () => { + throw Error("set is read-only") + }), Object.freeze(e), Object.getOwnPropertyNames(e).forEach((t => { + var a = e[t]; + "object" != typeof a || Object.isFrozen(a) || n(a) + })), e +} +e.exports = n, e.exports.default = n; +class t { + constructor(e) { + void 0 === e.data && (e.data = {}), this.data = e.data, this.isMatchIgnored = !1 + } + ignoreMatch() { + this.isMatchIgnored = !0 + } +} + +function a(e) { + return e.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, + "'") +} + +function i(e, ...n) { + const t = Object.create(null); + for (const n in e) t[n] = e[n]; + return n.forEach((e => { + for (const n in e) t[n] = e[n] + })), t +} +const r = e => !!e.scope || e.sublanguage && e.language; +class s { + constructor(e, n) { + this.buffer = "", this.classPrefix = n.classPrefix, e.walk(this) + } + addText(e) { + this.buffer += a(e) + } + openNode(e) { + if (!r(e)) return; + let n = ""; + n = e.sublanguage ? "language-" + e.language : ((e, { + prefix: n + }) => { + if (e.includes(".")) { + const t = e.split("."); + return [`${n}${t.shift()}`, ...t.map(((e, n) => `${e}${"_".repeat(n+1)}`))].join(" ") + } + return `${n}${e}` + })(e.scope, { + prefix: this.classPrefix + }), this.span(n) + } + closeNode(e) { + r(e) && (this.buffer += "") + } + value() { + return this.buffer + } + span(e) { + this.buffer += `` + } +} +const o = (e = {}) => { + const n = { + children: [] + }; + return Object.assign(n, e), n +}; +class l { + constructor() { + this.rootNode = o(), this.stack = [this.rootNode] + } + get top() { + return this.stack[this.stack.length - 1] + } + get root() { + return this.rootNode + } + add(e) { + this.top.children.push(e) + } + openNode(e) { + const n = o({ + scope: e + }); + this.add(n), this.stack.push(n) + } + closeNode() { + if (this.stack.length > 1) return this.stack.pop() + } + closeAllNodes() { + for (; this.closeNode();); + } + toJSON() { + return JSON.stringify(this.rootNode, null, 4) + } + walk(e) { + return this.constructor._walk(e, this.rootNode) + } + static _walk(e, n) { + return "string" == typeof n ? e.addText(n) : n.children && (e.openNode(n), + n.children.forEach((n => this._walk(e, n))), e.closeNode(n)), e + } + static _collapse(e) { + "string" != typeof e && e.children && (e.children.every((e => "string" == typeof e)) ? e.children = [e.children + .join("") + ] : e.children.forEach((e => { + l._collapse(e) + }))) + } +} +class c extends l { + constructor(e) { + super(), this.options = e + } + addKeyword(e, n) { + "" !== e && (this.openNode(n), this.addText(e), this.closeNode()) + } + addText(e) { + "" !== e && this.add(e) + } + addSublanguage(e, n) { + const t = e.root; + t.sublanguage = !0, t.language = n, this.add(t) + } + toHTML() { + return new s(this, this.options).value() + } + finalize() { + return !0 + } +} + +function d(e) { + return e ? "string" == typeof e ? e : e.source : null +} + +function g(e) { + return m("(?=", e, ")") +} + +function u(e) { + return m("(?:", e, ")*") +} + +function b(e) { + return m("(?:", e, ")?") +} + +function m(...e) { + return e.map((e => d(e))).join("") +} + +function p(...e) { + const n = (e => { + const n = e[e.length - 1]; + return "object" == typeof n && n.constructor === Object ? (e.splice(e.length - 1, 1), n) : {} + })(e); + return "(" + (n.capture ? "" : "?:") + e.map((e => d(e))).join("|") + ")" +} + +function _(e) { + return RegExp(e.toString() + "|").exec("").length - 1 +} +const h = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./; + +function f(e, { + joinWith: n +}) { + let t = 0; + return e.map((e => { + t += 1; + const n = t; + let a = d(e), + i = ""; + for (; a.length > 0;) { + const e = h.exec(a); + if (!e) { + i += a; + break + } + i += a.substring(0, e.index), + a = a.substring(e.index + e[0].length), "\\" === e[0][0] && e[1] ? i += "\\" + (Number(e[1]) + n) : (i += + e[0], + "(" === e[0] && t++) + } + return i + })).map((e => `(${e})`)).join(n) +} +const E = "(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)", + y = { + begin: "\\\\[\\s\\S]", + relevance: 0 + }, + w = { + scope: "string", + begin: "'", + end: "'", + illegal: "\\n", + contains: [y] + }, + N = { + scope: "string", + begin: '"', + end: '"', + illegal: "\\n", + contains: [y] + }, + v = (e, n, t = {}) => { + const a = i({ + scope: "comment", + begin: e, + end: n, + contains: [] + }, t); + a.contains.push({ + scope: "doctag", + begin: "[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", + end: /(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/, + excludeBegin: !0, + relevance: 0 + }); + const r = p("I", "a", "is", "so", "us", "to", "at", "if", "in", "it", "on", /[A-Za-z]+['](d|ve|re|ll|t|s|n)/, + /[A-Za-z]+[-][a-z]+/, /[A-Za-z][a-z]{2,}/); + return a.contains.push({ + begin: m(/[ ]+/, "(", r, /[.]?[:]?([.][ ]|[ ])/, "){3}") + }), a + }, + O = v("//", "$"), + k = v("/\\*", "\\*/"), + x = v("#", "$"); +var M = Object.freeze({ + __proto__: null, + MATCH_NOTHING_RE: /\b\B/, + IDENT_RE: "[a-zA-Z]\\w*", + UNDERSCORE_IDENT_RE: "[a-zA-Z_]\\w*", + NUMBER_RE: "\\b\\d+(\\.\\d+)?", + C_NUMBER_RE: E, + BINARY_NUMBER_RE: "\\b(0b[01]+)", + RE_STARTERS_RE: "!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", + SHEBANG: (e = {}) => { + const n = /^#![ ]*\//; + return e.binary && (e.begin = m(n, /.*\b/, e.binary, /\b.*/)), i({ + scope: "meta", + begin: n, + end: /$/, + relevance: 0, + "on:begin": (e, n) => { + 0 !== e.index && n.ignoreMatch() + } + }, e) + }, + BACKSLASH_ESCAPE: y, + APOS_STRING_MODE: w, + QUOTE_STRING_MODE: N, + PHRASAL_WORDS_MODE: { + begin: /\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ + }, + COMMENT: v, + C_LINE_COMMENT_MODE: O, + C_BLOCK_COMMENT_MODE: k, + HASH_COMMENT_MODE: x, + NUMBER_MODE: { + scope: "number", + begin: "\\b\\d+(\\.\\d+)?", + relevance: 0 + }, + C_NUMBER_MODE: { + scope: "number", + begin: E, + relevance: 0 + }, + BINARY_NUMBER_MODE: { + scope: "number", + begin: "\\b(0b[01]+)", + relevance: 0 + }, + REGEXP_MODE: { + begin: /(?=\/[^/\n]*\/)/, + contains: [{ + scope: "regexp", + begin: /\//, + end: /\/[gimuy]*/, + illegal: /\n/, + contains: [y, { + begin: /\[/, + end: /\]/, + relevance: 0, + contains: [y] + }] + }] + }, + TITLE_MODE: { + scope: "title", + begin: "[a-zA-Z]\\w*", + relevance: 0 + }, + UNDERSCORE_TITLE_MODE: { + scope: "title", + begin: "[a-zA-Z_]\\w*", + relevance: 0 + }, + METHOD_GUARD: { + begin: "\\.\\s*[a-zA-Z_]\\w*", + relevance: 0 + }, + END_SAME_AS_BEGIN: e => Object.assign(e, { + "on:begin": (e, n) => { + n.data._beginMatch = e[1] + }, + "on:end": (e, n) => { + n.data._beginMatch !== e[1] && n.ignoreMatch() + } + }) +}); + +function S(e, n) { + "." === e.input[e.index - 1] && n.ignoreMatch() +} + +function A(e, n) { + void 0 !== e.className && (e.scope = e.className, delete e.className) +} + +function C(e, n) { + n && e.beginKeywords && (e.begin = "\\b(" + e.beginKeywords.split(" ").join("|") + ")(?!\\.)(?=\\b|\\s)", + e.__beforeBegin = S, e.keywords = e.keywords || e.beginKeywords, delete e.beginKeywords, + void 0 === e.relevance && (e.relevance = 0)) +} + +function T(e, n) { + Array.isArray(e.illegal) && (e.illegal = p(...e.illegal)) +} + +function R(e, n) { + if (e.match) { + if (e.begin || e.end) throw Error("begin & end are not supported with match"); + e.begin = e.match, delete e.match + } +} + +function D(e, n) { + void 0 === e.relevance && (e.relevance = 1) +} +const I = (e, n) => { + if (!e.beforeMatch) return; + if (e.starts) throw Error("beforeMatch cannot be used with starts"); + const t = Object.assign({}, e); + Object.keys(e).forEach((n => { + delete e[n] + })), e.keywords = t.keywords, e.begin = m(t.beforeMatch, g(t.begin)), e.starts = { + relevance: 0, + contains: [Object.assign(t, { + endsParent: !0 + })] + }, e.relevance = 0, delete t.beforeMatch + }, + L = ["of", "and", "for", "in", "not", "or", "if", "then", "parent", "list", "value"]; + +function B(e, n, t = "keyword") { + const a = Object.create(null); + return "string" == typeof e ? i(t, e.split(" ")) : Array.isArray(e) ? i(t, e) : Object.keys(e).forEach((t => { + Object.assign(a, B(e[t], n, t)) + })), a; + + function i(e, t) { + n && (t = t.map((e => e.toLowerCase()))), t.forEach((n => { + const t = n.split("|"); + a[t[0]] = [e, $(t[0], t[1])] + })) + } +} + +function $(e, n) { + return n ? Number(n) : (e => L.includes(e.toLowerCase()))(e) ? 0 : 1 +} +const z = {}, + F = e => { + console.error(e) + }, + U = (e, ...n) => { + console.log("WARN: " + e, ...n) + }, + j = (e, n) => { + z[`${e}/${n}`] || (console.log(`Deprecated as of ${e}. ${n}`), z[`${e}/${n}`] = !0) + }, + P = Error(); + +function K(e, n, { + key: t +}) { + let a = 0; + const i = e[t], + r = {}, + s = {}; + for (let e = 1; e <= n.length; e++) s[e + a] = i[e], r[e + a] = !0, a += _(n[e - 1]); + e[t] = s, e[t]._emit = r, e[t]._multi = !0 +} + +function H(e) { + (e => { + e.scope && "object" == typeof e.scope && null !== e.scope && (e.beginScope = e.scope, + delete e.scope) + })(e), "string" == typeof e.beginScope && (e.beginScope = { + _wrap: e.beginScope + }), "string" == typeof e.endScope && (e.endScope = { + _wrap: e.endScope + }), (e => { + if (Array.isArray(e.begin)) { + if (e.skip || e.excludeBegin || e.returnBegin) throw F( + "skip, excludeBegin, returnBegin not compatible with beginScope: {}"), + P; + if ("object" != typeof e.beginScope || null === e.beginScope) throw F("beginScope must be object"), + P; + K(e, e.begin, { + key: "beginScope" + }), e.begin = f(e.begin, { + joinWith: "" + }) + } + })(e), (e => { + if (Array.isArray(e.end)) { + if (e.skip || e.excludeEnd || e.returnEnd) throw F( + "skip, excludeEnd, returnEnd not compatible with endScope: {}"), + P; + if ("object" != typeof e.endScope || null === e.endScope) throw F("endScope must be object"), + P; + K(e, e.end, { + key: "endScope" + }), e.end = f(e.end, { + joinWith: "" + }) + } + })(e) +} + +function q(e) { + function n(n, t) { + return RegExp(d(n), "m" + (e.case_insensitive ? "i" : "") + (e.unicodeRegex ? "u" : "") + (t ? "g" : "")) + } + class t { + constructor() { + this.matchIndexes = {}, this.regexes = [], this.matchAt = 1, this.position = 0 + } + addRule(e, n) { + n.position = this.position++, this.matchIndexes[this.matchAt] = n, this.regexes.push([n, e]), + this.matchAt += _(e) + 1 + } + compile() { + 0 === this.regexes.length && (this.exec = () => null); + const e = this.regexes.map((e => e[1])); + this.matcherRe = n(f(e, { + joinWith: "|" + }), !0), this.lastIndex = 0 + } + exec(e) { + this.matcherRe.lastIndex = this.lastIndex; + const n = this.matcherRe.exec(e); + if (!n) return null; + const t = n.findIndex(((e, n) => n > 0 && void 0 !== e)), + a = this.matchIndexes[t]; + return n.splice(0, t), Object.assign(n, a) + } + } + class a { + constructor() { + this.rules = [], this.multiRegexes = [], + this.count = 0, this.lastIndex = 0, this.regexIndex = 0 + } + getMatcher(e) { + if (this.multiRegexes[e]) return this.multiRegexes[e]; + const n = new t; + return this.rules.slice(e).forEach((([e, t]) => n.addRule(e, t))), + n.compile(), this.multiRegexes[e] = n, n + } + resumingScanAtSamePosition() { + return 0 !== this.regexIndex + } + considerAll() { + this.regexIndex = 0 + } + addRule(e, n) { + this.rules.push([e, n]), "begin" === n.type && this.count++ + } + exec(e) { + const n = this.getMatcher(this.regexIndex); + n.lastIndex = this.lastIndex; + let t = n.exec(e); + if (this.resumingScanAtSamePosition()) + if (t && t.index === this.lastIndex); + else { + const n = this.getMatcher(0); + n.lastIndex = this.lastIndex + 1, t = n.exec(e) + } + return t && (this.regexIndex += t.position + 1, + this.regexIndex === this.count && this.considerAll()), t + } + } + if (e.compilerExtensions || (e.compilerExtensions = []), + e.contains && e.contains.includes("self")) throw Error( + "ERR: contains `self` is not supported at the top-level of a language. See documentation."); + return e.classNameAliases = i(e.classNameAliases || {}), + function t(r, s) { + const o = r; + if (r.isCompiled) return o; + [A, R, H, I].forEach((e => e(r, s))), e.compilerExtensions.forEach((e => e(r, s))), + r.__beforeBegin = null, [C, T, D].forEach((e => e(r, s))), r.isCompiled = !0; + let l = null; + return "object" == typeof r.keywords && r.keywords.$pattern && (r.keywords = Object.assign({}, r.keywords), + l = r.keywords.$pattern, + delete r.keywords.$pattern), l = l || /\w+/, r.keywords && (r.keywords = B(r.keywords, e.case_insensitive)), + o.keywordPatternRe = n(l, !0), + s && (r.begin || (r.begin = /\B|\b/), o.beginRe = n(o.begin), r.end || r.endsWithParent || (r.end = /\B|\b/), + r.end && (o.endRe = n(o.end)), + o.terminatorEnd = d(o.end) || "", r.endsWithParent && s.terminatorEnd && (o.terminatorEnd += (r.end ? "|" : + "") + s.terminatorEnd)), + r.illegal && (o.illegalRe = n(r.illegal)), + r.contains || (r.contains = []), r.contains = [].concat(...r.contains.map((e => (e => (e.variants && !e + .cachedVariants && (e.cachedVariants = e.variants.map((n => i(e, { + variants: null + }, n)))), e.cachedVariants ? e.cachedVariants : Z(e) ? i(e, { + starts: e.starts ? i(e.starts) : null + }) : Object.isFrozen(e) ? i(e) : e))("self" === e ? r : e)))), r.contains.forEach((e => { + t(e, o) + })), r.starts && t(r.starts, s), o.matcher = (e => { + const n = new a; + return e.contains.forEach((e => n.addRule(e.begin, { + rule: e, + type: "begin" + }))), e.terminatorEnd && n.addRule(e.terminatorEnd, { + type: "end" + }), e.illegal && n.addRule(e.illegal, { + type: "illegal" + }), n + })(o), o + }(e) +} + +function Z(e) { + return !!e && (e.endsWithParent || Z(e.starts)) +} +class G extends Error { + constructor(e, n) { + super(e), this.name = "HTMLInjectionError", this.html = n + } +} +const W = a, + Q = i, + X = Symbol("nomatch"); +var V = (n => { + const a = Object.create(null), + i = Object.create(null), + r = []; + let s = !0; + const o = "Could not find the language '{}', did you forget to load/include a language module?", + l = { + disableAutodetect: !0, + name: "Plain text", + contains: [] + }; + let d = { + ignoreUnescapedHTML: !1, + throwUnescapedHTML: !1, + noHighlightRe: /^(no-?highlight)$/i, + languageDetectRe: /\blang(?:uage)?-([\w-]+)\b/i, + classPrefix: "hljs-", + cssSelector: "pre code", + languages: null, + __emitter: c + }; + + function _(e) { + return d.noHighlightRe.test(e) + } + + function h(e, n, t) { + let a = "", + i = ""; + "object" == typeof n ? (a = e, + t = n.ignoreIllegals, i = n.language) : (j("10.7.0", "highlight(lang, code, ...args) has been deprecated."), + j("10.7.0", + "Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), + i = e, a = n), void 0 === t && (t = !0); + const r = { + code: a, + language: i + }; + x("before:highlight", r); + const s = r.result ? r.result : f(r.language, r.code, t); + return s.code = r.code, x("after:highlight", s), s + } + + function f(e, n, i, r) { + const l = Object.create(null); + + function c() { + if (!k.keywords) return void M.addText(S); + let e = 0; + k.keywordPatternRe.lastIndex = 0; + let n = k.keywordPatternRe.exec(S), + t = ""; + for (; n;) { + t += S.substring(e, n.index); + const i = w.case_insensitive ? n[0].toLowerCase() : n[0], + r = (a = i, k.keywords[a]); + if (r) { + const [e, a] = r + ; + if (M.addText(t), t = "", l[i] = (l[i] || 0) + 1, l[i] <= 7 && (A += a), e.startsWith("_")) t += n[0]; + else { + const t = w.classNameAliases[e] || e; + M.addKeyword(n[0], t) + } + } else t += n[0]; + e = k.keywordPatternRe.lastIndex, n = k.keywordPatternRe.exec(S) + } + var a; + t += S.substring(e), M.addText(t) + } + + function g() { + null != k.subLanguage ? (() => { + if ("" === S) return; + let e = null; + if ("string" == typeof k.subLanguage) { + if (!a[k.subLanguage]) return void M.addText(S); + e = f(k.subLanguage, S, !0, x[k.subLanguage]), x[k.subLanguage] = e._top + } else e = E(S, k.subLanguage.length ? k.subLanguage : null); + k.relevance > 0 && (A += e.relevance), M.addSublanguage(e._emitter, e.language) + })() : c(), S = "" + } + + function u(e, n) { + let t = 1; + const a = n.length - 1; + for (; t <= a;) { + if (!e._emit[t]) { + t++; + continue + } + const a = w.classNameAliases[e[t]] || e[t], + i = n[t]; + a ? M.addKeyword(i, a) : (S = i, c(), S = ""), t++ + } + } + + function b(e, n) { + return e.scope && "string" == typeof e.scope && M.openNode(w.classNameAliases[e.scope] || e.scope), + e.beginScope && (e.beginScope._wrap ? (M.addKeyword(S, w.classNameAliases[e.beginScope._wrap] || e + .beginScope._wrap), + S = "") : e.beginScope._multi && (u(e.beginScope, n), S = "")), k = Object.create(e, { + parent: { + value: k + } + }), k + } + + function m(e, n, a) { + let i = ((e, n) => { + const t = e && e.exec(n); + return t && 0 === t.index + })(e.endRe, a); + if (i) { + if (e["on:end"]) { + const a = new t(e); + e["on:end"](n, a), a.isMatchIgnored && (i = !1) + } + if (i) { + for (; e.endsParent && e.parent;) e = e.parent; + return e + } + } + if (e.endsWithParent) return m(e.parent, n, a) + } + + function p(e) { + return 0 === k.matcher.regexIndex ? (S += e[0], 1) : (R = !0, 0) + } + + function _(e) { + const t = e[0], + a = n.substring(e.index), + i = m(k, e, a); + if (!i) return X; + const r = k; + k.endScope && k.endScope._wrap ? (g(), + M.addKeyword(t, k.endScope._wrap)) : k.endScope && k.endScope._multi ? (g(), + u(k.endScope, e)) : r.skip ? S += t : (r.returnEnd || r.excludeEnd || (S += t), + g(), r.excludeEnd && (S = t)); + do { + k.scope && M.closeNode(), k.skip || k.subLanguage || (A += k.relevance), k = k.parent + } while (k !== i.parent); + return i.starts && b(i.starts, e), r.returnEnd ? 0 : t.length + } + let h = {}; + + function y(a, r) { + const o = r && r[0]; + if (S += a, null == o) return g(), 0; + if ("begin" === h.type && "end" === r.type && h.index === r.index && "" === o) { + if (S += n.slice(r.index, r.index + 1), !s) { + const n = Error(`0 width match regex (${e})`); + throw n.languageName = e, n.badRule = h.rule, n + } + return 1 + } + if (h = r, "begin" === r.type) return (e => { + const n = e[0], + a = e.rule, + i = new t(a), + r = [a.__beforeBegin, a["on:begin"]]; + for (const t of r) + if (t && (t(e, i), i.isMatchIgnored)) return p(n); + return a.skip ? S += n : (a.excludeBegin && (S += n), + g(), a.returnBegin || a.excludeBegin || (S = n)), b(a, e), a.returnBegin ? 0 : n.length + })(r); + if ("illegal" === r.type && !i) { + const e = Error('Illegal lexeme "' + o + '" for mode "' + (k.scope || "") + '"'); + throw e.mode = k, e + } + if ("end" === r.type) { + const e = _(r); + if (e !== X) return e + } + if ("illegal" === r.type && "" === o) return 1; + if (T > 1e5 && T > 3 * r.index) throw Error("potential infinite loop, way more iterations than matches"); + return S += o, o.length + } + const w = v(e); + if (!w) throw F(o.replace("{}", e)), Error('Unknown language: "' + e + '"'); + const N = q(w); + let O = "", + k = r || N; + const x = {}, + M = new d.__emitter(d); + (() => { + const e = []; + for (let n = k; n !== w; n = n.parent) n.scope && e.unshift(n.scope); + e.forEach((e => M.openNode(e))) + })(); + let S = "", + A = 0, + C = 0, + T = 0, + R = !1; + try { + for (k.matcher.considerAll();;) { + T++, R ? R = !1 : k.matcher.considerAll(), k.matcher.lastIndex = C; + const e = k.matcher.exec(n); + if (!e) break; + const t = y(n.substring(C, e.index), e); + C = e.index + t + } + return y(n.substring(C)), M.closeAllNodes(), M.finalize(), O = M.toHTML(), { + language: e, + value: O, + relevance: A, + illegal: !1, + _emitter: M, + _top: k + } + } catch (t) { + if (t.message && t.message.includes("Illegal")) return { + language: e, + value: W(n), + illegal: !0, + relevance: 0, + _illegalBy: { + message: t.message, + index: C, + context: n.slice(C - 100, C + 100), + mode: t.mode, + resultSoFar: O + }, + _emitter: M + }; + if (s) return { + language: e, + value: W(n), + illegal: !1, + relevance: 0, + errorRaised: t, + _emitter: M, + _top: k + }; + throw t + } + } + + function E(e, n) { + n = n || d.languages || Object.keys(a); + const t = (e => { + const n = { + value: W(e), + illegal: !1, + relevance: 0, + _top: l, + _emitter: new d.__emitter(d) + }; + return n._emitter.addText(e), n + })(e), + i = n.filter(v).filter(k).map((n => f(n, e, !1))); + i.unshift(t); + const r = i.sort(((e, n) => { + if (e.relevance !== n.relevance) return n.relevance - e.relevance; + if (e.language && n.language) { + if (v(e.language).supersetOf === n.language) return 1; + if (v(n.language).supersetOf === e.language) return -1 + } + return 0 + })), + [s, o] = r, + c = s; + return c.secondBest = o, c + } + + function y(e) { + let n = null; + const t = (e => { + let n = e.className + " "; + n += e.parentNode ? e.parentNode.className : ""; + const t = d.languageDetectRe.exec(n); + if (t) { + const n = v(t[1]); + return n || (U(o.replace("{}", t[1])), + U("Falling back to no-highlight mode for this block.", e)), n ? t[1] : "no-highlight" + } + return n.split(/\s+/).find((e => _(e) || v(e))) + })(e); + if (_(t)) return; + if (x("before:highlightElement", { + el: e, + language: t + }), e.children.length > 0 && (d.ignoreUnescapedHTML || (console.warn( + "One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), + console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), + console.warn("The element with unescaped HTML:"), + console.warn(e)), d.throwUnescapedHTML)) throw new G("One of your code blocks includes unescaped HTML.", e + .innerHTML); + n = e; + const a = n.textContent, + r = t ? h(a, { + language: t, + ignoreIllegals: !0 + }) : E(a); + e.innerHTML = r.value, ((e, n, t) => { + const a = n && i[n] || t; + e.classList.add("hljs"), e.classList.add("language-" + a) + })(e, t, r.language), e.result = { + language: r.language, + re: r.relevance, + relevance: r.relevance + }, r.secondBest && (e.secondBest = { + language: r.secondBest.language, + relevance: r.secondBest.relevance + }), x("after:highlightElement", { + el: e, + result: r, + text: a + }) + } + let w = !1; + + function N() { + "loading" !== document.readyState ? document.querySelectorAll(d.cssSelector).forEach(y) : w = !0 + } + + function v(e) { + return e = (e || "").toLowerCase(), a[e] || a[i[e]] + } + + function O(e, { + languageName: n + }) { + "string" == typeof e && (e = [e]), e.forEach((e => { + i[e.toLowerCase()] = n + })) + } + + function k(e) { + const n = v(e); + return n && !n.disableAutodetect + } + + function x(e, n) { + const t = e; + r.forEach((e => { + e[t] && e[t](n) + })) + } + "undefined" != typeof window && window.addEventListener && window.addEventListener("DOMContentLoaded", (() => { + w && N() + }), !1), Object.assign(n, { + highlight: h, + highlightAuto: E, + highlightAll: N, + highlightElement: y, + highlightBlock: e => (j("10.7.0", "highlightBlock will be removed entirely in v12.0"), + j("10.7.0", "Please use highlightElement now."), y(e)), + configure: e => { + d = Q(d, e) + }, + initHighlighting: () => { + N(), j("10.6.0", "initHighlighting() deprecated. Use highlightAll() now.") + }, + initHighlightingOnLoad: () => { + N(), j("10.6.0", "initHighlightingOnLoad() deprecated. Use highlightAll() now.") + }, + registerLanguage: (e, t) => { + let i = null; + try { + i = t(n) + } catch (n) { + if (F("Language definition for '{}' could not be registered.".replace("{}", e)), + !s) throw n; + F(n), i = l + } + i.name || (i.name = e), a[e] = i, i.rawDefinition = t.bind(null, n), i.aliases && O(i.aliases, { + languageName: e + }) + }, + unregisterLanguage: e => { + delete a[e]; + for (const n of Object.keys(i)) i[n] === e && delete i[n] + }, + listLanguages: () => Object.keys(a), + getLanguage: v, + registerAliases: O, + autoDetection: k, + inherit: Q, + addPlugin: e => { + (e => { + e["before:highlightBlock"] && !e["before:highlightElement"] && (e["before:highlightElement"] = + n => { + e["before:highlightBlock"](Object.assign({ + block: n.el + }, n)) + }), e["after:highlightBlock"] && !e["after:highlightElement"] && (e["after:highlightElement"] = + n => { + e["after:highlightBlock"](Object.assign({ + block: n.el + }, n)) + }) + })(e), r.push(e) + } + }), n.debugMode = () => { + s = !1 + }, n.safeMode = () => { + s = !0 + }, n.versionString = "11.7.0", n.regex = { + concat: m, + lookahead: g, + either: p, + optional: b, + anyNumberOfTimes: u + }; + for (const n in M) "object" == typeof M[n] && e.exports(M[n]); + return Object.assign(n, M), n +})({}); +const J = e => ({ + IMPORTANT: { + scope: "meta", + begin: "!important" + }, + BLOCK_COMMENT: e.C_BLOCK_COMMENT_MODE, + HEXCOLOR: { + scope: "number", + begin: /#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/ + }, + FUNCTION_DISPATCH: { + className: "built_in", + begin: /[\w-]+(?=\()/ + }, + ATTRIBUTE_SELECTOR_MODE: { + scope: "selector-attr", + begin: /\[/, + end: /\]/, + illegal: "$", + contains: [e.APOS_STRING_MODE, e.QUOTE_STRING_MODE] + }, + CSS_NUMBER_MODE: { + scope: "number", + begin: e.NUMBER_RE + + "(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", + relevance: 0 + }, + CSS_VARIABLE: { + className: "attr", + begin: /--[A-Za-z][A-Za-z0-9_-]*/ + } + }), + Y = ["a", "abbr", "address", "article", "aside", "audio", "b", "blockquote", "body", "button", "canvas", "caption", + "cite", "code", "dd", "del", "details", "dfn", "div", "dl", "dt", "em", "fieldset", "figcaption", "figure", + "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "html", "i", "iframe", "img", "input", + "ins", "kbd", "label", "legend", "li", "main", "mark", "menu", "nav", "object", "ol", "p", "q", "quote", "samp", + "section", "span", "strong", "summary", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "time", + "tr", "ul", "var", "video" + ], + ee = ["any-hover", "any-pointer", "aspect-ratio", "color", "color-gamut", "color-index", "device-aspect-ratio", + "device-height", "device-width", "display-mode", "forced-colors", "grid", "height", "hover", "inverted-colors", + "monochrome", "orientation", "overflow-block", "overflow-inline", "pointer", "prefers-color-scheme", + "prefers-contrast", "prefers-reduced-motion", "prefers-reduced-transparency", "resolution", "scan", "scripting", + "update", "width", "min-width", "max-width", "min-height", "max-height" + ], + ne = ["active", "any-link", "blank", "checked", "current", "default", "defined", "dir", "disabled", "drop", "empty", + "enabled", "first", "first-child", "first-of-type", "fullscreen", "future", "focus", "focus-visible", + "focus-within", "has", "host", "host-context", "hover", "indeterminate", "in-range", "invalid", "is", "lang", + "last-child", "last-of-type", "left", "link", "local-link", "not", "nth-child", "nth-col", "nth-last-child", + "nth-last-col", "nth-last-of-type", "nth-of-type", "only-child", "only-of-type", "optional", "out-of-range", "past", + "placeholder-shown", "read-only", "read-write", "required", "right", "root", "scope", "target", "target-within", + "user-invalid", "valid", "visited", "where" + ], + te = ["after", "backdrop", "before", "cue", "cue-region", "first-letter", "first-line", "grammar-error", "marker", + "part", "placeholder", "selection", "slotted", "spelling-error" + ], + ae = ["align-content", "align-items", "align-self", "all", "animation", "animation-delay", "animation-direction", + "animation-duration", "animation-fill-mode", "animation-iteration-count", "animation-name", "animation-play-state", + "animation-timing-function", "backface-visibility", "background", "background-attachment", "background-blend-mode", + "background-clip", "background-color", "background-image", "background-origin", "background-position", + "background-repeat", "background-size", "block-size", "border", "border-block", "border-block-color", + "border-block-end", "border-block-end-color", "border-block-end-style", "border-block-end-width", + "border-block-start", "border-block-start-color", "border-block-start-style", "border-block-start-width", + "border-block-style", "border-block-width", "border-bottom", "border-bottom-color", "border-bottom-left-radius", + "border-bottom-right-radius", "border-bottom-style", "border-bottom-width", "border-collapse", "border-color", + "border-image", "border-image-outset", "border-image-repeat", "border-image-slice", "border-image-source", + "border-image-width", "border-inline", "border-inline-color", "border-inline-end", "border-inline-end-color", + "border-inline-end-style", "border-inline-end-width", "border-inline-start", "border-inline-start-color", + "border-inline-start-style", "border-inline-start-width", "border-inline-style", "border-inline-width", + "border-left", "border-left-color", "border-left-style", "border-left-width", "border-radius", "border-right", + "border-right-color", "border-right-style", "border-right-width", "border-spacing", "border-style", "border-top", + "border-top-color", "border-top-left-radius", "border-top-right-radius", "border-top-style", "border-top-width", + "border-width", "bottom", "box-decoration-break", "box-shadow", "box-sizing", "break-after", "break-before", + "break-inside", "caption-side", "caret-color", "clear", "clip", "clip-path", "clip-rule", "color", "column-count", + "column-fill", "column-gap", "column-rule", "column-rule-color", "column-rule-style", "column-rule-width", + "column-span", "column-width", "columns", "contain", "content", "content-visibility", "counter-increment", + "counter-reset", "cue", "cue-after", "cue-before", "cursor", "direction", "display", "empty-cells", "filter", + "flex", "flex-basis", "flex-direction", "flex-flow", "flex-grow", "flex-shrink", "flex-wrap", "float", "flow", + "font", "font-display", "font-family", "font-feature-settings", "font-kerning", "font-language-override", + "font-size", "font-size-adjust", "font-smoothing", "font-stretch", "font-style", "font-synthesis", "font-variant", + "font-variant-caps", "font-variant-east-asian", "font-variant-ligatures", "font-variant-numeric", + "font-variant-position", "font-variation-settings", "font-weight", "gap", "glyph-orientation-vertical", "grid", + "grid-area", "grid-auto-columns", "grid-auto-flow", "grid-auto-rows", "grid-column", "grid-column-end", + "grid-column-start", "grid-gap", "grid-row", "grid-row-end", "grid-row-start", "grid-template", + "grid-template-areas", "grid-template-columns", "grid-template-rows", "hanging-punctuation", "height", "hyphens", + "icon", "image-orientation", "image-rendering", "image-resolution", "ime-mode", "inline-size", "isolation", + "justify-content", "left", "letter-spacing", "line-break", "line-height", "list-style", "list-style-image", + "list-style-position", "list-style-type", "margin", "margin-block", "margin-block-end", "margin-block-start", + "margin-bottom", "margin-inline", "margin-inline-end", "margin-inline-start", "margin-left", "margin-right", + "margin-top", "marks", "mask", "mask-border", "mask-border-mode", "mask-border-outset", "mask-border-repeat", + "mask-border-slice", "mask-border-source", "mask-border-width", "mask-clip", "mask-composite", "mask-image", + "mask-mode", "mask-origin", "mask-position", "mask-repeat", "mask-size", "mask-type", "max-block-size", + "max-height", "max-inline-size", "max-width", "min-block-size", "min-height", "min-inline-size", "min-width", + "mix-blend-mode", "nav-down", "nav-index", "nav-left", "nav-right", "nav-up", "none", "normal", "object-fit", + "object-position", "opacity", "order", "orphans", "outline", "outline-color", "outline-offset", "outline-style", + "outline-width", "overflow", "overflow-wrap", "overflow-x", "overflow-y", "padding", "padding-block", + "padding-block-end", "padding-block-start", "padding-bottom", "padding-inline", "padding-inline-end", + "padding-inline-start", "padding-left", "padding-right", "padding-top", "page-break-after", "page-break-before", + "page-break-inside", "pause", "pause-after", "pause-before", "perspective", "perspective-origin", "pointer-events", + "position", "quotes", "resize", "rest", "rest-after", "rest-before", "right", "row-gap", "scroll-margin", + "scroll-margin-block", "scroll-margin-block-end", "scroll-margin-block-start", "scroll-margin-bottom", + "scroll-margin-inline", "scroll-margin-inline-end", "scroll-margin-inline-start", "scroll-margin-left", + "scroll-margin-right", "scroll-margin-top", "scroll-padding", "scroll-padding-block", "scroll-padding-block-end", + "scroll-padding-block-start", "scroll-padding-bottom", "scroll-padding-inline", "scroll-padding-inline-end", + "scroll-padding-inline-start", "scroll-padding-left", "scroll-padding-right", "scroll-padding-top", + "scroll-snap-align", "scroll-snap-stop", "scroll-snap-type", "scrollbar-color", "scrollbar-gutter", + "scrollbar-width", "shape-image-threshold", "shape-margin", "shape-outside", "speak", "speak-as", "src", "tab-size", + "table-layout", "text-align", "text-align-all", "text-align-last", "text-combine-upright", "text-decoration", + "text-decoration-color", "text-decoration-line", "text-decoration-style", "text-emphasis", "text-emphasis-color", + "text-emphasis-position", "text-emphasis-style", "text-indent", "text-justify", "text-orientation", "text-overflow", + "text-rendering", "text-shadow", "text-transform", "text-underline-position", "top", "transform", "transform-box", + "transform-origin", "transform-style", "transition", "transition-delay", "transition-duration", + "transition-property", "transition-timing-function", "unicode-bidi", "vertical-align", "visibility", + "voice-balance", "voice-duration", "voice-family", "voice-pitch", "voice-range", "voice-rate", "voice-stress", + "voice-volume", "white-space", "widows", "width", "will-change", "word-break", "word-spacing", "word-wrap", + "writing-mode", "z-index" + ].reverse(), + ie = ne.concat(te); +var re = "\\.([0-9](_*[0-9])*)", + se = "[0-9a-fA-F](_*[0-9a-fA-F])*", + oe = { + className: "number", + variants: [{ + begin: `(\\b([0-9](_*[0-9])*)((${re})|\\.)?|(${re}))[eE][+-]?([0-9](_*[0-9])*)[fFdD]?\\b` + }, { + begin: `\\b([0-9](_*[0-9])*)((${re})[fFdD]?\\b|\\.([fFdD]\\b)?)` + }, { + begin: `(${re})[fFdD]?\\b` + }, { + begin: "\\b([0-9](_*[0-9])*)[fFdD]\\b" + }, { + begin: `\\b0[xX]((${se})\\.?|(${se})?\\.(${se}))[pP][+-]?([0-9](_*[0-9])*)[fFdD]?\\b` + }, { + begin: "\\b(0|[1-9](_*[0-9])*)[lL]?\\b" + }, { + begin: `\\b0[xX](${se})[lL]?\\b` + }, { + begin: "\\b0(_*[0-7])*[lL]?\\b" + }, { + begin: "\\b0[bB][01](_*[01])*[lL]?\\b" + }], + relevance: 0 + }; + +function le(e, n, t) { + return -1 === t ? "" : e.replace(n, (a => le(e, n, t - 1))) +} +const ce = "[A-Za-z$_][0-9A-Za-z$_]*", + de = ["as", "in", "of", "if", "for", "while", "finally", "var", "new", "function", "do", "return", "void", "else", + "break", "catch", "instanceof", "with", "throw", "case", "default", "try", "switch", "continue", "typeof", "delete", + "let", "yield", "const", "class", "debugger", "async", "await", "static", "import", "from", "export", "extends" + ], + ge = ["true", "false", "null", "undefined", "NaN", "Infinity"], + ue = ["Object", "Function", "Boolean", "Symbol", "Math", "Date", "Number", "BigInt", "String", "RegExp", "Array", + "Float32Array", "Float64Array", "Int8Array", "Uint8Array", "Uint8ClampedArray", "Int16Array", "Int32Array", + "Uint16Array", "Uint32Array", "BigInt64Array", "BigUint64Array", "Set", "Map", "WeakSet", "WeakMap", "ArrayBuffer", + "SharedArrayBuffer", "Atomics", "DataView", "JSON", "Promise", "Generator", "GeneratorFunction", "AsyncFunction", + "Reflect", "Proxy", "Intl", "WebAssembly" + ], + be = ["Error", "EvalError", "InternalError", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError"], + me = ["setInterval", "setTimeout", "clearInterval", "clearTimeout", "require", "exports", "eval", "isFinite", "isNaN", + "parseFloat", "parseInt", "decodeURI", "decodeURIComponent", "encodeURI", "encodeURIComponent", "escape", "unescape" + ], + pe = ["arguments", "this", "super", "console", "window", "document", "localStorage", "module", "global"], + _e = [].concat(me, ue, be); + +function he(e) { + const n = e.regex, + t = ce, + a = { + begin: /<[A-Za-z0-9\\._:-]+/, + end: /\/[A-Za-z0-9\\._:-]+>|\/>/, + isTrulyOpeningTag: (e, n) => { + const t = e[0].length + e.index, + a = e.input[t]; + if ("<" === a || "," === a) return void n.ignoreMatch(); + let i; + ">" === a && (((e, { + after: n + }) => { + const t = "", + k = { + match: [/const|var|let/, /\s+/, t, /\s*/, /=\s*/, /(async\s*)?/, n.lookahead(O)], + keywords: "async", + className: { + 1: "keyword", + 3: "title.function" + }, + contains: [_] + }; + return { + name: "Javascript", + aliases: ["js", "jsx", "mjs", "cjs"], + keywords: i, + exports: { + PARAMS_CONTAINS: p, + CLASS_REFERENCE: f + }, + illegal: /#(?![$_A-z])/, + contains: [e.SHEBANG({ + label: "shebang", + binary: "node", + relevance: 5 + }), { + label: "use_strict", + className: "meta", + relevance: 10, + begin: /^\s*['"]use (strict|asm)['"]/ + }, e.APOS_STRING_MODE, e.QUOTE_STRING_MODE, c, d, g, u, { + match: /\$\d+/ + }, o, f, { + className: "attr", + begin: t + n.lookahead(":"), + relevance: 0 + }, k, { + begin: "(" + e.RE_STARTERS_RE + "|\\b(case|return|throw)\\b)\\s*", + keywords: "return throw case", + relevance: 0, + contains: [u, e.REGEXP_MODE, { + className: "function", + begin: O, + returnBegin: !0, + end: "\\s*=>", + contains: [{ + className: "params", + variants: [{ + begin: e.UNDERSCORE_IDENT_RE, + relevance: 0 + }, { + className: null, + begin: /\(\s*\)/, + skip: !0 + }, { + begin: /\(/, + end: /\)/, + excludeBegin: !0, + excludeEnd: !0, + keywords: i, + contains: p + }] + }] + }, { + begin: /,/, + relevance: 0 + }, { + match: /\s+/, + relevance: 0 + }, { + variants: [{ + begin: "<>", + end: "" + }, { + match: /<[A-Za-z0-9\\._:-]+\s*\/>/ + }, { + begin: a.begin, + "on:begin": a.isTrulyOpeningTag, + end: a.end + }], + subLanguage: "xml", + contains: [{ + begin: a.begin, + end: a.end, + skip: !0, + contains: ["self"] + }] + }] + }, E, { + beginKeywords: "while if switch catch for" + }, { + begin: "\\b(?!function)" + e.UNDERSCORE_IDENT_RE + + "\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", + returnBegin: !0, + label: "func.def", + contains: [_, e.inherit(e.TITLE_MODE, { + begin: t, + className: "title.function" + })] + }, { + match: /\.\.\./, + relevance: 0 + }, N, { + match: "\\$" + t, + relevance: 0 + }, { + match: [/\bconstructor(?=\s*\()/], + className: { + 1: "title.function" + }, + contains: [_] + }, y, { + relevance: 0, + match: /\b[A-Z][A-Z_0-9]+\b/, + className: "variable.constant" + }, h, v, { + match: /\$[(.]/ + }] + } +} +const fe = e => m(/\b/, e, /\w$/.test(e) ? /\b/ : /\B/), + Ee = ["Protocol", "Type"].map(fe), + ye = ["init", "self"].map(fe), + we = ["Any", "Self"], + Ne = ["actor", "any", "associatedtype", "async", "await", /as\?/, /as!/, "as", "break", "case", "catch", "class", + "continue", "convenience", "default", "defer", "deinit", "didSet", "distributed", "do", "dynamic", "else", "enum", + "extension", "fallthrough", /fileprivate\(set\)/, "fileprivate", "final", "for", "func", "get", "guard", "if", + "import", "indirect", "infix", /init\?/, /init!/, "inout", /internal\(set\)/, "internal", "in", "is", "isolated", + "nonisolated", "lazy", "let", "mutating", "nonmutating", /open\(set\)/, "open", "operator", "optional", "override", + "postfix", "precedencegroup", "prefix", /private\(set\)/, "private", "protocol", /public\(set\)/, "public", + "repeat", "required", "rethrows", "return", "set", "some", "static", "struct", "subscript", "super", "switch", + "throws", "throw", /try\?/, /try!/, "try", "typealias", /unowned\(safe\)/, /unowned\(unsafe\)/, "unowned", "var", + "weak", "where", "while", "willSet" + ], + ve = ["false", "nil", "true"], + Oe = ["assignment", "associativity", "higherThan", "left", "lowerThan", "none", "right"], + ke = ["#colorLiteral", "#column", "#dsohandle", "#else", "#elseif", "#endif", "#error", "#file", "#fileID", + "#fileLiteral", "#filePath", "#function", "#if", "#imageLiteral", "#keyPath", "#line", "#selector", + "#sourceLocation", "#warn_unqualified_access", "#warning" + ], + xe = ["abs", "all", "any", "assert", "assertionFailure", "debugPrint", "dump", "fatalError", "getVaList", + "isKnownUniquelyReferenced", "max", "min", "numericCast", "pointwiseMax", "pointwiseMin", "precondition", + "preconditionFailure", "print", "readLine", "repeatElement", "sequence", "stride", "swap", + "swift_unboxFromSwiftValueWithType", "transcode", "type", "unsafeBitCast", "unsafeDowncast", "withExtendedLifetime", + "withUnsafeMutablePointer", "withUnsafePointer", "withVaList", "withoutActuallyEscaping", "zip" + ], + Me = p(/[/=\-+!*%<>&|^~?]/, /[\u00A1-\u00A7]/, /[\u00A9\u00AB]/, /[\u00AC\u00AE]/, /[\u00B0\u00B1]/, + /[\u00B6\u00BB\u00BF\u00D7\u00F7]/, /[\u2016-\u2017]/, /[\u2020-\u2027]/, /[\u2030-\u203E]/, /[\u2041-\u2053]/, + /[\u2055-\u205E]/, /[\u2190-\u23FF]/, /[\u2500-\u2775]/, /[\u2794-\u2BFF]/, /[\u2E00-\u2E7F]/, /[\u3001-\u3003]/, + /[\u3008-\u3020]/, /[\u3030]/), + Se = p(Me, /[\u0300-\u036F]/, /[\u1DC0-\u1DFF]/, /[\u20D0-\u20FF]/, /[\uFE00-\uFE0F]/, /[\uFE20-\uFE2F]/), + Ae = m(Me, Se, "*"), + Ce = p(/[a-zA-Z_]/, /[\u00A8\u00AA\u00AD\u00AF\u00B2-\u00B5\u00B7-\u00BA]/, + /[\u00BC-\u00BE\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]/, /[\u0100-\u02FF\u0370-\u167F\u1681-\u180D\u180F-\u1DBF]/, + /[\u1E00-\u1FFF]/, /[\u200B-\u200D\u202A-\u202E\u203F-\u2040\u2054\u2060-\u206F]/, + /[\u2070-\u20CF\u2100-\u218F\u2460-\u24FF\u2776-\u2793]/, /[\u2C00-\u2DFF\u2E80-\u2FFF]/, + /[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/, /[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/, + /[\uFE47-\uFEFE\uFF00-\uFFFD]/), + Te = p(Ce, /\d/, /[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/), + Re = m(Ce, Te, "*"), + De = m(/[A-Z]/, Te, "*"), + Ie = ["autoclosure", m(/convention\(/, p("swift", "block", "c"), /\)/), "discardableResult", "dynamicCallable", + "dynamicMemberLookup", "escaping", "frozen", "GKInspectable", "IBAction", "IBDesignable", "IBInspectable", + "IBOutlet", "IBSegueAction", "inlinable", "main", "nonobjc", "NSApplicationMain", "NSCopying", "NSManaged", m( + /objc\(/, Re, /\)/), "objc", "objcMembers", "propertyWrapper", "requires_stored_property_inits", "resultBuilder", + "testable", "UIApplicationMain", "unknown", "usableFromInline" + ], + Le = ["iOS", "iOSApplicationExtension", "macOS", "macOSApplicationExtension", "macCatalyst", + "macCatalystApplicationExtension", "watchOS", "watchOSApplicationExtension", "tvOS", "tvOSApplicationExtension", + "swift" + ]; +var Be = Object.freeze({ + __proto__: null, + grmr_bash: e => { + const n = e.regex, + t = {}, + a = { + begin: /\$\{/, + end: /\}/, + contains: ["self", { + begin: /:-/, + contains: [t] + }] + }; + Object.assign(t, { + className: "variable", + variants: [{ + begin: n.concat(/\$[\w\d#@][\w\d_]*/, "(?![\\w\\d])(?![$])") + }, a] + }); + const i = { + className: "subst", + begin: /\$\(/, + end: /\)/, + contains: [e.BACKSLASH_ESCAPE] + }, + r = { + begin: /<<-?\s*(?=\w+)/, + starts: { + contains: [e.END_SAME_AS_BEGIN({ + begin: /(\w+)/, + end: /(\w+)/, + className: "string" + })] + } + }, + s = { + className: "string", + begin: /"/, + end: /"/, + contains: [e.BACKSLASH_ESCAPE, t, i] + }; + i.contains.push(s); + const o = { + begin: /\$?\(\(/, + end: /\)\)/, + contains: [{ + begin: /\d+#[0-9a-f]+/, + className: "number" + }, e.NUMBER_MODE, t] + }, + l = e.SHEBANG({ + binary: "(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)", + relevance: 10 + }), + c = { + className: "function", + begin: /\w[\w\d_]*\s*\(\s*\)\s*\{/, + returnBegin: !0, + contains: [e.inherit(e.TITLE_MODE, { + begin: /\w[\w\d_]*/ + })], + relevance: 0 + }; + return { + name: "Bash", + aliases: ["sh"], + keywords: { + $pattern: /\b[a-z][a-z0-9._-]+\b/, + keyword: ["if", "then", "else", "elif", "fi", "for", "while", "in", "do", "done", "case", "esac", + "function" + ], + literal: ["true", "false"], + built_in: ["break", "cd", "continue", "eval", "exec", "exit", "export", "getopts", "hash", "pwd", + "readonly", "return", "shift", "test", "times", "trap", "umask", "unset", "alias", "bind", "builtin", + "caller", "command", "declare", "echo", "enable", "help", "let", "local", "logout", "mapfile", + "printf", "read", "readarray", "source", "type", "typeset", "ulimit", "unalias", "set", "shopt", + "autoload", "bg", "bindkey", "bye", "cap", "chdir", "clone", "comparguments", "compcall", "compctl", + "compdescribe", "compfiles", "compgroups", "compquote", "comptags", "comptry", "compvalues", "dirs", + "disable", "disown", "echotc", "echoti", "emulate", "fc", "fg", "float", "functions", "getcap", + "getln", "history", "integer", "jobs", "kill", "limit", "log", "noglob", "popd", "print", "pushd", + "pushln", "rehash", "sched", "setcap", "setopt", "stat", "suspend", "ttyctl", "unfunction", "unhash", + "unlimit", "unsetopt", "vared", "wait", "whence", "where", "which", "zcompile", "zformat", "zftp", + "zle", "zmodload", "zparseopts", "zprof", "zpty", "zregexparse", "zsocket", "zstyle", "ztcp", "chcon", + "chgrp", "chown", "chmod", "cp", "dd", "df", "dir", "dircolors", "ln", "ls", "mkdir", "mkfifo", + "mknod", "mktemp", "mv", "realpath", "rm", "rmdir", "shred", "sync", "touch", "truncate", "vdir", + "b2sum", "base32", "base64", "cat", "cksum", "comm", "csplit", "cut", "expand", "fmt", "fold", "head", + "join", "md5sum", "nl", "numfmt", "od", "paste", "ptx", "pr", "sha1sum", "sha224sum", "sha256sum", + "sha384sum", "sha512sum", "shuf", "sort", "split", "sum", "tac", "tail", "tr", "tsort", "unexpand", + "uniq", "wc", "arch", "basename", "chroot", "date", "dirname", "du", "echo", "env", "expr", "factor", + "groups", "hostid", "id", "link", "logname", "nice", "nohup", "nproc", "pathchk", "pinky", "printenv", + "printf", "pwd", "readlink", "runcon", "seq", "sleep", "stat", "stdbuf", "stty", "tee", "test", + "timeout", "tty", "uname", "unlink", "uptime", "users", "who", "whoami", "yes" + ] + }, + contains: [l, e.SHEBANG(), c, o, e.HASH_COMMENT_MODE, r, { + match: /(\/[a-z._-]+)+/ + }, s, { + className: "", + begin: /\\"/ + }, { + className: "string", + begin: /'/, + end: /'/ + }, t] + } + }, + grmr_c: e => { + const n = e.regex, + t = e.COMMENT("//", "$", { + contains: [{ + begin: /\\\n/ + }] + }), + a = "[a-zA-Z_]\\w*::", + i = "(decltype\\(auto\\)|" + n.optional(a) + "[a-zA-Z_]\\w*" + n.optional("<[^<>]+>") + ")", + r = { + className: "type", + variants: [{ + begin: "\\b[a-z\\d_]*_t\\b" + }, { + match: /\batomic_[a-z]{3,6}\b/ + }] + }, + s = { + className: "string", + variants: [{ + begin: '(u8?|U|L)?"', + end: '"', + illegal: "\\n", + contains: [e.BACKSLASH_ESCAPE] + }, { + begin: "(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", + end: "'", + illegal: "." + }, e.END_SAME_AS_BEGIN({ + begin: /(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/, + end: /\)([^()\\ ]{0,16})"/ + })] + }, + o = { + className: "number", + variants: [{ + begin: "\\b(0b[01']+)" + }, { + begin: "(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" + }, { + begin: "(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" + }], + relevance: 0 + }, + l = { + className: "meta", + begin: /#\s*[a-z]+\b/, + end: /$/, + keywords: { + keyword: "if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" + }, + contains: [{ + begin: /\\\n/, + relevance: 0 + }, e.inherit(s, { + className: "string" + }), { + className: "string", + begin: /<.*?>/ + }, t, e.C_BLOCK_COMMENT_MODE] + }, + c = { + className: "title", + begin: n.optional(a) + e.IDENT_RE, + relevance: 0 + }, + d = n.optional(a) + e.IDENT_RE + "\\s*\\(", + g = { + keyword: ["asm", "auto", "break", "case", "continue", "default", "do", "else", "enum", "extern", "for", + "fortran", "goto", "if", "inline", "register", "restrict", "return", "sizeof", "struct", "switch", + "typedef", "union", "volatile", "while", "_Alignas", "_Alignof", "_Atomic", "_Generic", "_Noreturn", + "_Static_assert", "_Thread_local", "alignas", "alignof", "noreturn", "static_assert", "thread_local", + "_Pragma" + ], + type: ["float", "double", "signed", "unsigned", "int", "short", "long", "char", "void", "_Bool", + "_Complex", "_Imaginary", "_Decimal32", "_Decimal64", "_Decimal128", "const", "static", "complex", + "bool", "imaginary" + ], + literal: "true false NULL", + built_in: "std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr" + }, + u = [l, r, t, e.C_BLOCK_COMMENT_MODE, o, s], + b = { + variants: [{ + begin: /=/, + end: /;/ + }, { + begin: /\(/, + end: /\)/ + }, { + beginKeywords: "new throw return else", + end: /;/ + }], + keywords: g, + contains: u.concat([{ + begin: /\(/, + end: /\)/, + keywords: g, + contains: u.concat(["self"]), + relevance: 0 + }]), + relevance: 0 + }, + m = { + begin: "(" + i + "[\\*&\\s]+)+" + d, + returnBegin: !0, + end: /[{;=]/, + excludeEnd: !0, + keywords: g, + illegal: /[^\w\s\*&:<>.]/, + contains: [{ + begin: "decltype\\(auto\\)", + keywords: g, + relevance: 0 + }, { + begin: d, + returnBegin: !0, + contains: [e.inherit(c, { + className: "title.function" + })], + relevance: 0 + }, { + relevance: 0, + match: /,/ + }, { + className: "params", + begin: /\(/, + end: /\)/, + keywords: g, + relevance: 0, + contains: [t, e.C_BLOCK_COMMENT_MODE, s, o, r, { + begin: /\(/, + end: /\)/, + keywords: g, + relevance: 0, + contains: ["self", t, e.C_BLOCK_COMMENT_MODE, s, o, r] + }] + }, r, t, e.C_BLOCK_COMMENT_MODE, l] + }; + return { + name: "C", + aliases: ["h"], + keywords: g, + disableAutodetect: !0, + illegal: "=]/, + contains: [{ + beginKeywords: "final class struct" + }, e.TITLE_MODE] + }]), + exports: { + preprocessor: l, + strings: s, + keywords: g + } + } + }, + grmr_cpp: e => { + const n = e.regex, + t = e.COMMENT("//", "$", { + contains: [{ + begin: /\\\n/ + }] + }), + a = "[a-zA-Z_]\\w*::", + i = "(?!struct)(decltype\\(auto\\)|" + n.optional(a) + "[a-zA-Z_]\\w*" + n.optional("<[^<>]+>") + ")", + r = { + className: "type", + begin: "\\b[a-z\\d_]*_t\\b" + }, + s = { + className: "string", + variants: [{ + begin: '(u8?|U|L)?"', + end: '"', + illegal: "\\n", + contains: [e.BACKSLASH_ESCAPE] + }, { + begin: "(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", + end: "'", + illegal: "." + }, e.END_SAME_AS_BEGIN({ + begin: /(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/, + end: /\)([^()\\ ]{0,16})"/ + })] + }, + o = { + className: "number", + variants: [{ + begin: "\\b(0b[01']+)" + }, { + begin: "(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" + }, { + begin: "(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" + }], + relevance: 0 + }, + l = { + className: "meta", + begin: /#\s*[a-z]+\b/, + end: /$/, + keywords: { + keyword: "if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" + }, + contains: [{ + begin: /\\\n/, + relevance: 0 + }, e.inherit(s, { + className: "string" + }), { + className: "string", + begin: /<.*?>/ + }, t, e.C_BLOCK_COMMENT_MODE] + }, + c = { + className: "title", + begin: n.optional(a) + e.IDENT_RE, + relevance: 0 + }, + d = n.optional(a) + e.IDENT_RE + "\\s*\\(", + g = { + type: ["bool", "char", "char16_t", "char32_t", "char8_t", "double", "float", "int", "long", "short", + "void", "wchar_t", "unsigned", "signed", "const", "static" + ], + keyword: ["alignas", "alignof", "and", "and_eq", "asm", "atomic_cancel", "atomic_commit", + "atomic_noexcept", "auto", "bitand", "bitor", "break", "case", "catch", "class", "co_await", + "co_return", "co_yield", "compl", "concept", "const_cast|10", "consteval", "constexpr", "constinit", + "continue", "decltype", "default", "delete", "do", "dynamic_cast|10", "else", "enum", "explicit", + "export", "extern", "false", "final", "for", "friend", "goto", "if", "import", "inline", "module", + "mutable", "namespace", "new", "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq", + "override", "private", "protected", "public", "reflexpr", "register", "reinterpret_cast|10", + "requires", "return", "sizeof", "static_assert", "static_cast|10", "struct", "switch", "synchronized", + "template", "this", "thread_local", "throw", "transaction_safe", "transaction_safe_dynamic", "true", + "try", "typedef", "typeid", "typename", "union", "using", "virtual", "volatile", "while", "xor", + "xor_eq" + ], + literal: ["NULL", "false", "nullopt", "nullptr", "true"], + built_in: ["_Pragma"], + _type_hints: ["any", "auto_ptr", "barrier", "binary_semaphore", "bitset", "complex", "condition_variable", + "condition_variable_any", "counting_semaphore", "deque", "false_type", "future", "imaginary", + "initializer_list", "istringstream", "jthread", "latch", "lock_guard", "multimap", "multiset", + "mutex", "optional", "ostringstream", "packaged_task", "pair", "promise", "priority_queue", "queue", + "recursive_mutex", "recursive_timed_mutex", "scoped_lock", "set", "shared_future", "shared_lock", + "shared_mutex", "shared_timed_mutex", "shared_ptr", "stack", "string_view", "stringstream", + "timed_mutex", "thread", "true_type", "tuple", "unique_lock", "unique_ptr", "unordered_map", + "unordered_multimap", "unordered_multiset", "unordered_set", "variant", "vector", "weak_ptr", + "wstring", "wstring_view" + ] + }, + u = { + className: "function.dispatch", + relevance: 0, + keywords: { + _hint: ["abort", "abs", "acos", "apply", "as_const", "asin", "atan", "atan2", "calloc", "ceil", "cerr", + "cin", "clog", "cos", "cosh", "cout", "declval", "endl", "exchange", "exit", "exp", "fabs", "floor", + "fmod", "forward", "fprintf", "fputs", "free", "frexp", "fscanf", "future", "invoke", "isalnum", + "isalpha", "iscntrl", "isdigit", "isgraph", "islower", "isprint", "ispunct", "isspace", "isupper", + "isxdigit", "labs", "launder", "ldexp", "log", "log10", "make_pair", "make_shared", + "make_shared_for_overwrite", "make_tuple", "make_unique", "malloc", "memchr", "memcmp", "memcpy", + "memset", "modf", "move", "pow", "printf", "putchar", "puts", "realloc", "scanf", "sin", "sinh", + "snprintf", "sprintf", "sqrt", "sscanf", "std", "stderr", "stdin", "stdout", "strcat", "strchr", + "strcmp", "strcpy", "strcspn", "strlen", "strncat", "strncmp", "strncpy", "strpbrk", "strrchr", + "strspn", "strstr", "swap", "tan", "tanh", "terminate", "to_underlying", "tolower", "toupper", + "vfprintf", "visit", "vprintf", "vsprintf" + ] + }, + begin: n.concat(/\b/, /(?!decltype)/, /(?!if)/, /(?!for)/, /(?!switch)/, /(?!while)/, e.IDENT_RE, n + .lookahead(/(<[^<>]+>|)\s*\(/)) + }, + b = [u, l, r, t, e.C_BLOCK_COMMENT_MODE, o, s], + m = { + variants: [{ + begin: /=/, + end: /;/ + }, { + begin: /\(/, + end: /\)/ + }, { + beginKeywords: "new throw return else", + end: /;/ + }], + keywords: g, + contains: b.concat([{ + begin: /\(/, + end: /\)/, + keywords: g, + contains: b.concat(["self"]), + relevance: 0 + }]), + relevance: 0 + }, + p = { + className: "function", + begin: "(" + i + "[\\*&\\s]+)+" + d, + returnBegin: !0, + end: /[{;=]/, + excludeEnd: !0, + keywords: g, + illegal: /[^\w\s\*&:<>.]/, + contains: [{ + begin: "decltype\\(auto\\)", + keywords: g, + relevance: 0 + }, { + begin: d, + returnBegin: !0, + contains: [c], + relevance: 0 + }, { + begin: /::/, + relevance: 0 + }, { + begin: /:/, + endsWithParent: !0, + contains: [s, o] + }, { + relevance: 0, + match: /,/ + }, { + className: "params", + begin: /\(/, + end: /\)/, + keywords: g, + relevance: 0, + contains: [t, e.C_BLOCK_COMMENT_MODE, s, o, r, { + begin: /\(/, + end: /\)/, + keywords: g, + relevance: 0, + contains: ["self", t, e.C_BLOCK_COMMENT_MODE, s, o, r] + }] + }, r, t, e.C_BLOCK_COMMENT_MODE, l] + }; + return { + name: "C++", + aliases: ["cc", "c++", "h++", "hpp", "hh", "hxx", "cxx"], + keywords: g, + illegal: "", + keywords: g, + contains: ["self", r] + }, { + begin: e.IDENT_RE + "::", + keywords: g + }, { + match: [/\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/, /\s+/, /\w+/], + className: { + 1: "keyword", + 3: "title.class" + } + }]) + } + }, + grmr_csharp: e => { + const n = { + keyword: ["abstract", "as", "base", "break", "case", "catch", "class", "const", "continue", "do", "else", + "event", "explicit", "extern", "finally", "fixed", "for", "foreach", "goto", "if", "implicit", "in", + "interface", "internal", "is", "lock", "namespace", "new", "operator", "out", "override", "params", + "private", "protected", "public", "readonly", "record", "ref", "return", "scoped", "sealed", "sizeof", + "stackalloc", "static", "struct", "switch", "this", "throw", "try", "typeof", "unchecked", "unsafe", + "using", "virtual", "void", "volatile", "while" + ].concat(["add", "alias", "and", "ascending", "async", "await", "by", "descending", "equals", "from", + "get", "global", "group", "init", "into", "join", "let", "nameof", "not", "notnull", "on", "or", + "orderby", "partial", "remove", "select", "set", "unmanaged", "value|0", "var", "when", "where", + "with", "yield" + ]), + built_in: ["bool", "byte", "char", "decimal", "delegate", "double", "dynamic", "enum", "float", "int", + "long", "nint", "nuint", "object", "sbyte", "short", "string", "ulong", "uint", "ushort" + ], + literal: ["default", "false", "null", "true"] + }, + t = e.inherit(e.TITLE_MODE, { + begin: "[a-zA-Z](\\.?\\w)*" + }), + a = { + className: "number", + variants: [{ + begin: "\\b(0b[01']+)" + }, { + begin: "(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)" + }, { + begin: "(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" + }], + relevance: 0 + }, + i = { + className: "string", + begin: '@"', + end: '"', + contains: [{ + begin: '""' + }] + }, + r = e.inherit(i, { + illegal: /\n/ + }), + s = { + className: "subst", + begin: /\{/, + end: /\}/, + keywords: n + }, + o = e.inherit(s, { + illegal: /\n/ + }), + l = { + className: "string", + begin: /\$"/, + end: '"', + illegal: /\n/, + contains: [{ + begin: /\{\{/ + }, { + begin: /\}\}/ + }, e.BACKSLASH_ESCAPE, o] + }, + c = { + className: "string", + begin: /\$@"/, + end: '"', + contains: [{ + begin: /\{\{/ + }, { + begin: /\}\}/ + }, { + begin: '""' + }, s] + }, + d = e.inherit(c, { + illegal: /\n/, + contains: [{ + begin: /\{\{/ + }, { + begin: /\}\}/ + }, { + begin: '""' + }, o] + }); + s.contains = [c, l, i, e.APOS_STRING_MODE, e.QUOTE_STRING_MODE, a, e.C_BLOCK_COMMENT_MODE], + o.contains = [d, l, r, e.APOS_STRING_MODE, e.QUOTE_STRING_MODE, a, e.inherit(e.C_BLOCK_COMMENT_MODE, { + illegal: /\n/ + })]; + const g = { + variants: [c, l, i, e.APOS_STRING_MODE, e.QUOTE_STRING_MODE] + }, + u = { + begin: "<", + end: ">", + contains: [{ + beginKeywords: "in out" + }, t] + }, + b = e.IDENT_RE + "(<" + e.IDENT_RE + "(\\s*,\\s*" + e.IDENT_RE + ")*>)?(\\[\\])?", + m = { + begin: "@" + e.IDENT_RE, + relevance: 0 + }; + return { + name: "C#", + aliases: ["cs", "c#"], + keywords: n, + illegal: /::/, + contains: [e.COMMENT("///", "$", { + returnBegin: !0, + contains: [{ + className: "doctag", + variants: [{ + begin: "///", + relevance: 0 + }, { + begin: "\x3c!--|--\x3e" + }, { + begin: "" + }] + }] + }), e.C_LINE_COMMENT_MODE, e.C_BLOCK_COMMENT_MODE, { + className: "meta", + begin: "#", + end: "$", + keywords: { + keyword: "if else elif endif define undef warning error line region endregion pragma checksum" + } + }, g, a, { + beginKeywords: "class interface", + relevance: 0, + end: /[{;=]/, + illegal: /[^\s:,]/, + contains: [{ + beginKeywords: "where class" + }, t, u, e.C_LINE_COMMENT_MODE, e.C_BLOCK_COMMENT_MODE] + }, { + beginKeywords: "namespace", + relevance: 0, + end: /[{;=]/, + illegal: /[^\s:]/, + contains: [t, e.C_LINE_COMMENT_MODE, e.C_BLOCK_COMMENT_MODE] + }, { + beginKeywords: "record", + relevance: 0, + end: /[{;=]/, + illegal: /[^\s:]/, + contains: [t, u, e.C_LINE_COMMENT_MODE, e.C_BLOCK_COMMENT_MODE] + }, { + className: "meta", + begin: "^\\s*\\[(?=[\\w])", + excludeBegin: !0, + end: "\\]", + excludeEnd: !0, + contains: [{ + className: "string", + begin: /"/, + end: /"/ + }] + }, { + beginKeywords: "new return throw await else", + relevance: 0 + }, { + className: "function", + begin: "(" + b + "\\s+)+" + e.IDENT_RE + "\\s*(<[^=]+>\\s*)?\\(", + returnBegin: !0, + end: /\s*[{;=]/, + excludeEnd: !0, + keywords: n, + contains: [{ + beginKeywords: "public private protected static internal protected abstract async extern override unsafe virtual new sealed partial", + relevance: 0 + }, { + begin: e.IDENT_RE + "\\s*(<[^=]+>\\s*)?\\(", + returnBegin: !0, + contains: [e.TITLE_MODE, u], + relevance: 0 + }, { + match: /\(\)/ + }, { + className: "params", + begin: /\(/, + end: /\)/, + excludeBegin: !0, + excludeEnd: !0, + keywords: n, + relevance: 0, + contains: [g, a, e.C_BLOCK_COMMENT_MODE] + }, e.C_LINE_COMMENT_MODE, e.C_BLOCK_COMMENT_MODE] + }, m] + } + }, + grmr_css: e => { + const n = e.regex, + t = J(e), + a = [e.APOS_STRING_MODE, e.QUOTE_STRING_MODE]; + return { + name: "CSS", + case_insensitive: !0, + illegal: /[=|'\$]/, + keywords: { + keyframePosition: "from to" + }, + classNameAliases: { + keyframePosition: "selector-tag" + }, + contains: [t.BLOCK_COMMENT, { + begin: /-(webkit|moz|ms|o)-(?=[a-z])/ + }, t.CSS_NUMBER_MODE, { + className: "selector-id", + begin: /#[A-Za-z0-9_-]+/, + relevance: 0 + }, { + className: "selector-class", + begin: "\\.[a-zA-Z-][a-zA-Z0-9_-]*", + relevance: 0 + }, t.ATTRIBUTE_SELECTOR_MODE, { + className: "selector-pseudo", + variants: [{ + begin: ":(" + ne.join("|") + ")" + }, { + begin: ":(:)?(" + te.join("|") + ")" + }] + }, t.CSS_VARIABLE, { + className: "attribute", + begin: "\\b(" + ae.join("|") + ")\\b" + }, { + begin: /:/, + end: /[;}{]/, + contains: [t.BLOCK_COMMENT, t.HEXCOLOR, t.IMPORTANT, t.CSS_NUMBER_MODE, ...a, { + begin: /(url|data-uri)\(/, + end: /\)/, + relevance: 0, + keywords: { + built_in: "url data-uri" + }, + contains: [...a, { + className: "string", + begin: /[^)]/, + endsWithParent: !0, + excludeEnd: !0 + }] + }, t.FUNCTION_DISPATCH] + }, { + begin: n.lookahead(/@/), + end: "[{;]", + relevance: 0, + illegal: /:/, + contains: [{ + className: "keyword", + begin: /@-?\w[\w]*(-\w+)*/ + }, { + begin: /\s/, + endsWithParent: !0, + excludeEnd: !0, + relevance: 0, + keywords: { + $pattern: /[a-z-]+/, + keyword: "and or not only", + attribute: ee.join(" ") + }, + contains: [{ + begin: /[a-z-]+(?=:)/, + className: "attribute" + }, ...a, t.CSS_NUMBER_MODE] + }] + }, { + className: "selector-tag", + begin: "\\b(" + Y.join("|") + ")\\b" + }] + } + }, + grmr_diff: e => { + const n = e.regex; + return { + name: "Diff", + aliases: ["patch"], + contains: [{ + className: "meta", + relevance: 10, + match: n.either(/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/, /^\*\*\* +\d+,\d+ +\*\*\*\*$/, /^--- +\d+,\d+ +----$/) + }, { + className: "comment", + variants: [{ + begin: n.either(/Index: /, /^index/, /={3,}/, /^-{3}/, /^\*{3} /, /^\+{3}/, /^diff --git/), + end: /$/ + }, { + match: /^\*{15}$/ + }] + }, { + className: "addition", + begin: /^\+/, + end: /$/ + }, { + className: "deletion", + begin: /^-/, + end: /$/ + }, { + className: "addition", + begin: /^!/, + end: /$/ + }] + } + }, + grmr_go: e => { + const n = { + keyword: ["break", "case", "chan", "const", "continue", "default", "defer", "else", "fallthrough", "for", + "func", "go", "goto", "if", "import", "interface", "map", "package", "range", "return", "select", + "struct", "switch", "type", "var" + ], + type: ["bool", "byte", "complex64", "complex128", "error", "float32", "float64", "int8", "int16", "int32", + "int64", "string", "uint8", "uint16", "uint32", "uint64", "int", "uint", "uintptr", "rune" + ], + literal: ["true", "false", "iota", "nil"], + built_in: ["append", "cap", "close", "complex", "copy", "imag", "len", "make", "new", "panic", "print", + "println", "real", "recover", "delete" + ] + }; + return { + name: "Go", + aliases: ["golang"], + keywords: n, + illegal: " { + const n = e.regex; + return { + name: "GraphQL", + aliases: ["gql"], + case_insensitive: !0, + disableAutodetect: !1, + keywords: { + keyword: ["query", "mutation", "subscription", "type", "input", "schema", "directive", "interface", + "union", "scalar", "fragment", "enum", "on" + ], + literal: ["true", "false", "null"] + }, + contains: [e.HASH_COMMENT_MODE, e.QUOTE_STRING_MODE, e.NUMBER_MODE, { + scope: "punctuation", + match: /[.]{3}/, + relevance: 0 + }, { + scope: "punctuation", + begin: /[\!\(\)\:\=\[\]\{\|\}]{1}/, + relevance: 0 + }, { + scope: "variable", + begin: /\$/, + end: /\W/, + excludeEnd: !0, + relevance: 0 + }, { + scope: "meta", + match: /@\w+/, + excludeEnd: !0 + }, { + scope: "symbol", + begin: n.concat(/[_A-Za-z][_0-9A-Za-z]*/, n.lookahead(/\s*:/)), + relevance: 0 + }], + illegal: [/[;<']/, /BEGIN/] + } + }, + grmr_ini: e => { + const n = e.regex, + t = { + className: "number", + relevance: 0, + variants: [{ + begin: /([+-]+)?[\d]+_[\d_]+/ + }, { + begin: e.NUMBER_RE + }] + }, + a = e.COMMENT(); + a.variants = [{ + begin: /;/, + end: /$/ + }, { + begin: /#/, + end: /$/ + }]; + const i = { + className: "variable", + variants: [{ + begin: /\$[\w\d"][\w\d_]*/ + }, { + begin: /\$\{(.*?)\}/ + }] + }, + r = { + className: "literal", + begin: /\bon|off|true|false|yes|no\b/ + }, + s = { + className: "string", + contains: [e.BACKSLASH_ESCAPE], + variants: [{ + begin: "'''", + end: "'''", + relevance: 10 + }, { + begin: '"""', + end: '"""', + relevance: 10 + }, { + begin: '"', + end: '"' + }, { + begin: "'", + end: "'" + }] + }, + o = { + begin: /\[/, + end: /\]/, + contains: [a, r, i, s, t, "self"], + relevance: 0 + }, + l = n.either(/[A-Za-z0-9_-]+/, /"(\\"|[^"])*"/, /'[^']*'/); + return { + name: "TOML, also INI", + aliases: ["toml"], + case_insensitive: !0, + illegal: /\S/, + contains: [a, { + className: "section", + begin: /\[+/, + end: /\]+/ + }, { + begin: n.concat(l, "(\\s*\\.\\s*", l, ")*", n.lookahead(/\s*=\s*[^#\s]/)), + className: "attr", + starts: { + end: /$/, + contains: [a, o, r, i, s, t] + } + }] + } + }, + grmr_java: e => { + const n = e.regex, + t = "[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*", + a = t + le("(?:<" + t + "~~~(?:\\s*,\\s*" + t + "~~~)*>)?", /~~~/g, 2), + i = { + keyword: ["synchronized", "abstract", "private", "var", "static", "if", "const ", "for", "while", + "strictfp", "finally", "protected", "import", "native", "final", "void", "enum", "else", "break", + "transient", "catch", "instanceof", "volatile", "case", "assert", "package", "default", "public", + "try", "switch", "continue", "throws", "protected", "public", "private", "module", "requires", + "exports", "do", "sealed", "yield", "permits" + ], + literal: ["false", "true", "null"], + type: ["char", "boolean", "long", "float", "int", "byte", "short", "double"], + built_in: ["super", "this"] + }, + r = { + className: "meta", + begin: "@" + t, + contains: [{ + begin: /\(/, + end: /\)/, + contains: ["self"] + }] + }, + s = { + className: "params", + begin: /\(/, + end: /\)/, + keywords: i, + relevance: 0, + contains: [e.C_BLOCK_COMMENT_MODE], + endsParent: !0 + }; + return { + name: "Java", + aliases: ["jsp"], + keywords: i, + illegal: /<\/|#/, + contains: [e.COMMENT("/\\*\\*", "\\*/", { + relevance: 0, + contains: [{ + begin: /\w+@/, + relevance: 0 + }, { + className: "doctag", + begin: "@[A-Za-z]+" + }] + }), { + begin: /import java\.[a-z]+\./, + keywords: "import", + relevance: 2 + }, e.C_LINE_COMMENT_MODE, e.C_BLOCK_COMMENT_MODE, { + begin: /"""/, + end: /"""/, + className: "string", + contains: [e.BACKSLASH_ESCAPE] + }, e.APOS_STRING_MODE, e.QUOTE_STRING_MODE, { + match: [/\b(?:class|interface|enum|extends|implements|new)/, /\s+/, t], + className: { + 1: "keyword", + 3: "title.class" + } + }, { + match: /non-sealed/, + scope: "keyword" + }, { + begin: [n.concat(/(?!else)/, t), /\s+/, t, /\s+/, /=(?!=)/], + className: { + 1: "type", + 3: "variable", + 5: "operator" + } + }, { + begin: [/record/, /\s+/, t], + className: { + 1: "keyword", + 3: "title.class" + }, + contains: [s, e.C_LINE_COMMENT_MODE, e.C_BLOCK_COMMENT_MODE] + }, { + beginKeywords: "new throw return else", + relevance: 0 + }, { + begin: ["(?:" + a + "\\s+)", e.UNDERSCORE_IDENT_RE, /\s*(?=\()/], + className: { + 2: "title.function" + }, + keywords: i, + contains: [{ + className: "params", + begin: /\(/, + end: /\)/, + keywords: i, + relevance: 0, + contains: [r, e.APOS_STRING_MODE, e.QUOTE_STRING_MODE, oe, e.C_BLOCK_COMMENT_MODE] + }, e.C_LINE_COMMENT_MODE, e.C_BLOCK_COMMENT_MODE] + }, oe, r] + } + }, + grmr_javascript: he, + grmr_json: e => { + const n = ["true", "false", "null"], + t = { + scope: "literal", + beginKeywords: n.join(" ") + }; + return { + name: "JSON", + keywords: { + literal: n + }, + contains: [{ + className: "attr", + begin: /"(\\.|[^\\"\r\n])*"(?=\s*:)/, + relevance: 1.01 + }, { + match: /[{}[\],:]/, + className: "punctuation", + relevance: 0 + }, e.QUOTE_STRING_MODE, t, e.C_NUMBER_MODE, e.C_LINE_COMMENT_MODE, e.C_BLOCK_COMMENT_MODE], + illegal: "\\S" + } + }, + grmr_kotlin: e => { + const n = { + keyword: "abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual", + built_in: "Byte Short Char Int Long Boolean Float Double Void Unit Nothing", + literal: "true false null" + }, + t = { + className: "symbol", + begin: e.UNDERSCORE_IDENT_RE + "@" + }, + a = { + className: "subst", + begin: /\$\{/, + end: /\}/, + contains: [e.C_NUMBER_MODE] + }, + i = { + className: "variable", + begin: "\\$" + e.UNDERSCORE_IDENT_RE + }, + r = { + className: "string", + variants: [{ + begin: '"""', + end: '"""(?=[^"])', + contains: [i, a] + }, { + begin: "'", + end: "'", + illegal: /\n/, + contains: [e.BACKSLASH_ESCAPE] + }, { + begin: '"', + end: '"', + illegal: /\n/, + contains: [e.BACKSLASH_ESCAPE, i, a] + }] + }; + a.contains.push(r); + const s = { + className: "meta", + begin: "@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*" + e + .UNDERSCORE_IDENT_RE + ")?" + }, + o = { + className: "meta", + begin: "@" + e.UNDERSCORE_IDENT_RE, + contains: [{ + begin: /\(/, + end: /\)/, + contains: [e.inherit(r, { + className: "string" + }), "self"] + }] + }, + l = oe, + c = e.COMMENT("/\\*", "\\*/", { + contains: [e.C_BLOCK_COMMENT_MODE] + }), + d = { + variants: [{ + className: "type", + begin: e.UNDERSCORE_IDENT_RE + }, { + begin: /\(/, + end: /\)/, + contains: [] + }] + }, + g = d; + return g.variants[1].contains = [d], d.variants[1].contains = [g], { + name: "Kotlin", + aliases: ["kt", "kts"], + keywords: n, + contains: [e.COMMENT("/\\*\\*", "\\*/", { + relevance: 0, + contains: [{ + className: "doctag", + begin: "@[A-Za-z]+" + }] + }), e.C_LINE_COMMENT_MODE, c, { + className: "keyword", + begin: /\b(break|continue|return|this)\b/, + starts: { + contains: [{ + className: "symbol", + begin: /@\w+/ + }] + } + }, t, s, o, { + className: "function", + beginKeywords: "fun", + end: "[(]|$", + returnBegin: !0, + excludeEnd: !0, + keywords: n, + relevance: 5, + contains: [{ + begin: e.UNDERSCORE_IDENT_RE + "\\s*\\(", + returnBegin: !0, + relevance: 0, + contains: [e.UNDERSCORE_TITLE_MODE] + }, { + className: "type", + begin: //, + keywords: "reified", + relevance: 0 + }, { + className: "params", + begin: /\(/, + end: /\)/, + endsParent: !0, + keywords: n, + relevance: 0, + contains: [{ + begin: /:/, + end: /[=,\/]/, + endsWithParent: !0, + contains: [d, e.C_LINE_COMMENT_MODE, c], + relevance: 0 + }, e.C_LINE_COMMENT_MODE, c, s, o, r, e.C_NUMBER_MODE] + }, c] + }, { + begin: [/class|interface|trait/, /\s+/, e.UNDERSCORE_IDENT_RE], + beginScope: { + 3: "title.class" + }, + keywords: "class interface trait", + end: /[:\{(]|$/, + excludeEnd: !0, + illegal: "extends implements", + contains: [{ + beginKeywords: "public protected internal private constructor" + }, e.UNDERSCORE_TITLE_MODE, { + className: "type", + begin: //, + excludeBegin: !0, + excludeEnd: !0, + relevance: 0 + }, { + className: "type", + begin: /[,:]\s*/, + end: /[<\(,){\s]|$/, + excludeBegin: !0, + returnEnd: !0 + }, s, o] + }, r, { + className: "meta", + begin: "^#!/usr/bin/env", + end: "$", + illegal: "\n" + }, l] + } + }, + grmr_less: e => { + const n = J(e), + t = ie, + a = "([\\w-]+|@\\{[\\w-]+\\})", + i = [], + r = [], + s = e => ({ + className: "string", + begin: "~?" + e + ".*?" + e + }), + o = (e, n, t) => ({ + className: e, + begin: n, + relevance: t + }), + l = { + $pattern: /[a-z-]+/, + keyword: "and or not only", + attribute: ee.join(" ") + }, + c = { + begin: "\\(", + end: "\\)", + contains: r, + keywords: l, + relevance: 0 + }; + r.push(e.C_LINE_COMMENT_MODE, e.C_BLOCK_COMMENT_MODE, s("'"), s('"'), n.CSS_NUMBER_MODE, { + begin: "(url|data-uri)\\(", + starts: { + className: "string", + end: "[\\)\\n]", + excludeEnd: !0 + } + }, n.HEXCOLOR, c, o("variable", "@@?[\\w-]+", 10), o("variable", "@\\{[\\w-]+\\}"), o("built_in", + "~?`[^`]*?`"), { + className: "attribute", + begin: "[\\w-]+\\s*:", + end: ":", + returnBegin: !0, + excludeEnd: !0 + }, n.IMPORTANT, { + beginKeywords: "and not" + }, n.FUNCTION_DISPATCH); + const d = r.concat({ + begin: /\{/, + end: /\}/, + contains: i + }), + g = { + beginKeywords: "when", + endsWithParent: !0, + contains: [{ + beginKeywords: "and not" + }].concat(r) + }, + u = { + begin: a + "\\s*:", + returnBegin: !0, + end: /[;}]/, + relevance: 0, + contains: [{ + begin: /-(webkit|moz|ms|o)-/ + }, n.CSS_VARIABLE, { + className: "attribute", + begin: "\\b(" + ae.join("|") + ")\\b", + end: /(?=:)/, + starts: { + endsWithParent: !0, + illegal: "[<=$]", + relevance: 0, + contains: r + } + }] + }, + b = { + className: "keyword", + begin: "@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b", + starts: { + end: "[;{}]", + keywords: l, + returnEnd: !0, + contains: r, + relevance: 0 + } + }, + m = { + className: "variable", + variants: [{ + begin: "@[\\w-]+\\s*:", + relevance: 15 + }, { + begin: "@[\\w-]+" + }], + starts: { + end: "[;}]", + returnEnd: !0, + contains: d + } + }, + p = { + variants: [{ + begin: "[\\.#:&\\[>]", + end: "[;{}]" + }, { + begin: a, + end: /\{/ + }], + returnBegin: !0, + returnEnd: !0, + illegal: "[<='$\"]", + relevance: 0, + contains: [e.C_LINE_COMMENT_MODE, e.C_BLOCK_COMMENT_MODE, g, o("keyword", "all\\b"), o("variable", + "@\\{[\\w-]+\\}"), { + begin: "\\b(" + Y.join("|") + ")\\b", + className: "selector-tag" + }, n.CSS_NUMBER_MODE, o("selector-tag", a, 0), o("selector-id", "#" + a), o("selector-class", "\\." + + a, 0), o("selector-tag", "&", 0), n.ATTRIBUTE_SELECTOR_MODE, { + className: "selector-pseudo", + begin: ":(" + ne.join("|") + ")" + }, { + className: "selector-pseudo", + begin: ":(:)?(" + te.join("|") + ")" + }, { + begin: /\(/, + end: /\)/, + relevance: 0, + contains: d + }, { + begin: "!important" + }, n.FUNCTION_DISPATCH] + }, + _ = { + begin: `[\\w-]+:(:)?(${t.join("|")})`, + returnBegin: !0, + contains: [p] + }; + return i.push(e.C_LINE_COMMENT_MODE, e.C_BLOCK_COMMENT_MODE, b, m, _, u, p, g, n.FUNCTION_DISPATCH), { + name: "Less", + case_insensitive: !0, + illegal: "[=>'/<($\"]", + contains: i + } + }, + grmr_lua: e => { + const n = "\\[=*\\[", + t = "\\]=*\\]", + a = { + begin: n, + end: t, + contains: ["self"] + }, + i = [e.COMMENT("--(?!\\[=*\\[)", "$"), e.COMMENT("--\\[=*\\[", t, { + contains: [a], + relevance: 10 + })]; + return { + name: "Lua", + keywords: { + $pattern: e.UNDERSCORE_IDENT_RE, + literal: "true false nil", + keyword: "and break do else elseif end for goto if in local not or repeat return then until while", + built_in: "_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove" + }, + contains: i.concat([{ + className: "function", + beginKeywords: "function", + end: "\\)", + contains: [e.inherit(e.TITLE_MODE, { + begin: "([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*" + }), { + className: "params", + begin: "\\(", + endsWithParent: !0, + contains: i + }].concat(i) + }, e.C_NUMBER_MODE, e.APOS_STRING_MODE, e.QUOTE_STRING_MODE, { + className: "string", + begin: n, + end: t, + contains: [a], + relevance: 5 + }]) + } + }, + grmr_makefile: e => { + const n = { + className: "variable", + variants: [{ + begin: "\\$\\(" + e.UNDERSCORE_IDENT_RE + "\\)", + contains: [e.BACKSLASH_ESCAPE] + }, { + begin: /\$[@% { + const n = e.regex, + t = n.concat( + /(?:[A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD23\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF1C\uDF27\uDF30-\uDF45\uDF70-\uDF81\uDFB0-\uDFC4\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE3F\uDE40\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDEB8\uDF00-\uDF1A\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCDF\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDEE0-\uDEF2\uDF02\uDF04-\uDF10\uDF12-\uDF33\uDFB0]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883\uD885-\uD887][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2F\uDC41-\uDC46]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE70-\uDEBE\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD32\uDD50-\uDD52\uDD55\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD837[\uDF00-\uDF1E\uDF25-\uDF2A]|\uD838[\uDC30-\uDC6D\uDD00-\uDD2C\uDD37-\uDD3D\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB]|\uD839[\uDCD0-\uDCEB\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43\uDD4B]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF39\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A\uDF50-\uDFFF]|\uD888[\uDC00-\uDFAF])/, + n.optional( + /(?:[\x2D\.0-9A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD23\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF1C\uDF27\uDF30-\uDF45\uDF70-\uDF81\uDFB0-\uDFC4\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE3F\uDE40\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDEB8\uDF00-\uDF1A\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCDF\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDEE0-\uDEF2\uDF02\uDF04-\uDF10\uDF12-\uDF33\uDFB0]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883\uD885-\uD887][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2F\uDC41-\uDC46]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE70-\uDEBE\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD32\uDD50-\uDD52\uDD55\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD837[\uDF00-\uDF1E\uDF25-\uDF2A]|\uD838[\uDC30-\uDC6D\uDD00-\uDD2C\uDD37-\uDD3D\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB]|\uD839[\uDCD0-\uDCEB\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43\uDD4B]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF39\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A\uDF50-\uDFFF]|\uD888[\uDC00-\uDFAF])*:/ + ), + /(?:[\x2D\.0-9A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD23\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF1C\uDF27\uDF30-\uDF45\uDF70-\uDF81\uDFB0-\uDFC4\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE3F\uDE40\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDEB8\uDF00-\uDF1A\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCDF\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDEE0-\uDEF2\uDF02\uDF04-\uDF10\uDF12-\uDF33\uDFB0]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883\uD885-\uD887][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2F\uDC41-\uDC46]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE70-\uDEBE\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD32\uDD50-\uDD52\uDD55\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD837[\uDF00-\uDF1E\uDF25-\uDF2A]|\uD838[\uDC30-\uDC6D\uDD00-\uDD2C\uDD37-\uDD3D\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB]|\uD839[\uDCD0-\uDCEB\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43\uDD4B]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF39\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A\uDF50-\uDFFF]|\uD888[\uDC00-\uDFAF])*/ + ), + a = { + className: "symbol", + begin: /&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/ + }, + i = { + begin: /\s/, + contains: [{ + className: "keyword", + begin: /#?[a-z_][a-z1-9_-]+/, + illegal: /\n/ + }] + }, + r = e.inherit(i, { + begin: /\(/, + end: /\)/ + }), + s = e.inherit(e.APOS_STRING_MODE, { + className: "string" + }), + o = e.inherit(e.QUOTE_STRING_MODE, { + className: "string" + }), + l = { + endsWithParent: !0, + illegal: /`]+/ + }] + }] + }] + }; + return { + name: "HTML, XML", + aliases: ["html", "xhtml", "rss", "atom", "xjb", "xsd", "xsl", "plist", "wsf", "svg"], + case_insensitive: !0, + unicodeRegex: !0, + contains: [{ + className: "meta", + begin: //, + relevance: 10, + contains: [i, o, s, r, { + begin: /\[/, + end: /\]/, + contains: [{ + className: "meta", + begin: //, + contains: [i, r, o, s] + }] + }] + }, e.COMMENT(//, { + relevance: 10 + }), { + begin: //, + relevance: 10 + }, a, { + className: "meta", + end: /\?>/, + variants: [{ + begin: /<\?xml/, + relevance: 10, + contains: [o] + }, { + begin: /<\?[a-z][a-z0-9]+/ + }] + }, { + className: "tag", + begin: /)/, + end: />/, + keywords: { + name: "style" + }, + contains: [l], + starts: { + end: /<\/style>/, + returnEnd: !0, + subLanguage: ["css", "xml"] + } + }, { + className: "tag", + begin: /)/, + end: />/, + keywords: { + name: "script" + }, + contains: [l], + starts: { + end: /<\/script>/, + returnEnd: !0, + subLanguage: ["javascript", "handlebars", "xml"] + } + }, { + className: "tag", + begin: /<>|<\/>/ + }, { + className: "tag", + begin: n.concat(//, />/, /\s/)))), + end: /\/?>/, + contains: [{ + className: "name", + begin: t, + relevance: 0, + starts: l + }] + }, { + className: "tag", + begin: n.concat(/<\//, n.lookahead(n.concat(t, />/))), + contains: [{ + className: "name", + begin: t, + relevance: 0 + }, { + begin: />/, + relevance: 0, + endsParent: !0 + }] + }] + } + }, + grmr_markdown: e => { + const n = { + begin: /<\/?[A-Za-z_]/, + end: ">", + subLanguage: "xml", + relevance: 0 + }, + t = { + variants: [{ + begin: /\[.+?\]\[.*?\]/, + relevance: 0 + }, { + begin: /\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/, + relevance: 2 + }, { + begin: e.regex.concat(/\[.+?\]\(/, /[A-Za-z][A-Za-z0-9+.-]*/, /:\/\/.*?\)/), + relevance: 2 + }, { + begin: /\[.+?\]\([./?&#].*?\)/, + relevance: 1 + }, { + begin: /\[.*?\]\(.*?\)/, + relevance: 0 + }], + returnBegin: !0, + contains: [{ + match: /\[(?=\])/ + }, { + className: "string", + relevance: 0, + begin: "\\[", + end: "\\]", + excludeBegin: !0, + returnEnd: !0 + }, { + className: "link", + relevance: 0, + begin: "\\]\\(", + end: "\\)", + excludeBegin: !0, + excludeEnd: !0 + }, { + className: "symbol", + relevance: 0, + begin: "\\]\\[", + end: "\\]", + excludeBegin: !0, + excludeEnd: !0 + }] + }, + a = { + className: "strong", + contains: [], + variants: [{ + begin: /_{2}(?!\s)/, + end: /_{2}/ + }, { + begin: /\*{2}(?!\s)/, + end: /\*{2}/ + }] + }, + i = { + className: "emphasis", + contains: [], + variants: [{ + begin: /\*(?![*\s])/, + end: /\*/ + }, { + begin: /_(?![_\s])/, + end: /_/, + relevance: 0 + }] + }, + r = e.inherit(a, { + contains: [] + }), + s = e.inherit(i, { + contains: [] + }); + a.contains.push(s), i.contains.push(r); + let o = [n, t]; + return [a, i, r, s].forEach((e => { + e.contains = e.contains.concat(o) + })), o = o.concat(a, i), { + name: "Markdown", + aliases: ["md", "mkdown", "mkd"], + contains: [{ + className: "section", + variants: [{ + begin: "^#{1,6}", + end: "$", + contains: o + }, { + begin: "(?=^.+?\\n[=-]{2,}$)", + contains: [{ + begin: "^[=-]*$" + }, { + begin: "^", + end: "\\n", + contains: o + }] + }] + }, n, { + className: "bullet", + begin: "^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)", + end: "\\s+", + excludeEnd: !0 + }, a, i, { + className: "quote", + begin: "^>\\s+", + contains: o, + end: "$" + }, { + className: "code", + variants: [{ + begin: "(`{3,})[^`](.|\\n)*?\\1`*[ ]*" + }, { + begin: "(~{3,})[^~](.|\\n)*?\\1~*[ ]*" + }, { + begin: "```", + end: "```+[ ]*$" + }, { + begin: "~~~", + end: "~~~+[ ]*$" + }, { + begin: "`.+?`" + }, { + begin: "(?=^( {4}|\\t))", + contains: [{ + begin: "^( {4}|\\t)", + end: "(\\n)$" + }], + relevance: 0 + }] + }, { + begin: "^[-\\*]{3,}", + end: "$" + }, t, { + begin: /^\[[^\n]+\]:/, + returnBegin: !0, + contains: [{ + className: "symbol", + begin: /\[/, + end: /\]/, + excludeBegin: !0, + excludeEnd: !0 + }, { + className: "link", + begin: /:\s*/, + end: /$/, + excludeBegin: !0 + }] + }] + } + }, + grmr_objectivec: e => { + const n = /[a-zA-Z@][a-zA-Z0-9_]*/, + t = { + $pattern: n, + keyword: ["@interface", "@class", "@protocol", "@implementation"] + }; + return { + name: "Objective-C", + aliases: ["mm", "objc", "obj-c", "obj-c++", "objective-c++"], + keywords: { + "variable.language": ["this", "super"], + $pattern: n, + keyword: ["while", "export", "sizeof", "typedef", "const", "struct", "for", "union", "volatile", "static", + "mutable", "if", "do", "return", "goto", "enum", "else", "break", "extern", "asm", "case", "default", + "register", "explicit", "typename", "switch", "continue", "inline", "readonly", "assign", "readwrite", + "self", "@synchronized", "id", "typeof", "nonatomic", "IBOutlet", "IBAction", "strong", "weak", + "copy", "in", "out", "inout", "bycopy", "byref", "oneway", "__strong", "__weak", "__block", + "__autoreleasing", "@private", "@protected", "@public", "@try", "@property", "@end", "@throw", + "@catch", "@finally", "@autoreleasepool", "@synthesize", "@dynamic", "@selector", "@optional", + "@required", "@encode", "@package", "@import", "@defs", "@compatibility_alias", "__bridge", + "__bridge_transfer", "__bridge_retained", "__bridge_retain", "__covariant", "__contravariant", + "__kindof", "_Nonnull", "_Nullable", "_Null_unspecified", "__FUNCTION__", "__PRETTY_FUNCTION__", + "__attribute__", "getter", "setter", "retain", "unsafe_unretained", "nonnull", "nullable", + "null_unspecified", "null_resettable", "class", "instancetype", "NS_DESIGNATED_INITIALIZER", + "NS_UNAVAILABLE", "NS_REQUIRES_SUPER", "NS_RETURNS_INNER_POINTER", "NS_INLINE", "NS_AVAILABLE", + "NS_DEPRECATED", "NS_ENUM", "NS_OPTIONS", "NS_SWIFT_UNAVAILABLE", "NS_ASSUME_NONNULL_BEGIN", + "NS_ASSUME_NONNULL_END", "NS_REFINED_FOR_SWIFT", "NS_SWIFT_NAME", "NS_SWIFT_NOTHROW", "NS_DURING", + "NS_HANDLER", "NS_ENDHANDLER", "NS_VALUERETURN", "NS_VOIDRETURN" + ], + literal: ["false", "true", "FALSE", "TRUE", "nil", "YES", "NO", "NULL"], + built_in: ["dispatch_once_t", "dispatch_queue_t", "dispatch_sync", "dispatch_async", "dispatch_once"], + type: ["int", "float", "char", "unsigned", "signed", "short", "long", "double", "wchar_t", "unichar", + "void", "bool", "BOOL", "id|0", "_Bool" + ] + }, + illegal: "/, + end: /$/, + illegal: "\\n" + }, e.C_LINE_COMMENT_MODE, e.C_BLOCK_COMMENT_MODE] + }, { + className: "class", + begin: "(" + t.keyword.join("|") + ")\\b", + end: /(\{|$)/, + excludeEnd: !0, + keywords: t, + contains: [e.UNDERSCORE_TITLE_MODE] + }, { + begin: "\\." + e.UNDERSCORE_IDENT_RE, + relevance: 0 + } + ] + } + }, + grmr_perl: e => { + const n = e.regex, + t = /[dualxmsipngr]{0,12}/, + a = { + $pattern: /[\w.]+/, + keyword: "abs accept alarm and atan2 bind binmode bless break caller chdir chmod chomp chop chown chr chroot close closedir connect continue cos crypt dbmclose dbmopen defined delete die do dump each else elsif endgrent endhostent endnetent endprotoent endpwent endservent eof eval exec exists exit exp fcntl fileno flock for foreach fork format formline getc getgrent getgrgid getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr getnetbyname getnetent getpeername getpgrp getpriority getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid getservbyname getservbyport getservent getsockname getsockopt given glob gmtime goto grep gt hex if index int ioctl join keys kill last lc lcfirst length link listen local localtime log lstat lt ma map mkdir msgctl msgget msgrcv msgsnd my ne next no not oct open opendir or ord our pack package pipe pop pos print printf prototype push q|0 qq quotemeta qw qx rand read readdir readline readlink readpipe recv redo ref rename require reset return reverse rewinddir rindex rmdir say scalar seek seekdir select semctl semget semop send setgrent sethostent setnetent setpgrp setpriority setprotoent setpwent setservent setsockopt shift shmctl shmget shmread shmwrite shutdown sin sleep socket socketpair sort splice split sprintf sqrt srand stat state study sub substr symlink syscall sysopen sysread sysseek system syswrite tell telldir tie tied time times tr truncate uc ucfirst umask undef unless unlink unpack unshift untie until use utime values vec wait waitpid wantarray warn when while write x|0 xor y|0" + }, + i = { + className: "subst", + begin: "[$@]\\{", + end: "\\}", + keywords: a + }, + r = { + begin: /->\{/, + end: /\}/ + }, + s = { + variants: [{ + begin: /\$\d/ + }, { + begin: n.concat(/[$%@](\^\w\b|#\w+(::\w+)*|\{\w+\}|\w+(::\w*)*)/, "(?![A-Za-z])(?![@$%])") + }, { + begin: /[$%@][^\s\w{]/, + relevance: 0 + }] + }, + o = [e.BACKSLASH_ESCAPE, i, s], + l = [/!/, /\//, /\|/, /\?/, /'/, /"/, /#/], + c = (e, a, i = "\\1") => { + const r = "\\1" === i ? i : n.concat(i, a); + return n.concat(n.concat("(?:", e, ")"), a, /(?:\\.|[^\\\/])*?/, r, /(?:\\.|[^\\\/])*?/, i, t) + }, + d = (e, a, i) => n.concat(n.concat("(?:", e, ")"), a, /(?:\\.|[^\\\/])*?/, i, t), + g = [s, e.HASH_COMMENT_MODE, e.COMMENT(/^=\w/, /=cut/, { + endsWithParent: !0 + }), r, { + className: "string", + contains: o, + variants: [{ + begin: "q[qwxr]?\\s*\\(", + end: "\\)", + relevance: 5 + }, { + begin: "q[qwxr]?\\s*\\[", + end: "\\]", + relevance: 5 + }, { + begin: "q[qwxr]?\\s*\\{", + end: "\\}", + relevance: 5 + }, { + begin: "q[qwxr]?\\s*\\|", + end: "\\|", + relevance: 5 + }, { + begin: "q[qwxr]?\\s*<", + end: ">", + relevance: 5 + }, { + begin: "qw\\s+q", + end: "q", + relevance: 5 + }, { + begin: "'", + end: "'", + contains: [e.BACKSLASH_ESCAPE] + }, { + begin: '"', + end: '"' + }, { + begin: "`", + end: "`", + contains: [e.BACKSLASH_ESCAPE] + }, { + begin: /\{\w+\}/, + relevance: 0 + }, { + begin: "-?\\w+\\s*=>", + relevance: 0 + }] + }, { + className: "number", + begin: "(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b", + relevance: 0 + }, { + begin: "(\\/\\/|" + e.RE_STARTERS_RE + "|\\b(split|return|print|reverse|grep)\\b)\\s*", + keywords: "split return print reverse grep", + relevance: 0, + contains: [e.HASH_COMMENT_MODE, { + className: "regexp", + variants: [{ + begin: c("s|tr|y", n.either(...l, { + capture: !0 + })) + }, { + begin: c("s|tr|y", "\\(", "\\)") + }, { + begin: c("s|tr|y", "\\[", "\\]") + }, { + begin: c("s|tr|y", "\\{", "\\}") + }], + relevance: 2 + }, { + className: "regexp", + variants: [{ + begin: /(m|qr)\/\//, + relevance: 0 + }, { + begin: d("(?:m|qr)?", /\//, /\//) + }, { + begin: d("m|qr", n.either(...l, { + capture: !0 + }), /\1/) + }, { + begin: d("m|qr", /\(/, /\)/) + }, { + begin: d("m|qr", /\[/, /\]/) + }, { + begin: d("m|qr", /\{/, /\}/) + }] + }] + }, { + className: "function", + beginKeywords: "sub", + end: "(\\s*\\(.*?\\))?[;{]", + excludeEnd: !0, + relevance: 5, + contains: [e.TITLE_MODE] + }, { + begin: "-\\w\\b", + relevance: 0 + }, { + begin: "^__DATA__$", + end: "^__END__$", + subLanguage: "mojolicious", + contains: [{ + begin: "^@@.*", + end: "$", + className: "comment" + }] + }]; + return i.contains = g, r.contains = g, { + name: "Perl", + aliases: ["pl", "pm"], + keywords: a, + contains: g + } + }, + grmr_php: e => { + const n = e.regex, + t = /(?![A-Za-z0-9])(?![$])/, + a = n.concat(/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/, t), + i = n.concat(/(\\?[A-Z][a-z0-9_\x7f-\xff]+|\\?[A-Z]+(?=[A-Z][a-z0-9_\x7f-\xff])){1,}/, t), + r = { + scope: "variable", + match: "\\$+" + a + }, + s = { + scope: "subst", + variants: [{ + begin: /\$\w+/ + }, { + begin: /\{\$/, + end: /\}/ + }] + }, + o = e.inherit(e.APOS_STRING_MODE, { + illegal: null + }), + l = "[ \t\n]", + c = { + scope: "string", + variants: [e.inherit(e.QUOTE_STRING_MODE, { + illegal: null, + contains: e.QUOTE_STRING_MODE.contains.concat(s) + }), o, e.END_SAME_AS_BEGIN({ + begin: /<<<[ \t]*(\w+)\n/, + end: /[ \t]*(\w+)\b/, + contains: e.QUOTE_STRING_MODE.contains.concat(s) + })] + }, + d = { + scope: "number", + variants: [{ + begin: "\\b0[bB][01]+(?:_[01]+)*\\b" + }, { + begin: "\\b0[oO][0-7]+(?:_[0-7]+)*\\b" + }, { + begin: "\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b" + }, { + begin: "(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?" + }], + relevance: 0 + }, + g = ["false", "null", "true"], + u = ["__CLASS__", "__DIR__", "__FILE__", "__FUNCTION__", "__COMPILER_HALT_OFFSET__", "__LINE__", + "__METHOD__", "__NAMESPACE__", "__TRAIT__", "die", "echo", "exit", "include", "include_once", "print", + "require", "require_once", "array", "abstract", "and", "as", "binary", "bool", "boolean", "break", + "callable", "case", "catch", "class", "clone", "const", "continue", "declare", "default", "do", "double", + "else", "elseif", "empty", "enddeclare", "endfor", "endforeach", "endif", "endswitch", "endwhile", "enum", + "eval", "extends", "final", "finally", "float", "for", "foreach", "from", "global", "goto", "if", + "implements", "instanceof", "insteadof", "int", "integer", "interface", "isset", "iterable", "list", + "match|0", "mixed", "new", "never", "object", "or", "private", "protected", "public", "readonly", "real", + "return", "string", "switch", "throw", "trait", "try", "unset", "use", "var", "void", "while", "xor", + "yield" + ], + b = ["Error|0", "AppendIterator", "ArgumentCountError", "ArithmeticError", "ArrayIterator", "ArrayObject", + "AssertionError", "BadFunctionCallException", "BadMethodCallException", "CachingIterator", + "CallbackFilterIterator", "CompileError", "Countable", "DirectoryIterator", "DivisionByZeroError", + "DomainException", "EmptyIterator", "ErrorException", "Exception", "FilesystemIterator", "FilterIterator", + "GlobIterator", "InfiniteIterator", "InvalidArgumentException", "IteratorIterator", "LengthException", + "LimitIterator", "LogicException", "MultipleIterator", "NoRewindIterator", "OutOfBoundsException", + "OutOfRangeException", "OuterIterator", "OverflowException", "ParentIterator", "ParseError", + "RangeException", "RecursiveArrayIterator", "RecursiveCachingIterator", "RecursiveCallbackFilterIterator", + "RecursiveDirectoryIterator", "RecursiveFilterIterator", "RecursiveIterator", "RecursiveIteratorIterator", + "RecursiveRegexIterator", "RecursiveTreeIterator", "RegexIterator", "RuntimeException", + "SeekableIterator", "SplDoublyLinkedList", "SplFileInfo", "SplFileObject", "SplFixedArray", "SplHeap", + "SplMaxHeap", "SplMinHeap", "SplObjectStorage", "SplObserver", "SplPriorityQueue", "SplQueue", "SplStack", + "SplSubject", "SplTempFileObject", "TypeError", "UnderflowException", "UnexpectedValueException", + "UnhandledMatchError", "ArrayAccess", "BackedEnum", "Closure", "Fiber", "Generator", "Iterator", + "IteratorAggregate", "Serializable", "Stringable", "Throwable", "Traversable", "UnitEnum", + "WeakReference", "WeakMap", "Directory", "__PHP_Incomplete_Class", "parent", "php_user_filter", "self", + "static", "stdClass" + ], + m = { + keyword: u, + literal: (e => { + const n = []; + return e.forEach((e => { + n.push(e), e.toLowerCase() === e ? n.push(e.toUpperCase()) : n.push(e.toLowerCase()) + })), n + })(g), + built_in: b + }, + p = e => e.map((e => e.replace(/\|\d+$/, ""))), + _ = { + variants: [{ + match: [/new/, n.concat(l, "+"), n.concat("(?!", p(b).join("\\b|"), "\\b)"), i], + scope: { + 1: "keyword", + 4: "title.class" + } + }] + }, + h = n.concat(a, "\\b(?!\\()"), + f = { + variants: [{ + match: [n.concat(/::/, n.lookahead(/(?!class\b)/)), h], + scope: { + 2: "variable.constant" + } + }, { + match: [/::/, /class/], + scope: { + 2: "variable.language" + } + }, { + match: [i, n.concat(/::/, n.lookahead(/(?!class\b)/)), h], + scope: { + 1: "title.class", + 3: "variable.constant" + } + }, { + match: [i, n.concat("::", n.lookahead(/(?!class\b)/))], + scope: { + 1: "title.class" + } + }, { + match: [i, /::/, /class/], + scope: { + 1: "title.class", + 3: "variable.language" + } + }] + }, + E = { + scope: "attr", + match: n.concat(a, n.lookahead(":"), n.lookahead(/(?!::)/)) + }, + y = { + relevance: 0, + begin: /\(/, + end: /\)/, + keywords: m, + contains: [E, r, f, e.C_BLOCK_COMMENT_MODE, c, d, _] + }, + w = { + relevance: 0, + match: [/\b/, n.concat("(?!fn\\b|function\\b|", p(u).join("\\b|"), "|", p(b).join("\\b|"), "\\b)"), a, n + .concat(l, "*"), n.lookahead(/(?=\()/) + ], + scope: { + 3: "title.function.invoke" + }, + contains: [y] + }; + y.contains.push(w); + const N = [E, f, e.C_BLOCK_COMMENT_MODE, c, d, _]; + return { + case_insensitive: !1, + keywords: m, + contains: [{ + begin: n.concat(/#\[\s*/, i), + beginScope: "meta", + end: /]/, + endScope: "meta", + keywords: { + literal: g, + keyword: ["new", "array"] + }, + contains: [{ + begin: /\[/, + end: /]/, + keywords: { + literal: g, + keyword: ["new", "array"] + }, + contains: ["self", ...N] + }, ...N, { + scope: "meta", + match: i + }] + }, e.HASH_COMMENT_MODE, e.COMMENT("//", "$"), e.COMMENT("/\\*", "\\*/", { + contains: [{ + scope: "doctag", + match: "@[A-Za-z]+" + }] + }), { + match: /__halt_compiler\(\);/, + keywords: "__halt_compiler", + starts: { + scope: "comment", + end: e.MATCH_NOTHING_RE, + contains: [{ + match: /\?>/, + scope: "meta", + endsParent: !0 + }] + } + }, { + scope: "meta", + variants: [{ + begin: /<\?php/, + relevance: 10 + }, { + begin: /<\?=/ + }, { + begin: /<\?/, + relevance: .1 + }, { + begin: /\?>/ + }] + }, { + scope: "variable.language", + match: /\$this\b/ + }, r, w, f, { + match: [/const/, /\s/, a], + scope: { + 1: "keyword", + 3: "variable.constant" + } + }, _, { + scope: "function", + relevance: 0, + beginKeywords: "fn function", + end: /[;{]/, + excludeEnd: !0, + illegal: "[$%\\[]", + contains: [{ + beginKeywords: "use" + }, e.UNDERSCORE_TITLE_MODE, { + begin: "=>", + endsParent: !0 + }, { + scope: "params", + begin: "\\(", + end: "\\)", + excludeBegin: !0, + excludeEnd: !0, + keywords: m, + contains: ["self", r, f, e.C_BLOCK_COMMENT_MODE, c, d] + }] + }, { + scope: "class", + variants: [{ + beginKeywords: "enum", + illegal: /[($"]/ + }, { + beginKeywords: "class interface trait", + illegal: /[:($"]/ + }], + relevance: 0, + end: /\{/, + excludeEnd: !0, + contains: [{ + beginKeywords: "extends implements" + }, e.UNDERSCORE_TITLE_MODE] + }, { + beginKeywords: "namespace", + relevance: 0, + end: ";", + illegal: /[.']/, + contains: [e.inherit(e.UNDERSCORE_TITLE_MODE, { + scope: "title.class" + })] + }, { + beginKeywords: "use", + relevance: 0, + end: ";", + contains: [{ + match: /\b(as|const|function)\b/, + scope: "keyword" + }, e.UNDERSCORE_TITLE_MODE] + }, c, d] + } + }, + grmr_php_template: e => ({ + name: "PHP template", + subLanguage: "xml", + contains: [{ + begin: /<\?(php|=)?/, + end: /\?>/, + subLanguage: "php", + contains: [{ + begin: "/\\*", + end: "\\*/", + skip: !0 + }, { + begin: 'b"', + end: '"', + skip: !0 + }, { + begin: "b'", + end: "'", + skip: !0 + }, e.inherit(e.APOS_STRING_MODE, { + illegal: null, + className: null, + contains: null, + skip: !0 + }), e.inherit(e.QUOTE_STRING_MODE, { + illegal: null, + className: null, + contains: null, + skip: !0 + })] + }] + }), + grmr_plaintext: e => ({ + name: "Plain text", + aliases: ["text", "txt"], + disableAutodetect: !0 + }), + grmr_python: e => { + const n = e.regex, + t = /(?:[A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037B-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFC5D\uFC64-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDF9\uFE71\uFE73\uFE77\uFE79\uFE7B\uFE7D\uFE7F-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFF9D\uFFA0-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD23\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF1C\uDF27\uDF30-\uDF45\uDF70-\uDF81\uDFB0-\uDFC4\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE3F\uDE40\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDEB8\uDF00-\uDF1A\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCDF\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDEE0-\uDEF2\uDF02\uDF04-\uDF10\uDF12-\uDF33\uDFB0]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883\uD885-\uD887][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2F\uDC41-\uDC46]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE70-\uDEBE\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD32\uDD50-\uDD52\uDD55\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD837[\uDF00-\uDF1E\uDF25-\uDF2A]|\uD838[\uDC30-\uDC6D\uDD00-\uDD2C\uDD37-\uDD3D\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB]|\uD839[\uDCD0-\uDCEB\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43\uDD4B]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF39\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A\uDF50-\uDFFF]|\uD888[\uDC00-\uDFAF])(?:[0-9A-Z_a-z\xAA\xB5\xB7\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037B-\u037D\u037F\u0386-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05EF-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u07FD\u0800-\u082D\u0840-\u085B\u0860-\u086A\u0870-\u0887\u0889-\u088E\u0898-\u08E1\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u09FC\u09FE\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9-\u0AFF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B55-\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3C-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C5D\u0C60-\u0C63\u0C66-\u0C6F\u0C80-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDD\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1-\u0CF3\u0D00-\u0D0C\u0D0E-\u0D10\u0D12-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D54-\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D81-\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECE\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1369-\u1371\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u1715\u171F-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u180F-\u1819\u1820-\u1878\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1ABF-\u1ACE\u1B00-\u1B4C\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CD0-\u1CD2\u1CD4-\u1CFA\u1D00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA827\uA82C\uA840-\uA873\uA880-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFC5D\uFC64-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDF9\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE71\uFE73\uFE77\uFE79\uFE7B\uFE7D\uFE7F-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD27\uDD30-\uDD39\uDE80-\uDEA9\uDEAB\uDEAC\uDEB0\uDEB1\uDEFD-\uDF1C\uDF27\uDF30-\uDF50\uDF70-\uDF85\uDFB0-\uDFC4\uDFE0-\uDFF6]|\uD804[\uDC00-\uDC46\uDC66-\uDC75\uDC7F-\uDCBA\uDCC2\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD44-\uDD47\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDC9-\uDDCC\uDDCE-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE3E-\uDE41\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3B-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC00-\uDC4A\uDC50-\uDC59\uDC5E-\uDC61\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB8\uDEC0-\uDEC9\uDF00-\uDF1A\uDF1D-\uDF2B\uDF30-\uDF39\uDF40-\uDF46]|\uD806[\uDC00-\uDC3A\uDCA0-\uDCE9\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD35\uDD37\uDD38\uDD3B-\uDD43\uDD50-\uDD59\uDDA0-\uDDA7\uDDAA-\uDDD7\uDDDA-\uDDE1\uDDE3\uDDE4\uDE00-\uDE3E\uDE47\uDE50-\uDE99\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC36\uDC38-\uDC40\uDC50-\uDC59\uDC72-\uDC8F\uDC92-\uDCA7\uDCA9-\uDCB6\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD36\uDD3A\uDD3C\uDD3D\uDD3F-\uDD47\uDD50-\uDD59\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD8E\uDD90\uDD91\uDD93-\uDD98\uDDA0-\uDDA9\uDEE0-\uDEF6\uDF00-\uDF10\uDF12-\uDF3A\uDF3E-\uDF42\uDF50-\uDF59\uDFB0]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883\uD885-\uD887][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2F\uDC40-\uDC55]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDE70-\uDEBE\uDEC0-\uDEC9\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF4A\uDF4F-\uDF87\uDF8F-\uDF9F\uDFE0\uDFE1\uDFE3\uDFE4\uDFF0\uDFF1]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD32\uDD50-\uDD52\uDD55\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD833[\uDF00-\uDF2D\uDF30-\uDF46]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD837[\uDF00-\uDF1E\uDF25-\uDF2A]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A\uDC30-\uDC6D\uDC8F\uDD00-\uDD2C\uDD30-\uDD3D\uDD40-\uDD49\uDD4E\uDE90-\uDEAE\uDEC0-\uDEF9]|\uD839[\uDCD0-\uDCF9\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6\uDD00-\uDD4B\uDD50-\uDD59]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD83E[\uDFF0-\uDFF9]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF39\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A\uDF50-\uDFFF]|\uD888[\uDC00-\uDFAF]|\uDB40[\uDD00-\uDDEF])*/, + a = ["and", "as", "assert", "async", "await", "break", "case", "class", "continue", "def", "del", "elif", + "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "match", + "nonlocal|10", "not", "or", "pass", "raise", "return", "try", "while", "with", "yield" + ], + i = { + $pattern: /[A-Za-z]\w+|__\w+__/, + keyword: a, + built_in: ["__import__", "abs", "all", "any", "ascii", "bin", "bool", "breakpoint", "bytearray", "bytes", + "callable", "chr", "classmethod", "compile", "complex", "delattr", "dict", "dir", "divmod", + "enumerate", "eval", "exec", "filter", "float", "format", "frozenset", "getattr", "globals", + "hasattr", "hash", "help", "hex", "id", "input", "int", "isinstance", "issubclass", "iter", "len", + "list", "locals", "map", "max", "memoryview", "min", "next", "object", "oct", "open", "ord", "pow", + "print", "property", "range", "repr", "reversed", "round", "set", "setattr", "slice", "sorted", + "staticmethod", "str", "sum", "super", "tuple", "type", "vars", "zip" + ], + literal: ["__debug__", "Ellipsis", "False", "None", "NotImplemented", "True"], + type: ["Any", "Callable", "Coroutine", "Dict", "List", "Literal", "Generic", "Optional", "Sequence", + "Set", "Tuple", "Type", "Union" + ] + }, + r = { + className: "meta", + begin: /^(>>>|\.\.\.) / + }, + s = { + className: "subst", + begin: /\{/, + end: /\}/, + keywords: i, + illegal: /#/ + }, + o = { + begin: /\{\{/, + relevance: 0 + }, + l = { + className: "string", + contains: [e.BACKSLASH_ESCAPE], + variants: [{ + begin: /([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/, + end: /'''/, + contains: [e.BACKSLASH_ESCAPE, r], + relevance: 10 + }, { + begin: /([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/, + end: /"""/, + contains: [e.BACKSLASH_ESCAPE, r], + relevance: 10 + }, { + begin: /([fF][rR]|[rR][fF]|[fF])'''/, + end: /'''/, + contains: [e.BACKSLASH_ESCAPE, r, o, s] + }, { + begin: /([fF][rR]|[rR][fF]|[fF])"""/, + end: /"""/, + contains: [e.BACKSLASH_ESCAPE, r, o, s] + }, { + begin: /([uU]|[rR])'/, + end: /'/, + relevance: 10 + }, { + begin: /([uU]|[rR])"/, + end: /"/, + relevance: 10 + }, { + begin: /([bB]|[bB][rR]|[rR][bB])'/, + end: /'/ + }, { + begin: /([bB]|[bB][rR]|[rR][bB])"/, + end: /"/ + }, { + begin: /([fF][rR]|[rR][fF]|[fF])'/, + end: /'/, + contains: [e.BACKSLASH_ESCAPE, o, s] + }, { + begin: /([fF][rR]|[rR][fF]|[fF])"/, + end: /"/, + contains: [e.BACKSLASH_ESCAPE, o, s] + }, e.APOS_STRING_MODE, e.QUOTE_STRING_MODE] + }, + c = "[0-9](_?[0-9])*", + d = `(\\b(${c}))?\\.(${c})|\\b(${c})\\.`, + g = "\\b|" + a.join("|"), + u = { + className: "number", + relevance: 0, + variants: [{ + begin: `(\\b(${c})|(${d}))[eE][+-]?(${c})[jJ]?(?=${g})` + }, { + begin: `(${d})[jJ]?` + }, { + begin: `\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${g})` + }, { + begin: `\\b0[bB](_?[01])+[lL]?(?=${g})` + }, { + begin: `\\b0[oO](_?[0-7])+[lL]?(?=${g})` + }, { + begin: `\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${g})` + }, { + begin: `\\b(${c})[jJ](?=${g})` + }] + }, + b = { + className: "comment", + begin: n.lookahead(/# type:/), + end: /$/, + keywords: i, + contains: [{ + begin: /# type:/ + }, { + begin: /#/, + end: /\b\B/, + endsWithParent: !0 + }] + }, + m = { + className: "params", + variants: [{ + className: "", + begin: /\(\s*\)/, + skip: !0 + }, { + begin: /\(/, + end: /\)/, + excludeBegin: !0, + excludeEnd: !0, + keywords: i, + contains: ["self", r, u, l, e.HASH_COMMENT_MODE] + }] + }; + return s.contains = [l, u, r], { + name: "Python", + aliases: ["py", "gyp", "ipython"], + unicodeRegex: !0, + keywords: i, + illegal: /(<\/|->|\?)|=>/, + contains: [r, u, { + begin: /\bself\b/ + }, { + beginKeywords: "if", + relevance: 0 + }, l, b, e.HASH_COMMENT_MODE, { + match: [/\bdef/, /\s+/, t], + scope: { + 1: "keyword", + 3: "title.function" + }, + contains: [m] + }, { + variants: [{ + match: [/\bclass/, /\s+/, t, /\s*/, /\(\s*/, t, /\s*\)/] + }, { + match: [/\bclass/, /\s+/, t] + }], + scope: { + 1: "keyword", + 3: "title.class", + 6: "title.class.inherited" + } + }, { + className: "meta", + begin: /^[\t ]*@/, + end: /(?=#)|$/, + contains: [u, m, l] + }] + } + }, + grmr_python_repl: e => ({ + aliases: ["pycon"], + contains: [{ + className: "meta.prompt", + starts: { + end: / |$/, + starts: { + end: "$", + subLanguage: "python" + } + }, + variants: [{ + begin: /^>>>(?=[ ]|$)/ + }, { + begin: /^\.\.\.(?=[ ]|$)/ + }] + }] + }), + grmr_r: e => { + const n = e.regex, + t = /(?:(?:[a-zA-Z]|\.[._a-zA-Z])[._a-zA-Z0-9]*)|\.(?!\d)/, + a = n.either(/0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*[pP][+-]?\d+i?/, /0[xX][0-9a-fA-F]+(?:[pP][+-]?\d+)?[Li]?/, + /(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[Li]?/), + i = /[=!<>:]=|\|\||&&|:::?|<-|<<-|->>|->|\|>|[-+*\/?!$&|:<=>@^~]|\*\*/, + r = n.either(/[()]/, /[{}]/, /\[\[/, /[[\]]/, /\\/, /,/); + return { + name: "R", + keywords: { + $pattern: t, + keyword: "function if in break next repeat else for while", + literal: "NULL NA TRUE FALSE Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10", + built_in: "LETTERS letters month.abb month.name pi T F abs acos acosh all any anyNA Arg as.call as.character as.complex as.double as.environment as.integer as.logical as.null.default as.numeric as.raw asin asinh atan atanh attr attributes baseenv browser c call ceiling class Conj cos cosh cospi cummax cummin cumprod cumsum digamma dim dimnames emptyenv exp expression floor forceAndCall gamma gc.time globalenv Im interactive invisible is.array is.atomic is.call is.character is.complex is.double is.environment is.expression is.finite is.function is.infinite is.integer is.language is.list is.logical is.matrix is.na is.name is.nan is.null is.numeric is.object is.pairlist is.raw is.recursive is.single is.symbol lazyLoadDBfetch length lgamma list log max min missing Mod names nargs nzchar oldClass on.exit pos.to.env proc.time prod quote range Re rep retracemem return round seq_along seq_len seq.int sign signif sin sinh sinpi sqrt standardGeneric substitute sum switch tan tanh tanpi tracemem trigamma trunc unclass untracemem UseMethod xtfrm" + }, + contains: [e.COMMENT(/#'/, /$/, { + contains: [{ + scope: "doctag", + match: /@examples/, + starts: { + end: n.lookahead(n.either(/\n^#'\s*(?=@[a-zA-Z]+)/, /\n^(?!#')/)), + endsParent: !0 + } + }, { + scope: "doctag", + begin: "@param", + end: /$/, + contains: [{ + scope: "variable", + variants: [{ + match: t + }, { + match: /`(?:\\.|[^`\\])+`/ + }], + endsParent: !0 + }] + }, { + scope: "doctag", + match: /@[a-zA-Z]+/ + }, { + scope: "keyword", + match: /\\[a-zA-Z]+/ + }] + }), e.HASH_COMMENT_MODE, { + scope: "string", + contains: [e.BACKSLASH_ESCAPE], + variants: [e.END_SAME_AS_BEGIN({ + begin: /[rR]"(-*)\(/, + end: /\)(-*)"/ + }), e.END_SAME_AS_BEGIN({ + begin: /[rR]"(-*)\{/, + end: /\}(-*)"/ + }), e.END_SAME_AS_BEGIN({ + begin: /[rR]"(-*)\[/, + end: /\](-*)"/ + }), e.END_SAME_AS_BEGIN({ + begin: /[rR]'(-*)\(/, + end: /\)(-*)'/ + }), e.END_SAME_AS_BEGIN({ + begin: /[rR]'(-*)\{/, + end: /\}(-*)'/ + }), e.END_SAME_AS_BEGIN({ + begin: /[rR]'(-*)\[/, + end: /\](-*)'/ + }), { + begin: '"', + end: '"', + relevance: 0 + }, { + begin: "'", + end: "'", + relevance: 0 + }] + }, { + relevance: 0, + variants: [{ + scope: { + 1: "operator", + 2: "number" + }, + match: [i, a] + }, { + scope: { + 1: "operator", + 2: "number" + }, + match: [/%[^%]*%/, a] + }, { + scope: { + 1: "punctuation", + 2: "number" + }, + match: [r, a] + }, { + scope: { + 2: "number" + }, + match: [/[^a-zA-Z0-9._]|^/, a] + }] + }, { + scope: { + 3: "operator" + }, + match: [t, /\s+/, /<-/, /\s+/] + }, { + scope: "operator", + relevance: 0, + variants: [{ + match: i + }, { + match: /%[^%]*%/ + }] + }, { + scope: "punctuation", + relevance: 0, + match: r + }, { + begin: "`", + end: "`", + contains: [{ + begin: /\\./ + }] + }] + } + }, + grmr_ruby: e => { + const n = e.regex, + t = "([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)", + a = n.either(/\b([A-Z]+[a-z0-9]+)+/, /\b([A-Z]+[a-z0-9]+)+[A-Z]+/), + i = n.concat(a, /(::\w+)*/), + r = { + "variable.constant": ["__FILE__", "__LINE__", "__ENCODING__"], + "variable.language": ["self", "super"], + keyword: ["alias", "and", "begin", "BEGIN", "break", "case", "class", "defined", "do", "else", "elsif", + "end", "END", "ensure", "for", "if", "in", "module", "next", "not", "or", "redo", "require", "rescue", + "retry", "return", "then", "undef", "unless", "until", "when", "while", "yield", "include", "extend", + "prepend", "public", "private", "protected", "raise", "throw" + ], + built_in: ["proc", "lambda", "attr_accessor", "attr_reader", "attr_writer", "define_method", + "private_constant", "module_function" + ], + literal: ["true", "false", "nil"] + }, + s = { + className: "doctag", + begin: "@[A-Za-z]+" + }, + o = { + begin: "#<", + end: ">" + }, + l = [e.COMMENT("#", "$", { + contains: [s] + }), e.COMMENT("^=begin", "^=end", { + contains: [s], + relevance: 10 + }), e.COMMENT("^__END__", e.MATCH_NOTHING_RE)], + c = { + className: "subst", + begin: /#\{/, + end: /\}/, + keywords: r + }, + d = { + className: "string", + contains: [e.BACKSLASH_ESCAPE, c], + variants: [{ + begin: /'/, + end: /'/ + }, { + begin: /"/, + end: /"/ + }, { + begin: /`/, + end: /`/ + }, { + begin: /%[qQwWx]?\(/, + end: /\)/ + }, { + begin: /%[qQwWx]?\[/, + end: /\]/ + }, { + begin: /%[qQwWx]?\{/, + end: /\}/ + }, { + begin: /%[qQwWx]?/ + }, { + begin: /%[qQwWx]?\//, + end: /\// + }, { + begin: /%[qQwWx]?%/, + end: /%/ + }, { + begin: /%[qQwWx]?-/, + end: /-/ + }, { + begin: /%[qQwWx]?\|/, + end: /\|/ + }, { + begin: /\B\?(\\\d{1,3})/ + }, { + begin: /\B\?(\\x[A-Fa-f0-9]{1,2})/ + }, { + begin: /\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/ + }, { + begin: /\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/ + }, { + begin: /\B\?\\(c|C-)[\x20-\x7e]/ + }, { + begin: /\B\?\\?\S/ + }, { + begin: n.concat(/<<[-~]?'?/, n.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)), + contains: [e.END_SAME_AS_BEGIN({ + begin: /(\w+)/, + end: /(\w+)/, + contains: [e.BACKSLASH_ESCAPE, c] + })] + }] + }, + g = "[0-9](_?[0-9])*", + u = { + className: "number", + relevance: 0, + variants: [{ + begin: `\\b([1-9](_?[0-9])*|0)(\\.(${g}))?([eE][+-]?(${g})|r)?i?\\b` + }, { + begin: "\\b0[dD][0-9](_?[0-9])*r?i?\\b" + }, { + begin: "\\b0[bB][0-1](_?[0-1])*r?i?\\b" + }, { + begin: "\\b0[oO][0-7](_?[0-7])*r?i?\\b" + }, { + begin: "\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b" + }, { + begin: "\\b0(_?[0-7])+r?i?\\b" + }] + }, + b = { + variants: [{ + match: /\(\)/ + }, { + className: "params", + begin: /\(/, + end: /(?=\))/, + excludeBegin: !0, + endsParent: !0, + keywords: r + }] + }, + m = [d, { + variants: [{ + match: [/class\s+/, i, /\s+<\s+/, i] + }, { + match: [/\b(class|module)\s+/, i] + }], + scope: { + 2: "title.class", + 4: "title.class.inherited" + }, + keywords: r + }, { + match: [/(include|extend)\s+/, i], + scope: { + 2: "title.class" + }, + keywords: r + }, { + relevance: 0, + match: [i, /\.new[. (]/], + scope: { + 1: "title.class" + } + }, { + relevance: 0, + match: /\b[A-Z][A-Z_0-9]+\b/, + className: "variable.constant" + }, { + relevance: 0, + match: a, + scope: "title.class" + }, { + match: [/def/, /\s+/, t], + scope: { + 1: "keyword", + 3: "title.function" + }, + contains: [b] + }, { + begin: e.IDENT_RE + "::" + }, { + className: "symbol", + begin: e.UNDERSCORE_IDENT_RE + "(!|\\?)?:", + relevance: 0 + }, { + className: "symbol", + begin: ":(?!\\s)", + contains: [d, { + begin: t + }], + relevance: 0 + }, u, { + className: "variable", + begin: "(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])" + }, { + className: "params", + begin: /\|/, + end: /\|/, + excludeBegin: !0, + excludeEnd: !0, + relevance: 0, + keywords: r + }, { + begin: "(" + e.RE_STARTERS_RE + "|unless)\\s*", + keywords: "unless", + contains: [{ + className: "regexp", + contains: [e.BACKSLASH_ESCAPE, c], + illegal: /\n/, + variants: [{ + begin: "/", + end: "/[a-z]*" + }, { + begin: /%r\{/, + end: /\}[a-z]*/ + }, { + begin: "%r\\(", + end: "\\)[a-z]*" + }, { + begin: "%r!", + end: "![a-z]*" + }, { + begin: "%r\\[", + end: "\\][a-z]*" + }] + }].concat(o, l), + relevance: 0 + }].concat(o, l); + c.contains = m, b.contains = m; + const p = [{ + begin: /^\s*=>/, + starts: { + end: "$", + contains: m + } + }, { + className: "meta.prompt", + begin: "^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])", + starts: { + end: "$", + keywords: r, + contains: m + } + }]; + return l.unshift(o), { + name: "Ruby", + aliases: ["rb", "gemspec", "podspec", "thor", "irb"], + keywords: r, + illegal: /\/\*/, + contains: [e.SHEBANG({ + binary: "ruby" + })].concat(p).concat(l).concat(m) + } + }, + grmr_rust: e => { + const n = e.regex, + t = { + className: "title.function.invoke", + relevance: 0, + begin: n.concat(/\b/, /(?!let\b)/, e.IDENT_RE, n.lookahead(/\s*\(/)) + }, + a = "([ui](8|16|32|64|128|size)|f(32|64))?", + i = ["drop ", "Copy", "Send", "Sized", "Sync", "Drop", "Fn", "FnMut", "FnOnce", "ToOwned", "Clone", "Debug", + "PartialEq", "PartialOrd", "Eq", "Ord", "AsRef", "AsMut", "Into", "From", "Default", "Iterator", "Extend", + "IntoIterator", "DoubleEndedIterator", "ExactSizeIterator", "SliceConcatExt", "ToString", "assert!", + "assert_eq!", "bitflags!", "bytes!", "cfg!", "col!", "concat!", "concat_idents!", "debug_assert!", + "debug_assert_eq!", "env!", "panic!", "file!", "format!", "format_args!", "include_bytes!", + "include_str!", "line!", "local_data_key!", "module_path!", "option_env!", "print!", "println!", + "select!", "stringify!", "try!", "unimplemented!", "unreachable!", "vec!", "write!", "writeln!", + "macro_rules!", "assert_ne!", "debug_assert_ne!" + ], + r = ["i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize", "f32", "f64", + "str", "char", "bool", "Box", "Option", "Result", "String", "Vec" + ]; + return { + name: "Rust", + aliases: ["rs"], + keywords: { + $pattern: e.IDENT_RE + "!?", + type: r, + keyword: ["abstract", "as", "async", "await", "become", "box", "break", "const", "continue", "crate", + "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "if", "impl", "in", "let", + "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub", "ref", "return", "self", + "Self", "static", "struct", "super", "trait", "true", "try", "type", "typeof", "unsafe", "unsized", + "use", "virtual", "where", "while", "yield" + ], + literal: ["true", "false", "Some", "None", "Ok", "Err"], + built_in: i + }, + illegal: "" + }, t] + } + }, + grmr_scss: e => { + const n = J(e), + t = te, + a = ne, + i = "@[a-z-]+", + r = { + className: "variable", + begin: "(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b", + relevance: 0 + }; + return { + name: "SCSS", + case_insensitive: !0, + illegal: "[=/|']", + contains: [e.C_LINE_COMMENT_MODE, e.C_BLOCK_COMMENT_MODE, n.CSS_NUMBER_MODE, { + className: "selector-id", + begin: "#[A-Za-z0-9_-]+", + relevance: 0 + }, { + className: "selector-class", + begin: "\\.[A-Za-z0-9_-]+", + relevance: 0 + }, n.ATTRIBUTE_SELECTOR_MODE, { + className: "selector-tag", + begin: "\\b(" + Y.join("|") + ")\\b", + relevance: 0 + }, { + className: "selector-pseudo", + begin: ":(" + a.join("|") + ")" + }, { + className: "selector-pseudo", + begin: ":(:)?(" + t.join("|") + ")" + }, r, { + begin: /\(/, + end: /\)/, + contains: [n.CSS_NUMBER_MODE] + }, n.CSS_VARIABLE, { + className: "attribute", + begin: "\\b(" + ae.join("|") + ")\\b" + }, { + begin: "\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b" + }, { + begin: /:/, + end: /[;}{]/, + relevance: 0, + contains: [n.BLOCK_COMMENT, r, n.HEXCOLOR, n.CSS_NUMBER_MODE, e.QUOTE_STRING_MODE, e.APOS_STRING_MODE, + n.IMPORTANT, n.FUNCTION_DISPATCH + ] + }, { + begin: "@(page|font-face)", + keywords: { + $pattern: i, + keyword: "@page @font-face" + } + }, { + begin: "@", + end: "[{;]", + returnBegin: !0, + keywords: { + $pattern: /[a-z-]+/, + keyword: "and or not only", + attribute: ee.join(" ") + }, + contains: [{ + begin: i, + className: "keyword" + }, { + begin: /[a-z-]+(?=:)/, + className: "attribute" + }, r, e.QUOTE_STRING_MODE, e.APOS_STRING_MODE, n.HEXCOLOR, n.CSS_NUMBER_MODE] + }, n.FUNCTION_DISPATCH] + } + }, + grmr_shell: e => ({ + name: "Shell Session", + aliases: ["console", "shellsession"], + contains: [{ + className: "meta.prompt", + begin: /^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/, + starts: { + end: /[^\\](?=\s*$)/, + subLanguage: "bash" + } + }] + }), + grmr_sql: e => { + const n = e.regex, + t = e.COMMENT("--", "$"), + a = ["true", "false", "unknown"], + i = ["bigint", "binary", "blob", "boolean", "char", "character", "clob", "date", "dec", "decfloat", + "decimal", "float", "int", "integer", "interval", "nchar", "nclob", "national", "numeric", "real", "row", + "smallint", "time", "timestamp", "varchar", "varying", "varbinary" + ], + r = ["abs", "acos", "array_agg", "asin", "atan", "avg", "cast", "ceil", "ceiling", "coalesce", "corr", + "cos", "cosh", "count", "covar_pop", "covar_samp", "cume_dist", "dense_rank", "deref", "element", "exp", + "extract", "first_value", "floor", "json_array", "json_arrayagg", "json_exists", "json_object", + "json_objectagg", "json_query", "json_table", "json_table_primitive", "json_value", "lag", "last_value", + "lead", "listagg", "ln", "log", "log10", "lower", "max", "min", "mod", "nth_value", "ntile", "nullif", + "percent_rank", "percentile_cont", "percentile_disc", "position", "position_regex", "power", "rank", + "regr_avgx", "regr_avgy", "regr_count", "regr_intercept", "regr_r2", "regr_slope", "regr_sxx", "regr_sxy", + "regr_syy", "row_number", "sin", "sinh", "sqrt", "stddev_pop", "stddev_samp", "substring", + "substring_regex", "sum", "tan", "tanh", "translate", "translate_regex", "treat", "trim", "trim_array", + "unnest", "upper", "value_of", "var_pop", "var_samp", "width_bucket" + ], + s = ["create table", "insert into", "primary key", "foreign key", "not null", "alter table", + "add constraint", "grouping sets", "on overflow", "character set", "respect nulls", "ignore nulls", + "nulls first", "nulls last", "depth first", "breadth first" + ], + o = r, + l = ["abs", "acos", "all", "allocate", "alter", "and", "any", "are", "array", "array_agg", + "array_max_cardinality", "as", "asensitive", "asin", "asymmetric", "at", "atan", "atomic", + "authorization", "avg", "begin", "begin_frame", "begin_partition", "between", "bigint", "binary", "blob", + "boolean", "both", "by", "call", "called", "cardinality", "cascaded", "case", "cast", "ceil", "ceiling", + "char", "char_length", "character", "character_length", "check", "classifier", "clob", "close", + "coalesce", "collate", "collect", "column", "commit", "condition", "connect", "constraint", "contains", + "convert", "copy", "corr", "corresponding", "cos", "cosh", "count", "covar_pop", "covar_samp", "create", + "cross", "cube", "cume_dist", "current", "current_catalog", "current_date", + "current_default_transform_group", "current_path", "current_role", "current_row", "current_schema", + "current_time", "current_timestamp", "current_path", "current_role", "current_transform_group_for_type", + "current_user", "cursor", "cycle", "date", "day", "deallocate", "dec", "decimal", "decfloat", "declare", + "default", "define", "delete", "dense_rank", "deref", "describe", "deterministic", "disconnect", + "distinct", "double", "drop", "dynamic", "each", "element", "else", "empty", "end", "end_frame", + "end_partition", "end-exec", "equals", "escape", "every", "except", "exec", "execute", "exists", "exp", + "external", "extract", "false", "fetch", "filter", "first_value", "float", "floor", "for", "foreign", + "frame_row", "free", "from", "full", "function", "fusion", "get", "global", "grant", "group", "grouping", + "groups", "having", "hold", "hour", "identity", "in", "indicator", "initial", "inner", "inout", + "insensitive", "insert", "int", "integer", "intersect", "intersection", "interval", "into", "is", "join", + "json_array", "json_arrayagg", "json_exists", "json_object", "json_objectagg", "json_query", "json_table", + "json_table_primitive", "json_value", "lag", "language", "large", "last_value", "lateral", "lead", + "leading", "left", "like", "like_regex", "listagg", "ln", "local", "localtime", "localtimestamp", "log", + "log10", "lower", "match", "match_number", "match_recognize", "matches", "max", "member", "merge", + "method", "min", "minute", "mod", "modifies", "module", "month", "multiset", "national", "natural", + "nchar", "nclob", "new", "no", "none", "normalize", "not", "nth_value", "ntile", "null", "nullif", + "numeric", "octet_length", "occurrences_regex", "of", "offset", "old", "omit", "on", "one", "only", + "open", "or", "order", "out", "outer", "over", "overlaps", "overlay", "parameter", "partition", "pattern", + "per", "percent", "percent_rank", "percentile_cont", "percentile_disc", "period", "portion", "position", + "position_regex", "power", "precedes", "precision", "prepare", "primary", "procedure", "ptf", "range", + "rank", "reads", "real", "recursive", "ref", "references", "referencing", "regr_avgx", "regr_avgy", + "regr_count", "regr_intercept", "regr_r2", "regr_slope", "regr_sxx", "regr_sxy", "regr_syy", "release", + "result", "return", "returns", "revoke", "right", "rollback", "rollup", "row", "row_number", "rows", + "running", "savepoint", "scope", "scroll", "search", "second", "seek", "select", "sensitive", + "session_user", "set", "show", "similar", "sin", "sinh", "skip", "smallint", "some", "specific", + "specifictype", "sql", "sqlexception", "sqlstate", "sqlwarning", "sqrt", "start", "static", "stddev_pop", + "stddev_samp", "submultiset", "subset", "substring", "substring_regex", "succeeds", "sum", "symmetric", + "system", "system_time", "system_user", "table", "tablesample", "tan", "tanh", "then", "time", + "timestamp", "timezone_hour", "timezone_minute", "to", "trailing", "translate", "translate_regex", + "translation", "treat", "trigger", "trim", "trim_array", "true", "truncate", "uescape", "union", "unique", + "unknown", "unnest", "update", "upper", "user", "using", "value", "values", "value_of", "var_pop", + "var_samp", "varbinary", "varchar", "varying", "versioning", "when", "whenever", "where", "width_bucket", + "window", "with", "within", "without", "year", "add", "asc", "collation", "desc", "final", "first", + "last", "view" + ].filter((e => !r.includes(e))), + c = { + begin: n.concat(/\b/, n.either(...o), /\s*\(/), + relevance: 0, + keywords: { + built_in: o + } + }; + return { + name: "SQL", + case_insensitive: !0, + illegal: /[{}]|<\//, + keywords: { + $pattern: /\b[\w\.]+/, + keyword: ((e, { + exceptions: n, + when: t + } = {}) => { + const a = t; + return n = n || [], e.map((e => e.match(/\|\d+$/) || n.includes(e) ? e : a(e) ? e + "|0" : e)) + })(l, { + when: e => e.length < 3 + }), + literal: a, + type: i, + built_in: ["current_catalog", "current_date", "current_default_transform_group", "current_path", + "current_role", "current_schema", "current_transform_group_for_type", "current_user", "session_user", + "system_time", "system_user", "current_time", "localtime", "current_timestamp", "localtimestamp" + ] + }, + contains: [{ + begin: n.either(...s), + relevance: 0, + keywords: { + $pattern: /[\w\.]+/, + keyword: l.concat(s), + literal: a, + type: i + } + }, { + className: "type", + begin: n.either("double precision", "large object", "with timezone", "without timezone") + }, c, { + className: "variable", + begin: /@[a-z0-9]+/ + }, { + className: "string", + variants: [{ + begin: /'/, + end: /'/, + contains: [{ + begin: /''/ + }] + }] + }, { + begin: /"/, + end: /"/, + contains: [{ + begin: /""/ + }] + }, e.C_NUMBER_MODE, e.C_BLOCK_COMMENT_MODE, t, { + className: "operator", + begin: /[-+*/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?/, + relevance: 0 + }] + } + }, + grmr_swift: e => { + const n = { + match: /\s+/, + relevance: 0 + }, + t = e.COMMENT("/\\*", "\\*/", { + contains: ["self"] + }), + a = [e.C_LINE_COMMENT_MODE, t], + i = { + match: [/\./, p(...Ee, ...ye)], + className: { + 2: "keyword" + } + }, + r = { + match: m(/\./, p(...Ne)), + relevance: 0 + }, + s = Ne.filter((e => "string" == typeof e)).concat(["_|0"]), + o = { + variants: [{ + className: "keyword", + match: p(...Ne.filter((e => "string" != typeof e)).concat(we).map(fe), ...ye) + }] + }, + l = { + $pattern: p(/\b\w+/, /#\w+/), + keyword: s.concat(ke), + literal: ve + }, + c = [i, r, o], + d = [{ + match: m(/\./, p(...xe)), + relevance: 0 + }, { + className: "built_in", + match: m(/\b/, p(...xe), /(?=\()/) + }], + u = { + match: /->/, + relevance: 0 + }, + b = [u, { + className: "operator", + relevance: 0, + variants: [{ + match: Ae + }, { + match: `\\.(\\.|${Se})+` + }] + }], + _ = "([0-9a-fA-F]_*)+", + h = { + className: "number", + relevance: 0, + variants: [{ + match: "\\b(([0-9]_*)+)(\\.(([0-9]_*)+))?([eE][+-]?(([0-9]_*)+))?\\b" + }, { + match: `\\b0x(${_})(\\.(${_}))?([pP][+-]?(([0-9]_*)+))?\\b` + }, { + match: /\b0o([0-7]_*)+\b/ + }, { + match: /\b0b([01]_*)+\b/ + }] + }, + f = (e = "") => ({ + className: "subst", + variants: [{ + match: m(/\\/, e, /[0\\tnr"']/) + }, { + match: m(/\\/, e, /u\{[0-9a-fA-F]{1,8}\}/) + }] + }), + E = (e = "") => ({ + className: "subst", + match: m(/\\/, e, /[\t ]*(?:[\r\n]|\r\n)/) + }), + y = (e = "") => ({ + className: "subst", + label: "interpol", + begin: m(/\\/, e, /\(/), + end: /\)/ + }), + w = (e = "") => ({ + begin: m(e, /"""/), + end: m(/"""/, e), + contains: [f(e), E(e), y(e)] + }), + N = (e = "") => ({ + begin: m(e, /"/), + end: m(/"/, e), + contains: [f(e), y(e)] + }), + v = { + className: "string", + variants: [w(), w("#"), w("##"), w("###"), N(), N("#"), N("##"), N("###")] + }, + O = { + match: m(/`/, Re, /`/) + }, + k = [O, { + className: "variable", + match: /\$\d+/ + }, { + className: "variable", + match: `\\$${Te}+` + }], + x = [{ + match: /(@|#(un)?)available/, + className: "keyword", + starts: { + contains: [{ + begin: /\(/, + end: /\)/, + keywords: Le, + contains: [...b, h, v] + }] + } + }, { + className: "keyword", + match: m(/@/, p(...Ie)) + }, { + className: "meta", + match: m(/@/, Re) + }], + M = { + match: g(/\b[A-Z]/), + relevance: 0, + contains: [{ + className: "type", + match: m(/(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)/, Te, "+") + }, { + className: "type", + match: De, + relevance: 0 + }, { + match: /[?!]+/, + relevance: 0 + }, { + match: /\.\.\./, + relevance: 0 + }, { + match: m(/\s+&\s+/, g(De)), + relevance: 0 + }] + }, + S = { + begin: //, + keywords: l, + contains: [...a, ...c, ...x, u, M] + }; + M.contains.push(S); + const A = { + begin: /\(/, + end: /\)/, + relevance: 0, + keywords: l, + contains: ["self", { + match: m(Re, /\s*:/), + keywords: "_|0", + relevance: 0 + }, ...a, ...c, ...d, ...b, h, v, ...k, ...x, M] + }, + C = { + begin: //, + contains: [...a, M] + }, + T = { + begin: /\(/, + end: /\)/, + keywords: l, + contains: [{ + begin: p(g(m(Re, /\s*:/)), g(m(Re, /\s+/, Re, /\s*:/))), + end: /:/, + relevance: 0, + contains: [{ + className: "keyword", + match: /\b_\b/ + }, { + className: "params", + match: Re + }] + }, ...a, ...c, ...b, h, v, ...x, M, A], + endsParent: !0, + illegal: /["']/ + }, + R = { + match: [/func/, /\s+/, p(O.match, Re, Ae)], + className: { + 1: "keyword", + 3: "title.function" + }, + contains: [C, T, n], + illegal: [/\[/, /%/] + }, + D = { + match: [/\b(?:subscript|init[?!]?)/, /\s*(?=[<(])/], + className: { + 1: "keyword" + }, + contains: [C, T, n], + illegal: /\[|%/ + }, + I = { + match: [/operator/, /\s+/, Ae], + className: { + 1: "keyword", + 3: "title" + } + }, + L = { + begin: [/precedencegroup/, /\s+/, De], + className: { + 1: "keyword", + 3: "title" + }, + contains: [M], + keywords: [...Oe, ...ve], + end: /}/ + }; + for (const e of v.variants) { + const n = e.contains.find((e => "interpol" === e.label)); + n.keywords = l; + const t = [...c, ...d, ...b, h, v, ...k]; + n.contains = [...t, { + begin: /\(/, + end: /\)/, + contains: ["self", ...t] + }] + } + return { + name: "Swift", + keywords: l, + contains: [...a, R, D, { + beginKeywords: "struct protocol class extension enum actor", + end: "\\{", + excludeEnd: !0, + keywords: l, + contains: [e.inherit(e.TITLE_MODE, { + className: "title.class", + begin: /[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/ + }), ...c] + }, I, L, { + beginKeywords: "import", + end: /$/, + contains: [...a], + relevance: 0 + }, ...c, ...d, ...b, h, v, ...k, ...x, M, A] + } + }, + grmr_typescript: e => { + const n = he(e), + t = ["any", "void", "number", "boolean", "string", "object", "never", "symbol", "bigint", "unknown"], + a = { + beginKeywords: "namespace", + end: /\{/, + excludeEnd: !0, + contains: [n.exports.CLASS_REFERENCE] + }, + i = { + beginKeywords: "interface", + end: /\{/, + excludeEnd: !0, + keywords: { + keyword: "interface extends", + built_in: t + }, + contains: [n.exports.CLASS_REFERENCE] + }, + r = { + $pattern: ce, + keyword: de.concat(["type", "namespace", "interface", "public", "private", "protected", "implements", + "declare", "abstract", "readonly", "enum", "override" + ]), + literal: ge, + built_in: _e.concat(t), + "variable.language": pe + }, + s = { + className: "meta", + begin: "@[A-Za-z$_][0-9A-Za-z$_]*" + }, + o = (e, n, t) => { + const a = e.contains.findIndex((e => e.label === n)); + if (-1 === a) throw Error("can not find mode to replace"); + e.contains.splice(a, 1, t) + }; + return Object.assign(n.keywords, r), + n.exports.PARAMS_CONTAINS.push(s), n.contains = n.contains.concat([s, a, i]), + o(n, "shebang", e.SHEBANG()), o(n, "use_strict", { + className: "meta", + relevance: 10, + begin: /^\s*['"]use strict['"]/ + }), n.contains.find((e => "func.def" === e.label)).relevance = 0, Object.assign(n, { + name: "TypeScript", + aliases: ["ts", "tsx"] + }), n + }, + grmr_vbnet: e => { + const n = e.regex, + t = /\d{1,2}\/\d{1,2}\/\d{4}/, + a = /\d{4}-\d{1,2}-\d{1,2}/, + i = /(\d|1[012])(:\d+){0,2} *(AM|PM)/, + r = /\d{1,2}(:\d{1,2}){1,2}/, + s = { + className: "literal", + variants: [{ + begin: n.concat(/# */, n.either(a, t), / *#/) + }, { + begin: n.concat(/# */, r, / *#/) + }, { + begin: n.concat(/# */, i, / *#/) + }, { + begin: n.concat(/# */, n.either(a, t), / +/, n.either(i, r), / *#/) + }] + }, + o = e.COMMENT(/'''/, /$/, { + contains: [{ + className: "doctag", + begin: /<\/?/, + end: />/ + }] + }), + l = e.COMMENT(null, /$/, { + variants: [{ + begin: /'/ + }, { + begin: /([\t ]|^)REM(?=\s)/ + }] + }); + return { + name: "Visual Basic .NET", + aliases: ["vb"], + case_insensitive: !0, + classNameAliases: { + label: "symbol" + }, + keywords: { + keyword: "addhandler alias aggregate ansi as async assembly auto binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into iterator join key let lib loop me mid module mustinherit mustoverride mybase myclass namespace narrowing new next notinheritable notoverridable of off on operator option optional order overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly yield", + built_in: "addressof and andalso await directcast gettype getxmlnamespace is isfalse isnot istrue like mod nameof new not or orelse trycast typeof xor cbool cbyte cchar cdate cdbl cdec cint clng cobj csbyte cshort csng cstr cuint culng cushort", + type: "boolean byte char date decimal double integer long object sbyte short single string uinteger ulong ushort", + literal: "true false nothing" + }, + illegal: "//|\\{|\\}|endif|gosub|variant|wend|^\\$ ", + contains: [{ + className: "string", + begin: /"(""|[^/n])"C\b/ + }, { + className: "string", + begin: /"/, + end: /"/, + illegal: /\n/, + contains: [{ + begin: /""/ + }] + }, s, { + className: "number", + relevance: 0, + variants: [{ + begin: /\b\d[\d_]*((\.[\d_]+(E[+-]?[\d_]+)?)|(E[+-]?[\d_]+))[RFD@!#]?/ + }, { + begin: /\b\d[\d_]*((U?[SIL])|[%&])?/ + }, { + begin: /&H[\dA-F_]+((U?[SIL])|[%&])?/ + }, { + begin: /&O[0-7_]+((U?[SIL])|[%&])?/ + }, { + begin: /&B[01_]+((U?[SIL])|[%&])?/ + }] + }, { + className: "label", + begin: /^\w+:/ + }, o, l, { + className: "meta", + begin: /[\t ]*#(const|disable|else|elseif|enable|end|externalsource|if|region)\b/, + end: /$/, + keywords: { + keyword: "const disable else elseif enable end externalsource if region then" + }, + contains: [l] + }] + } + }, + grmr_wasm: e => { + e.regex; + const n = e.COMMENT(/\(;/, /;\)/); + return n.contains.push("self"), { + name: "WebAssembly", + keywords: { + $pattern: /[\w.]+/, + keyword: ["anyfunc", "block", "br", "br_if", "br_table", "call", "call_indirect", "data", "drop", + "elem", "else", "end", "export", "func", "global.get", "global.set", "local.get", "local.set", + "local.tee", "get_global", "get_local", "global", "if", "import", "local", "loop", "memory", + "memory.grow", "memory.size", "module", "mut", "nop", "offset", "param", "result", "return", + "select", "set_global", "set_local", "start", "table", "tee_local", "then", "type", "unreachable" + ] + }, + contains: [e.COMMENT(/;;/, /$/), n, { + match: [/(?:offset|align)/, /\s*/, /=/], + className: { + 1: "keyword", + 3: "operator" + } + }, { + className: "variable", + begin: /\$[\w_]+/ + }, { + match: /(\((?!;)|\))+/, + className: "punctuation", + relevance: 0 + }, { + begin: [/(?:func|call|call_indirect)/, /\s+/, /\$[^\s)]+/], + className: { + 1: "keyword", + 3: "title.function" + } + }, e.QUOTE_STRING_MODE, { + match: /(i32|i64|f32|f64)(?!\.)/, + className: "type" + }, { + className: "keyword", + match: /\b(f32|f64|i32|i64)(?:\.(?:abs|add|and|ceil|clz|const|convert_[su]\/i(?:32|64)|copysign|ctz|demote\/f64|div(?:_[su])?|eqz?|extend_[su]\/i32|floor|ge(?:_[su])?|gt(?:_[su])?|le(?:_[su])?|load(?:(?:8|16|32)_[su])?|lt(?:_[su])?|max|min|mul|nearest|neg?|or|popcnt|promote\/f32|reinterpret\/[fi](?:32|64)|rem_[su]|rot[lr]|shl|shr_[su]|store(?:8|16|32)?|sqrt|sub|trunc(?:_[su]\/f(?:32|64))?|wrap\/i64|xor))\b/ + }, { + className: "number", + relevance: 0, + match: /[+-]?\b(?:\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|0x[\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-D])*)?(?:[pP][+-]?\d(?:_?\d)*)?)\b|\binf\b|\bnan(?::0x[\da-fA-F](?:_?[\da-fA-D])*)?\b/ + }] + } + }, + grmr_yaml: e => { + const n = "true false yes no null", + t = "[\\w#;/?:@&=+$,.~*'()[\\]]+", + a = { + className: "string", + relevance: 0, + variants: [{ + begin: /'/, + end: /'/ + }, { + begin: /"/, + end: /"/ + }, { + begin: /\S+/ + }], + contains: [e.BACKSLASH_ESCAPE, { + className: "template-variable", + variants: [{ + begin: /\{\{/, + end: /\}\}/ + }, { + begin: /%\{/, + end: /\}/ + }] + }] + }, + i = e.inherit(a, { + variants: [{ + begin: /'/, + end: /'/ + }, { + begin: /"/, + end: /"/ + }, { + begin: /[^\s,{}[\]]+/ + }] + }), + r = { + end: ",", + endsWithParent: !0, + excludeEnd: !0, + keywords: n, + relevance: 0 + }, + s = { + begin: /\{/, + end: /\}/, + contains: [r], + illegal: "\\n", + relevance: 0 + }, + o = { + begin: "\\[", + end: "\\]", + contains: [r], + illegal: "\\n", + relevance: 0 + }, + l = [{ + className: "attr", + variants: [{ + begin: "\\w[\\w :\\/.-]*:(?=[ \t]|$)" + }, { + begin: '"\\w[\\w :\\/.-]*":(?=[ \t]|$)' + }, { + begin: "'\\w[\\w :\\/.-]*':(?=[ \t]|$)" + }] + }, { + className: "meta", + begin: "^---\\s*$", + relevance: 10 + }, { + className: "string", + begin: "[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*" + }, { + begin: "<%[%=-]?", + end: "[%-]?%>", + subLanguage: "ruby", + excludeBegin: !0, + excludeEnd: !0, + relevance: 0 + }, { + className: "type", + begin: "!\\w+!" + t + }, { + className: "type", + begin: "!<" + t + ">" + }, { + className: "type", + begin: "!" + t + }, { + className: "type", + begin: "!!" + t + }, { + className: "meta", + begin: "&" + e.UNDERSCORE_IDENT_RE + "$" + }, { + className: "meta", + begin: "\\*" + e.UNDERSCORE_IDENT_RE + "$" + }, { + className: "bullet", + begin: "-(?=[ ]|$)", + relevance: 0 + }, e.HASH_COMMENT_MODE, { + beginKeywords: n, + keywords: { + literal: n + } + }, { + className: "number", + begin: "\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b" + }, { + className: "number", + begin: e.C_NUMBER_RE + "\\b", + relevance: 0 + }, s, o, a], + c = [...l]; + return c.pop(), c.push(i), r.contains = c, { + name: "YAML", + case_insensitive: !0, + aliases: ["yml"], + contains: l + } + } +}); +const $e = V; +for (const e of Object.keys(Be)) { + const n = e.replace("grmr_", "").replace("_", "-"); + $e.registerLanguage(n, Be[e]) +} +export { + $e as + default +}; \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/lib/html-parser.js b/uni-im示例/uni_modules/uni-im/lib/html-parser.js new file mode 100644 index 0000000..0d28a40 --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/lib/html-parser.js @@ -0,0 +1,352 @@ +/* + * HTML5 Parser By Sam Blowes + * + * Designed for HTML5 documents + * + * Original code by John Resig (ejohn.org) + * http://ejohn.org/blog/pure-javascript-html-parser/ + * Original code by Erik Arvidsson, Mozilla Public License + * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js + * + * ---------------------------------------------------------------------------- + * License + * ---------------------------------------------------------------------------- + * + * This code is triple licensed using Apache Software License 2.0, + * Mozilla Public License or GNU Public License + * + * //////////////////////////////////////////////////////////////////////////// + * + * 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 + * + * //////////////////////////////////////////////////////////////////////////// + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (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.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is Simple HTML Parser. + * + * The Initial Developer of the Original Code is Erik Arvidsson. + * Portions created by Erik Arvidssson are Copyright (C) 2004. All Rights + * Reserved. + * + * //////////////////////////////////////////////////////////////////////////// + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ---------------------------------------------------------------------------- + * Usage + * ---------------------------------------------------------------------------- + * + * // Use like so: + * HTMLParser(htmlString, { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + * // or to get an XML string: + * HTMLtoXML(htmlString); + * + * // or to get an XML DOM Document + * HTMLtoDOM(htmlString); + * + * // or to inject into an existing document/DOM node + * HTMLtoDOM(htmlString, document); + * HTMLtoDOM(htmlString, document.body); + * + */ +// Regular Expressions for parsing tags and attributes +var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/; +var endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/; +var attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; // Empty Elements - HTML 5 + +var empty = makeMap('area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr'); // Block Elements - HTML 5 +// fixed by xxx 将 ins 标签从块级名单中移除 + +var block = makeMap('a,address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video'); // Inline Elements - HTML 5 + +var inline = makeMap('abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var'); // Elements that you can, intentionally, leave open +// (and which close themselves) + +var closeSelf = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr'); // Attributes that have their values filled in disabled="disabled" + +var fillAttrs = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected'); // Special Elements (can contain anything) + +var special = makeMap('script,style'); +function HTMLParser(html, handler) { + var index; + var chars; + var match; + var stack = []; + var last = html; + + stack.last = function () { + return this[this.length - 1]; + }; + + while (html) { + chars = true; // Make sure we're not in a script or style element + + if (!stack.last() || !special[stack.last()]) { + // Comment + if (html.indexOf(''); + + if (index >= 0) { + if (handler.comment) { + handler.comment(html.substring(4, index)); + } + + html = html.substring(index + 3); + chars = false; + } // end tag + + } else if (html.indexOf(']*>'), function (all, text) { + text = text.replace(/|/g, '$1$2'); + + if (handler.chars) { + handler.chars(text); + } + + return ''; + }); + parseEndTag('', stack.last()); + } + + if (html == last) { + throw 'Parse Error: ' + html; + } + + last = html; + } // Clean up any remaining tags + + + parseEndTag(); + + function parseStartTag(tag, tagName, rest, unary) { + tagName = tagName.toLowerCase(); + + if (block[tagName]) { + while (stack.last() && inline[stack.last()]) { + parseEndTag('', stack.last()); + } + } + + if (closeSelf[tagName] && stack.last() == tagName) { + parseEndTag('', tagName); + } + + unary = empty[tagName] || !!unary; + + if (!unary) { + stack.push(tagName); + } + + if (handler.start) { + var attrs = []; + rest.replace(attr, function (match, name) { + var value = arguments[2] ? arguments[2] : arguments[3] ? arguments[3] : arguments[4] ? arguments[4] : fillAttrs[name] ? name : ''; + attrs.push({ + name: name, + value: value, + escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') // " + + }); + }); + + if (handler.start) { + handler.start(tagName, attrs, unary); + } + } + } + + function parseEndTag(tag, tagName) { + // If no tag name is provided, clean shop + if (!tagName) { + var pos = 0; + } // Find the closest opened tag of the same type + else { + for (var pos = stack.length - 1; pos >= 0; pos--) { + if (stack[pos] == tagName) { + break; + } + } + } + + if (pos >= 0) { + // Close all the open elements, up the stack + for (var i = stack.length - 1; i >= pos; i--) { + if (handler.end) { + handler.end(stack[i]); + } + } // Remove the open elements from the stack + + + stack.length = pos; + } + } +} + +function makeMap(str) { + var obj = {}; + var items = str.split(','); + + for (var i = 0; i < items.length; i++) { + obj[items[i]] = true; + } + + return obj; +} + +function removeDOCTYPE(html) { + return html.replace(/<\?xml.*\?>\n/, '').replace(/\n/, '').replace(/\n/, ''); +} + +function parseAttrs(attrs) { + return attrs.reduce(function (pre, attr) { + var value = attr.value; + var name = attr.name; + + if (pre[name]) { + pre[name] = pre[name] + " " + value; + } else { + pre[name] = value; + } + + return pre; + }, {}); +} + +function parseHtml(html) { + html = removeDOCTYPE(html); + var stacks = []; + var results = { + node: 'root', + children: [] + }; + HTMLParser(html, { + start: function start(tag, attrs, unary) { + var node = { + name: tag + }; + + if (attrs.length !== 0) { + node.attrs = parseAttrs(attrs); + } + + if (unary) { + var parent = stacks[0] || results; + + if (!parent.children) { + parent.children = []; + } + + parent.children.push(node); + } else { + stacks.unshift(node); + } + }, + end: function end(tag) { + var node = stacks.shift(); + if (node.name !== tag) console.error('invalid state: mismatch end tag'); + + if (stacks.length === 0) { + results.children.push(node); + } else { + var parent = stacks[0]; + + if (!parent.children) { + parent.children = []; + } + + parent.children.push(node); + } + }, + chars: function chars(text) { + var node = { + type: 'text', + text: text + }; + + if (stacks.length === 0) { + results.children.push(node); + } else { + var parent = stacks[0]; + + if (!parent.children) { + parent.children = []; + } + + parent.children.push(node); + } + }, + comment: function comment(text) { + var node = { + node: 'comment', + text: text + }; + var parent = stacks[0]; + + if (!parent.children) { + parent.children = []; + } + + parent.children.push(node); + } + }); + return results.children; +} + +export default parseHtml; \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/lib/main.js b/uni-im示例/uni_modules/uni-im/lib/main.js new file mode 100644 index 0000000..0b9aead --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/lib/main.js @@ -0,0 +1,819 @@ +import utils from '@/uni_modules/uni-im/common/utils.js'; +import MsgManager from './MsgManager.js'; +import createObservable from './createObservable'; + +const db = uniCloud.database(); +const uniImCo = uniCloud.importObject("uni-im-co", { + customUI: true +}) + +function current_uid() { + return uniCloud.getCurrentUserInfo().uid +} + +const state = createObservable({ + // 会话数据 + conversation: { + dataList: [], + hasMore: true, + loading: false // 加锁防止意外重复请求时出错 + }, + // 正在对话的会话id + currentConversationId: false, + // 全局响应式心跳,用于更新消息距离当前时长 等 + heartbeat: '', + // 好友列表 + friend: { + dataList: [], + hasMore: true + }, + // 群列表 + group: { + dataList: [], + hasMore: true + }, + // 系统通知消息 + notification: { + dataList: [], + hasMore: true + }, + //存储所有出现过的用户信息,包括群好友信息 + usersInfo: {}, + //是否为pc宽屏 + isWidescreen: false, + //系统信息 + systemInfo: {}, + // #ifndef H5 + indexDB: false, + // #endif + audioContext: false, + // sqlite数据库是否已经打开 + dataBaseIsOpen: false, + // 记录socket连接次数(用于处理,断开重连) + socketOpenIndex: 0 +}) + +const methods = { + /** + * 会话对象 + * data:会话对象数据模型(conversationDatas是原始数据,data为经过转化的数据) + * loadMore:加载更多数据方法 + */ + conversation: { + // 撤回消息,参数: 消息id 会话id ,操作者id + async revokeMsg({ + msg_id, + _id, + conversation_id, + user_id = false, + create_time + }) { + if (_id) { + msg_id = _id + } + + // console.log({ + // msg_id, + // conversation_id, + // user_id + // }); + // console.log('msg_id---',msg_id); + + // userId为操作者id,false表示当前用户操作的。需要广播同步给其他人。收到其他人的撤回指令,会带上触发者的id + if (!user_id) { // 为空表示当前用户就是操作者 + try { + let res = await uniImCo.revokeMsg(msg_id) + // console.log('res', res); + } catch (err) { + console.log('err', err); + return uni.showToast({ + title: err.message, + icon: 'none' + }); + } + } + + let conversation = await this.get(conversation_id) + + // 处理列表显示 + let msgList = conversation.msgList + let index = msgList.findIndex(item => item._id == msg_id) + // console.log('index',index); + if (index != -1) { + let msg = msgList[index] + // console.log(111111112222,JSON.stringify(msg)); + msg.is_revoke = true + msg.body = '[此消息已被撤回]' + conversation.msgList.splice(index, 1, Object.assign({}, msg)) + // console.log('conversation.msgList', conversation.msgList); + } + + // 删除本地存储中的数据 + let localMsgs = await conversation.msgManager.localMsg.get({ + "minTime": create_time - 1, + "maxTime": create_time + 1 + }) + let localMsg = localMsgs.find(item => item._id == msg_id) + // console.log('res查本地存储中是否有这条消息',localMsgs,localMsg); + if (localMsg) { + localMsg.is_revoke = true + localMsg.body = '[此消息已被撤回]' + conversation.msgManager.localMsg.update(localMsg.unique_id, localMsg) + } + }, + async get(param) { + /** + * 字符串 会话id + * 数组 一组会话id(暂不支持) + * 对象类型 会话对方信息(用于如果本地不存在则需要自动创建的场景),包括:{friend_uid,to_uid,from_uid,group_id,user_info} + */ + let conversationId = false + if (param) { + if (typeof param == 'object') { + let { + friend_uid, + user_id, + group_id, + conversation_id + } = param + conversationId = conversation_id + if (user_id) { + friend_uid = user_id + param.friend_uid = user_id + } + if (!conversationId) { + if (!group_id && !friend_uid) { + console.log('param---------', param); + throw new Error("会话对象不详,请检查参数", param) + } + conversationId = utils.getConversationId(friend_uid || group_id, friend_uid ? 'single' : 'group') + } + } else if (typeof param == 'string') { + conversationId = param + } else { + throw new Error("会话对象不详,请检查参数", param) + } + } + let conversationDatas = state.conversation.dataList + if (conversationId) { + conversationDatas = conversationDatas.filter(i => i.id == conversationId) + if (conversationDatas.length == 0) { + // 本地没有没有就联网查找 + let conversationData = await this.loadMore(conversationId) + if (conversationData) { + conversationDatas = [conversationData] + } else { + if (param.group_id) { + throw new Error("未找到此群会话") + } + if (typeof param != 'object') { + console.log('param', param); + throw new Error("参数错误") + } + // 非群会话,网络也没有就本地创建一个 + if (!param.user_info) { + let res = await uniCloud.database() + .collection('uni-id-users') + .doc(param.friend_uid) + .field('_id,nickname,avatar_file') + .get() + console.log('user_info', res) + param.user_info = res.result.data[0] + // console.log('param.user_info', param.user_info); + if (!param.user_info) { + throw new Error("用户查找失败") + } + } + let conversationData = { + group_id: param.group_id, + friend_uid: param.friend_uid, + unread_count: 0 + } + + try { + const db = uniCloud.database(); + let res = await db.collection('uni-im-conversation').add(conversationData) + // console.log('res',res) + } catch (e) { + throw new Error(e) + } + + conversationData = Object.assign(conversationData, { + user_id: current_uid(), + id: conversationId, + user_info: param.user_info, + type: param.friend_uid ? 1 : 2, + msgList: [], + update_time: Date.now() + }) + + this.add(conversationData) + conversationDatas.push(conversationData) + } + } + } + + // console.log('conversationDatas---',conversationDatas,param); + // console.log('conversationDatas---',conversationDatas.length,param); + + // console.log('999conversationDatas',conversationDatas,conversationId); + if (conversationId) { + let conversationData = conversationDatas[0] + + // 指定获取某个id的群会话时,判断如果群会话的 群成员为空就从云端拉取 + if (conversationData.group_id && Object.keys(conversationData.group_member).length == 0) { + let res = await db.collection( + db.collection('uni-im-group-member').where({ + group_id: conversationData.group_id + }).getTemp(), + db.collection('uni-id-users').field('_id,nickname,avatar_file').getTemp() + ) + .limit(1000) + .get() + let group_member = {} + + res.result.data.forEach(item => { + let usersInfo = item.user_id[0] + group_member[usersInfo._id] = usersInfo + }) + methods.mergeUsersInfo(group_member) + conversationData.group_member = group_member + // console.log('conversationData.group_member', conversationData.group_member) + } + + // console.log('conversationData*-*--*-**-',conversationData) + // #ifdef APP + if (!conversationData.isInit) { + conversationData.msgManager = new MsgManager(conversationData) + } + // #endif + return conversationData + } else { + return conversationDatas + } + }, + async loadMore(conversation_id) { + // console.log('loadMore-----','loadMore') + if (!conversation_id) { + // console.log('state.conversation.loading',state.conversation.loading) + //上一次正在调用,下一次不能马上开始 + if (state.conversation.loading) { + // console.log('加载中') + return [] + } else { + state.conversation.loading = true + } + } + + let conversationDatas = await this.get() + let lastConversation = conversationDatas[conversationDatas.length - 1] + // console.log('conversationDatas.length',conversationDatas.length,lastConversation) + let maxUpdateTime = lastConversation ? lastConversation.update_time : '' + if (conversation_id) { + // 已有会话id的情况下,不设置更新时间条件 + maxUpdateTime = '' + } + let res = { + data: [] + } + try { + res = await uniImCo.getConversationList({ + maxUpdateTime, + limit: 30, + conversation_id + }) + } catch (e) { + console.log(e) + if (!conversation_id) { + state.conversation.loading = false + } + } + + if (res.data.length) { + // console.log('getConversationList res', res, { + // maxUpdateTime, + // limit: 30, + // conversation_id + // }); + this.add(res.data) + } + if (!conversation_id) { + state.conversation.loading = false + state.conversation.hasMore = (res.data.length == 30) + + /** + * 处理云端任务,比如撤回消息等 + */ + let whereString = "user_id == $cloudEnv_uid" + let group_ids = res.data.filter(item => item.group_id).map(i => i.group_id) || [] + if (group_ids.length) { + whereString = `(user_id == $cloudEnv_uid || "group_id" in ${JSON.stringify(group_ids)})` + } + let lastTaskTime = uni.getStorageSync('uni-im-lastTaskTime') + // console.log('lastTaskTime',lastTaskTime); + if (lastTaskTime) { + whereString += `&& "create_time" > ${lastTaskTime}` + } + db.collection('uni-im-task') + .where(whereString) + .orderBy('create_time desc') + .get() + .then((e) => { + // console.log('uni-im-task', e.result.data); + if (e.result.data.length) { + e.result.data.forEach(item => { + if (item.type == "revoke_msg") { + this.revokeMsg(item.payload) + } + }) + uni.setStorageSync('uni-im-lastTaskTime', e.result.data[0].create_time) + } + }) + .catch(e => { + console.error(e); + }) + return res.data + } else { + return res.data[0] + } + }, + add(data) { + if (!Array.isArray(data)) { + data = [data] + } + data.forEach(item => { + // 服务端联查到的数据,群和用户信息是数组。这里兼容 客户端add时可直接传object + if (Array.isArray(item.user_info)) { + item.user_info = item.user_info[0] + } + if (Array.isArray(item.group_info)) { + item.group_info = item.group_info[0] + if (item.group_id) { + if (!item.group_member) { + item.group_member = {} + } + if (item.group_info.introduction === undefined) { + item.group_info.introduction = '' + } + if (item.group_info.avatar_file === undefined) { + item.group_info.avatar_file = { + url: "" + } + } + } + } + + // item.chatText = "" + // item.isInit = false + // item.title = "" + // item.avatar_file = {} + + item = Object.assign(item, { + isInit: false, + title: "", + chatText: "", + avatar_file: {}, + call_list: [] + }) + + if (item.user_info) { + Object.defineProperties(item, { + title: { + get() { + return item.user_info.nickname + } + }, + avatar_file: { + get() { + return item.user_info.avatar_file + } + }, + group_info: { + value: false + } + }) + } else if (item.group_info) { + Object.defineProperties(item, { + title: { + get() { + return item.group_info.name + } + }, + avatar_file: { + get() { + return item.group_info.avatar_file + } + }, + user_info: { + value: false + } + }) + } else { + console.error('会话列表失效,疑似关联用户/群被删除(请改为软删除避免系统异常):', JSON.stringify(item)); + } + + let update_time = item.update_time + Object.defineProperties(item, { + last_msg_note: { + get() { + let last_msg_note = "暂无记录" + let last_msg = item.msgList[item.msgList.length - 1] + // console.log('---last_msg',last_msg) + if (item.chatText && state.currentConversationId != item.id) { + last_msg = { + body: "[uni-im-draft]" + item.chatText, + type: 'text', + create_time: Date.now() + } + } + if (last_msg) { + last_msg_note = '[多媒体]' + if (last_msg.type == 'text') { + last_msg_note = last_msg.body.toString() + last_msg_note = last_msg_note.replace(/[\r\n]/g, ""); + last_msg_note = last_msg_note.slice(0, 30) + } + if (last_msg.is_revoke) { + last_msg_note = "消息已被撤回" + } + if (last_msg.is_delete) { + last_msg_note = "消息已被删除" + } + } + return last_msg_note + } + }, + update_time: { + get() { + let last_msg = item.msgList[item.msgList.length - 1] + if (last_msg) { + return last_msg.create_time + } else { + return update_time + } + } + } + }) + + // 把user_info统一存到一个对象 + let { + user_info, + group_member + } = item + let usersInfo = {} + if (user_info) { + usersInfo[user_info._id] = user_info + } + methods.mergeUsersInfo(usersInfo) + item.msgManager = new MsgManager(item) + // console.log('state.conversation.dataList',state.conversation.dataList); + + let initMsg = (msgList) => { + for (let i = 0; i < msgList.length; i++) { + let msg = msgList[i] + if (msg && typeof msg == 'object') { + if (!('is_delete' in msg)) { + msg.is_delete = false + } + } + } + + let methodsList = ['push', 'unshift'] + // #ifdef VUE2 + let arr_methods = Object.create(Array.prototype); + methodsList.forEach(methods => { + arr_methods[methods] = function() { + // console.log(`使用的是${methods}方法`) + initMsg(arguments) + Array.prototype[methods].apply(this, arguments) + } + }) + // #endif + + // #ifdef VUE3 + methodsList.forEach(methods => { + msgList[methods] = function() { + // console.log(`使用的是${methods}方法`) + initMsg(arguments) + Array.prototype[methods].apply(this, arguments) + } + }); + // #endif + return msgList + } + initMsg(item.msgList); + + item.msgList.clear = function() { + // console.log('clear'); + // #ifdef VUE3 + this.length = 0 + // #endif + + // #ifdef VUE2 + item.msgList = [] + initMsg(item.msgList) + // #endif + } + + // #ifdef VUE3 + Object.defineProperty(item, 'msgList', { + writable: false + }) + // #endif + + // 判断是否存在,再新增。 + if (!state.conversation.dataList.find(conversation => conversation.id == item.id)) { + state.conversation.dataList.push(item) + } else { + // console.log('重复新增已存在的会话', item) + } + }) + + // console.log('存到storage',state.conversation) + // 存到storage + uni.setStorage({ + key: 'uni-im-conversation' + '_uid:' + current_uid(), + data: state.conversation + }) + return data + }, + // 统计所有消息的未读数 + unreadCount() { + let conversationDatas = state.conversation.dataList + return conversationDatas.reduce((sum, item, index, array) => sum + item.unread_count, 0) + }, + remove(id) { + let index = state.conversation.dataList.findIndex(i => i.id == id) + state.conversation.dataList.splice(index, 1) + } + }, + /** + * 系统消息 + */ + notification: { + get: ({ + type, + excludeType + } = {}) => { + const notificationDatas = state.notification.dataList + if (notificationDatas) { + return notificationDatas.reduce((sum, item) => { + // 指定需要的类型 + if (type) { + //兼容字符串和数组 + typeof type == 'string' ? type = [type] : '' + if (type.includes(item.payload.subType)) { + sum.push(item) + } + // 排查指定的类型 + } else if (excludeType) { + //兼容字符串和数组 + typeof excludeType == 'string' ? excludeType = [excludeType] : '' + if (!excludeType.includes(item.payload.subType)) { + sum.push(item) + } + } else { + sum.push(item) + } + return sum + }, []) + } else { + return false + } + }, + async loadMore() { + let res = await db.collection('uni-im-notification') + .aggregate() + .match('"payload.type" == "uni-im-notification" && "user_id" == $cloudEnv_uid') + .sort({ + create_time: -1 + }) + .limit(1000) + .end() + this.add(res.result.data) + this.hasMore == (res.result.data.length != 0) + }, + add(datas) { + if (!Array.isArray(datas)) { + datas = [datas] + } + + let notificationDatas = datas.concat(state.notification.dataList) + // 正序,实现时间大的覆盖时间小的 + notificationDatas.sort((a, b) => a.create_time - b.create_time) + // console.log('notificationDatas',notificationDatas); + let obj = {} + for (var i = 0; i < notificationDatas.length; i++) { + let item = notificationDatas[i] + // 去重操作 + let { + subType, + unique + } = item.payload + obj[unique ? (subType + "_" + unique) : (Date.now() + "_" + i)] = item + } + let dataList = [] + for (let key in obj) { + let item = obj[key] + dataList.push(item) + } + // 倒序 实现,最新的消息在最上面 + dataList.sort((a, b) => b.create_time - a.create_time) + // console.log('dataList',dataList) + state.notification.dataList = dataList + }, + unreadCount(param = {}) { + let notificationDatas = this.get(param) + let unreadCount = notificationDatas.reduce((sum, item, index, array) => { + if (!item.is_read) { + sum++ + } + return sum + }, 0) + + // console.log('最新的未读数:', unreadCount, data); + // 注意:在非tabbar页面无法设置 badge + if (unreadCount === 0) { + uni.removeTabBarBadge({ + index: 2, + complete: (e) => { + // console.log(e) + } + }) + } else { + uni.setTabBarBadge({ + index: 2, + text: unreadCount + '', + complete: (e) => { + // console.log(e) + } + }) + } + + if (unreadCount) { + return unreadCount + '' + } else { + return '' + } + } + }, + friend: { + get() { + return state.friend.dataList + }, + async loadMore({ + friend_uid + } = {}) { + let whereString = '"user_id" == $cloudEnv_uid' + if (friend_uid) { + whereString += `&& "friend_uid" == "${friend_uid}"` + // console.log('whereString',whereString); + } + let res = await db.collection( + db.collection('uni-im-friend').where(whereString).field('friend_uid,mark,class_name') + .getTemp(), + db.collection('uni-id-users').field('_id,nickname,avatar_file').getTemp() + ) + .limit(500) + .get() + let data = res.result.data + // console.log('data',data); + data.forEach((item, index) => { + data[index] = item.friend_uid[0] + let uid = data[index]._id + if (!state.usersInfo[uid]) { + state.usersInfo[uid] = item.friend_uid[0] + } + }) + state.friend.hasMore = data.length == 500 + state.friend.dataList.push(...data) + }, + remove(friend_uid) { + let friendList = state.friend.dataList + let index = friendList.findIndex(i => i._id == friend_uid) + friendList.splice(index, 1) + } + }, + group: { + get() { + return state.group.dataList + }, + async loadMore({ + group_id + } = {}) { + let whereString = '"user_id" == $cloudEnv_uid ' + if (group_id) { + whereString += `&& "group_id" == "${group_id}"` + } + // console.log('whereString',whereString) + let res = await db.collection( + db.collection('uni-im-group-member').where(whereString).getTemp(), + db.collection('uni-im-group').getTemp() + ) + .limit(500) + .get() + res.result.data.map(item => { + item.group_info = item.group_id[0] + delete item.group_id + return item + }) + // 过滤掉已经被删除的群 + res.result.data = res.result.data.filter(item=>item.group_info) + + state.group.hasMore = res.result.data.length == 500 + if (group_id) { + state.group.dataList.push(...res.result.data) + } else { + state.group.dataList = res.result.data + } + }, + remove({ + group_id + }) { + let groupList = state.group.dataList + let index = groupList.findIndex(i => i.group_info._id == group_id) + // console.log('index',group_id,index,groupList) + if (index != -1) { + groupList.splice(index, 1) + // console.log('state.group.dataList',state.group.dataList) + } + }, + }, + mergeUsersInfo(usersInfo) { + state.usersInfo = Object.assign({}, state.usersInfo, usersInfo) + }, + async clearUnreadCount(conversation_id) { + let conversation = await this.conversation.get(conversation_id) + setTimeout(function() { + conversation.unread_count = 0 + }, 10); + // 触发器会触发消息表的 is_read = true + uniCloud.database() + .collection('uni-im-conversation') + .where({ + user_id: current_uid(), + id: conversation_id + }) + .update({ + "unread_count": 0 + }).then(e => { + console.log('设置为已读', e.result.updated); + }) + } +} + +const mapState = function(keys = []) { + let obj = {} + keys.forEach((key) => { + let keyName = key, + keyCName = false + if (key.includes(' as ')) { + let _key = key.trim().split(' as ') + keyName = _key[0] + keyCName = _key[1] + } + obj[keyCName || keyName] = function() { + return state[keyName] + } + }) + return obj +} + +export default deepAssign(state, methods, { + mapState +}) + + + +/** + *深度合并多个对象的方法 + */ +function deepAssign() { + let len = arguments.length, + target = arguments[0] + if (!isPlainObject(target)) { + target = {} + } + for (let i = 1; i < len; i++) { + let source = arguments[i] + if (isPlainObject(source)) { + for (let s in source) { + if (s === '__proto__' || target === source[s]) { + continue + } + if (isPlainObject(source[s])) { + target[s] = deepAssign(target[s], source[s]) + } else { + target[s] = source[s] + } + } + } + } + return target +} +/** + *判断对象是否是一个纯粹的对象 + */ +function isPlainObject(obj) { + return typeof obj === 'object' && Object.prototype.toString.call(obj) === '[object Object]' +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/lib/markdown-it.min.js b/uni-im示例/uni_modules/uni-im/lib/markdown-it.min.js new file mode 100644 index 0000000..ab490d7 --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/lib/markdown-it.min.js @@ -0,0 +1,2 @@ +function e(e){if(e.__esModule)return e;var r=Object.defineProperty({},"__esModule",{value:!0});return Object.keys(e).forEach((function(t){var n=Object.getOwnPropertyDescriptor(e,t);Object.defineProperty(r,t,n.get?n:{enumerable:!0,get:function(){return e[t]}})})),r}var r={},t={Aacute:"Á",aacute:"á",Abreve:"Ă",abreve:"ă",ac:"∾",acd:"∿",acE:"∾̳",Acirc:"Â",acirc:"â",acute:"´",Acy:"А",acy:"а",AElig:"Æ",aelig:"æ",af:"⁡",Afr:"𝔄",afr:"𝔞",Agrave:"À",agrave:"à",alefsym:"ℵ",aleph:"ℵ",Alpha:"Α",alpha:"α",Amacr:"Ā",amacr:"ā",amalg:"⨿",amp:"&",AMP:"&",andand:"⩕",And:"⩓",and:"∧",andd:"⩜",andslope:"⩘",andv:"⩚",ang:"∠",ange:"⦤",angle:"∠",angmsdaa:"⦨",angmsdab:"⦩",angmsdac:"⦪",angmsdad:"⦫",angmsdae:"⦬",angmsdaf:"⦭",angmsdag:"⦮",angmsdah:"⦯",angmsd:"∡",angrt:"∟",angrtvb:"⊾",angrtvbd:"⦝",angsph:"∢",angst:"Å",angzarr:"⍼",Aogon:"Ą",aogon:"ą",Aopf:"𝔸",aopf:"𝕒",apacir:"⩯",ap:"≈",apE:"⩰",ape:"≊",apid:"≋",apos:"'",ApplyFunction:"⁡",approx:"≈",approxeq:"≊",Aring:"Å",aring:"å",Ascr:"𝒜",ascr:"𝒶",Assign:"≔",ast:"*",asymp:"≈",asympeq:"≍",Atilde:"Ã",atilde:"ã",Auml:"Ä",auml:"ä",awconint:"∳",awint:"⨑",backcong:"≌",backepsilon:"϶",backprime:"‵",backsim:"∽",backsimeq:"⋍",Backslash:"∖",Barv:"⫧",barvee:"⊽",barwed:"⌅",Barwed:"⌆",barwedge:"⌅",bbrk:"⎵",bbrktbrk:"⎶",bcong:"≌",Bcy:"Б",bcy:"б",bdquo:"„",becaus:"∵",because:"∵",Because:"∵",bemptyv:"⦰",bepsi:"϶",bernou:"ℬ",Bernoullis:"ℬ",Beta:"Β",beta:"β",beth:"ℶ",between:"≬",Bfr:"𝔅",bfr:"𝔟",bigcap:"⋂",bigcirc:"◯",bigcup:"⋃",bigodot:"⨀",bigoplus:"⨁",bigotimes:"⨂",bigsqcup:"⨆",bigstar:"★",bigtriangledown:"▽",bigtriangleup:"△",biguplus:"⨄",bigvee:"⋁",bigwedge:"⋀",bkarow:"⤍",blacklozenge:"⧫",blacksquare:"▪",blacktriangle:"▴",blacktriangledown:"▾",blacktriangleleft:"◂",blacktriangleright:"▸",blank:"␣",blk12:"▒",blk14:"░",blk34:"▓",block:"█",bne:"=⃥",bnequiv:"≡⃥",bNot:"⫭",bnot:"⌐",Bopf:"𝔹",bopf:"𝕓",bot:"⊥",bottom:"⊥",bowtie:"⋈",boxbox:"⧉",boxdl:"┐",boxdL:"╕",boxDl:"╖",boxDL:"╗",boxdr:"┌",boxdR:"╒",boxDr:"╓",boxDR:"╔",boxh:"─",boxH:"═",boxhd:"┬",boxHd:"╤",boxhD:"╥",boxHD:"╦",boxhu:"┴",boxHu:"╧",boxhU:"╨",boxHU:"╩",boxminus:"⊟",boxplus:"⊞",boxtimes:"⊠",boxul:"┘",boxuL:"╛",boxUl:"╜",boxUL:"╝",boxur:"└",boxuR:"╘",boxUr:"╙",boxUR:"╚",boxv:"│",boxV:"║",boxvh:"┼",boxvH:"╪",boxVh:"╫",boxVH:"╬",boxvl:"┤",boxvL:"╡",boxVl:"╢",boxVL:"╣",boxvr:"├",boxvR:"╞",boxVr:"╟",boxVR:"╠",bprime:"‵",breve:"˘",Breve:"˘",brvbar:"¦",bscr:"𝒷",Bscr:"ℬ",bsemi:"⁏",bsim:"∽",bsime:"⋍",bsolb:"⧅",bsol:"\\",bsolhsub:"⟈",bull:"•",bullet:"•",bump:"≎",bumpE:"⪮",bumpe:"≏",Bumpeq:"≎",bumpeq:"≏",Cacute:"Ć",cacute:"ć",capand:"⩄",capbrcup:"⩉",capcap:"⩋",cap:"∩",Cap:"⋒",capcup:"⩇",capdot:"⩀",CapitalDifferentialD:"ⅅ",caps:"∩︀",caret:"⁁",caron:"ˇ",Cayleys:"ℭ",ccaps:"⩍",Ccaron:"Č",ccaron:"č",Ccedil:"Ç",ccedil:"ç",Ccirc:"Ĉ",ccirc:"ĉ",Cconint:"∰",ccups:"⩌",ccupssm:"⩐",Cdot:"Ċ",cdot:"ċ",cedil:"¸",Cedilla:"¸",cemptyv:"⦲",cent:"¢",centerdot:"·",CenterDot:"·",cfr:"𝔠",Cfr:"ℭ",CHcy:"Ч",chcy:"ч",check:"✓",checkmark:"✓",Chi:"Χ",chi:"χ",circ:"ˆ",circeq:"≗",circlearrowleft:"↺",circlearrowright:"↻",circledast:"⊛",circledcirc:"⊚",circleddash:"⊝",CircleDot:"⊙",circledR:"®",circledS:"Ⓢ",CircleMinus:"⊖",CirclePlus:"⊕",CircleTimes:"⊗",cir:"○",cirE:"⧃",cire:"≗",cirfnint:"⨐",cirmid:"⫯",cirscir:"⧂",ClockwiseContourIntegral:"∲",CloseCurlyDoubleQuote:"”",CloseCurlyQuote:"’",clubs:"♣",clubsuit:"♣",colon:":",Colon:"∷",Colone:"⩴",colone:"≔",coloneq:"≔",comma:",",commat:"@",comp:"∁",compfn:"∘",complement:"∁",complexes:"ℂ",cong:"≅",congdot:"⩭",Congruent:"≡",conint:"∮",Conint:"∯",ContourIntegral:"∮",copf:"𝕔",Copf:"ℂ",coprod:"∐",Coproduct:"∐",copy:"©",COPY:"©",copysr:"℗",CounterClockwiseContourIntegral:"∳",crarr:"↵",cross:"✗",Cross:"⨯",Cscr:"𝒞",cscr:"𝒸",csub:"⫏",csube:"⫑",csup:"⫐",csupe:"⫒",ctdot:"⋯",cudarrl:"⤸",cudarrr:"⤵",cuepr:"⋞",cuesc:"⋟",cularr:"↶",cularrp:"⤽",cupbrcap:"⩈",cupcap:"⩆",CupCap:"≍",cup:"∪",Cup:"⋓",cupcup:"⩊",cupdot:"⊍",cupor:"⩅",cups:"∪︀",curarr:"↷",curarrm:"⤼",curlyeqprec:"⋞",curlyeqsucc:"⋟",curlyvee:"⋎",curlywedge:"⋏",curren:"¤",curvearrowleft:"↶",curvearrowright:"↷",cuvee:"⋎",cuwed:"⋏",cwconint:"∲",cwint:"∱",cylcty:"⌭",dagger:"†",Dagger:"‡",daleth:"ℸ",darr:"↓",Darr:"↡",dArr:"⇓",dash:"‐",Dashv:"⫤",dashv:"⊣",dbkarow:"⤏",dblac:"˝",Dcaron:"Ď",dcaron:"ď",Dcy:"Д",dcy:"д",ddagger:"‡",ddarr:"⇊",DD:"ⅅ",dd:"ⅆ",DDotrahd:"⤑",ddotseq:"⩷",deg:"°",Del:"∇",Delta:"Δ",delta:"δ",demptyv:"⦱",dfisht:"⥿",Dfr:"𝔇",dfr:"𝔡",dHar:"⥥",dharl:"⇃",dharr:"⇂",DiacriticalAcute:"´",DiacriticalDot:"˙",DiacriticalDoubleAcute:"˝",DiacriticalGrave:"`",DiacriticalTilde:"˜",diam:"⋄",diamond:"⋄",Diamond:"⋄",diamondsuit:"♦",diams:"♦",die:"¨",DifferentialD:"ⅆ",digamma:"ϝ",disin:"⋲",div:"÷",divide:"÷",divideontimes:"⋇",divonx:"⋇",DJcy:"Ђ",djcy:"ђ",dlcorn:"⌞",dlcrop:"⌍",dollar:"$",Dopf:"𝔻",dopf:"𝕕",Dot:"¨",dot:"˙",DotDot:"⃜",doteq:"≐",doteqdot:"≑",DotEqual:"≐",dotminus:"∸",dotplus:"∔",dotsquare:"⊡",doublebarwedge:"⌆",DoubleContourIntegral:"∯",DoubleDot:"¨",DoubleDownArrow:"⇓",DoubleLeftArrow:"⇐",DoubleLeftRightArrow:"⇔",DoubleLeftTee:"⫤",DoubleLongLeftArrow:"⟸",DoubleLongLeftRightArrow:"⟺",DoubleLongRightArrow:"⟹",DoubleRightArrow:"⇒",DoubleRightTee:"⊨",DoubleUpArrow:"⇑",DoubleUpDownArrow:"⇕",DoubleVerticalBar:"∥",DownArrowBar:"⤓",downarrow:"↓",DownArrow:"↓",Downarrow:"⇓",DownArrowUpArrow:"⇵",DownBreve:"̑",downdownarrows:"⇊",downharpoonleft:"⇃",downharpoonright:"⇂",DownLeftRightVector:"⥐",DownLeftTeeVector:"⥞",DownLeftVectorBar:"⥖",DownLeftVector:"↽",DownRightTeeVector:"⥟",DownRightVectorBar:"⥗",DownRightVector:"⇁",DownTeeArrow:"↧",DownTee:"⊤",drbkarow:"⤐",drcorn:"⌟",drcrop:"⌌",Dscr:"𝒟",dscr:"𝒹",DScy:"Ѕ",dscy:"ѕ",dsol:"⧶",Dstrok:"Đ",dstrok:"đ",dtdot:"⋱",dtri:"▿",dtrif:"▾",duarr:"⇵",duhar:"⥯",dwangle:"⦦",DZcy:"Џ",dzcy:"џ",dzigrarr:"⟿",Eacute:"É",eacute:"é",easter:"⩮",Ecaron:"Ě",ecaron:"ě",Ecirc:"Ê",ecirc:"ê",ecir:"≖",ecolon:"≕",Ecy:"Э",ecy:"э",eDDot:"⩷",Edot:"Ė",edot:"ė",eDot:"≑",ee:"ⅇ",efDot:"≒",Efr:"𝔈",efr:"𝔢",eg:"⪚",Egrave:"È",egrave:"è",egs:"⪖",egsdot:"⪘",el:"⪙",Element:"∈",elinters:"⏧",ell:"ℓ",els:"⪕",elsdot:"⪗",Emacr:"Ē",emacr:"ē",empty:"∅",emptyset:"∅",EmptySmallSquare:"◻",emptyv:"∅",EmptyVerySmallSquare:"▫",emsp13:" ",emsp14:" ",emsp:" ",ENG:"Ŋ",eng:"ŋ",ensp:" ",Eogon:"Ę",eogon:"ę",Eopf:"𝔼",eopf:"𝕖",epar:"⋕",eparsl:"⧣",eplus:"⩱",epsi:"ε",Epsilon:"Ε",epsilon:"ε",epsiv:"ϵ",eqcirc:"≖",eqcolon:"≕",eqsim:"≂",eqslantgtr:"⪖",eqslantless:"⪕",Equal:"⩵",equals:"=",EqualTilde:"≂",equest:"≟",Equilibrium:"⇌",equiv:"≡",equivDD:"⩸",eqvparsl:"⧥",erarr:"⥱",erDot:"≓",escr:"ℯ",Escr:"ℰ",esdot:"≐",Esim:"⩳",esim:"≂",Eta:"Η",eta:"η",ETH:"Ð",eth:"ð",Euml:"Ë",euml:"ë",euro:"€",excl:"!",exist:"∃",Exists:"∃",expectation:"ℰ",exponentiale:"ⅇ",ExponentialE:"ⅇ",fallingdotseq:"≒",Fcy:"Ф",fcy:"ф",female:"♀",ffilig:"ffi",fflig:"ff",ffllig:"ffl",Ffr:"𝔉",ffr:"𝔣",filig:"fi",FilledSmallSquare:"◼",FilledVerySmallSquare:"▪",fjlig:"fj",flat:"♭",fllig:"fl",fltns:"▱",fnof:"ƒ",Fopf:"𝔽",fopf:"𝕗",forall:"∀",ForAll:"∀",fork:"⋔",forkv:"⫙",Fouriertrf:"ℱ",fpartint:"⨍",frac12:"½",frac13:"⅓",frac14:"¼",frac15:"⅕",frac16:"⅙",frac18:"⅛",frac23:"⅔",frac25:"⅖",frac34:"¾",frac35:"⅗",frac38:"⅜",frac45:"⅘",frac56:"⅚",frac58:"⅝",frac78:"⅞",frasl:"⁄",frown:"⌢",fscr:"𝒻",Fscr:"ℱ",gacute:"ǵ",Gamma:"Γ",gamma:"γ",Gammad:"Ϝ",gammad:"ϝ",gap:"⪆",Gbreve:"Ğ",gbreve:"ğ",Gcedil:"Ģ",Gcirc:"Ĝ",gcirc:"ĝ",Gcy:"Г",gcy:"г",Gdot:"Ġ",gdot:"ġ",ge:"≥",gE:"≧",gEl:"⪌",gel:"⋛",geq:"≥",geqq:"≧",geqslant:"⩾",gescc:"⪩",ges:"⩾",gesdot:"⪀",gesdoto:"⪂",gesdotol:"⪄",gesl:"⋛︀",gesles:"⪔",Gfr:"𝔊",gfr:"𝔤",gg:"≫",Gg:"⋙",ggg:"⋙",gimel:"ℷ",GJcy:"Ѓ",gjcy:"ѓ",gla:"⪥",gl:"≷",glE:"⪒",glj:"⪤",gnap:"⪊",gnapprox:"⪊",gne:"⪈",gnE:"≩",gneq:"⪈",gneqq:"≩",gnsim:"⋧",Gopf:"𝔾",gopf:"𝕘",grave:"`",GreaterEqual:"≥",GreaterEqualLess:"⋛",GreaterFullEqual:"≧",GreaterGreater:"⪢",GreaterLess:"≷",GreaterSlantEqual:"⩾",GreaterTilde:"≳",Gscr:"𝒢",gscr:"ℊ",gsim:"≳",gsime:"⪎",gsiml:"⪐",gtcc:"⪧",gtcir:"⩺",gt:">",GT:">",Gt:"≫",gtdot:"⋗",gtlPar:"⦕",gtquest:"⩼",gtrapprox:"⪆",gtrarr:"⥸",gtrdot:"⋗",gtreqless:"⋛",gtreqqless:"⪌",gtrless:"≷",gtrsim:"≳",gvertneqq:"≩︀",gvnE:"≩︀",Hacek:"ˇ",hairsp:" ",half:"½",hamilt:"ℋ",HARDcy:"Ъ",hardcy:"ъ",harrcir:"⥈",harr:"↔",hArr:"⇔",harrw:"↭",Hat:"^",hbar:"ℏ",Hcirc:"Ĥ",hcirc:"ĥ",hearts:"♥",heartsuit:"♥",hellip:"…",hercon:"⊹",hfr:"𝔥",Hfr:"ℌ",HilbertSpace:"ℋ",hksearow:"⤥",hkswarow:"⤦",hoarr:"⇿",homtht:"∻",hookleftarrow:"↩",hookrightarrow:"↪",hopf:"𝕙",Hopf:"ℍ",horbar:"―",HorizontalLine:"─",hscr:"𝒽",Hscr:"ℋ",hslash:"ℏ",Hstrok:"Ħ",hstrok:"ħ",HumpDownHump:"≎",HumpEqual:"≏",hybull:"⁃",hyphen:"‐",Iacute:"Í",iacute:"í",ic:"⁣",Icirc:"Î",icirc:"î",Icy:"И",icy:"и",Idot:"İ",IEcy:"Е",iecy:"е",iexcl:"¡",iff:"⇔",ifr:"𝔦",Ifr:"ℑ",Igrave:"Ì",igrave:"ì",ii:"ⅈ",iiiint:"⨌",iiint:"∭",iinfin:"⧜",iiota:"℩",IJlig:"IJ",ijlig:"ij",Imacr:"Ī",imacr:"ī",image:"ℑ",ImaginaryI:"ⅈ",imagline:"ℐ",imagpart:"ℑ",imath:"ı",Im:"ℑ",imof:"⊷",imped:"Ƶ",Implies:"⇒",incare:"℅",in:"∈",infin:"∞",infintie:"⧝",inodot:"ı",intcal:"⊺",int:"∫",Int:"∬",integers:"ℤ",Integral:"∫",intercal:"⊺",Intersection:"⋂",intlarhk:"⨗",intprod:"⨼",InvisibleComma:"⁣",InvisibleTimes:"⁢",IOcy:"Ё",iocy:"ё",Iogon:"Į",iogon:"į",Iopf:"𝕀",iopf:"𝕚",Iota:"Ι",iota:"ι",iprod:"⨼",iquest:"¿",iscr:"𝒾",Iscr:"ℐ",isin:"∈",isindot:"⋵",isinE:"⋹",isins:"⋴",isinsv:"⋳",isinv:"∈",it:"⁢",Itilde:"Ĩ",itilde:"ĩ",Iukcy:"І",iukcy:"і",Iuml:"Ï",iuml:"ï",Jcirc:"Ĵ",jcirc:"ĵ",Jcy:"Й",jcy:"й",Jfr:"𝔍",jfr:"𝔧",jmath:"ȷ",Jopf:"𝕁",jopf:"𝕛",Jscr:"𝒥",jscr:"𝒿",Jsercy:"Ј",jsercy:"ј",Jukcy:"Є",jukcy:"є",Kappa:"Κ",kappa:"κ",kappav:"ϰ",Kcedil:"Ķ",kcedil:"ķ",Kcy:"К",kcy:"к",Kfr:"𝔎",kfr:"𝔨",kgreen:"ĸ",KHcy:"Х",khcy:"х",KJcy:"Ќ",kjcy:"ќ",Kopf:"𝕂",kopf:"𝕜",Kscr:"𝒦",kscr:"𝓀",lAarr:"⇚",Lacute:"Ĺ",lacute:"ĺ",laemptyv:"⦴",lagran:"ℒ",Lambda:"Λ",lambda:"λ",lang:"⟨",Lang:"⟪",langd:"⦑",langle:"⟨",lap:"⪅",Laplacetrf:"ℒ",laquo:"«",larrb:"⇤",larrbfs:"⤟",larr:"←",Larr:"↞",lArr:"⇐",larrfs:"⤝",larrhk:"↩",larrlp:"↫",larrpl:"⤹",larrsim:"⥳",larrtl:"↢",latail:"⤙",lAtail:"⤛",lat:"⪫",late:"⪭",lates:"⪭︀",lbarr:"⤌",lBarr:"⤎",lbbrk:"❲",lbrace:"{",lbrack:"[",lbrke:"⦋",lbrksld:"⦏",lbrkslu:"⦍",Lcaron:"Ľ",lcaron:"ľ",Lcedil:"Ļ",lcedil:"ļ",lceil:"⌈",lcub:"{",Lcy:"Л",lcy:"л",ldca:"⤶",ldquo:"“",ldquor:"„",ldrdhar:"⥧",ldrushar:"⥋",ldsh:"↲",le:"≤",lE:"≦",LeftAngleBracket:"⟨",LeftArrowBar:"⇤",leftarrow:"←",LeftArrow:"←",Leftarrow:"⇐",LeftArrowRightArrow:"⇆",leftarrowtail:"↢",LeftCeiling:"⌈",LeftDoubleBracket:"⟦",LeftDownTeeVector:"⥡",LeftDownVectorBar:"⥙",LeftDownVector:"⇃",LeftFloor:"⌊",leftharpoondown:"↽",leftharpoonup:"↼",leftleftarrows:"⇇",leftrightarrow:"↔",LeftRightArrow:"↔",Leftrightarrow:"⇔",leftrightarrows:"⇆",leftrightharpoons:"⇋",leftrightsquigarrow:"↭",LeftRightVector:"⥎",LeftTeeArrow:"↤",LeftTee:"⊣",LeftTeeVector:"⥚",leftthreetimes:"⋋",LeftTriangleBar:"⧏",LeftTriangle:"⊲",LeftTriangleEqual:"⊴",LeftUpDownVector:"⥑",LeftUpTeeVector:"⥠",LeftUpVectorBar:"⥘",LeftUpVector:"↿",LeftVectorBar:"⥒",LeftVector:"↼",lEg:"⪋",leg:"⋚",leq:"≤",leqq:"≦",leqslant:"⩽",lescc:"⪨",les:"⩽",lesdot:"⩿",lesdoto:"⪁",lesdotor:"⪃",lesg:"⋚︀",lesges:"⪓",lessapprox:"⪅",lessdot:"⋖",lesseqgtr:"⋚",lesseqqgtr:"⪋",LessEqualGreater:"⋚",LessFullEqual:"≦",LessGreater:"≶",lessgtr:"≶",LessLess:"⪡",lesssim:"≲",LessSlantEqual:"⩽",LessTilde:"≲",lfisht:"⥼",lfloor:"⌊",Lfr:"𝔏",lfr:"𝔩",lg:"≶",lgE:"⪑",lHar:"⥢",lhard:"↽",lharu:"↼",lharul:"⥪",lhblk:"▄",LJcy:"Љ",ljcy:"љ",llarr:"⇇",ll:"≪",Ll:"⋘",llcorner:"⌞",Lleftarrow:"⇚",llhard:"⥫",lltri:"◺",Lmidot:"Ŀ",lmidot:"ŀ",lmoustache:"⎰",lmoust:"⎰",lnap:"⪉",lnapprox:"⪉",lne:"⪇",lnE:"≨",lneq:"⪇",lneqq:"≨",lnsim:"⋦",loang:"⟬",loarr:"⇽",lobrk:"⟦",longleftarrow:"⟵",LongLeftArrow:"⟵",Longleftarrow:"⟸",longleftrightarrow:"⟷",LongLeftRightArrow:"⟷",Longleftrightarrow:"⟺",longmapsto:"⟼",longrightarrow:"⟶",LongRightArrow:"⟶",Longrightarrow:"⟹",looparrowleft:"↫",looparrowright:"↬",lopar:"⦅",Lopf:"𝕃",lopf:"𝕝",loplus:"⨭",lotimes:"⨴",lowast:"∗",lowbar:"_",LowerLeftArrow:"↙",LowerRightArrow:"↘",loz:"◊",lozenge:"◊",lozf:"⧫",lpar:"(",lparlt:"⦓",lrarr:"⇆",lrcorner:"⌟",lrhar:"⇋",lrhard:"⥭",lrm:"‎",lrtri:"⊿",lsaquo:"‹",lscr:"𝓁",Lscr:"ℒ",lsh:"↰",Lsh:"↰",lsim:"≲",lsime:"⪍",lsimg:"⪏",lsqb:"[",lsquo:"‘",lsquor:"‚",Lstrok:"Ł",lstrok:"ł",ltcc:"⪦",ltcir:"⩹",lt:"<",LT:"<",Lt:"≪",ltdot:"⋖",lthree:"⋋",ltimes:"⋉",ltlarr:"⥶",ltquest:"⩻",ltri:"◃",ltrie:"⊴",ltrif:"◂",ltrPar:"⦖",lurdshar:"⥊",luruhar:"⥦",lvertneqq:"≨︀",lvnE:"≨︀",macr:"¯",male:"♂",malt:"✠",maltese:"✠",Map:"⤅",map:"↦",mapsto:"↦",mapstodown:"↧",mapstoleft:"↤",mapstoup:"↥",marker:"▮",mcomma:"⨩",Mcy:"М",mcy:"м",mdash:"—",mDDot:"∺",measuredangle:"∡",MediumSpace:" ",Mellintrf:"ℳ",Mfr:"𝔐",mfr:"𝔪",mho:"℧",micro:"µ",midast:"*",midcir:"⫰",mid:"∣",middot:"·",minusb:"⊟",minus:"−",minusd:"∸",minusdu:"⨪",MinusPlus:"∓",mlcp:"⫛",mldr:"…",mnplus:"∓",models:"⊧",Mopf:"𝕄",mopf:"𝕞",mp:"∓",mscr:"𝓂",Mscr:"ℳ",mstpos:"∾",Mu:"Μ",mu:"μ",multimap:"⊸",mumap:"⊸",nabla:"∇",Nacute:"Ń",nacute:"ń",nang:"∠⃒",nap:"≉",napE:"⩰̸",napid:"≋̸",napos:"ʼn",napprox:"≉",natural:"♮",naturals:"ℕ",natur:"♮",nbsp:" ",nbump:"≎̸",nbumpe:"≏̸",ncap:"⩃",Ncaron:"Ň",ncaron:"ň",Ncedil:"Ņ",ncedil:"ņ",ncong:"≇",ncongdot:"⩭̸",ncup:"⩂",Ncy:"Н",ncy:"н",ndash:"–",nearhk:"⤤",nearr:"↗",neArr:"⇗",nearrow:"↗",ne:"≠",nedot:"≐̸",NegativeMediumSpace:"​",NegativeThickSpace:"​",NegativeThinSpace:"​",NegativeVeryThinSpace:"​",nequiv:"≢",nesear:"⤨",nesim:"≂̸",NestedGreaterGreater:"≫",NestedLessLess:"≪",NewLine:"\n",nexist:"∄",nexists:"∄",Nfr:"𝔑",nfr:"𝔫",ngE:"≧̸",nge:"≱",ngeq:"≱",ngeqq:"≧̸",ngeqslant:"⩾̸",nges:"⩾̸",nGg:"⋙̸",ngsim:"≵",nGt:"≫⃒",ngt:"≯",ngtr:"≯",nGtv:"≫̸",nharr:"↮",nhArr:"⇎",nhpar:"⫲",ni:"∋",nis:"⋼",nisd:"⋺",niv:"∋",NJcy:"Њ",njcy:"њ",nlarr:"↚",nlArr:"⇍",nldr:"‥",nlE:"≦̸",nle:"≰",nleftarrow:"↚",nLeftarrow:"⇍",nleftrightarrow:"↮",nLeftrightarrow:"⇎",nleq:"≰",nleqq:"≦̸",nleqslant:"⩽̸",nles:"⩽̸",nless:"≮",nLl:"⋘̸",nlsim:"≴",nLt:"≪⃒",nlt:"≮",nltri:"⋪",nltrie:"⋬",nLtv:"≪̸",nmid:"∤",NoBreak:"⁠",NonBreakingSpace:" ",nopf:"𝕟",Nopf:"ℕ",Not:"⫬",not:"¬",NotCongruent:"≢",NotCupCap:"≭",NotDoubleVerticalBar:"∦",NotElement:"∉",NotEqual:"≠",NotEqualTilde:"≂̸",NotExists:"∄",NotGreater:"≯",NotGreaterEqual:"≱",NotGreaterFullEqual:"≧̸",NotGreaterGreater:"≫̸",NotGreaterLess:"≹",NotGreaterSlantEqual:"⩾̸",NotGreaterTilde:"≵",NotHumpDownHump:"≎̸",NotHumpEqual:"≏̸",notin:"∉",notindot:"⋵̸",notinE:"⋹̸",notinva:"∉",notinvb:"⋷",notinvc:"⋶",NotLeftTriangleBar:"⧏̸",NotLeftTriangle:"⋪",NotLeftTriangleEqual:"⋬",NotLess:"≮",NotLessEqual:"≰",NotLessGreater:"≸",NotLessLess:"≪̸",NotLessSlantEqual:"⩽̸",NotLessTilde:"≴",NotNestedGreaterGreater:"⪢̸",NotNestedLessLess:"⪡̸",notni:"∌",notniva:"∌",notnivb:"⋾",notnivc:"⋽",NotPrecedes:"⊀",NotPrecedesEqual:"⪯̸",NotPrecedesSlantEqual:"⋠",NotReverseElement:"∌",NotRightTriangleBar:"⧐̸",NotRightTriangle:"⋫",NotRightTriangleEqual:"⋭",NotSquareSubset:"⊏̸",NotSquareSubsetEqual:"⋢",NotSquareSuperset:"⊐̸",NotSquareSupersetEqual:"⋣",NotSubset:"⊂⃒",NotSubsetEqual:"⊈",NotSucceeds:"⊁",NotSucceedsEqual:"⪰̸",NotSucceedsSlantEqual:"⋡",NotSucceedsTilde:"≿̸",NotSuperset:"⊃⃒",NotSupersetEqual:"⊉",NotTilde:"≁",NotTildeEqual:"≄",NotTildeFullEqual:"≇",NotTildeTilde:"≉",NotVerticalBar:"∤",nparallel:"∦",npar:"∦",nparsl:"⫽⃥",npart:"∂̸",npolint:"⨔",npr:"⊀",nprcue:"⋠",nprec:"⊀",npreceq:"⪯̸",npre:"⪯̸",nrarrc:"⤳̸",nrarr:"↛",nrArr:"⇏",nrarrw:"↝̸",nrightarrow:"↛",nRightarrow:"⇏",nrtri:"⋫",nrtrie:"⋭",nsc:"⊁",nsccue:"⋡",nsce:"⪰̸",Nscr:"𝒩",nscr:"𝓃",nshortmid:"∤",nshortparallel:"∦",nsim:"≁",nsime:"≄",nsimeq:"≄",nsmid:"∤",nspar:"∦",nsqsube:"⋢",nsqsupe:"⋣",nsub:"⊄",nsubE:"⫅̸",nsube:"⊈",nsubset:"⊂⃒",nsubseteq:"⊈",nsubseteqq:"⫅̸",nsucc:"⊁",nsucceq:"⪰̸",nsup:"⊅",nsupE:"⫆̸",nsupe:"⊉",nsupset:"⊃⃒",nsupseteq:"⊉",nsupseteqq:"⫆̸",ntgl:"≹",Ntilde:"Ñ",ntilde:"ñ",ntlg:"≸",ntriangleleft:"⋪",ntrianglelefteq:"⋬",ntriangleright:"⋫",ntrianglerighteq:"⋭",Nu:"Ν",nu:"ν",num:"#",numero:"№",numsp:" ",nvap:"≍⃒",nvdash:"⊬",nvDash:"⊭",nVdash:"⊮",nVDash:"⊯",nvge:"≥⃒",nvgt:">⃒",nvHarr:"⤄",nvinfin:"⧞",nvlArr:"⤂",nvle:"≤⃒",nvlt:"<⃒",nvltrie:"⊴⃒",nvrArr:"⤃",nvrtrie:"⊵⃒",nvsim:"∼⃒",nwarhk:"⤣",nwarr:"↖",nwArr:"⇖",nwarrow:"↖",nwnear:"⤧",Oacute:"Ó",oacute:"ó",oast:"⊛",Ocirc:"Ô",ocirc:"ô",ocir:"⊚",Ocy:"О",ocy:"о",odash:"⊝",Odblac:"Ő",odblac:"ő",odiv:"⨸",odot:"⊙",odsold:"⦼",OElig:"Œ",oelig:"œ",ofcir:"⦿",Ofr:"𝔒",ofr:"𝔬",ogon:"˛",Ograve:"Ò",ograve:"ò",ogt:"⧁",ohbar:"⦵",ohm:"Ω",oint:"∮",olarr:"↺",olcir:"⦾",olcross:"⦻",oline:"‾",olt:"⧀",Omacr:"Ō",omacr:"ō",Omega:"Ω",omega:"ω",Omicron:"Ο",omicron:"ο",omid:"⦶",ominus:"⊖",Oopf:"𝕆",oopf:"𝕠",opar:"⦷",OpenCurlyDoubleQuote:"“",OpenCurlyQuote:"‘",operp:"⦹",oplus:"⊕",orarr:"↻",Or:"⩔",or:"∨",ord:"⩝",order:"ℴ",orderof:"ℴ",ordf:"ª",ordm:"º",origof:"⊶",oror:"⩖",orslope:"⩗",orv:"⩛",oS:"Ⓢ",Oscr:"𝒪",oscr:"ℴ",Oslash:"Ø",oslash:"ø",osol:"⊘",Otilde:"Õ",otilde:"õ",otimesas:"⨶",Otimes:"⨷",otimes:"⊗",Ouml:"Ö",ouml:"ö",ovbar:"⌽",OverBar:"‾",OverBrace:"⏞",OverBracket:"⎴",OverParenthesis:"⏜",para:"¶",parallel:"∥",par:"∥",parsim:"⫳",parsl:"⫽",part:"∂",PartialD:"∂",Pcy:"П",pcy:"п",percnt:"%",period:".",permil:"‰",perp:"⊥",pertenk:"‱",Pfr:"𝔓",pfr:"𝔭",Phi:"Φ",phi:"φ",phiv:"ϕ",phmmat:"ℳ",phone:"☎",Pi:"Π",pi:"π",pitchfork:"⋔",piv:"ϖ",planck:"ℏ",planckh:"ℎ",plankv:"ℏ",plusacir:"⨣",plusb:"⊞",pluscir:"⨢",plus:"+",plusdo:"∔",plusdu:"⨥",pluse:"⩲",PlusMinus:"±",plusmn:"±",plussim:"⨦",plustwo:"⨧",pm:"±",Poincareplane:"ℌ",pointint:"⨕",popf:"𝕡",Popf:"ℙ",pound:"£",prap:"⪷",Pr:"⪻",pr:"≺",prcue:"≼",precapprox:"⪷",prec:"≺",preccurlyeq:"≼",Precedes:"≺",PrecedesEqual:"⪯",PrecedesSlantEqual:"≼",PrecedesTilde:"≾",preceq:"⪯",precnapprox:"⪹",precneqq:"⪵",precnsim:"⋨",pre:"⪯",prE:"⪳",precsim:"≾",prime:"′",Prime:"″",primes:"ℙ",prnap:"⪹",prnE:"⪵",prnsim:"⋨",prod:"∏",Product:"∏",profalar:"⌮",profline:"⌒",profsurf:"⌓",prop:"∝",Proportional:"∝",Proportion:"∷",propto:"∝",prsim:"≾",prurel:"⊰",Pscr:"𝒫",pscr:"𝓅",Psi:"Ψ",psi:"ψ",puncsp:" ",Qfr:"𝔔",qfr:"𝔮",qint:"⨌",qopf:"𝕢",Qopf:"ℚ",qprime:"⁗",Qscr:"𝒬",qscr:"𝓆",quaternions:"ℍ",quatint:"⨖",quest:"?",questeq:"≟",quot:'"',QUOT:'"',rAarr:"⇛",race:"∽̱",Racute:"Ŕ",racute:"ŕ",radic:"√",raemptyv:"⦳",rang:"⟩",Rang:"⟫",rangd:"⦒",range:"⦥",rangle:"⟩",raquo:"»",rarrap:"⥵",rarrb:"⇥",rarrbfs:"⤠",rarrc:"⤳",rarr:"→",Rarr:"↠",rArr:"⇒",rarrfs:"⤞",rarrhk:"↪",rarrlp:"↬",rarrpl:"⥅",rarrsim:"⥴",Rarrtl:"⤖",rarrtl:"↣",rarrw:"↝",ratail:"⤚",rAtail:"⤜",ratio:"∶",rationals:"ℚ",rbarr:"⤍",rBarr:"⤏",RBarr:"⤐",rbbrk:"❳",rbrace:"}",rbrack:"]",rbrke:"⦌",rbrksld:"⦎",rbrkslu:"⦐",Rcaron:"Ř",rcaron:"ř",Rcedil:"Ŗ",rcedil:"ŗ",rceil:"⌉",rcub:"}",Rcy:"Р",rcy:"р",rdca:"⤷",rdldhar:"⥩",rdquo:"”",rdquor:"”",rdsh:"↳",real:"ℜ",realine:"ℛ",realpart:"ℜ",reals:"ℝ",Re:"ℜ",rect:"▭",reg:"®",REG:"®",ReverseElement:"∋",ReverseEquilibrium:"⇋",ReverseUpEquilibrium:"⥯",rfisht:"⥽",rfloor:"⌋",rfr:"𝔯",Rfr:"ℜ",rHar:"⥤",rhard:"⇁",rharu:"⇀",rharul:"⥬",Rho:"Ρ",rho:"ρ",rhov:"ϱ",RightAngleBracket:"⟩",RightArrowBar:"⇥",rightarrow:"→",RightArrow:"→",Rightarrow:"⇒",RightArrowLeftArrow:"⇄",rightarrowtail:"↣",RightCeiling:"⌉",RightDoubleBracket:"⟧",RightDownTeeVector:"⥝",RightDownVectorBar:"⥕",RightDownVector:"⇂",RightFloor:"⌋",rightharpoondown:"⇁",rightharpoonup:"⇀",rightleftarrows:"⇄",rightleftharpoons:"⇌",rightrightarrows:"⇉",rightsquigarrow:"↝",RightTeeArrow:"↦",RightTee:"⊢",RightTeeVector:"⥛",rightthreetimes:"⋌",RightTriangleBar:"⧐",RightTriangle:"⊳",RightTriangleEqual:"⊵",RightUpDownVector:"⥏",RightUpTeeVector:"⥜",RightUpVectorBar:"⥔",RightUpVector:"↾",RightVectorBar:"⥓",RightVector:"⇀",ring:"˚",risingdotseq:"≓",rlarr:"⇄",rlhar:"⇌",rlm:"‏",rmoustache:"⎱",rmoust:"⎱",rnmid:"⫮",roang:"⟭",roarr:"⇾",robrk:"⟧",ropar:"⦆",ropf:"𝕣",Ropf:"ℝ",roplus:"⨮",rotimes:"⨵",RoundImplies:"⥰",rpar:")",rpargt:"⦔",rppolint:"⨒",rrarr:"⇉",Rrightarrow:"⇛",rsaquo:"›",rscr:"𝓇",Rscr:"ℛ",rsh:"↱",Rsh:"↱",rsqb:"]",rsquo:"’",rsquor:"’",rthree:"⋌",rtimes:"⋊",rtri:"▹",rtrie:"⊵",rtrif:"▸",rtriltri:"⧎",RuleDelayed:"⧴",ruluhar:"⥨",rx:"℞",Sacute:"Ś",sacute:"ś",sbquo:"‚",scap:"⪸",Scaron:"Š",scaron:"š",Sc:"⪼",sc:"≻",sccue:"≽",sce:"⪰",scE:"⪴",Scedil:"Ş",scedil:"ş",Scirc:"Ŝ",scirc:"ŝ",scnap:"⪺",scnE:"⪶",scnsim:"⋩",scpolint:"⨓",scsim:"≿",Scy:"С",scy:"с",sdotb:"⊡",sdot:"⋅",sdote:"⩦",searhk:"⤥",searr:"↘",seArr:"⇘",searrow:"↘",sect:"§",semi:";",seswar:"⤩",setminus:"∖",setmn:"∖",sext:"✶",Sfr:"𝔖",sfr:"𝔰",sfrown:"⌢",sharp:"♯",SHCHcy:"Щ",shchcy:"щ",SHcy:"Ш",shcy:"ш",ShortDownArrow:"↓",ShortLeftArrow:"←",shortmid:"∣",shortparallel:"∥",ShortRightArrow:"→",ShortUpArrow:"↑",shy:"­",Sigma:"Σ",sigma:"σ",sigmaf:"ς",sigmav:"ς",sim:"∼",simdot:"⩪",sime:"≃",simeq:"≃",simg:"⪞",simgE:"⪠",siml:"⪝",simlE:"⪟",simne:"≆",simplus:"⨤",simrarr:"⥲",slarr:"←",SmallCircle:"∘",smallsetminus:"∖",smashp:"⨳",smeparsl:"⧤",smid:"∣",smile:"⌣",smt:"⪪",smte:"⪬",smtes:"⪬︀",SOFTcy:"Ь",softcy:"ь",solbar:"⌿",solb:"⧄",sol:"/",Sopf:"𝕊",sopf:"𝕤",spades:"♠",spadesuit:"♠",spar:"∥",sqcap:"⊓",sqcaps:"⊓︀",sqcup:"⊔",sqcups:"⊔︀",Sqrt:"√",sqsub:"⊏",sqsube:"⊑",sqsubset:"⊏",sqsubseteq:"⊑",sqsup:"⊐",sqsupe:"⊒",sqsupset:"⊐",sqsupseteq:"⊒",square:"□",Square:"□",SquareIntersection:"⊓",SquareSubset:"⊏",SquareSubsetEqual:"⊑",SquareSuperset:"⊐",SquareSupersetEqual:"⊒",SquareUnion:"⊔",squarf:"▪",squ:"□",squf:"▪",srarr:"→",Sscr:"𝒮",sscr:"𝓈",ssetmn:"∖",ssmile:"⌣",sstarf:"⋆",Star:"⋆",star:"☆",starf:"★",straightepsilon:"ϵ",straightphi:"ϕ",strns:"¯",sub:"⊂",Sub:"⋐",subdot:"⪽",subE:"⫅",sube:"⊆",subedot:"⫃",submult:"⫁",subnE:"⫋",subne:"⊊",subplus:"⪿",subrarr:"⥹",subset:"⊂",Subset:"⋐",subseteq:"⊆",subseteqq:"⫅",SubsetEqual:"⊆",subsetneq:"⊊",subsetneqq:"⫋",subsim:"⫇",subsub:"⫕",subsup:"⫓",succapprox:"⪸",succ:"≻",succcurlyeq:"≽",Succeeds:"≻",SucceedsEqual:"⪰",SucceedsSlantEqual:"≽",SucceedsTilde:"≿",succeq:"⪰",succnapprox:"⪺",succneqq:"⪶",succnsim:"⋩",succsim:"≿",SuchThat:"∋",sum:"∑",Sum:"∑",sung:"♪",sup1:"¹",sup2:"²",sup3:"³",sup:"⊃",Sup:"⋑",supdot:"⪾",supdsub:"⫘",supE:"⫆",supe:"⊇",supedot:"⫄",Superset:"⊃",SupersetEqual:"⊇",suphsol:"⟉",suphsub:"⫗",suplarr:"⥻",supmult:"⫂",supnE:"⫌",supne:"⊋",supplus:"⫀",supset:"⊃",Supset:"⋑",supseteq:"⊇",supseteqq:"⫆",supsetneq:"⊋",supsetneqq:"⫌",supsim:"⫈",supsub:"⫔",supsup:"⫖",swarhk:"⤦",swarr:"↙",swArr:"⇙",swarrow:"↙",swnwar:"⤪",szlig:"ß",Tab:"\t",target:"⌖",Tau:"Τ",tau:"τ",tbrk:"⎴",Tcaron:"Ť",tcaron:"ť",Tcedil:"Ţ",tcedil:"ţ",Tcy:"Т",tcy:"т",tdot:"⃛",telrec:"⌕",Tfr:"𝔗",tfr:"𝔱",there4:"∴",therefore:"∴",Therefore:"∴",Theta:"Θ",theta:"θ",thetasym:"ϑ",thetav:"ϑ",thickapprox:"≈",thicksim:"∼",ThickSpace:"  ",ThinSpace:" ",thinsp:" ",thkap:"≈",thksim:"∼",THORN:"Þ",thorn:"þ",tilde:"˜",Tilde:"∼",TildeEqual:"≃",TildeFullEqual:"≅",TildeTilde:"≈",timesbar:"⨱",timesb:"⊠",times:"×",timesd:"⨰",tint:"∭",toea:"⤨",topbot:"⌶",topcir:"⫱",top:"⊤",Topf:"𝕋",topf:"𝕥",topfork:"⫚",tosa:"⤩",tprime:"‴",trade:"™",TRADE:"™",triangle:"▵",triangledown:"▿",triangleleft:"◃",trianglelefteq:"⊴",triangleq:"≜",triangleright:"▹",trianglerighteq:"⊵",tridot:"◬",trie:"≜",triminus:"⨺",TripleDot:"⃛",triplus:"⨹",trisb:"⧍",tritime:"⨻",trpezium:"⏢",Tscr:"𝒯",tscr:"𝓉",TScy:"Ц",tscy:"ц",TSHcy:"Ћ",tshcy:"ћ",Tstrok:"Ŧ",tstrok:"ŧ",twixt:"≬",twoheadleftarrow:"↞",twoheadrightarrow:"↠",Uacute:"Ú",uacute:"ú",uarr:"↑",Uarr:"↟",uArr:"⇑",Uarrocir:"⥉",Ubrcy:"Ў",ubrcy:"ў",Ubreve:"Ŭ",ubreve:"ŭ",Ucirc:"Û",ucirc:"û",Ucy:"У",ucy:"у",udarr:"⇅",Udblac:"Ű",udblac:"ű",udhar:"⥮",ufisht:"⥾",Ufr:"𝔘",ufr:"𝔲",Ugrave:"Ù",ugrave:"ù",uHar:"⥣",uharl:"↿",uharr:"↾",uhblk:"▀",ulcorn:"⌜",ulcorner:"⌜",ulcrop:"⌏",ultri:"◸",Umacr:"Ū",umacr:"ū",uml:"¨",UnderBar:"_",UnderBrace:"⏟",UnderBracket:"⎵",UnderParenthesis:"⏝",Union:"⋃",UnionPlus:"⊎",Uogon:"Ų",uogon:"ų",Uopf:"𝕌",uopf:"𝕦",UpArrowBar:"⤒",uparrow:"↑",UpArrow:"↑",Uparrow:"⇑",UpArrowDownArrow:"⇅",updownarrow:"↕",UpDownArrow:"↕",Updownarrow:"⇕",UpEquilibrium:"⥮",upharpoonleft:"↿",upharpoonright:"↾",uplus:"⊎",UpperLeftArrow:"↖",UpperRightArrow:"↗",upsi:"υ",Upsi:"ϒ",upsih:"ϒ",Upsilon:"Υ",upsilon:"υ",UpTeeArrow:"↥",UpTee:"⊥",upuparrows:"⇈",urcorn:"⌝",urcorner:"⌝",urcrop:"⌎",Uring:"Ů",uring:"ů",urtri:"◹",Uscr:"𝒰",uscr:"𝓊",utdot:"⋰",Utilde:"Ũ",utilde:"ũ",utri:"▵",utrif:"▴",uuarr:"⇈",Uuml:"Ü",uuml:"ü",uwangle:"⦧",vangrt:"⦜",varepsilon:"ϵ",varkappa:"ϰ",varnothing:"∅",varphi:"ϕ",varpi:"ϖ",varpropto:"∝",varr:"↕",vArr:"⇕",varrho:"ϱ",varsigma:"ς",varsubsetneq:"⊊︀",varsubsetneqq:"⫋︀",varsupsetneq:"⊋︀",varsupsetneqq:"⫌︀",vartheta:"ϑ",vartriangleleft:"⊲",vartriangleright:"⊳",vBar:"⫨",Vbar:"⫫",vBarv:"⫩",Vcy:"В",vcy:"в",vdash:"⊢",vDash:"⊨",Vdash:"⊩",VDash:"⊫",Vdashl:"⫦",veebar:"⊻",vee:"∨",Vee:"⋁",veeeq:"≚",vellip:"⋮",verbar:"|",Verbar:"‖",vert:"|",Vert:"‖",VerticalBar:"∣",VerticalLine:"|",VerticalSeparator:"❘",VerticalTilde:"≀",VeryThinSpace:" ",Vfr:"𝔙",vfr:"𝔳",vltri:"⊲",vnsub:"⊂⃒",vnsup:"⊃⃒",Vopf:"𝕍",vopf:"𝕧",vprop:"∝",vrtri:"⊳",Vscr:"𝒱",vscr:"𝓋",vsubnE:"⫋︀",vsubne:"⊊︀",vsupnE:"⫌︀",vsupne:"⊋︀",Vvdash:"⊪",vzigzag:"⦚",Wcirc:"Ŵ",wcirc:"ŵ",wedbar:"⩟",wedge:"∧",Wedge:"⋀",wedgeq:"≙",weierp:"℘",Wfr:"𝔚",wfr:"𝔴",Wopf:"𝕎",wopf:"𝕨",wp:"℘",wr:"≀",wreath:"≀",Wscr:"𝒲",wscr:"𝓌",xcap:"⋂",xcirc:"◯",xcup:"⋃",xdtri:"▽",Xfr:"𝔛",xfr:"𝔵",xharr:"⟷",xhArr:"⟺",Xi:"Ξ",xi:"ξ",xlarr:"⟵",xlArr:"⟸",xmap:"⟼",xnis:"⋻",xodot:"⨀",Xopf:"𝕏",xopf:"𝕩",xoplus:"⨁",xotime:"⨂",xrarr:"⟶",xrArr:"⟹",Xscr:"𝒳",xscr:"𝓍",xsqcup:"⨆",xuplus:"⨄",xutri:"△",xvee:"⋁",xwedge:"⋀",Yacute:"Ý",yacute:"ý",YAcy:"Я",yacy:"я",Ycirc:"Ŷ",ycirc:"ŷ",Ycy:"Ы",ycy:"ы",yen:"¥",Yfr:"𝔜",yfr:"𝔶",YIcy:"Ї",yicy:"ї",Yopf:"𝕐",yopf:"𝕪",Yscr:"𝒴",yscr:"𝓎",YUcy:"Ю",yucy:"ю",yuml:"ÿ",Yuml:"Ÿ",Zacute:"Ź",zacute:"ź",Zcaron:"Ž",zcaron:"ž",Zcy:"З",zcy:"з",Zdot:"Ż",zdot:"ż",zeetrf:"ℨ",ZeroWidthSpace:"​",Zeta:"Ζ",zeta:"ζ",zfr:"𝔷",Zfr:"ℨ",ZHcy:"Ж",zhcy:"ж",zigrarr:"⇝",zopf:"𝕫",Zopf:"ℤ",Zscr:"𝒵",zscr:"𝓏",zwj:"‍",zwnj:"‌"},n=/[!-#%-\*,-\/:;\?@\[-\]_\{\}\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u09FD\u0A76\u0AF0\u0C84\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E4E\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD803[\uDF55-\uDF59]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDC4B-\uDC4F\uDC5B\uDC5D\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDE60-\uDE6C\uDF3C-\uDF3E]|\uD806[\uDC3B\uDE3F-\uDE46\uDE9A-\uDE9C\uDE9E-\uDEA2]|\uD807[\uDC41-\uDC45\uDC70\uDC71\uDEF7\uDEF8]|\uD809[\uDC70-\uDC74]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD81B[\uDE97-\uDE9A]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]|\uD83A[\uDD5E\uDD5F]/,s={},o={};function i(e,r,t){var n,s,a,c,l,u="";for("string"!=typeof r&&(t=r,r=i.defaultChars),void 0===t&&(t=!0),l=function(e){var r,t,n=o[e];if(n)return n;for(n=o[e]=[],r=0;r<128;r++)t=String.fromCharCode(r),/^[0-9a-z]$/i.test(t)?n.push(t):n.push("%"+("0"+r.toString(16).toUpperCase()).slice(-2));for(r=0;r=55296&&a<=57343){if(a>=55296&&a<=56319&&n+1=56320&&c<=57343){u+=encodeURIComponent(e[n]+e[n+1]),n++;continue}u+="%EF%BF%BD"}else u+=encodeURIComponent(e[n]);return u}i.defaultChars=";/?:@&=+$,-_.!~*'()#",i.componentChars="-_.!~*'()";var a=i,c={};function l(e,r){var t;return"string"!=typeof r&&(r=l.defaultChars),t=function(e){var r,t,n=c[e];if(n)return n;for(n=c[e]=[],r=0;r<128;r++)t=String.fromCharCode(r),n.push(t);for(r=0;r=55296&&c<=57343?"���":String.fromCharCode(c),r+=6):240==(248&s)&&r+91114111?l+="����":(c-=65536,l+=String.fromCharCode(55296+(c>>10),56320+(1023&c))),r+=9):l+="�";return l}))}l.defaultChars=";/?:@&=+$,#",l.componentChars="";var u=l;function p(){this.protocol=null,this.slashes=null,this.auth=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.pathname=null}var h=/^([a-z0-9.+-]+:)/i,f=/:[0-9]*$/,d=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,m=["{","}","|","\\","^","`"].concat(["<",">",'"',"`"," ","\r","\n","\t"]),g=["'"].concat(m),_=["%","/","?",";","#"].concat(g),k=["/","?","#"],b=/^[+a-z0-9A-Z_-]{0,63}$/,v=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,C={javascript:!0,"javascript:":!0},y={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0};p.prototype.parse=function(e,r){var t,n,s,o,i,a=e;if(a=a.trim(),!r&&1===e.split("#").length){var c=d.exec(a);if(c)return this.pathname=c[1],c[2]&&(this.search=c[2]),this}var l=h.exec(a);if(l&&(s=(l=l[0]).toLowerCase(),this.protocol=l,a=a.substr(l.length)),(r||l||a.match(/^\/\/[^@\/]+@[^@\/]+/))&&(!(i="//"===a.substr(0,2))||l&&C[l]||(a=a.substr(2),this.slashes=!0)),!C[l]&&(i||l&&!y[l])){var u,p,f=-1;for(t=0;t127?D+="x":D+=x[w];if(!D.match(b)){var q=A.slice(0,t),S=A.slice(t+1),F=x.match(v);F&&(q.push(F[1]),S.unshift(F[2])),S.length&&(a=S.join(".")+a),this.hostname=q.join(".");break}}}}this.hostname.length>255&&(this.hostname=""),g&&(this.hostname=this.hostname.substr(1,this.hostname.length-2))}var L=a.indexOf("#");-1!==L&&(this.hash=a.substr(L),a=a.slice(0,L));var z=a.indexOf("?");return-1!==z&&(this.search=a.substr(z),a=a.slice(0,z)),a&&(this.pathname=a),y[s]&&this.hostname&&!this.pathname&&(this.pathname=""),this},p.prototype.parseHost=function(e){var r=f.exec(e);r&&(":"!==(r=r[0])&&(this.port=r.substr(1)),e=e.substr(0,e.length-r.length)),e&&(this.hostname=e)};var A=function(e,r){if(e&&e instanceof p)return e;var t=new p;return t.parse(e,r),t};s.encode=a,s.decode=u,s.format=function(e){var r="";return r+=e.protocol||"",r+=e.slashes?"//":"",r+=e.auth?e.auth+"@":"",e.hostname&&-1!==e.hostname.indexOf(":")?r+="["+e.hostname+"]":r+=e.hostname||"",r+=e.port?":"+e.port:"",r+=e.pathname||"",r+=e.search||"",r+=e.hash||""},s.parse=A;var x={},D=/[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,w=/[\0-\x1F\x7F-\x9F]/,E=/[ \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]/;x.Any=D,x.Cc=w,x.Cf=/[\xAD\u0600-\u0605\u061C\u06DD\u070F\u08E2\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF\uFFF9-\uFFFB]|\uD804[\uDCBD\uDCCD]|\uD82F[\uDCA0-\uDCA3]|\uD834[\uDD73-\uDD7A]|\uDB40[\uDC01\uDC20-\uDC7F]/,x.P=n,x.Z=E,function(e){var r=Object.prototype.hasOwnProperty;function o(e,t){return r.call(e,t)}function i(e){return!(e>=55296&&e<=57343)&&(!(e>=64976&&e<=65007)&&(65535!=(65535&e)&&65534!=(65535&e)&&(!(e>=0&&e<=8)&&(11!==e&&(!(e>=14&&e<=31)&&(!(e>=127&&e<=159)&&!(e>1114111)))))))}function a(e){if(e>65535){var r=55296+((e-=65536)>>10),t=56320+(1023&e);return String.fromCharCode(r,t)}return String.fromCharCode(e)}var c=/\\([!"#$%&'()*+,\-.\/:;<=>?@[\\\]^_`{|}~])/g,l=new RegExp(c.source+"|"+/&([a-z#][a-z0-9]{1,31});/gi.source,"gi"),u=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))/i,p=t;var h=/[&<>"]/,f=/[&<>"]/g,d={"&":"&","<":"<",">":">",'"':"""};function m(e){return d[e]}var g=/[.?*+^$[\]\\(){}|-]/g;var _=n;e.lib={},e.lib.mdurl=s,e.lib.ucmicro=x,e.assign=function(e){var r=Array.prototype.slice.call(arguments,1);return r.forEach((function(r){if(r){if("object"!=typeof r)throw new TypeError(r+"must be object");Object.keys(r).forEach((function(t){e[t]=r[t]}))}})),e},e.isString=function(e){return"[object String]"===function(e){return Object.prototype.toString.call(e)}(e)},e.has=o,e.unescapeMd=function(e){return e.indexOf("\\")<0?e:e.replace(c,"$1")},e.unescapeAll=function(e){return e.indexOf("\\")<0&&e.indexOf("&")<0?e:e.replace(l,(function(e,r,t){return r||function(e,r){var t=0;return o(p,r)?p[r]:35===r.charCodeAt(0)&&u.test(r)&&i(t="x"===r[1].toLowerCase()?parseInt(r.slice(2),16):parseInt(r.slice(1),10))?a(t):e}(e,t)}))},e.isValidEntityCode=i,e.fromCodePoint=a,e.escapeHtml=function(e){return h.test(e)?e.replace(f,m):e},e.arrayReplaceAt=function(e,r,t){return[].concat(e.slice(0,r),t,e.slice(r+1))},e.isSpace=function(e){switch(e){case 9:case 32:return!0}return!1},e.isWhiteSpace=function(e){if(e>=8192&&e<=8202)return!0;switch(e){case 9:case 10:case 11:case 12:case 13:case 32:case 160:case 5760:case 8239:case 8287:case 12288:return!0}return!1},e.isMdAsciiPunct=function(e){switch(e){case 33:case 34:case 35:case 36:case 37:case 38:case 39:case 40:case 41:case 42:case 43:case 44:case 45:case 46:case 47:case 58:case 59:case 60:case 61:case 62:case 63:case 64:case 91:case 92:case 93:case 94:case 95:case 96:case 123:case 124:case 125:case 126:return!0;default:return!1}},e.isPunctChar=function(e){return _.test(e)},e.escapeRE=function(e){return e.replace(g,"\\$&")},e.normalizeReference=function(e){return e=e.trim().replace(/\s+/g," "),"Ṿ"==="ẞ".toLowerCase()&&(e=e.replace(/ẞ/g,"ß")),e.toLowerCase().toUpperCase()}}(r);var q={},S=r.unescapeAll,F=r.unescapeAll;q.parseLinkLabel=function(e,r,t){var n,s,o,i,a=-1,c=e.posMax,l=e.pos;for(e.pos=r+1,n=1;e.pos32)return i;if(41===n){if(0===s)break;s--}r++}return o===r||0!==s||(i.str=S(e.slice(o,r)),i.lines=0,i.pos=r,i.ok=!0),i},q.parseLinkTitle=function(e,r,t){var n,s,o=0,i=r,a={ok:!1,pos:0,lines:0,str:""};if(r>=t)return a;if(34!==(s=e.charCodeAt(r))&&39!==s&&40!==s)return a;for(r++,40===s&&(s=41);r"+T(e[r].content)+""},I.code_block=function(e,r,t,n,s){var o=e[r];return""+T(e[r].content)+"\n"},I.fence=function(e,r,t,n,s){var o,i,a,c,l,u=e[r],p=u.info?z(u.info).trim():"",h="",f="";return p&&(h=(a=p.split(/(\s+)/g))[0],f=a.slice(2).join("")),0===(o=t.highlight&&t.highlight(u.content,h,f)||T(u.content)).indexOf(""+o+"\n"):"
"+o+"
\n"},I.image=function(e,r,t,n,s){var o=e[r];return o.attrs[o.attrIndex("alt")][1]=s.renderInlineAsText(o.children,t,n),s.renderToken(e,r,t)},I.hardbreak=function(e,r,t){return t.xhtmlOut?"
\n":"
\n"},I.softbreak=function(e,r,t){return t.breaks?t.xhtmlOut?"
\n":"
\n":"\n"},I.text=function(e,r){return T(e[r].content)},I.html_block=function(e,r){return e[r].content},I.html_inline=function(e,r){return e[r].content},M.prototype.renderAttrs=function(e){var r,t,n;if(!e.attrs)return"";for(n="",r=0,t=e.attrs.length;r\n":">")},M.prototype.renderInline=function(e,r,t){for(var n,s="",o=this.rules,i=0,a=e.length;i/i.test(e)}var V=/\+-|\.\.|\?\?\?\?|!!!!|,,|--/,Z=/\((c|tm|r)\)/i,$=/\((c|tm|r)\)/gi,G={c:"©",r:"®",tm:"™"};function H(e,r){return G[r.toLowerCase()]}function J(e){var r,t,n=0;for(r=e.length-1;r>=0;r--)"text"!==(t=e[r]).type||n||(t.content=t.content.replace($,H)),"link_open"===t.type&&"auto"===t.info&&n--,"link_close"===t.type&&"auto"===t.info&&n++}function W(e){var r,t,n=0;for(r=e.length-1;r>=0;r--)"text"!==(t=e[r]).type||n||V.test(t.content)&&(t.content=t.content.replace(/\+-/g,"±").replace(/\.{2,}/g,"…").replace(/([?!])…/g,"$1..").replace(/([?!]){4,}/g,"$1$1$1").replace(/,{2,}/g,",").replace(/(^|[^-])---(?=[^-]|$)/gm,"$1—").replace(/(^|\s)--(?=\s|$)/gm,"$1–").replace(/(^|[^-\s])--(?=[^-\s]|$)/gm,"$1–")),"link_open"===t.type&&"auto"===t.info&&n--,"link_close"===t.type&&"auto"===t.info&&n++}var Y=r.isWhiteSpace,K=r.isPunctChar,Q=r.isMdAsciiPunct,X=/['"]/,ee=/['"]/g;function re(e,r,t){return e.slice(0,r)+t+e.slice(r+1)}function te(e,r){var t,n,s,o,i,a,c,l,u,p,h,f,d,m,g,_,k,b,v,C,y;for(v=[],t=0;t=0&&!(v[k].level<=c);k--);if(v.length=k+1,"text"===n.type){i=0,a=(s=n.content).length;e:for(;i=0)u=s.charCodeAt(o.index-1);else for(k=t-1;k>=0&&("softbreak"!==e[k].type&&"hardbreak"!==e[k].type);k--)if(e[k].content){u=e[k].content.charCodeAt(e[k].content.length-1);break}if(p=32,i=48&&u<=57&&(_=g=!1),g&&_&&(g=h,_=f),g||_){if(_)for(k=v.length-1;k>=0&&(l=v[k],!(v[k].level=0&&(t=this.attrs[r][1]),t},ne.prototype.attrJoin=function(e,r){var t=this.attrIndex(e);t<0?this.attrPush([e,r]):this.attrs[t][1]=this.attrs[t][1]+" "+r};var se=ne,oe=se;function ie(e,r,t){this.src=e,this.env=t,this.tokens=[],this.inlineMode=!1,this.md=r}ie.prototype.Token=oe;var ae=ie,ce=N,le=[["normalize",function(e){var r;r=(r=e.src.replace(O,"\n")).replace(P,"�"),e.src=r}],["block",function(e){var r;e.inlineMode?((r=new e.Token("inline","",0)).content=e.src,r.map=[0,1],r.children=[],e.tokens.push(r)):e.md.block.parse(e.src,e.md,e.env,e.tokens)}],["inline",function(e){var r,t,n,s=e.tokens;for(t=0,n=s.length;t=0;r--)if("link_close"!==(i=s[r]).type){if("html_inline"===i.type&&(k=i.content,/^\s]/i.test(k)&&f>0&&f--,U(i.content)&&f++),!(f>0)&&"text"===i.type&&e.md.linkify.test(i.content)){for(l=i.content,_=e.md.linkify.match(l),a=[],h=i.level,p=0,_.length>0&&0===_[0].index&&r>0&&"text_special"===s[r-1].type&&(_=_.slice(1)),c=0;c<_.length;c++)d=_[c].url,m=e.md.normalizeLink(d),e.md.validateLink(m)&&(g=_[c].text,g=_[c].schema?"mailto:"!==_[c].schema||/^mailto:/i.test(g)?e.md.normalizeLinkText(g):e.md.normalizeLinkText("mailto:"+g).replace(/^mailto:/,""):e.md.normalizeLinkText("http://"+g).replace(/^http:\/\//,""),(u=_[c].index)>p&&((o=new e.Token("text","",0)).content=l.slice(p,u),o.level=h,a.push(o)),(o=new e.Token("link_open","a",1)).attrs=[["href",m]],o.level=h++,o.markup="linkify",o.info="auto",a.push(o),(o=new e.Token("text","",0)).content=g,o.level=h,a.push(o),(o=new e.Token("link_close","a",-1)).level=--h,o.markup="linkify",o.info="auto",a.push(o),p=_[c].lastIndex);p=0;r--)"inline"===e.tokens[r].type&&(Z.test(e.tokens[r].content)&&J(e.tokens[r].children),V.test(e.tokens[r].content)&&W(e.tokens[r].children))}],["smartquotes",function(e){var r;if(e.md.options.typographer)for(r=e.tokens.length-1;r>=0;r--)"inline"===e.tokens[r].type&&X.test(e.tokens[r].content)&&te(e.tokens[r].children,e)}],["text_join",function(e){var r,t,n,s,o,i,a=e.tokens;for(r=0,t=a.length;r=o)return-1;if((t=e.src.charCodeAt(s++))<48||t>57)return-1;for(;;){if(s>=o)return-1;if(!((t=e.src.charCodeAt(s++))>=48&&t<=57)){if(41===t||46===t)break;return-1}if(s-n>=10)return-1}return s`\\x00-\\x20]+|'[^']*'|\"[^\"]*\"))?)*\\s*\\/?>",xe="<\\/[A-Za-z][A-Za-z0-9\\-]*\\s*>",De=new RegExp("^(?:"+Ae+"|"+xe+"|\x3c!----\x3e|\x3c!--(?:-?[^>-])(?:-?[^-])*--\x3e|<[?][\\s\\S]*?[?]>|]*>|)"),we=new RegExp("^(?:"+Ae+"|"+xe+")");ye.HTML_TAG_RE=De,ye.HTML_OPEN_CLOSE_TAG_RE=we;var Ee=["address","article","aside","base","basefont","blockquote","body","caption","center","col","colgroup","dd","details","dialog","dir","div","dl","dt","fieldset","figcaption","figure","footer","form","frame","frameset","h1","h2","h3","h4","h5","h6","head","header","hr","html","iframe","legend","li","link","main","menu","menuitem","nav","noframes","ol","optgroup","option","p","param","section","source","summary","table","tbody","td","tfoot","th","thead","title","tr","track","ul"],qe=ye.HTML_OPEN_CLOSE_TAG_RE,Se=[[/^<(script|pre|style|textarea)(?=(\s|>|$))/i,/<\/(script|pre|style|textarea)>/i,!0],[/^/,!0],[/^<\?/,/\?>/,!0],[/^/,!0],[/^/,!0],[new RegExp("^|$))","i"),/^$/,!0],[new RegExp(qe.source+"\\s*$"),/^$/,!1]],Fe=r.isSpace,Le=se,ze=r.isSpace;function Te(e,r,t,n){var s,o,i,a,c,l,u,p;for(this.src=e,this.md=r,this.env=t,this.tokens=n,this.bMarks=[],this.eMarks=[],this.tShift=[],this.sCount=[],this.bsCount=[],this.blkIndent=0,this.line=0,this.lineMax=0,this.tight=!1,this.ddIndent=-1,this.listIndent=-1,this.parentType="root",this.level=0,this.result="",p=!1,i=a=l=u=0,c=(o=this.src).length;a0&&this.level++,this.tokens.push(n),n},Te.prototype.isEmpty=function(e){return this.bMarks[e]+this.tShift[e]>=this.eMarks[e]},Te.prototype.skipEmptyLines=function(e){for(var r=this.lineMax;er;)if(!ze(this.src.charCodeAt(--e)))return e+1;return e},Te.prototype.skipChars=function(e,r){for(var t=this.src.length;et;)if(r!==this.src.charCodeAt(--e))return e+1;return e},Te.prototype.getLines=function(e,r,t,n){var s,o,i,a,c,l,u,p=e;if(e>=r)return"";for(l=new Array(r-e),s=0;pt?new Array(o-t+1).join(" ")+this.src.slice(a,c):this.src.slice(a,c)}return l.join("")},Te.prototype.Token=Le;var Ie=Te,Me=N,Re=[["table",function(e,r,t,n){var s,o,i,a,c,l,u,p,h,f,d,m,g,_,k,b,v,C;if(r+2>t)return!1;if(l=r+1,e.sCount[l]=4)return!1;if((i=e.bMarks[l]+e.tShift[l])>=e.eMarks[l])return!1;if(124!==(v=e.src.charCodeAt(i++))&&45!==v&&58!==v)return!1;if(i>=e.eMarks[l])return!1;if(124!==(C=e.src.charCodeAt(i++))&&45!==C&&58!==C&&!he(C))return!1;if(45===v&&he(C))return!1;for(;i=4)return!1;if((u=de(o)).length&&""===u[0]&&u.shift(),u.length&&""===u[u.length-1]&&u.pop(),0===(p=u.length)||p!==f.length)return!1;if(n)return!0;for(_=e.parentType,e.parentType="table",b=e.md.block.ruler.getRules("blockquote"),(h=e.push("table_open","table",1)).map=m=[r,0],(h=e.push("thead_open","thead",1)).map=[r,r+1],(h=e.push("tr_open","tr",1)).map=[r,r+1],a=0;a=4)break;for((u=de(o)).length&&""===u[0]&&u.shift(),u.length&&""===u[u.length-1]&&u.pop(),l===r+2&&((h=e.push("tbody_open","tbody",1)).map=g=[r+2,0]),(h=e.push("tr_open","tr",1)).map=[l,l+1],a=0;a=4))break;s=++n}return e.line=s,(o=e.push("code_block","code",0)).content=e.getLines(r,s,4+e.blkIndent,!1)+"\n",o.map=[r,e.line],!0}],["fence",function(e,r,t,n){var s,o,i,a,c,l,u,p=!1,h=e.bMarks[r]+e.tShift[r],f=e.eMarks[r];if(e.sCount[r]-e.blkIndent>=4)return!1;if(h+3>f)return!1;if(126!==(s=e.src.charCodeAt(h))&&96!==s)return!1;if(c=h,(o=(h=e.skipChars(h,s))-c)<3)return!1;if(u=e.src.slice(c,h),i=e.src.slice(h,f),96===s&&i.indexOf(String.fromCharCode(s))>=0)return!1;if(n)return!0;for(a=r;!(++a>=t)&&!((h=c=e.bMarks[a]+e.tShift[a])<(f=e.eMarks[a])&&e.sCount[a]=4||(h=e.skipChars(h,s))-c=4)return!1;if(62!==e.src.charCodeAt(D++))return!1;if(n)return!0;for(a=h=e.sCount[r]+1,32===e.src.charCodeAt(D)?(D++,a++,h++,s=!1,b=!0):9===e.src.charCodeAt(D)?(b=!0,(e.bsCount[r]+h)%4==3?(D++,a++,h++,s=!1):s=!0):b=!1,f=[e.bMarks[r]],e.bMarks[r]=D;D=w,_=[e.sCount[r]],e.sCount[r]=h-a,k=[e.tShift[r]],e.tShift[r]=D-e.bMarks[r],C=e.md.block.ruler.getRules("blockquote"),g=e.parentType,e.parentType="blockquote",p=r+1;p=(w=e.eMarks[p])));p++)if(62!==e.src.charCodeAt(D++)||A){if(l)break;for(v=!1,i=0,c=C.length;i=w,d.push(e.bsCount[p]),e.bsCount[p]=e.sCount[p]+1+(b?1:0),_.push(e.sCount[p]),e.sCount[p]=h-a,k.push(e.tShift[p]),e.tShift[p]=D-e.bMarks[p]}for(m=e.blkIndent,e.blkIndent=0,(y=e.push("blockquote_open","blockquote",1)).markup=">",y.map=u=[r,0],e.md.block.tokenize(e,r,p),(y=e.push("blockquote_close","blockquote",-1)).markup=">",e.lineMax=x,e.parentType=g,u[1]=e.line,i=0;i=4)return!1;if(42!==(s=e.src.charCodeAt(c++))&&45!==s&&95!==s)return!1;for(o=1;c=4)return!1;if(e.listIndent>=0&&e.sCount[r]-e.listIndent>=4&&e.sCount[r]=e.blkIndent&&(z=!0),(w=be(e,r))>=0){if(u=!0,q=e.bMarks[r]+e.tShift[r],g=Number(e.src.slice(q,w-1)),z&&1!==g)return!1}else{if(!((w=ke(e,r))>=0))return!1;u=!1}if(z&&e.skipSpaces(w)>=e.eMarks[r])return!1;if(m=e.src.charCodeAt(w-1),n)return!0;for(d=e.tokens.length,u?(L=e.push("ordered_list_open","ol",1),1!==g&&(L.attrs=[["start",g]])):L=e.push("bullet_list_open","ul",1),L.map=f=[r,0],L.markup=String.fromCharCode(m),k=r,E=!1,F=e.md.block.ruler.getRules("list"),C=e.parentType,e.parentType="list";k=_?1:b-l)>4&&(c=1),a=l+c,(L=e.push("list_item_open","li",1)).markup=String.fromCharCode(m),L.map=p=[r,0],u&&(L.info=e.src.slice(q,w-1)),x=e.tight,A=e.tShift[r],y=e.sCount[r],v=e.listIndent,e.listIndent=e.blkIndent,e.blkIndent=a,e.tight=!0,e.tShift[r]=o-e.bMarks[r],e.sCount[r]=b,o>=_&&e.isEmpty(r+1)?e.line=Math.min(e.line+2,t):e.md.block.tokenize(e,r,t,!0),e.tight&&!E||(T=!1),E=e.line-r>1&&e.isEmpty(e.line-1),e.blkIndent=e.listIndent,e.listIndent=v,e.tShift[r]=A,e.sCount[r]=y,e.tight=x,(L=e.push("list_item_close","li",-1)).markup=String.fromCharCode(m),k=r=e.line,p[1]=k,o=e.bMarks[r],k>=t)break;if(e.sCount[k]=4)break;for(S=!1,i=0,h=F.length;i=4)return!1;if(91!==e.src.charCodeAt(C))return!1;for(;++C3||e.sCount[A]<0)){for(_=!1,l=0,u=k.length;l=4)return!1;if(!e.md.options.html)return!1;if(60!==e.src.charCodeAt(c))return!1;for(a=e.src.slice(c,l),s=0;s=4)return!1;if(35!==(s=e.src.charCodeAt(c))||c>=l)return!1;for(o=1,s=e.src.charCodeAt(++c);35===s&&c6||cc&&Fe(e.src.charCodeAt(i-1))&&(l=i),e.line=r+1,(a=e.push("heading_open","h"+String(o),1)).markup="########".slice(0,o),a.map=[r,e.line],(a=e.push("inline","",0)).content=e.src.slice(c,l).trim(),a.map=[r,e.line],a.children=[],(a=e.push("heading_close","h"+String(o),-1)).markup="########".slice(0,o)),!0)},["paragraph","reference","blockquote"]],["lheading",function(e,r,t){var n,s,o,i,a,c,l,u,p,h,f=r+1,d=e.md.block.ruler.getRules("paragraph");if(e.sCount[r]-e.blkIndent>=4)return!1;for(h=e.parentType,e.parentType="paragraph";f3)){if(e.sCount[f]>=e.blkIndent&&(c=e.bMarks[f]+e.tShift[f])<(l=e.eMarks[f])&&(45===(p=e.src.charCodeAt(c))||61===p)&&(c=e.skipChars(c,p),(c=e.skipSpaces(c))>=l)){u=61===p?1:2;break}if(!(e.sCount[f]<0)){for(s=!1,o=0,i=d.length;o3||e.sCount[c]<0)){for(n=!1,s=0,o=l.length;s=t))&&!(e.sCount[i]=c){e.line=t;break}for(n=0;n?@[]^_`{|}~-".split("").forEach((function(e){Ve[e.charCodeAt(0)]=1}));var $e={};function Ge(e,r){var t,n,s,o,i,a=[],c=r.length;for(t=0;t=0;t--)95!==(n=r[t]).marker&&42!==n.marker||-1!==n.end&&(s=r[n.end],a=t>0&&r[t-1].end===n.end+1&&r[t-1].marker===n.marker&&r[t-1].token===n.token-1&&r[n.end+1].token===s.token+1,i=String.fromCharCode(n.marker),(o=e.tokens[n.token]).type=a?"strong_open":"em_open",o.tag=a?"strong":"em",o.nesting=1,o.markup=a?i+i:i,o.content="",(o=e.tokens[s.token]).type=a?"strong_close":"em_close",o.tag=a?"strong":"em",o.nesting=-1,o.markup=a?i+i:i,o.content="",a&&(e.tokens[r[t-1].token].content="",e.tokens[r[n.end+1].token].content="",t--))}He.tokenize=function(e,r){var t,n,s=e.pos,o=e.src.charCodeAt(s);if(r)return!1;if(95!==o&&42!==o)return!1;for(n=e.scanDelims(e.pos,42===o),t=0;t\x00-\x20]*)$/,rr=ye.HTML_TAG_RE;var tr=t,nr=r.has,sr=r.isValidEntityCode,or=r.fromCodePoint,ir=/^&#((?:x[a-f0-9]{1,6}|[0-9]{1,7}));/i,ar=/^&([a-z][a-z0-9]{1,31});/i;function cr(e,r){var t,n,s,o,i,a,c,l,u={},p=r.length;if(p){var h=0,f=-2,d=[];for(t=0;ti;n-=d[n]+1)if((o=r[n]).marker===s.marker&&o.open&&o.end<0&&(c=!1,(o.close||s.open)&&(o.length+s.length)%3==0&&(o.length%3==0&&s.length%3==0||(c=!0)),!c)){l=n>0&&!r[n-1].open?d[n-1]+1:0,d[t]=t-n+l,d[n]=l,s.open=!1,o.end=t,o.close=!1,a=-1,f=-2;break}-1!==a&&(u[s.marker][(s.open?3:0)+(s.length||0)%3]=a)}}}var lr=se,ur=r.isWhiteSpace,pr=r.isPunctChar,hr=r.isMdAsciiPunct;function fr(e,r,t,n){this.src=e,this.env=t,this.md=r,this.tokens=n,this.tokens_meta=Array(n.length),this.pos=0,this.posMax=this.src.length,this.level=0,this.pending="",this.pendingLevel=0,this.cache={},this.delimiters=[],this._prev_delimiters=[],this.backticks={},this.backticksScanned=!1,this.linkLevel=0}fr.prototype.pushPending=function(){var e=new lr("text","",0);return e.content=this.pending,e.level=this.pendingLevel,this.tokens.push(e),this.pending="",e},fr.prototype.push=function(e,r,t){this.pending&&this.pushPending();var n=new lr(e,r,t),s=null;return t<0&&(this.level--,this.delimiters=this._prev_delimiters.pop()),n.level=this.level,t>0&&(this.level++,this._prev_delimiters.push(this.delimiters),this.delimiters=[],s={delimiters:this.delimiters}),this.pendingLevel=this.level,this.tokens.push(n),this.tokens_meta.push(s),n},fr.prototype.scanDelims=function(e,r){var t,n,s,o,i,a,c,l,u,p=e,h=!0,f=!0,d=this.posMax,m=this.src.charCodeAt(e);for(t=e>0?this.src.charCodeAt(e-1):32;p0)&&(!((t=e.pos)+3>e.posMax)&&(58===e.src.charCodeAt(t)&&(47===e.src.charCodeAt(t+1)&&(47===e.src.charCodeAt(t+2)&&(!!(n=e.pending.match(Pe))&&(s=n[1],!!(o=e.md.linkify.matchAtStart(e.src.slice(t-s.length)))&&(i=(i=o.url).replace(/\*+$/,""),a=e.md.normalizeLink(i),!!e.md.validateLink(a)&&(r||(e.pending=e.pending.slice(0,-s.length),(c=e.push("link_open","a",1)).attrs=[["href",a]],c.markup="linkify",c.info="auto",(c=e.push("text","",0)).content=e.md.normalizeLinkText(i),(c=e.push("link_close","a",-1)).markup="linkify",c.info="auto"),e.pos+=i.length-s.length,!0)))))))))}],["newline",function(e,r){var t,n,s,o=e.pos;if(10!==e.src.charCodeAt(o))return!1;if(t=e.pending.length-1,n=e.posMax,!r)if(t>=0&&32===e.pending.charCodeAt(t))if(t>=1&&32===e.pending.charCodeAt(t-1)){for(s=t-1;s>=1&&32===e.pending.charCodeAt(s-1);)s--;e.pending=e.pending.slice(0,s),e.push("hardbreak","br",0)}else e.pending=e.pending.slice(0,-1),e.push("softbreak","br",0);else e.push("softbreak","br",0);for(o++;o=c)return!1;if(10===(t=e.src.charCodeAt(a))){for(r||e.push("hardbreak","br",0),a++;a=55296&&t<=56319&&a+1=56320&&n<=57343&&(o+=e.src[a+1],a++),s="\\"+o,r||(i=e.push("text_special","",0),t<256&&0!==Ve[t]?i.content=o:i.content=s,i.markup=s,i.info="escape"),e.pos=a+1,!0}],["backticks",function(e,r){var t,n,s,o,i,a,c,l,u=e.pos;if(96!==e.src.charCodeAt(u))return!1;for(t=u,u++,n=e.posMax;u=f)return!1;if(d=a,(c=e.md.helpers.parseLinkDestination(e.src,a,e.posMax)).ok){for(u=e.md.normalizeLink(c.str),e.md.validateLink(u)?a=c.pos:u="",d=a;a=f||41!==e.src.charCodeAt(a))&&(m=!0),a++}if(m){if(void 0===e.env.references)return!1;if(a=0?s=e.src.slice(d,a++):a=o+1):a=o+1,s||(s=e.src.slice(i,o)),!(l=e.env.references[We(s)]))return e.pos=h,!1;u=l.href,p=l.title}return r||(e.pos=i,e.posMax=o,e.push("link_open","a",1).attrs=t=[["href",u]],p&&t.push(["title",p]),e.linkLevel++,e.md.inline.tokenize(e),e.linkLevel--,e.push("link_close","a",-1)),e.pos=a,e.posMax=f,!0}],["image",function(e,r){var t,n,s,o,i,a,c,l,u,p,h,f,d,m="",g=e.pos,_=e.posMax;if(33!==e.src.charCodeAt(e.pos))return!1;if(91!==e.src.charCodeAt(e.pos+1))return!1;if(a=e.pos+2,(i=e.md.helpers.parseLinkLabel(e,e.pos+1,!1))<0)return!1;if((c=i+1)<_&&40===e.src.charCodeAt(c)){for(c++;c<_&&(n=e.src.charCodeAt(c),Qe(n)||10===n);c++);if(c>=_)return!1;for(d=c,(u=e.md.helpers.parseLinkDestination(e.src,c,e.posMax)).ok&&(m=e.md.normalizeLink(u.str),e.md.validateLink(m)?c=u.pos:m=""),d=c;c<_&&(n=e.src.charCodeAt(c),Qe(n)||10===n);c++);if(u=e.md.helpers.parseLinkTitle(e.src,c,e.posMax),c<_&&d!==c&&u.ok)for(p=u.str,c=u.pos;c<_&&(n=e.src.charCodeAt(c),Qe(n)||10===n);c++);else p="";if(c>=_||41!==e.src.charCodeAt(c))return e.pos=g,!1;c++}else{if(void 0===e.env.references)return!1;if(c<_&&91===e.src.charCodeAt(c)?(d=c+1,(c=e.md.helpers.parseLinkLabel(e,c))>=0?o=e.src.slice(d,c++):c=i+1):c=i+1,o||(o=e.src.slice(a,i)),!(l=e.env.references[Ke(o)]))return e.pos=g,!1;m=l.href,p=l.title}return r||(s=e.src.slice(a,i),e.md.inline.parse(s,e.md,e.env,f=[]),(h=e.push("image","img",0)).attrs=t=[["src",m],["alt",""]],h.children=f,h.content=s,p&&t.push(["title",p])),e.pos=c,e.posMax=_,!0}],["autolink",function(e,r){var t,n,s,o,i,a,c=e.pos;if(60!==e.src.charCodeAt(c))return!1;for(i=e.pos,a=e.posMax;;){if(++c>=a)return!1;if(60===(o=e.src.charCodeAt(c)))return!1;if(62===o)break}return t=e.src.slice(i+1,c),er.test(t)?(n=e.md.normalizeLink(t),!!e.md.validateLink(n)&&(r||((s=e.push("link_open","a",1)).attrs=[["href",n]],s.markup="autolink",s.info="auto",(s=e.push("text","",0)).content=e.md.normalizeLinkText(t),(s=e.push("link_close","a",-1)).markup="autolink",s.info="auto"),e.pos+=t.length+2,!0)):!!Xe.test(t)&&(n=e.md.normalizeLink("mailto:"+t),!!e.md.validateLink(n)&&(r||((s=e.push("link_open","a",1)).attrs=[["href",n]],s.markup="autolink",s.info="auto",(s=e.push("text","",0)).content=e.md.normalizeLinkText(t),(s=e.push("link_close","a",-1)).markup="autolink",s.info="auto"),e.pos+=t.length+2,!0))}],["html_inline",function(e,r){var t,n,s,o,i,a=e.pos;return!!e.md.options.html&&(s=e.posMax,!(60!==e.src.charCodeAt(a)||a+2>=s)&&(!(33!==(t=e.src.charCodeAt(a+1))&&63!==t&&47!==t&&!function(e){var r=32|e;return r>=97&&r<=122}(t))&&(!!(n=e.src.slice(a).match(rr))&&(r||((o=e.push("html_inline","",0)).content=e.src.slice(a,a+n[0].length),i=o.content,/^\s]/i.test(i)&&e.linkLevel++,function(e){return/^<\/a\s*>/i.test(e)}(o.content)&&e.linkLevel--),e.pos+=n[0].length,!0))))}],["entity",function(e,r){var t,n,s,o=e.pos,i=e.posMax;if(38!==e.src.charCodeAt(o))return!1;if(o+1>=i)return!1;if(35===e.src.charCodeAt(o+1)){if(n=e.src.slice(o).match(ir))return r||(t="x"===n[1][0].toLowerCase()?parseInt(n[1].slice(1),16):parseInt(n[1],10),(s=e.push("text_special","",0)).content=sr(t)?or(t):or(65533),s.markup=n[0],s.info="entity"),e.pos+=n[0].length,!0}else if((n=e.src.slice(o).match(ar))&&nr(tr,n[1]))return r||((s=e.push("text_special","",0)).content=tr[n[1]],s.markup=n[0],s.info="entity"),e.pos+=n[0].length,!0;return!1}]],_r=[["balance_pairs",function(e){var r,t=e.tokens_meta,n=e.tokens_meta.length;for(cr(0,e.delimiters),r=0;r0&&n++,"text"===s[r].type&&r+1=o)break}else e.pending+=e.src[e.pos++]}e.pending&&e.pushPending()},kr.prototype.parse=function(e,r,t,n){var s,o,i,a=new this.State(e,r,t,n);for(this.tokenize(a),i=(o=this.ruler2.getRules("")).length,s=0;s=3&&":"===e[r-3]||r>=3&&"/"===e[r-3]?0:n.match(t.re.no_http)[0].length:0}},"mailto:":{validate:function(e,r,t){var n=e.slice(r);return t.re.mailto||(t.re.mailto=new RegExp("^"+t.re.src_email_name+"@"+t.re.src_host_strict,"i")),t.re.mailto.test(n)?n.match(t.re.mailto)[0].length:0}}},wr="biz|com|edu|gov|net|org|pro|web|xxx|aero|asia|coop|info|museum|name|shop|рф".split("|");function Er(e){var r=e.re=function(e){var r={};return e=e||{},r.src_Any=D.source,r.src_Cc=w.source,r.src_Z=E.source,r.src_P=n.source,r.src_ZPCc=[r.src_Z,r.src_P,r.src_Cc].join("|"),r.src_ZCc=[r.src_Z,r.src_Cc].join("|"),r.src_pseudo_letter="(?:(?![><|]|"+r.src_ZPCc+")"+r.src_Any+")",r.src_ip4="(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)",r.src_auth="(?:(?:(?!"+r.src_ZCc+"|[@/\\[\\]()]).)+@)?",r.src_port="(?::(?:6(?:[0-4]\\d{3}|5(?:[0-4]\\d{2}|5(?:[0-2]\\d|3[0-5])))|[1-5]?\\d{1,4}))?",r.src_host_terminator="(?=$|[><|]|"+r.src_ZPCc+")(?!"+(e["---"]?"-(?!--)|":"-|")+"_|:\\d|\\.-|\\.(?!$|"+r.src_ZPCc+"))",r.src_path="(?:[/?#](?:(?!"+r.src_ZCc+"|[><|]|[()[\\]{}.,\"'?!\\-;]).|\\[(?:(?!"+r.src_ZCc+"|\\]).)*\\]|\\((?:(?!"+r.src_ZCc+"|[)]).)*\\)|\\{(?:(?!"+r.src_ZCc+'|[}]).)*\\}|\\"(?:(?!'+r.src_ZCc+'|["]).)+\\"|\\\'(?:(?!'+r.src_ZCc+"|[']).)+\\'|\\'(?="+r.src_pseudo_letter+"|[-])|\\.{2,}[a-zA-Z0-9%/&]|\\.(?!"+r.src_ZCc+"|[.]|$)|"+(e["---"]?"\\-(?!--(?:[^-]|$))(?:-*)|":"\\-+|")+",(?!"+r.src_ZCc+"|$)|;(?!"+r.src_ZCc+"|$)|\\!+(?!"+r.src_ZCc+"|[!]|$)|\\?(?!"+r.src_ZCc+"|[?]|$))+|\\/)?",r.src_email_name='[\\-;:&=\\+\\$,\\.a-zA-Z0-9_][\\-;:&=\\+\\$,\\"\\.a-zA-Z0-9_]*',r.src_xn="xn--[a-z0-9\\-]{1,59}",r.src_domain_root="(?:"+r.src_xn+"|"+r.src_pseudo_letter+"{1,63})",r.src_domain="(?:"+r.src_xn+"|(?:"+r.src_pseudo_letter+")|(?:"+r.src_pseudo_letter+"(?:-|"+r.src_pseudo_letter+"){0,61}"+r.src_pseudo_letter+"))",r.src_host="(?:(?:(?:(?:"+r.src_domain+")\\.)*"+r.src_domain+"))",r.tpl_host_fuzzy="(?:"+r.src_ip4+"|(?:(?:(?:"+r.src_domain+")\\.)+(?:%TLDS%)))",r.tpl_host_no_ip_fuzzy="(?:(?:(?:"+r.src_domain+")\\.)+(?:%TLDS%))",r.src_host_strict=r.src_host+r.src_host_terminator,r.tpl_host_fuzzy_strict=r.tpl_host_fuzzy+r.src_host_terminator,r.src_host_port_strict=r.src_host+r.src_port+r.src_host_terminator,r.tpl_host_port_fuzzy_strict=r.tpl_host_fuzzy+r.src_port+r.src_host_terminator,r.tpl_host_port_no_ip_fuzzy_strict=r.tpl_host_no_ip_fuzzy+r.src_port+r.src_host_terminator,r.tpl_host_fuzzy_test="localhost|www\\.|\\.\\d{1,3}\\.|(?:\\.(?:%TLDS%)(?:"+r.src_ZPCc+"|>|$))",r.tpl_email_fuzzy='(^|[><|]|"|\\(|'+r.src_ZCc+")("+r.src_email_name+"@"+r.tpl_host_fuzzy_strict+")",r.tpl_link_fuzzy="(^|(?![.:/\\-_@])(?:[$+<=>^`||]|"+r.src_ZPCc+"))((?![$+<=>^`||])"+r.tpl_host_port_fuzzy_strict+r.src_path+")",r.tpl_link_no_ip_fuzzy="(^|(?![.:/\\-_@])(?:[$+<=>^`||]|"+r.src_ZPCc+"))((?![$+<=>^`||])"+r.tpl_host_port_no_ip_fuzzy_strict+r.src_path+")",r}(e.__opts__),t=e.__tlds__.slice();function s(e){return e.replace("%TLDS%",r.src_tlds)}e.onCompile(),e.__tlds_replaced__||t.push("a[cdefgilmnoqrstuwxz]|b[abdefghijmnorstvwyz]|c[acdfghiklmnoruvwxyz]|d[ejkmoz]|e[cegrstu]|f[ijkmor]|g[abdefghilmnpqrstuwy]|h[kmnrtu]|i[delmnoqrst]|j[emop]|k[eghimnprwyz]|l[abcikrstuvy]|m[acdeghklmnopqrstuvwxyz]|n[acefgilopruz]|om|p[aefghklmnrstwy]|qa|r[eosuw]|s[abcdeghijklmnortuvxyz]|t[cdfghjklmnortvwz]|u[agksyz]|v[aceginu]|w[fs]|y[et]|z[amw]"),t.push(r.src_xn),r.src_tlds=t.join("|"),r.email_fuzzy=RegExp(s(r.tpl_email_fuzzy),"i"),r.link_fuzzy=RegExp(s(r.tpl_link_fuzzy),"i"),r.link_no_ip_fuzzy=RegExp(s(r.tpl_link_no_ip_fuzzy),"i"),r.host_fuzzy_test=RegExp(s(r.tpl_host_fuzzy_test),"i");var o=[];function i(e,r){throw new Error('(LinkifyIt) Invalid schema "'+e+'": '+r)}e.__compiled__={},Object.keys(e.__schemas__).forEach((function(r){var t=e.__schemas__[r];if(null!==t){var n={validate:null,link:null};if(e.__compiled__[r]=n,"[object Object]"===Cr(t))return!function(e){return"[object RegExp]"===Cr(e)}(t.validate)?yr(t.validate)?n.validate=t.validate:i(r,t):n.validate=function(e){return function(r,t){var n=r.slice(t);return e.test(n)?n.match(e)[0].length:0}}(t.validate),void(yr(t.normalize)?n.normalize=t.normalize:t.normalize?i(r,t):n.normalize=function(e,r){r.normalize(e)});!function(e){return"[object String]"===Cr(e)}(t)?i(r,t):o.push(r)}})),o.forEach((function(r){e.__compiled__[e.__schemas__[r]]&&(e.__compiled__[r].validate=e.__compiled__[e.__schemas__[r]].validate,e.__compiled__[r].normalize=e.__compiled__[e.__schemas__[r]].normalize)})),e.__compiled__[""]={validate:null,normalize:function(e,r){r.normalize(e)}};var a=Object.keys(e.__compiled__).filter((function(r){return r.length>0&&e.__compiled__[r]})).map(Ar).join("|");e.re.schema_test=RegExp("(^|(?!_)(?:[><|]|"+r.src_ZPCc+"))("+a+")","i"),e.re.schema_search=RegExp("(^|(?!_)(?:[><|]|"+r.src_ZPCc+"))("+a+")","ig"),e.re.schema_at_start=RegExp("^"+e.re.schema_search.source,"i"),e.re.pretest=RegExp("("+e.re.schema_test.source+")|("+e.re.host_fuzzy_test.source+")|@","i"),function(e){e.__index__=-1,e.__text_cache__=""}(e)}function qr(e,r){var t=e.__index__,n=e.__last_index__,s=e.__text_cache__.slice(t,n);this.schema=e.__schema__.toLowerCase(),this.index=t+r,this.lastIndex=n+r,this.raw=s,this.text=s,this.url=s}function Sr(e,r){var t=new qr(e,r);return e.__compiled__[t.schema].normalize(t,e),t}function Fr(e,r){if(!(this instanceof Fr))return new Fr(e,r);var t;r||(t=e,Object.keys(t||{}).reduce((function(e,r){return e||xr.hasOwnProperty(r)}),!1)&&(r=e,e={})),this.__opts__=vr({},xr,r),this.__index__=-1,this.__last_index__=-1,this.__schema__="",this.__text_cache__="",this.__schemas__=vr({},Dr,e),this.__compiled__={},this.__tlds__=wr,this.__tlds_replaced__=!1,this.re={},Er(this)}Fr.prototype.add=function(e,r){return this.__schemas__[e]=r,Er(this),this},Fr.prototype.set=function(e){return this.__opts__=vr(this.__opts__,e),this},Fr.prototype.test=function(e){if(this.__text_cache__=e,this.__index__=-1,!e.length)return!1;var r,t,n,s,o,i,a,c;if(this.re.schema_test.test(e))for((a=this.re.schema_search).lastIndex=0;null!==(r=a.exec(e));)if(s=this.testSchemaAt(e,r[2],a.lastIndex)){this.__schema__=r[2],this.__index__=r.index+r[1].length,this.__last_index__=r.index+r[0].length+s;break}return this.__opts__.fuzzyLink&&this.__compiled__["http:"]&&(c=e.search(this.re.host_fuzzy_test))>=0&&(this.__index__<0||c=0&&null!==(n=e.match(this.re.email_fuzzy))&&(o=n.index+n[1].length,i=n.index+n[0].length,(this.__index__<0||othis.__last_index__)&&(this.__schema__="mailto:",this.__index__=o,this.__last_index__=i)),this.__index__>=0},Fr.prototype.pretest=function(e){return this.re.pretest.test(e)},Fr.prototype.testSchemaAt=function(e,r,t){return this.__compiled__[r.toLowerCase()]?this.__compiled__[r.toLowerCase()].validate(e,t,this):0},Fr.prototype.match=function(e){var r=0,t=[];this.__index__>=0&&this.__text_cache__===e&&(t.push(Sr(this,r)),r=this.__last_index__);for(var n=r?e.slice(r):e;this.test(n);)t.push(Sr(this,r)),n=n.slice(this.__last_index__),r+=this.__last_index__;return t.length?t:null},Fr.prototype.matchAtStart=function(e){if(this.__text_cache__=e,this.__index__=-1,!e.length)return null;var r=this.re.schema_at_start.exec(e);if(!r)return null;var t=this.testSchemaAt(e,r[2],r[0].length);return t?(this.__schema__=r[2],this.__index__=r.index+r[1].length,this.__last_index__=r.index+r[0].length+t,Sr(this,0)):null},Fr.prototype.tlds=function(e,r){return e=Array.isArray(e)?e:[e],r?(this.__tlds__=this.__tlds__.concat(e).sort().filter((function(e,r,t){return e!==t[r-1]})).reverse(),Er(this),this):(this.__tlds__=e.slice(),this.__tlds_replaced__=!0,Er(this),this)},Fr.prototype.normalize=function(e){e.schema||(e.url="http://"+e.url),"mailto:"!==e.schema||/^mailto:/i.test(e.url)||(e.url="mailto:"+e.url)},Fr.prototype.onCompile=function(){};var Lr=Fr,zr=2147483647,Tr=/^xn--/,Ir=/[^\x20-\x7E]/,Mr=/[\x2E\u3002\uFF0E\uFF61]/g,Rr={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},Br=Math.floor,Nr=String.fromCharCode; +/*! https://mths.be/punycode v1.4.1 by @mathias */function Or(e){throw new RangeError(Rr[e])}function Pr(e,r){for(var t=e.length,n=[];t--;)n[t]=r(e[t]);return n}function jr(e,r){var t=e.split("@"),n="";return t.length>1&&(n=t[0]+"@",e=t[1]),n+Pr((e=e.replace(Mr,".")).split("."),r).join(".")}function Ur(e){for(var r,t,n=[],s=0,o=e.length;s=55296&&r<=56319&&s65535&&(r+=Nr((e-=65536)>>>10&1023|55296),e=56320|1023&e),r+=Nr(e)})).join("")}function Zr(e,r){return e+22+75*(e<26)-((0!=r)<<5)}function $r(e,r,t){var n=0;for(e=t?Br(e/700):e>>1,e+=Br(e/r);e>455;n+=36)e=Br(e/35);return Br(n+36*e/(e+38))}function Gr(e){var r,t,n,s,o,i,a,c,l,u,p,h=[],f=e.length,d=0,m=128,g=72;for((t=e.lastIndexOf("-"))<0&&(t=0),n=0;n=128&&Or("not-basic"),h.push(e.charCodeAt(n));for(s=t>0?t+1:0;s=f&&Or("invalid-input"),((c=(p=e.charCodeAt(s++))-48<10?p-22:p-65<26?p-65:p-97<26?p-97:36)>=36||c>Br((zr-d)/i))&&Or("overflow"),d+=c*i,!(c<(l=a<=g?1:a>=g+26?26:a-g));a+=36)i>Br(zr/(u=36-l))&&Or("overflow"),i*=u;g=$r(d-o,r=h.length+1,0==o),Br(d/r)>zr-m&&Or("overflow"),m+=Br(d/r),d%=r,h.splice(d++,0,m)}return Vr(h)}function Hr(e){var r,t,n,s,o,i,a,c,l,u,p,h,f,d,m,g=[];for(h=(e=Ur(e)).length,r=128,t=0,o=72,i=0;i=r&&pBr((zr-t)/(f=n+1))&&Or("overflow"),t+=(a-r)*f,r=a,i=0;izr&&Or("overflow"),p==r){for(c=t,l=36;!(c<(u=l<=o?1:l>=o+26?26:l-o));l+=36)m=c-u,d=36-u,g.push(Nr(Zr(u+m%d,0))),c=Br(m/d);g.push(Nr(Zr(c,0))),o=$r(t,f,n==s),t=0,++n}++t,++r}return g.join("")}function Jr(e){return jr(e,(function(e){return Tr.test(e)?Gr(e.slice(4).toLowerCase()):e}))}function Wr(e){return jr(e,(function(e){return Ir.test(e)?"xn--"+Hr(e):e}))}var Yr={decode:Ur,encode:Vr},Kr={version:"1.4.1",ucs2:Yr,toASCII:Wr,toUnicode:Jr,encode:Hr,decode:Gr},Qr=r,Xr=q,et=R,rt=pe,tt=Ne,nt=br,st=Lr,ot=s,it=e(Object.freeze({__proto__:null,decode:Gr,encode:Hr,toUnicode:Jr,toASCII:Wr,version:"1.4.1",ucs2:Yr,default:Kr})),at={default:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"“”‘’",highlight:null,maxNesting:100},components:{core:{},block:{},inline:{}}},zero:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"“”‘’",highlight:null,maxNesting:20},components:{core:{rules:["normalize","block","inline","text_join"]},block:{rules:["paragraph"]},inline:{rules:["text"],rules2:["balance_pairs","fragments_join"]}}},commonmark:{options:{html:!0,xhtmlOut:!0,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"“”‘’",highlight:null,maxNesting:20},components:{core:{rules:["normalize","block","inline","text_join"]},block:{rules:["blockquote","code","fence","heading","hr","html_block","lheading","list","reference","paragraph"]},inline:{rules:["autolink","backticks","emphasis","entity","escape","html_inline","image","link","newline","text"],rules2:["balance_pairs","emphasis","fragments_join"]}}}},ct=/^(vbscript|javascript|file|data):/,lt=/^data:image\/(gif|png|jpeg|webp);/;function ut(e){var r=e.trim().toLowerCase();return!ct.test(r)||!!lt.test(r)}var pt=["http:","https:","mailto:"];function ht(e){var r=ot.parse(e,!0);if(r.hostname&&(!r.protocol||pt.indexOf(r.protocol)>=0))try{r.hostname=it.toASCII(r.hostname)}catch(e){}return ot.encode(ot.format(r))}function ft(e){var r=ot.parse(e,!0);if(r.hostname&&(!r.protocol||pt.indexOf(r.protocol)>=0))try{r.hostname=it.toUnicode(r.hostname)}catch(e){}return ot.decode(ot.format(r),ot.decode.defaultChars+"%")}function dt(e,r){if(!(this instanceof dt))return new dt(e,r);r||Qr.isString(e)||(r=e||{},e="default"),this.inline=new nt,this.block=new tt,this.core=new rt,this.renderer=new et,this.linkify=new st,this.validateLink=ut,this.normalizeLink=ht,this.normalizeLinkText=ft,this.utils=Qr,this.helpers=Qr.assign({},Xr),this.options={},this.configure(e),r&&this.set(r)}dt.prototype.set=function(e){return Qr.assign(this.options,e),this},dt.prototype.configure=function(e){var r,t=this;if(Qr.isString(e)&&!(e=at[r=e]))throw new Error('Wrong `markdown-it` preset "'+r+'", check name');if(!e)throw new Error("Wrong `markdown-it` preset, can't be empty");return e.options&&t.set(e.options),e.components&&Object.keys(e.components).forEach((function(r){e.components[r].rules&&t[r].ruler.enableOnly(e.components[r].rules),e.components[r].rules2&&t[r].ruler2.enableOnly(e.components[r].rules2)})),this},dt.prototype.enable=function(e,r){var t=[];Array.isArray(e)||(e=[e]),["core","block","inline"].forEach((function(r){t=t.concat(this[r].ruler.enable(e,!0))}),this),t=t.concat(this.inline.ruler2.enable(e,!0));var n=e.filter((function(e){return t.indexOf(e)<0}));if(n.length&&!r)throw new Error("MarkdownIt. Failed to enable unknown rule(s): "+n);return this},dt.prototype.disable=function(e,r){var t=[];Array.isArray(e)||(e=[e]),["core","block","inline"].forEach((function(r){t=t.concat(this[r].ruler.disable(e,!0))}),this),t=t.concat(this.inline.ruler2.disable(e,!0));var n=e.filter((function(e){return t.indexOf(e)<0}));if(n.length&&!r)throw new Error("MarkdownIt. Failed to disable unknown rule(s): "+n);return this},dt.prototype.use=function(e){var r=[this].concat(Array.prototype.slice.call(arguments,1));return e.apply(e,r),this},dt.prototype.parse=function(e,r){if("string"!=typeof e)throw new Error("Input data should be a String");var t=new this.core.State(e,this,r);return this.core.process(t),t.tokens},dt.prototype.render=function(e,r){return r=r||{},this.renderer.render(this.parse(e,r),this.options,r)},dt.prototype.parseInline=function(e,r){var t=new this.core.State(e,this,r);return t.inlineMode=!0,this.core.process(t),t.tokens},dt.prototype.renderInline=function(e,r){return r=r||{},this.renderer.render(this.parseInline(e,r),this.options,r)};var mt=dt;export default mt; diff --git a/uni-im示例/uni_modules/uni-im/license.md b/uni-im示例/uni_modules/uni-im/license.md new file mode 100644 index 0000000..52be38a --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/license.md @@ -0,0 +1,36 @@ +uni-im源码使用许可协议 + +2022年10月 + +本许可协议,是数字天堂(北京)网络技术有限公司(以下简称DCloud)对其所拥有著作权的“DCloud uni-im”(以下简称软件),提供的使用许可协议。 + +您对“软件”的复制、使用、修改及分发受本许可协议的条款的约束,如您不接受本协议,则不能使用、复制、修改本软件。 + +授权许可范围 + +a) 授予您永久性的、全球性的、免费的、非独占的、不可撤销的本软件的源码使用许可,您可以使用这些源码制作自己的应用。 + +b) 您只能在DCloud产品体系内使用本软件及其源码。您不能将源码修改后运行在DCloud产品体系之外的环境,比如客户端脱离uni-app,或服务端脱离uniCloud。 + +c) DCloud未向您授权商标使用许可。您在根据本软件源码制作自己的应用时,需以自己的名义发布软件,而不是以DCloud名义发布。 + +d) 本协议不构成代理关系。 + +DCloud的责任限制 +“软件”在提供时不带任何明示或默示的担保。在任何情况下,DCloud不对任何人因使用“软件”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。 + +您的责任限制 + +a) 您需要在授权许可范围内使用软件。 + +b) 您在分发自己的应用时,不得侵犯DCloud商标和名誉权利。 + +c) 您不得进行破解、反编译、套壳等侵害DCloud知识产权的行为。您不得利用DCloud系统漏洞谋利或侵害DCloud利益,如您发现DCloud系统漏洞应第一时间通知DCloud。您不得进行攻击DCloud的服务器、网络等妨碍DCloud运营的行为。您不得利用DCloud的产品进行与DCloud争夺开发者的行为。 + +d) 如您违反本许可协议,需承担因此给DCloud造成的损失。 + +本协议签订地点为中华人民共和国北京市海淀区。 + +根据发展,DCloud可能会对本协议进行修改。修改时,DCloud会在产品或者网页中显著的位置发布相关信息以便及时通知到用户。如果您选择继续使用本框架,即表示您同意接受这些修改。 + +条款结束 \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/package.json b/uni-im示例/uni_modules/uni-im/package.json new file mode 100644 index 0000000..c9a7764 --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/package.json @@ -0,0 +1,89 @@ +{ + "id": "uni-im", + "displayName": "uni-im", + "version": "2.0.14", + "description": "uni-im是云端一体的、全平台的、免费的、开源即时通讯系统", + "keywords": [ + "im,即时通讯,客服,聊天" + ], + "repository": "https://gitcode.net/dcloud/hello-uni-im", + "engines": { + "HBuilderX": "^3.6.16" + }, + "dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "", + "type": "unicloud-template-page" + }, + "uni_modules": { + "dependencies": [ + "uni-id-pages", + "uni-config-center", + "uni-search-bar", + "uni-segmented-control", + "Sansnn-uQRCode", + "uni-nav-bar" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "Vue": { + "vue2": "y", + "vue3": "y" + }, + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "n", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "u", + "Edge": "u", + "Firefox": "u", + "Safari": "n" + }, + "小程序": { + "微信": "y", + "阿里": "n", + "百度": "n", + "字节跳动": "n", + "QQ": "n", + "钉钉": "n", + "快手": "n", + "飞书": "n", + "京东": "n" + }, + "快应用": { + "华为": "n", + "联盟": "n" + } + } + } + }, + "dependencies": {} +} \ No newline at end of file diff --git a/uni-im示例/uni_modules/uni-im/pages/chat/chat.nvue b/uni-im示例/uni_modules/uni-im/pages/chat/chat.nvue new file mode 100644 index 0000000..0776a8d --- /dev/null +++ b/uni-im示例/uni_modules/uni-im/pages/chat/chat.nvue @@ -0,0 +1,1203 @@ +