This commit is contained in:
2026-04-25 16:36:34 +08:00
commit db90e7579b
1876 changed files with 189777 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
import { NAME_REGEXP } from '../const';
describe('NAME_REGEXP', () => {
describe('allow', () => {
test.each([
'test',
'test01',
'你好世界',
'你好world',
'最大八个汉字内容',
'maxis16charactor',
'1234567812345678',
])('%s', (input) => {
expect(NAME_REGEXP.test(input)).toBe(true);
});
});
describe('deny', () => {
test.each([
'世 界',
'你好 world',
'超过了八个汉字内容',
'overmax16charactor',
'12345678123456781',
])('%s', (input) => {
expect(NAME_REGEXP.test(input)).toBe(false);
});
});
});

View File

@@ -0,0 +1,66 @@
import {
checkPathMatch,
generateRandomStr,
getEmailAddress,
isValidStr,
sleep,
} from '../utils';
describe('getEmailAddress', () => {
test.each([
['foo@example.com', 'foo'],
['foo.bar@example.com', 'foo.bar'],
['foo$bar@example.com', 'foo$bar'],
])('%s', (input, output) => {
expect(getEmailAddress(input)).toBe(output);
});
});
describe('generateRandomStr', () => {
test('should generate string with length 10(default)', () => {
expect(generateRandomStr()).toHaveLength(10);
});
test('should generate string with manual length', () => {
expect(generateRandomStr(4)).toHaveLength(4);
});
});
describe('isValidStr', () => {
test.each<[any, boolean]>([
[false, false],
[true, false],
[0, false],
[1, false],
['', false],
[{}, false],
[[], false],
['foo', true],
])('%p is %p', (input, output) => {
expect(isValidStr(input)).toBe(output);
});
});
test('sleep', async () => {
const start = new Date().valueOf();
await sleep(1000);
const end = new Date().valueOf();
const duration = end - start;
expect(duration).toBeGreaterThanOrEqual(1000);
expect(duration).toBeLessThan(1050);
});
describe('checkPathMatch', () => {
const testList = ['/foo/bar'];
test.each([
['/foo/bar', true],
['/foo/bar?query=1', true],
['/foo', false],
['/foo/baz', false],
['/foo/baz?bar=', false],
])('%s', (input, output) => {
expect(checkPathMatch(testList, input)).toBe(output);
});
});

46
server/lib/const.ts Normal file
View File

@@ -0,0 +1,46 @@
export const NAME_REGEXP =
/^([0-9a-zA-Z]{1,2}|[\u4e00-\u9eff]|[\u3040-\u309Fー]|[\u30A0-\u30FF]){1,8}$/;
/**
* TODO: 待实现权限相关逻辑
*
* 标准群组权限
* key为权限
* value为默认值
*/
export const BUILTIN_GROUP_PERM = {
/**
* 查看频道
*/
displayChannel: true,
/**
* 管理频道
*/
manageChannel: false,
/**
* 管理角色
*/
manageRole: false,
/**
* 管理群组
*/
manageGroup: false,
/**
* 发送消息
*/
sendMessage: true,
/**
* 发送图片
*/
sendImage: true,
};
/**
* 系统用户id
*/
export const SYSTEM_USERID = '000000000000000000000000';

View File

@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`des encrypt D 1`] = `"ihmnn4VBPYE="`;
exports[`des encrypt bar 1`] = `"p/PIC32MPm4="`;
exports[`des encrypt foo 1`] = `"NP3+ABhEiY4="`;
exports[`des encrypt 你 1`] = `"O5kF0LXzjpE="`;

View File

@@ -0,0 +1,17 @@
import { desEncrypt, desDecrypt } from '../des';
describe('des', () => {
const key = '12345678';
describe('encrypt', () => {
test.each([['foo'], ['bar'], ['你'], ['D']])('%s', (input) => {
expect(desEncrypt(input, key)).toMatchSnapshot();
});
});
describe('decrypt', () => {
test.each([['foo'], ['bar'], ['你'], ['D']])('%s', (input) => {
expect(desDecrypt(desEncrypt(input, key), key)).toBe(input);
});
});
});

24
server/lib/crypto/des.ts Normal file
View File

@@ -0,0 +1,24 @@
import crypto from 'crypto';
import { config } from 'tailchat-server-sdk';
// DES 加密
export function desEncrypt(message: string, key: string = config.secret) {
key =
key.length >= 8 ? key.slice(0, 8) : key.concat('0'.repeat(8 - key.length));
const keyHex = new Buffer(key);
const cipher = crypto.createCipheriv('des-cbc', keyHex, keyHex);
let c = cipher.update(message, 'utf8', 'base64');
c += cipher.final('base64');
return c;
}
// DES 解密
export function desDecrypt(text: string, key: string = config.secret) {
key =
key.length >= 8 ? key.slice(0, 8) : key.concat('0'.repeat(8 - key.length));
const keyHex = new Buffer(key);
const cipher = crypto.createDecipheriv('des-cbc', keyHex, keyHex);
let c = cipher.update(text, 'base64', 'utf8');
c += cipher.final('utf8');
return c;
}

80
server/lib/utils.ts Normal file
View File

@@ -0,0 +1,80 @@
import randomString from 'crypto-random-string';
import _ from 'lodash';
import urlRegex from 'url-regex';
/**
* 返回电子邮箱的地址
* @param email 电子邮箱
* @returns 电子邮箱
*/
export function getEmailAddress(email: string) {
return email.split('@')[0];
}
/**
* 生成随机字符串
* @param length 随机字符串长度
*/
export function generateRandomStr(length = 10): string {
return randomString({ length });
}
export function generateRandomNumStr(length = 6) {
return randomString({
length,
type: 'numeric',
});
}
/**
* 是否一个可用的字符串
* 定义为有长度的字符串
*/
export function isValidStr(str: unknown): str is string {
return typeof str == 'string' && str !== '';
}
/**
* 判断是否是一个可用的url
*/
export function isValidUrl(str: unknown): str is string {
return typeof str == 'string' && urlRegex({ exact: true }).test(str);
}
/**
* 检测一个地址是否是一个合法的资源地址
*/
export function isValidStaticAssetsUrl(str: unknown): str is string {
if (typeof str !== 'string') {
return false;
}
const filename = _.last(str.split('/'));
if (filename.indexOf('.') === -1) {
return false;
}
return true;
}
/**
* 休眠一定时间
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) =>
setTimeout(() => {
resolve();
}, ms)
);
}
/**
* 检查url地址是否匹配
*/
export function checkPathMatch(urlList: string[], url: string): boolean {
const fuzzList = urlList.map((url) => url.replaceAll('/', '.'));
const fuzzUrl = url.split('?')[0].replaceAll('/', '.');
// 考虑到serviceName中间可能会有. 且注册的时候不可能把所有情况都列出来,因此进行模糊处理
return fuzzList.includes(fuzzUrl);
}