From b2a5b4a9f4e3a4b53d6d87030d800b7f3af8e9d0 Mon Sep 17 00:00:00 2001 From: iqovo <44756735+iqovo@users.noreply.github.com> Date: Mon, 24 Jan 2022 09:53:56 +0800 Subject: [PATCH 1/3] Delete system.ts --- packages/server/src/routes/system.ts | 356 --------------------------- 1 file changed, 356 deletions(-) delete mode 100644 packages/server/src/routes/system.ts diff --git a/packages/server/src/routes/system.ts b/packages/server/src/routes/system.ts deleted file mode 100644 index 98ff506af..000000000 --- a/packages/server/src/routes/system.ts +++ /dev/null @@ -1,356 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import axios from 'axios'; -import assert, { AssertionError } from 'assert'; -import { promisify } from 'util'; -import RegexEscape from 'regex-escape'; -import OSS, { STS } from 'ali-oss'; - -import config from '@fiora/config/server'; -import logger from '@fiora/utils/logger'; -import User from '@fiora/database/mongoose/models/user'; -import Group from '@fiora/database/mongoose/models/group'; - -import Socket from '@fiora/database/mongoose/models/socket'; -import { - getAllSealIp, - getAllSealUser, - getSealIpKey, - getSealUserKey, - DisableSendMessageKey, - DisableNewUserSendMessageKey, - Redis, -} from '@fiora/database/redis/initRedis'; - -/** 百度语言合成token */ -let baiduToken = ''; -/** 最后一次获取token的时间 */ -let lastBaiduTokenTime = Date.now(); - -/** - * 搜索用户和群组 - * @param ctx Context - */ -export async function search(ctx: Context<{ keywords: string }>) { - const keywords = ctx.data.keywords?.trim() || ''; - if (keywords === '') { - return { - users: [], - groups: [], - }; - } - - const escapedKeywords = RegexEscape(keywords); - const users = await User.find( - { username: { $regex: escapedKeywords } }, - { avatar: 1, username: 1 }, - ); - const groups = await Group.find( - { name: { $regex: escapedKeywords } }, - { avatar: 1, name: 1, members: 1 }, - ); - - return { - users, - groups: groups.map((group) => ({ - _id: group._id, - avatar: group.avatar, - name: group.name, - members: group.members.length, - })), - }; -} - -/** - * 搜索表情包, 爬其它站资源 - * @param ctx Context - */ -export async function searchExpression( - ctx: Context<{ keywords: string; limit?: number }>, -) { - const { keywords, limit = Infinity } = ctx.data; - if (keywords === '') { - return []; - } - - const res = await axios({ - method: 'get', - url: `https://pic.sogou.com/pics/json.jsp?query=${encodeURIComponent( - `${keywords} 表情`, - )}&st=5&start=0&xml_len=60&callback=callback&reqFrom=wap_result&`, - headers: { - accept: '*/*', - 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7', - 'cache-control': 'no-cache', - pragma: 'no-cache', - 'sec-fetch-mode': 'navigate', - 'sec-fetch-site': 'same-origin', - referrer: `https://pic.sogou.com/pic/emo/searchList.jsp?statref=search_form&uID=hTHHybkSPt37C46z&spver=0&rcer=&keyword=${encodeURIComponent( - keywords, - )}`, - referrerPolicy: 'no-referrer-when-downgrade', - 'user-agent': - 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', - }, - }); - assert(res.status === 200, '搜索表情包失败, 请重试'); - - try { - const parseDataResult = res.data.match(/callback\((.+)\)/); - const data = JSON.parse(`${parseDataResult[1]}`); - - type Image = { - locImageLink: string; - width: number; - height: number; - }; - const images = data.items as Image[]; - return images - .map(({ locImageLink, width, height }) => ({ - image: locImageLink, - width, - height, - })) - .filter((image, index) => - limit === Infinity ? true : index < limit, - ); - } catch (err) { - assert(false, '搜索表情包失败, 数据解析异常'); - } - - return []; -} - -/** - * 获取百度语言合成token - */ -export async function getBaiduToken() { - if (baiduToken && Date.now() < lastBaiduTokenTime) { - return { token: baiduToken }; - } - - const res = await axios.get( - 'https://openapi.baidu.com/oauth/2.0/token?grant_type=client_credentials&client_id=pw152BzvaSZVwrUf3Z2OHXM6&client_secret=fa273cc704b080e85ad61719abbf7794', - ); - assert(res.status === 200, '请求百度token失败'); - - baiduToken = res.data.access_token; - lastBaiduTokenTime = - Date.now() + (res.data.expires_in - 60 * 60 * 24) * 1000; - return { token: baiduToken }; -} - -/** - * 封禁用户, 需要管理员权限 - * @param ctx Context - */ -export async function sealUser(ctx: Context<{ username: string }>) { - const { username } = ctx.data; - assert(username !== '', 'username不能为空'); - - const user = await User.findOne({ username }); - if (!user) { - throw new AssertionError({ message: '用户不存在' }); - } - - const userId = user._id.toString(); - const isSealUser = await Redis.has(getSealUserKey(userId)); - assert(!isSealUser, '用户已在封禁名单'); - - await Redis.set(getSealUserKey(userId), userId, Redis.Minute * 10); - - return { - msg: 'ok', - }; -} - -/** - * 获取封禁列表, 包含用户封禁和ip封禁, 需要管理员权限 - */ -export async function getSealList() { - const sealUserList = await getAllSealUser(); - const sealIpList = await getAllSealIp(); - const users = await User.find({ _id: { $in: sealUserList } }); - - const result = { - users: users.map((user) => user.username), - ips: sealIpList, - }; - return result; -} - -const CantSealLocalIp = '不能封禁内网ip'; -const CantSealSelf = '闲的没事封自己干啥'; -const IpInSealList = 'ip已在封禁名单'; - -/** - * 封禁 ip 地址, 需要管理员权限 - */ -export async function sealIp(ctx: Context<{ ip: string }>) { - const { ip } = ctx.data; - assert(ip !== '::1' && ip !== '127.0.0.1', CantSealLocalIp); - assert(ip !== ctx.socket.ip, CantSealSelf); - - const isSealIp = await Redis.has(getSealIpKey(ip)); - assert(!isSealIp, IpInSealList); - - await Redis.set(getSealIpKey(ip), ip, Redis.Hour * 6); - - return { - msg: 'ok', - }; -} - -/** - * 封禁指定用户的所有在线 ip 地址, 需要管理员权限 - */ -export async function sealUserOnlineIp(ctx: Context<{ userId: string }>) { - const { userId } = ctx.data; - - const user = await User.findOne({ _id: userId }); - assert(user, '用户不存在'); - const sockets = await Socket.find({ user: userId }); - const ipList = [ - ...sockets.map((socket) => socket.ip), - user.lastLoginIp, - ].filter( - (ip) => - ip !== '' && - ip !== '::1' && - ip !== '127.0.0.1' && - ip !== ctx.socket.ip, - ); - - // 如果全部 ip 都已经封禁过了, 则直接提示 - const isSealIpList = await Promise.all( - ipList.map((ip) => Redis.has(getSealIpKey(ip))), - ); - assert(!isSealIpList.every((isSealIp) => isSealIp), IpInSealList); - - await Promise.all( - ipList.map(async (ip) => { - await Redis.set(getSealIpKey(ip), ip, Redis.Hour * 6); - }), - ); - - return { - msg: 'ok', - }; -} - -type STSResult = { - enable: boolean; - AccessKeyId: string; - AccessKeySecret: string; - bucket: string; - region: string; - SecurityToken: string; - endpoint: string; -}; - -// eslint-disable-next-line consistent-return -export async function getSTS(): Promise { - if (!config.aliyunOSS.enable) { - // @ts-ignore - return { - enable: false, - }; - } - - const sts = new STS({ - accessKeyId: config.aliyunOSS.accessKeyId, - accessKeySecret: config.aliyunOSS.accessKeySecret, - }); - try { - const result = await sts.assumeRole( - config.aliyunOSS.roleArn, - undefined, - undefined, - 'fiora-uploader', - ); - // @ts-ignore - return { - enable: true, - region: config.aliyunOSS.region, - bucket: config.aliyunOSS.bucket, - endpoint: config.aliyunOSS.endpoint, - ...result.credentials, - }; - } catch (err) { - const typedErr = err as Error; - assert.fail(`获取 STS 失败 - ${typedErr.message}`); - } -} - -export async function uploadFile( - ctx: Context<{ fileName: string; file: any; isBase64?: boolean }>, -) { - try { - if (config.aliyunOSS.enable) { - const sts = await getSTS(); - const client = new OSS({ - accessKeyId: sts.AccessKeyId, - accessKeySecret: sts.AccessKeySecret, - bucket: sts.bucket, - region: sts.region, - stsToken: sts.SecurityToken, - }); - const result = await client.put( - ctx.data.fileName, - ctx.data.isBase64 - ? Buffer.from(ctx.data.file, 'base64') - : ctx.data.file, - ); - if (result.res.status === 200) { - return { - url: `//${config.aliyunOSS.endpoint}/${result.name}`, - }; - } - throw Error('上传阿里云OSS失败'); - } - - const [directory, fileName] = ctx.data.fileName.split('/'); - const filePath = path.resolve('__dirname', '../public', directory); - const isExists = await promisify(fs.exists)(filePath); - if (!isExists) { - await promisify(fs.mkdir)(filePath); - } - await promisify(fs.writeFile)( - path.resolve(filePath, fileName), - ctx.data.file, - ); - return { - url: `/${ctx.data.fileName}`, - }; - } catch (err) { - const typedErr = err as Error; - logger.error('[uploadFile]', typedErr.message); - return `上传文件失败:${typedErr.message}`; - } -} - -export async function toggleSendMessage(ctx: Context<{ enable: boolean }>) { - const { enable } = ctx.data; - await Redis.set(DisableSendMessageKey, (!enable).toString()); - return { - msg: 'ok', - }; -} - -export async function toggleNewUserSendMessage( - ctx: Context<{ enable: boolean }>, -) { - const { enable } = ctx.data; - await Redis.set(DisableNewUserSendMessageKey, (!enable).toString()); - return { - msg: 'ok', - }; -} - -export async function getSystemConfig() { - return { - disableSendMessage: (await Redis.get(DisableSendMessageKey)) === 'true', - disableNewUserSendMessage: - (await Redis.get(DisableNewUserSendMessageKey)) === 'true', - }; -} From 61a40ad923966418227c7458412e455a74561e41 Mon Sep 17 00:00:00 2001 From: iqovo <44756735+iqovo@users.noreply.github.com> Date: Mon, 24 Jan 2022 09:59:32 +0800 Subject: [PATCH 2/3] Add files via upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复表情搜索403的问题 --- packages/server/src/routes/system.ts | 358 +++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 packages/server/src/routes/system.ts diff --git a/packages/server/src/routes/system.ts b/packages/server/src/routes/system.ts new file mode 100644 index 000000000..f7c1cf914 --- /dev/null +++ b/packages/server/src/routes/system.ts @@ -0,0 +1,358 @@ +import fs from 'fs'; +import path from 'path'; +import axios from 'axios'; +import assert, { AssertionError } from 'assert'; +import { promisify } from 'util'; +import RegexEscape from 'regex-escape'; +import OSS, { STS } from 'ali-oss'; + +import config from '@fiora/config/server'; +import logger from '@fiora/utils/logger'; +import User from '@fiora/database/mongoose/models/user'; +import Group from '@fiora/database/mongoose/models/group'; + +import Socket from '@fiora/database/mongoose/models/socket'; +import { + getAllSealIp, + getAllSealUser, + getSealIpKey, + getSealUserKey, + DisableSendMessageKey, + DisableNewUserSendMessageKey, + Redis, +} from '@fiora/database/redis/initRedis'; + +/** 百度语言合成token */ +let baiduToken = ''; +/** 最后一次获取token的时间 */ +let lastBaiduTokenTime = Date.now(); + +/** + * 搜索用户和群组 + * @param ctx Context + */ +export async function search(ctx: Context<{ keywords: string }>) { + const keywords = ctx.data.keywords?.trim() || ''; + if (keywords === '') { + return { + users: [], + groups: [], + }; + } + + const escapedKeywords = RegexEscape(keywords); + const users = await User.find( + { username: { $regex: escapedKeywords } }, + { avatar: 1, username: 1 }, + ); + const groups = await Group.find( + { name: { $regex: escapedKeywords } }, + { avatar: 1, name: 1, members: 1 }, + ); + + return { + users, + groups: groups.map((group) => ({ + _id: group._id, + avatar: group.avatar, + name: group.name, + members: group.members.length, + })), + }; +} + +/** + * 搜索表情包, 爬其它站资源,搜狗表情站 + * @param ctx Context + */ +export async function searchExpression( + ctx: Context<{ keywords: string; limit?: number }>, +) { + const { keywords, limit = Infinity } = ctx.data; + if (keywords === '') { + return []; + } + + const res = await axios({ + method: 'get', + url: `https://pic.sogou.com/napi/wap/emoji/searchlist?keyword=${encodeURIComponent(`${keywords}`)}&st=5&start=0&xml_len=60&callback=callback&reqFrom=wap_result&routeName=emosearch`, + headers: { + accept: '*/*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7', + 'cache-control': 'no-cache', + pragma: 'no-cache', + 'sec-fetch-mode': 'navigate', + 'sec-fetch-site': 'same-origin', + referrer: `https://pic.sogou.com/`, + referrerPolicy: 'no-referrer-when-downgrade', + 'user-agent': + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + }, + }); + assert(res.status === 200, '搜索表情包失败, 请重试'); + + + + + const data = res.data.data.emotions; + + type Image = { + locImageLink: string; + width: number; + height: number; + }; + + var img_arr=[]; + + data.forEach(element=>{ + + img_arr.push({image:element.thumbSrc,width:90,height:90}); + }) + + + const images = img_arr as Image[]; + + + + + + + return images; + + + +} + +/** + * 获取百度语言合成token + */ +export async function getBaiduToken() { + if (baiduToken && Date.now() < lastBaiduTokenTime) { + return { token: baiduToken }; + } + + const res = await axios.get( + 'https://openapi.baidu.com/oauth/2.0/token?grant_type=client_credentials&client_id=pw152BzvaSZVwrUf3Z2OHXM6&client_secret=fa273cc704b080e85ad61719abbf7794', + ); + assert(res.status === 200, '请求百度token失败'); + + baiduToken = res.data.access_token; + lastBaiduTokenTime = + Date.now() + (res.data.expires_in - 60 * 60 * 24) * 1000; + return { token: baiduToken }; +} + +/** + * 封禁用户, 需要管理员权限 + * @param ctx Context + */ +export async function sealUser(ctx: Context<{ username: string }>) { + const { username } = ctx.data; + assert(username !== '', 'username不能为空'); + + const user = await User.findOne({ username }); + if (!user) { + throw new AssertionError({ message: '用户不存在' }); + } + + const userId = user._id.toString(); + const isSealUser = await Redis.has(getSealUserKey(userId)); + assert(!isSealUser, '用户已在封禁名单'); + + await Redis.set(getSealUserKey(userId), userId, Redis.Minute * 10); + + return { + msg: 'ok', + }; +} + +/** + * 获取封禁列表, 包含用户封禁和ip封禁, 需要管理员权限 + */ +export async function getSealList() { + const sealUserList = await getAllSealUser(); + const sealIpList = await getAllSealIp(); + const users = await User.find({ _id: { $in: sealUserList } }); + + const result = { + users: users.map((user) => user.username), + ips: sealIpList, + }; + return result; +} + +const CantSealLocalIp = '不能封禁内网ip'; +const CantSealSelf = '闲的没事封自己干啥'; +const IpInSealList = 'ip已在封禁名单'; + +/** + * 封禁 ip 地址, 需要管理员权限 + */ +export async function sealIp(ctx: Context<{ ip: string }>) { + const { ip } = ctx.data; + assert(ip !== '::1' && ip !== '127.0.0.1', CantSealLocalIp); + assert(ip !== ctx.socket.ip, CantSealSelf); + + const isSealIp = await Redis.has(getSealIpKey(ip)); + assert(!isSealIp, IpInSealList); + + await Redis.set(getSealIpKey(ip), ip, Redis.Hour * 6); + + return { + msg: 'ok', + }; +} + +/** + * 封禁指定用户的所有在线 ip 地址, 需要管理员权限 + */ +export async function sealUserOnlineIp(ctx: Context<{ userId: string }>) { + const { userId } = ctx.data; + + const user = await User.findOne({ _id: userId }); + assert(user, '用户不存在'); + const sockets = await Socket.find({ user: userId }); + const ipList = [ + ...sockets.map((socket) => socket.ip), + user.lastLoginIp, + ].filter( + (ip) => + ip !== '' && + ip !== '::1' && + ip !== '127.0.0.1' && + ip !== ctx.socket.ip, + ); + + // 如果全部 ip 都已经封禁过了, 则直接提示 + const isSealIpList = await Promise.all( + ipList.map((ip) => Redis.has(getSealIpKey(ip))), + ); + assert(!isSealIpList.every((isSealIp) => isSealIp), IpInSealList); + + await Promise.all( + ipList.map(async (ip) => { + await Redis.set(getSealIpKey(ip), ip, Redis.Hour * 6); + }), + ); + + return { + msg: 'ok', + }; +} + +type STSResult = { + enable: boolean; + AccessKeyId: string; + AccessKeySecret: string; + bucket: string; + region: string; + SecurityToken: string; + endpoint: string; +}; + +// eslint-disable-next-line consistent-return +export async function getSTS(): Promise { + if (!config.aliyunOSS.enable) { + // @ts-ignore + return { + enable: false, + }; + } + + const sts = new STS({ + accessKeyId: config.aliyunOSS.accessKeyId, + accessKeySecret: config.aliyunOSS.accessKeySecret, + }); + try { + const result = await sts.assumeRole( + config.aliyunOSS.roleArn, + undefined, + undefined, + 'fiora-uploader', + ); + // @ts-ignore + return { + enable: true, + region: config.aliyunOSS.region, + bucket: config.aliyunOSS.bucket, + endpoint: config.aliyunOSS.endpoint, + ...result.credentials, + }; + } catch (err) { + const typedErr = err as Error; + assert.fail(`获取 STS 失败 - ${typedErr.message}`); + } +} + +export async function uploadFile( + ctx: Context<{ fileName: string; file: any; isBase64?: boolean }>, +) { + try { + if (config.aliyunOSS.enable) { + const sts = await getSTS(); + const client = new OSS({ + accessKeyId: sts.AccessKeyId, + accessKeySecret: sts.AccessKeySecret, + bucket: sts.bucket, + region: sts.region, + stsToken: sts.SecurityToken, + }); + const result = await client.put( + ctx.data.fileName, + ctx.data.isBase64 + ? Buffer.from(ctx.data.file, 'base64') + : ctx.data.file, + ); + if (result.res.status === 200) { + return { + url: `//${config.aliyunOSS.endpoint}/${result.name}`, + }; + } + throw Error('上传阿里云OSS失败'); + } + + const [directory, fileName] = ctx.data.fileName.split('/'); + const filePath = path.resolve('__dirname', '../public', directory); + const isExists = await promisify(fs.exists)(filePath); + if (!isExists) { + await promisify(fs.mkdir)(filePath); + } + await promisify(fs.writeFile)( + path.resolve(filePath, fileName), + ctx.data.file, + ); + return { + url: `/${ctx.data.fileName}`, + }; + } catch (err) { + const typedErr = err as Error; + logger.error('[uploadFile]', typedErr.message); + return `上传文件失败:${typedErr.message}`; + } +} + +export async function toggleSendMessage(ctx: Context<{ enable: boolean }>) { + const { enable } = ctx.data; + await Redis.set(DisableSendMessageKey, (!enable).toString()); + return { + msg: 'ok', + }; +} + +export async function toggleNewUserSendMessage( + ctx: Context<{ enable: boolean }>, +) { + const { enable } = ctx.data; + await Redis.set(DisableNewUserSendMessageKey, (!enable).toString()); + return { + msg: 'ok', + }; +} + +export async function getSystemConfig() { + return { + disableSendMessage: (await Redis.get(DisableSendMessageKey)) === 'true', + disableNewUserSendMessage: + (await Redis.get(DisableNewUserSendMessageKey)) === 'true', + }; +} From 77e5a7472639759f29f2a5f22657a762e51f1f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8F=9C=E8=8F=9C=E6=9E=9C?= <44756735+iqovo@users.noreply.github.com> Date: Sat, 3 Sep 2022 21:12:45 +0800 Subject: [PATCH 3/3] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1912edc3e..2a05dca26 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # [Fiora](https://fiora.suisuijiang.com/) · [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/yinxin630/fiora/blob/master/LICENSE) [![author](https://img.shields.io/badge/author-%E7%A2%8E%E7%A2%8E%E9%85%B1-blue.svg)](http://suisuijiang.com) [![Node.js Version](https://img.shields.io/badge/node.js-14.16.0-blue.svg)](http://nodejs.org/download) [![Test Status](https://github.com/yinxin630/fiora/workflows/Unit%20Test/badge.svg)](https://github.com/yinxin630/fiora/actions?query=workflow%3A%22Unit+Test%22) [![Typescript Status](https://github.com/yinxin630/fiora/workflows/Typescript%20Type%20Check/badge.svg)](https://github.com/yinxin630/fiora/actions?query=workflow%3A%22Typescript+Type+Check%22) +主题修改全局搜索theme Fiora is an interesting open source chat application. It is developed based on [node.js](https://nodejs.org/), [react](https://reactjs.org/) and [socket.io](https://socket.io/) technologies