feat: add invite code usage limit for every invite code

pull/105/head
moonrailgun 2 years ago
parent 518d12bb3e
commit 738eb75003

@ -32,6 +32,7 @@
"k1ea9f5ff": "Verified",
"k206eff71": "Nickname can not be blank",
"k21ee7a1f": "Roles",
"k226a52c3": "Invite settings modified successfully",
"k22856100": "Not found",
"k22ffe5e9": "Allow to manage users",
"k2317a90c": "Clear Inbox",
@ -41,6 +42,7 @@
"k249e23b9": "E-mail format is incorrect",
"k24ccd723": "Refresh now",
"k250e392c": "System is busy, please try again later",
"k263bff41": "Edit invite link",
"k267cc491": "Me",
"k2a1422d2": "Configuration",
"k2a8031e": "Homepage",
@ -87,6 +89,7 @@
"k41064134": "DM",
"k416e301a": "An exception occurred, Socket creation failed",
"k419da0ef": "Message explanation",
"k41a0bacf": "12 hours",
"k4231ab36": "Performance statistics",
"k42a98418": "File Service",
"k4539164b": "The OTP code is 6 digits",
@ -97,8 +100,10 @@
"k48a38bc1": "Group not found",
"k49721de0": "Reset Password",
"k4a573576": "Unable to modify the display name of the Everyone permission group",
"k4c6cd28": "6 hours",
"k4cda3b42": "30 minutes",
"k4d32a754": "Group Name",
"k4e456fee": "50 uses",
"k4f672109": "Failed to load global configuration",
"k4f69cbc9": "Forget Password",
"k50504f9e": "Upload picture to converse",
@ -156,6 +161,7 @@
"k6fc3abcd": "Enter now",
"k705ca774": "Hide member full name",
"k70751578": "Jump to converse",
"k70b73a14": "1 use",
"k7173d09e": "Account",
"k736edc2f": "Old and new passwords do not match",
"k73d9fc70": "Allow members to view admin channels",
@ -171,6 +177,7 @@
"k78102887": "Member Count",
"k78e52ed0": "Accept",
"k79304fa9": "Nickname changed successfully",
"k795c9a6e": "5 uses",
"k7a89720": "Open in new window",
"k7c232f9e": "Panel",
"k7cf4e7ff": "Message deleted successfully",
@ -180,6 +187,7 @@
"k7f0c746d": "Operation successful",
"k7fc7e508": "Dark Mode",
"k814bdc7a": "Is not a valid JSON string",
"k8157b129": "25 uses",
"k81662255": "Create invitation code",
"k821ff85a": "Common",
"k8266bcf2": "New password",
@ -191,6 +199,7 @@
"k87a609ad": "Please do not install plugins from unknown sources, it may steal your personal information in Tailchat",
"k87dd7754": "Mention (@) your message will appear here",
"k887e1f4b": "Copy plain text succeeded",
"k8990744f": "Maximum number of uses",
"k89df1d1e": "The network is abnormal",
"k8abdba5c": "Has been sent",
"k8acbe00": "Current service available",
@ -220,6 +229,8 @@
"k9fc409ec": "Install Succeed",
"ka01a00eb": "System language",
"ka0451c97": "Cancel",
"ka0be0bd9": "Allow members to edit invite links",
"ka0ccce44": "100 uses",
"ka29e9508": "Send Image",
"ka2c48894": "Customize your group",
"ka2ed2b61": "Are you sure you want to do this?",
@ -238,6 +249,7 @@
"ka697d1d8": "Password modify complete",
"ka7907771": "Save Successful",
"ka7ecc377": "Unpin",
"ka820940c": "Never",
"ka9481f95": "Creator",
"kaa040a8e": "Default Group",
"kaa7d786e": "Create Converse",
@ -254,6 +266,7 @@
"kaf403ef0": "Light Mode",
"kaf51d834": "Reset to default",
"kb030fbd1": "Member List",
"kb0584341": "Usage Count",
"kb07659b0": "Repeat password",
"kb123dbb9": "Your personal unique identifier",
"kb12cc88f": "Unmute",
@ -279,6 +292,7 @@
"kbef193d": "Invitation link copied to clipboard",
"kbef5b92e": "Copy Link",
"kc14b2ea3": "Back",
"kc161f3a6": "1 hour",
"kc1afdd08": "Don't worry, you can make changes anytime after this",
"kc1bfb977": "Login {{serverName}}",
"kc2d30ab7": "Plugin Name",
@ -338,6 +352,7 @@
"ke17b2c87": "Do not upload pictures that violate local laws and regulations",
"ke187440d": "Panel type cannot be empty",
"ke2431c67": "Plugin render function does not exist",
"ke3a77a77": "Unlimited",
"ke3d797fd": "Drop files to send into current converse",
"ke59ffe49": "Muted, there are {{remain}} left",
"ke6da074f": "The message was withdrawn successfully",
@ -395,5 +410,6 @@
"kfc07c0a4": "Here is the beginning of all messages, please feel free to speak up.",
"kfc0ccc0e": "It can be modified at any time in the user settings later",
"kfd340bbc": "Manage members",
"kfe731dfc": "Action"
"kfe731dfc": "Action",
"kfe9c1c6c": "10 uses"
}

@ -32,6 +32,7 @@
"k1ea9f5ff": "已认证",
"k206eff71": "昵称不能为空",
"k21ee7a1f": "身份组",
"k226a52c3": "邀请设置修改成功",
"k22856100": "未找到",
"k22ffe5e9": "允许管理用户",
"k2317a90c": "清空收件箱",
@ -41,6 +42,7 @@
"k249e23b9": "邮箱格式不正确",
"k24ccd723": "立即刷新",
"k250e392c": "系统忙, 请稍后再试",
"k263bff41": "编辑邀请链接",
"k267cc491": "我",
"k2a1422d2": "配置",
"k2a8031e": "个人主页",
@ -87,6 +89,7 @@
"k41064134": "私信",
"k416e301a": "出现异常, Socket 创建失败",
"k419da0ef": "消息解释",
"k41a0bacf": "12小时",
"k4231ab36": "性能统计",
"k42a98418": "文件服务",
"k4539164b": "校验码为6位",
@ -97,8 +100,10 @@
"k48a38bc1": "群组不存在",
"k49721de0": "重设密码",
"k4a573576": "无法修改所有人权限组的显示名称",
"k4c6cd28": "6小时",
"k4cda3b42": "30分钟",
"k4d32a754": "群组名称",
"k4e456fee": "50次使用",
"k4f672109": "全局配置加载失败",
"k4f69cbc9": "忘记密码",
"k50504f9e": "上传图片到会话",
@ -156,6 +161,7 @@
"k6fc3abcd": "立即进入",
"k705ca774": "隐藏成员完整名称",
"k70751578": "跳转到会话",
"k70b73a14": "1次使用",
"k7173d09e": "账户信息",
"k736edc2f": "新旧密码不匹配",
"k73d9fc70": "允许成员查看管理频道",
@ -171,6 +177,7 @@
"k78102887": "成员数",
"k78e52ed0": "接受",
"k79304fa9": "修改昵称成功",
"k795c9a6e": "5次使用",
"k7a89720": "在新窗口打开",
"k7c232f9e": "面板",
"k7cf4e7ff": "消息删除成功",
@ -180,6 +187,7 @@
"k7f0c746d": "操作成功",
"k7fc7e508": "暗黑模式",
"k814bdc7a": "不是一个合法的JSON字符串",
"k8157b129": "25次使用",
"k81662255": "创建邀请码",
"k821ff85a": "通用",
"k8266bcf2": "新密码",
@ -191,6 +199,7 @@
"k87a609ad": "请不要安装不明来源的插件,这可能会盗取你在 Tailchat 的个人信息",
"k87dd7754": "提及(@)您的消息会在这里出现哦",
"k887e1f4b": "复制纯文本成功",
"k8990744f": "最大使用次数",
"k89df1d1e": "网络出现异常",
"k8abdba5c": "已发送",
"k8acbe00": "当前服务可用",
@ -220,6 +229,8 @@
"k9fc409ec": "安装成功",
"ka01a00eb": "系统语言",
"ka0451c97": "取消",
"ka0be0bd9": "允许成员编辑邀请链接",
"ka0ccce44": "100次使用",
"ka29e9508": "发送图片",
"ka2c48894": "自定义你的群组",
"ka2ed2b61": "确认要进行该操作么?",
@ -238,6 +249,7 @@
"ka697d1d8": "密码修改成功",
"ka7907771": "保存成功",
"ka7ecc377": "Unpin",
"ka820940c": "永不",
"ka9481f95": "创建者",
"kaa040a8e": "默认群组",
"kaa7d786e": "创建会话",
@ -254,6 +266,7 @@
"kaf403ef0": "亮色模式",
"kaf51d834": "重置为默认值",
"kb030fbd1": "成员列表",
"kb0584341": "使用次数",
"kb07659b0": "重复密码",
"kb123dbb9": "您的个人唯一标识",
"kb12cc88f": "取消免打扰",
@ -279,6 +292,7 @@
"kbef193d": "邀请链接已复制到剪切板",
"kbef5b92e": "复制链接",
"kc14b2ea3": "返回",
"kc161f3a6": "1小时",
"kc1afdd08": "不要担心, 在此之后你可以随时进行变更",
"kc1bfb977": "登录 {{serverName}}",
"kc2d30ab7": "插件名",
@ -338,6 +352,7 @@
"ke17b2c87": "请勿上传违反当地法律法规的图片",
"ke187440d": "面板类型不能为空",
"ke2431c67": "插件渲染函数不存在",
"ke3a77a77": "无限制",
"ke3d797fd": "拖放文件以发送到当前会话",
"ke59ffe49": "禁言中, 还剩 {{remain}}",
"ke6da074f": "消息撤回成功",
@ -395,5 +410,6 @@
"kfc07c0a4": "这里是所有消息的开始,请畅所欲言。",
"kfc0ccc0e": "后续在用户设置中可以随时修改",
"kfd340bbc": "管理成员",
"kfe731dfc": "操作"
"kfe731dfc": "操作",
"kfe9c1c6c": "10次使用"
}

@ -97,6 +97,8 @@ export interface GroupInvite {
groupId: string;
creator: string;
expiredAt?: string;
usage: number;
usageLimit?: number;
}
/**
@ -267,6 +269,27 @@ export async function createGroupInviteCode(
return data;
}
/**
*
* @param groupId ID
* @param code
* @param expiredAt msundefined
* @param usageLimit 使undefined
*/
export async function editGroupInvite(
groupId: string,
code: string,
expiredAt?: number,
usageLimit?: number
) {
await request.post('/api/group/invite/editGroupInvite', {
groupId,
code,
expiredAt,
usageLimit,
});
}
/**
*
* @param groupId ID

@ -38,6 +38,7 @@ export const PERMISSION = {
message: 'core.message',
invite: 'core.invite',
unlimitedInvite: 'core.unlimitedInvite',
editInvite: 'core.editInvite',
groupDetail: 'core.groupDetail',
groupConfig: 'core.groupConfig',
manageUser: 'core.manageUser',
@ -68,6 +69,13 @@ export const getPermissionList = (): PermissionItemType[] => [
default: false,
required: [PERMISSION.core.invite],
},
{
key: PERMISSION.core.editInvite,
title: t('编辑邀请链接'),
desc: t('允许成员编辑邀请链接'),
default: false,
required: [PERMISSION.core.unlimitedInvite],
},
{
key: PERMISSION.core.groupDetail,
title: t('查看群组详情'),

@ -1,6 +1,7 @@
import { InviteCodeExpiredAt } from '@/components/InviteCodeExpiredAt';
import { closeModal, openModal } from '@/components/Modal';
import { generateInviteCodeUrl } from '@/utils/url-helper';
import { Menu, Typography, Dropdown, MenuProps } from 'antd';
import { Menu, Typography, Dropdown, MenuProps, Button } from 'antd';
import React, { useState } from 'react';
import {
useAsyncRequest,
@ -9,7 +10,10 @@ import {
GroupInvite,
PERMISSION,
useHasGroupPermission,
useEvent,
showToasts,
} from 'tailchat-shared';
import { EditGroupInvite } from '../EditGroupInvite';
import styles from './CreateInviteCode.module.less';
enum InviteCodeType {
@ -38,6 +42,23 @@ export const CreateInviteCode: React.FC<CreateInviteCodeProps> = React.memo(
PERMISSION.core.unlimitedInvite,
]);
const handleEditGroupInvite = useEvent(() => {
if (!createdInvite) {
return;
}
const key = openModal(
<EditGroupInvite
groupId={groupId}
code={createdInvite.code}
onEditSuccess={() => {
showToasts(t('邀请设置修改成功'), 'success');
closeModal(key);
}}
/>
);
});
const menu: MenuProps = {
items: [
{
@ -62,6 +83,9 @@ export const CreateInviteCode: React.FC<CreateInviteCodeProps> = React.memo(
</Typography.Title>
<p className="text-gray-500 text-sm">
<InviteCodeExpiredAt invite={createdInvite} />
<Button type="link" size="small" onClick={handleEditGroupInvite}>
{t('编辑')}
</Button>
</p>
</div>
) : (

@ -0,0 +1,117 @@
import { MetaFormFieldMeta, WebMetaForm } from 'tailchat-design';
import React from 'react';
import { model, t, useEvent } from 'tailchat-shared';
import { ModalWrapper } from '../../Modal';
interface MetaFormValues {
expiredAt: number;
usageLimit: number;
}
const fields: MetaFormFieldMeta[] = [
{
type: 'select',
name: 'expiredAt',
label: t('过期时间'),
defaultValue: -1,
options: [
{
label: t('30分钟'),
value: 30 * 60,
},
{
label: t('1小时'),
value: 60 * 60,
},
{
label: t('6小时'),
value: 6 * 60 * 60,
},
{
label: t('12小时'),
value: 12 * 60 * 60,
},
{
label: t('1天'),
value: 24 * 60 * 60,
},
{
label: t('7天'),
value: 7 * 24 * 60 * 60,
},
{
label: t('永不'),
value: -1,
},
],
},
{
type: 'select',
name: 'usageLimit',
label: t('最大使用次数'),
defaultValue: -1,
options: [
{
label: t('无限制'),
value: -1,
},
{
label: t('1次使用'),
value: 1,
},
{
label: t('5次使用'),
value: 5,
},
{
label: t('10次使用'),
value: 10,
},
{
label: t('25次使用'),
value: 25,
},
{
label: t('50次使用'),
value: 50,
},
{
label: t('100次使用'),
value: 100,
},
],
},
];
/**
*
*/
interface EditGroupInviteProps {
groupId: string;
code: string;
onEditSuccess: () => void;
}
export const EditGroupInvite: React.FC<EditGroupInviteProps> = React.memo(
(props) => {
const handleEditGroupInvite = useEvent(async (values: MetaFormValues) => {
await model.group.editGroupInvite(
props.groupId,
props.code,
values.expiredAt === -1
? undefined
: Date.now() + values.expiredAt * 1000,
values.usageLimit === -1 ? undefined : values.usageLimit
);
props.onEditSuccess();
});
return (
<ModalWrapper title={t('编辑邀请链接')}>
<WebMetaForm fields={fields} onSubmit={handleEditGroupInvite} />
</ModalWrapper>
);
}
);
EditGroupInvite.displayName = 'EditGroupInvite';

@ -8,17 +8,19 @@ import {
deleteGroupInvite,
useAsyncRefresh,
showToasts,
useEvent,
} from 'tailchat-shared';
import { Button, Space, Table, Tooltip } from 'antd';
import type { ColumnType } from 'antd/lib/table';
import { UserName } from '@/components/UserName';
import { openModal, openReconfirmModalP } from '@/components/Modal';
import { closeModal, openModal, openReconfirmModalP } from '@/components/Modal';
import { CreateGroupInvite } from '../CreateGroupInvite';
import { LoadingOnFirst } from '@/components/LoadingOnFirst';
import { IconBtn } from '@/components/IconBtn';
import copy from 'copy-to-clipboard';
import { generateInviteCodeUrl } from '@/utils/url-helper';
import { SensitiveText } from 'tailchat-design';
import { EditGroupInvite } from '../EditGroupInvite';
export const GroupInvite: React.FC<{
groupId: string;
@ -30,7 +32,7 @@ export const GroupInvite: React.FC<{
return list.reverse(); // 倒序返回
}, [groupId]);
const handleCreateInvite = useCallback(() => {
const handleCreateInvite = useEvent(() => {
openModal(
<CreateGroupInvite
groupId={groupId}
@ -39,22 +41,33 @@ export const GroupInvite: React.FC<{
}}
/>
);
}, [groupId, refresh]);
});
const handleCopyInviteCode = useCallback((inviteCode: string) => {
const handleEditInviteCode = useEvent((inviteCode: string) => {
const key = openModal(
<EditGroupInvite
groupId={groupId}
code={inviteCode}
onEditSuccess={() => {
showToasts(t('邀请设置修改成功'), 'success');
closeModal(key);
refresh();
}}
/>
);
});
const handleCopyInviteCode = useEvent((inviteCode: string) => {
copy(generateInviteCodeUrl(inviteCode));
showToasts(t('邀请链接已复制到剪切板'), 'success');
}, []);
});
const handleDeleteInvite = useCallback(
async (inviteId: string) => {
if (await openReconfirmModalP()) {
await deleteGroupInvite(groupId, inviteId);
await refresh();
}
},
[groupId, refresh]
);
const handleDeleteInvite = useEvent(async (inviteId: string) => {
if (await openReconfirmModalP()) {
await deleteGroupInvite(groupId, inviteId);
await refresh();
}
});
const columns: ColumnType<GroupInviteType>[] = useMemo(
() => [
@ -92,6 +105,18 @@ export const GroupInvite: React.FC<{
);
},
},
{
title: t('使用次数'),
dataIndex: 'usage',
render: (usage, record) => {
return (
<div>
{usage}
{record.usageLimit && ` / ${record.usageLimit}`}
</div>
);
},
},
{
title: t('创建者'),
dataIndex: 'creator',
@ -103,6 +128,12 @@ export const GroupInvite: React.FC<{
render: (id: string, record) => {
return (
<Space>
<IconBtn
title={t('编辑邀请链接')}
shape="square"
icon="mdi:edit"
onClick={() => handleEditInviteCode(record.code)}
/>
<IconBtn
title={t('复制邀请链接')}
shape="square"

@ -2,6 +2,7 @@
"k127fc33c": "User banned",
"k158d2868": "No delete permission",
"k16605863": "Token content is incorrect",
"k16ec3222": "No permission to edit invite code",
"k17f8532": "No message found",
"k1b3d8c72": "Group not found",
"k1bdc50f": "Please enter a username with a unique identifier such as: Nickname#0000",
@ -55,6 +56,7 @@
"kd3052d25": "No modification permission",
"kd389d15d": "This message was not found",
"kd470bc32": "The robot service of this application has not been activated",
"kde0762c6": "This invite code has been used up",
"ke050bc7a": "Username does not exist",
"ke0d53ced": "The other party is already your friend and cannot be added again",
"ke19c80a5": "User has no upload permission",

@ -2,6 +2,7 @@
"k127fc33c": "用户被封禁",
"k158d2868": "没有删除权限",
"k16605863": "Token 内容不正确",
"k16ec3222": "没有编辑邀请码权限",
"k17f8532": "没有找到消息",
"k1b3d8c72": "群组未找到",
"k1bdc50f": "请输入带唯一标识的用户名 如: Nickname#0000",
@ -55,6 +56,7 @@
"kd3052d25": "没有修改权限",
"kd389d15d": "该消息未找到",
"kd470bc32": "该应用的机器人服务尚未开通",
"kde0762c6": "该邀请码使用次数耗尽",
"ke050bc7a": "用户名不存在",
"ke0d53ced": "对方已经是您的好友, 不能再次添加",
"ke19c80a5": "用户无上传权限",

@ -36,9 +36,26 @@ export class GroupInvite extends TimeStamps implements Base {
})
groupId!: Ref<Group>;
/**
*
*/
@prop()
expiredAt?: Date;
/**
* 使
*/
@prop({
default: 0,
})
usage: number;
/**
* 使
*/
@prop()
usageLimit?: number;
/**
*
* @param groupId id

@ -7,6 +7,7 @@ export const PERMISSION = {
message: 'core.message',
invite: 'core.invite',
unlimitedInvite: 'core.unlimitedInvite',
editInvite: 'core.editInvite', // 编辑邀请码权限,需要有创建无限制邀请码的权限
groupDetail: 'core.groupDetail',
groupConfig: 'core.groupConfig',
manageUser: 'core.manageUser',

@ -12,6 +12,7 @@ import {
call,
NoPermissionError,
PERMISSION,
db,
} from 'tailchat-server-sdk';
interface GroupService
@ -31,6 +32,14 @@ class GroupService extends TcService {
inviteType: { type: 'enum', values: ['normal', 'permanent'] },
},
});
this.registerAction('editGroupInvite', this.editGroupInvite, {
params: {
code: 'string',
groupId: 'string',
expiredAt: { type: 'number', optional: true },
usageLimit: { type: 'number', optional: true },
},
});
this.registerAction('getAllGroupInviteCode', this.getAllGroupInviteCode, {
params: {
groupId: 'string',
@ -88,6 +97,49 @@ class GroupService extends TcService {
return await this.transformDocuments(ctx, {}, invite);
}
/**
*
*/
async editGroupInvite(
ctx: TcContext<{
code: string;
groupId: string;
expiredAt?: number; // 时间戳单位ms
usageLimit?: number;
}>
) {
const { code, groupId, expiredAt, usageLimit } = ctx.params;
const { userId, t } = ctx.meta;
// 检查权限
const [hasEditPermission] = await call(ctx).checkUserPermissions(
groupId,
userId,
[PERMISSION.core.editInvite]
);
if (!hasEditPermission) {
throw new NoPermissionError(t('没有编辑邀请码权限'));
}
const update = {};
if (expiredAt) {
_.set(update, ['expiredAt'], new Date(expiredAt));
} else {
_.set(update, ['$unset', 'expiredAt'], 1);
}
if (usageLimit) {
_.set(update, ['usageLimit'], usageLimit);
} else {
_.set(update, ['$unset', 'usageLimit'], 1);
}
await this.adapter.model.updateOne({ groupId, code }, update);
return true;
}
/**
*
*/
@ -143,6 +195,13 @@ class GroupService extends TcService {
code,
});
if (typeof invite.usageLimit === 'number') {
const usage = invite.usage || 0;
if (usage >= invite.usageLimit) {
throw new Error(t('该邀请码使用次数耗尽'));
}
}
if (new Date(invite.expiredAt).valueOf() < Date.now()) {
throw new Error(t('该邀请码已过期'));
}
@ -156,8 +215,18 @@ class GroupService extends TcService {
groupId: String(groupId),
});
const creatorInfo = await call(ctx).getUserInfo(String(invite.creator));
await this.adapter.model.updateOne(
{
_id: new db.Types.ObjectId(invite._id),
},
{
$inc: {
usage: 1,
},
}
);
const creatorInfo = await call(ctx).getUserInfo(String(invite.creator));
await call(ctx).addGroupSystemMessage(
String(groupId),
t('{{nickname}} 通过 {{creator}} 的邀请码加入群组', {

Loading…
Cancel
Save