feat: add welcome plugin for user which can send welcome message when user join group

welcome text support rich syntax and spec tag
pull/90/head
moonrailgun 2 years ago
parent 1c8108b36b
commit dbebbc54e6

@ -51,7 +51,7 @@ export const GroupConfig: React.FC<{
key={name}
title={item.title}
tip={item.tip}
content={item.render({
content={React.createElement(item.component, {
value: config[name],
onChange: (val: any) => handleModifyConfig(name, val),
loading,

@ -307,7 +307,7 @@ export const [pluginGroupConfigItems, regPluginGroupConfigItem] = buildRegList<{
name: string;
title: string;
tip?: string;
render: (props: {
component: (props: {
value: any;
onChange: (val: unknown) => void;
loading: boolean;

@ -149,6 +149,13 @@
border-color: rgba(255, 255, 255, 0.12);
}
// 文本框
.ant-input-textarea{
&.ant-input-textarea-show-count:after {
color: rgba(255, 255, 255, 0.45);
}
}
// 复选框
.ant-checkbox-wrapper {
color: white;

@ -147,6 +147,8 @@ declare module '@capital/common' {
deps?: React.DependencyList
) => [{ loading: boolean; value?: any }, T];
export const useEvent: any;
export const uploadFile: any;
export const showToasts: (
@ -170,6 +172,8 @@ declare module '@capital/common' {
export const joinArray: any;
export const useConverseMessageContext: any;
export const navigate: any;
export const useLocation: any;
@ -241,6 +245,10 @@ declare module '@capital/common' {
export const regChatInputAction: any;
export const pluginChatInputButtons: any;
export const regChatInputButton: any;
export const regSocketEventListener: (item: {
eventName: string;
eventFn: (...args: any[]) => void;
@ -328,6 +336,10 @@ declare module '@capital/common' {
export const regPluginInboxItemMap: any;
export const pluginGroupConfigItems: any;
export const regPluginGroupConfigItem: any;
export const useGroupIdContext: () => string;
export const useGroupPanelContext: () => {
@ -376,6 +388,10 @@ declare module '@capital/component' {
}>
>;
export const Popover: any;
export const Tag: any;
export const TextArea: any;
export const Avatar: any;
@ -410,6 +426,10 @@ declare module '@capital/component' {
export const MessageAckContainer: any;
export const BaseChatInputButton: any;
export const useChatInputActionContext: any;
export const GroupExtraDataPanel: any;
export const Image: any;
@ -431,8 +451,6 @@ declare module '@capital/component' {
export const PillTabPane: any;
export const LoadingSpinner: React.FC<{ tip?: string }>;
export const FullModalField: any;
export const DefaultFullModalInputEditorRender: any;
@ -480,6 +498,8 @@ declare module '@capital/component' {
children?: React.ReactNode;
}>;
export const LoadingSpinner: React.FC<{ tip?: string }>;
export const LoadingOnFirst: React.FC<{
spinning: boolean;
className?: string;

@ -14,7 +14,7 @@
"start:admin-next": "cd server/admin-next && pnpm start",
"build": "concurrently npm:build:web npm:build:server npm:build:admin && cp -r client/web/dist/* server/dist/public && cp -r client/web/dist/* server/dist/public",
"build:web": "cd client/web && pnpm build",
"build:server": "cd server && pnpm build && echo \"Install server side plugin:\" && pnpm run plugin:install com.msgbyte.tasks com.msgbyte.linkmeta com.msgbyte.github com.msgbyte.simplenotify com.msgbyte.topic com.msgbyte.agora com.msgbyte.wxpusher && mkdir -p ./dist/public && cp -r ./public/plugins ./dist/public && cp ./public/registry-be.json ./dist/public",
"build:server": "cd server && pnpm build && echo \"Install server side plugin:\" && pnpm run plugin:install com.msgbyte.tasks com.msgbyte.linkmeta com.msgbyte.github com.msgbyte.simplenotify com.msgbyte.topic com.msgbyte.agora com.msgbyte.wxpusher com.msgbyte.welcome && mkdir -p ./dist/public && cp -r ./public/plugins ./dist/public && cp ./public/registry-be.json ./dist/public",
"build:admin": "cd server/admin && pnpm build",
"build:admin-next": "cd server/admin-next && pnpm build",
"check:type": "concurrently npm:check:type:client npm:check:type:server",

@ -1998,6 +1998,31 @@ importers:
specifier: ^4.3.6
version: 4.3.6(immer@9.0.15)(react@18.2.0)
server/plugins/com.msgbyte.welcome:
dependencies:
tailchat-server-sdk:
specifier: '*'
version: link:../../packages/sdk
devDependencies:
'@types/react':
specifier: 18.0.20
version: 18.0.20
mini-star:
specifier: '*'
version: 1.3.1
server/plugins/com.msgbyte.welcome/web/plugins/com.msgbyte.welcome:
devDependencies:
'@types/styled-components':
specifier: ^5.1.26
version: 5.1.26
react:
specifier: 18.2.0
version: 18.2.0
styled-components:
specifier: ^5.3.6
version: 5.3.6(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)
server/plugins/com.msgbyte.wxpusher:
dependencies:
got:

@ -41,13 +41,24 @@ export function call(ctx: TcPureContext) {
}
);
},
/**
* id
*/
async getGroupLobbyConverseId(groupId: string): Promise<string | null> {
const lobbyConverseId: string = await ctx.call(
'group.getGroupLobbyConverseId',
{
groupId,
}
);
return lobbyConverseId;
},
/**
*
*/
async addGroupSystemMessage(groupId: string, message: string) {
const lobbyConverseId = await ctx.call('group.getGroupLobbyConverseId', {
groupId,
});
const lobbyConverseId = await call(ctx).getGroupLobbyConverseId(groupId);
if (!lobbyConverseId) {
// 如果没有文本频道则跳过

@ -52,4 +52,6 @@ export interface GroupStruct {
panels: GroupPanelStruct[];
roles?: GroupRoleStruct[];
config: Record<string, any>;
}

@ -1,7 +1,9 @@
export const userType = ['normalUser', 'pluginBot', 'openapiBot'] as const;
export type UserType = typeof userType[number];
export type UserType = (typeof userType)[number];
export interface UserStruct {
_id: string;
/**
*
* email

@ -0,0 +1,14 @@
const path = require('path');
module.exports = {
externalDeps: [
'react',
'react-router',
'axios',
'styled-components',
'zustand',
'zustand/middleware/immer',
],
pluginRoot: path.resolve(__dirname, './web'),
outDir: path.resolve(__dirname, '../../public'),
};

@ -0,0 +1,20 @@
{
"name": "tailchat-plugin-welcome",
"version": "1.0.0",
"main": "index.js",
"author": "moonrailgun",
"description": "加入群组时发送欢迎消息",
"license": "MIT",
"private": true,
"scripts": {
"build:web": "ministar buildPlugin all",
"build:web:watch": "ministar watchPlugin all"
},
"devDependencies": {
"@types/react": "18.0.20",
"mini-star": "*"
},
"dependencies": {
"tailchat-server-sdk": "*"
}
}

@ -0,0 +1,95 @@
import { call, TcContext } from 'tailchat-server-sdk';
import { TcService } from 'tailchat-server-sdk';
/**
*
*
*
*/
class WelcomeService extends TcService {
get serviceName() {
return 'plugin:com.msgbyte.welcome';
}
onInit() {
this.registryAfterActionHook('group.joinGroup', 'joinGroupCallback'); // not work
this.registerAction('joinGroupCallback', this.joinGroupCallback, {
params: {
groupId: 'string',
},
visibility: 'public',
});
}
async joinGroupCallback(
ctx: TcContext<{
groupId: string;
}>
) {
const { groupId } = ctx.params;
const groupInfo = await call(ctx).getGroupInfo(groupId);
if (groupInfo.config['plugin:groupWelcomeText']) {
// 有欢迎词
const lobbyConverseId = await call(ctx).getGroupLobbyConverseId(groupId);
if (!lobbyConverseId) {
// 如果没有文本频道则跳过
return;
}
const [welcomeText, meta] = await parseGroupWelcomeText(
ctx,
groupInfo.config['plugin:groupWelcomeText']
);
await ctx.call(
'chat.message.sendMessage',
{
converseId: lobbyConverseId,
groupId: groupId,
content: welcomeText,
meta,
},
{
meta: {
...ctx.meta,
userId: groupInfo.owner, // 以群组owner的名义
},
}
);
}
}
}
export default WelcomeService;
async function parseGroupWelcomeText(
ctx: TcContext,
welcomeText: string
): Promise<[string, {}]> {
const meta: Record<string, any> = {};
if (
welcomeText.includes('{nickname}') ||
welcomeText.includes('{@nickname}')
) {
const memberInfo = await call(ctx).getUserInfo(ctx.meta.userId);
const nickname = memberInfo.nickname;
const userId = String(memberInfo._id);
welcomeText = welcomeText.replaceAll('{nickname}', nickname);
if (welcomeText.includes('{@nickname}')) {
welcomeText = welcomeText.replaceAll(
'{@nickname}',
`[at=${userId}]${nickname}[/at]`
);
meta.mentions = [String(userId)];
}
}
return [welcomeText, meta];
}

@ -0,0 +1,11 @@
{
"label": "Group Welcome",
"label.zh-CN": "入群欢迎",
"name": "com.msgbyte.welcome",
"url": "{BACKEND}/plugins/com.msgbyte.welcome/index.js",
"version": "0.0.0",
"author": "moonrailgun",
"description": "Send a welcome message when joining a group",
"description.zh-CN": "加入群组时发送欢迎消息",
"requireRestart": true
}

@ -0,0 +1,16 @@
{
"name": "@plugins/com.msgbyte.welcome",
"main": "src/index.tsx",
"version": "0.0.0",
"description": "加入群组时发送欢迎消息",
"private": true,
"scripts": {
"sync:declaration": "tailchat declaration github"
},
"dependencies": {},
"devDependencies": {
"@types/styled-components": "^5.1.26",
"react": "18.2.0",
"styled-components": "^5.3.6"
}
}

@ -0,0 +1,37 @@
import { regPluginGroupConfigItem } from '@capital/common';
import { TextArea } from '@capital/component';
import React, { useState } from 'react';
import styled from 'styled-components';
import { Translate } from './translate';
console.log('Plugin Group Welcome is loaded');
const Desc = styled.div`
color: rgba(#999, 0.8);
font-size: 9px;
margin-top: 2px;
`;
regPluginGroupConfigItem({
name: 'groupWelcomeText',
title: Translate.welcomeText,
tip: Translate.welcomeTip,
component: ({ value, onChange, loading }) => {
const [text, setText] = useState(value ?? '');
return (
<>
<TextArea
disabled={loading}
value={text}
maxLength={1000}
showCount={true}
rows={5}
onChange={(e) => setText(e.target.value)}
onBlur={() => onChange(text)}
/>
<Desc>{Translate.welcomeDesc}</Desc>
</>
);
},
});

@ -0,0 +1,15 @@
import { localTrans } from '@capital/common';
export const Translate = {
welcomeText: localTrans({ 'zh-CN': '欢迎词', 'en-US': 'Welcome Text' }),
welcomeTip: localTrans({
'zh-CN': '向新成员发送入群欢迎消息',
'en-US': 'Send welcome message when new member',
}),
welcomeDesc: localTrans({
'zh-CN':
'清空则视为不启用,包含部分特殊写法,如{nickname}表示用户昵称, {@nickname}表示 @ 对方。同时支持富文本语法。',
'en-US':
'If it is empty, it will be regarded as disabled, including some special writing, such as {nickname} means user nickname, {@nickname} means @ target member.Also supports rich text syntax.',
}),
};

@ -0,0 +1,7 @@
{
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react",
"importsNotUsedAsValues": "error"
}
}

@ -0,0 +1,556 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/// <reference types="react" />
/**
* Tailchat
*
* : pnpm run plugins:declaration:generate
*/
/**
* Tailchat
*/
declare module '@capital/common' {
export const useGroupPanelParams: any;
/**
*
* @deprecated @capital/component
*/
export const openModal: (
content: React.ReactNode,
props?: {
/**
*
* @default false
*/
closable?: boolean;
/**
*
*/
maskClosable?: boolean;
/**
* modal
*/
onCloseModal?: () => void;
}
) => number;
/**
* @deprecated @capital/component
*/
export const closeModal: any;
/**
* @deprecated @capital/component
*/
export const ModalWrapper: any;
/**
* @deprecated @capital/component
*/
export const useModalContext: any;
/**
* @deprecated @capital/component
*/
export const openConfirmModal: any;
/**
* @deprecated @capital/component
*/
export const openReconfirmModal: any;
/**
* @deprecated @capital/component
*/
export const Loadable: any;
export const getGlobalState: any;
export const useGlobalSocketEvent: <T>(
eventName: string,
callback: (data: T) => void
) => void;
export const getJWTUserInfo: () => Promise<{
_id?: string;
nickname?: string;
email?: string;
avatar?: string;
}>;
export const dataUrlToFile: any;
export const urlSearchStringify: any;
export const urlSearchParse: any;
export const appendUrlSearch: any;
export const getServiceWorkerRegistration: any;
export const getServiceUrl: () => string;
export const getCachedUserInfo: (
userId: string,
refetch?: boolean
) => Promise<{
_id: string;
email: string;
nickname: string;
discriminator: string;
avatar: string | null;
temporary: boolean;
}>;
export const getCachedConverseInfo: any;
export const getCachedBaseGroupInfo: any;
export const getCachedUserSettings: any;
/**
*
* @example
* localTrans({'zh-CN': '你好', 'en-US': 'Hello'});
*
* @param trans
*/
export const localTrans: (trans: Record<'zh-CN' | 'en-US', string>) => string;
export const getLanguage: any;
export const sharedEvent: any;
export const useAsync: <T extends (...args: any[]) => Promise<any>>(
fn: T,
deps?: React.DependencyList
) => { loading: boolean; value?: any; error?: Error };
export const useAsyncFn: <T extends (...args: any[]) => Promise<any>>(
fn: T,
deps?: React.DependencyList
) => [{ loading: boolean; value?: any; error?: Error }, T];
export const useAsyncRefresh: <T extends (...args: any[]) => Promise<any>>(
fn: T,
deps?: React.DependencyList
) => { loading: boolean; value?: any; error?: Error; refresh: () => void };
export const useAsyncRequest: <T extends (...args: any[]) => Promise<any>>(
fn: T,
deps?: React.DependencyList
) => [{ loading: boolean; value?: any }, T];
export const useEvent: any;
export const uploadFile: any;
export const showToasts: (
message: string,
type?: 'info' | 'success' | 'error' | 'warning'
) => void;
export const showSuccessToasts: any;
export const showErrorToasts: (error: any) => void;
export const fetchAvailableServices: any;
export const isValidStr: (str: any) => str is string;
export const useGroupPanelInfo: any;
export const sendMessage: any;
export const showMessageTime: any;
export const joinArray: any;
export const useConverseMessageContext: any;
export const navigate: any;
export const useLocation: any;
export const useNavigate: any;
/**
* @deprecated please use createMetaFormSchema from @capital/component
*/
export const createFastFormSchema: any;
/**
* @deprecated please use metaFormFieldSchema from @capital/component
*/
export const fieldSchema: any;
export const useCurrentUserInfo: any;
export const createPluginRequest: (pluginName: string) => {
get: (actionName: string, config?: any) => Promise<any>;
post: (actionName: string, data?: any, config?: any) => Promise<any>;
};
export const postRequest: any;
export const pluginCustomPanel: any;
export const regCustomPanel: (info: {
position:
| 'personal'
| 'setting'
| 'groupdetail'
| 'navbar-more'
| 'navbar-group'
| 'navbar-personal';
icon: string;
name: string;
label: string;
render: React.ComponentType;
}) => void;
export const pluginGroupPanel: any;
export const regGroupPanel: any;
export const messageInterpreter: {
name?: string;
explainMessage: (message: string) => React.ReactNode;
}[];
export const regMessageInterpreter: (interpreter: {
name?: string;
explainMessage: (message: string) => React.ReactNode;
}) => void;
export const getMessageRender: (message: string) => React.ReactNode;
export const regMessageRender: (
render: (message: string) => React.ReactNode
) => void;
export const getMessageTextDecorators: any;
export const regMessageTextDecorators: any;
export const ChatInputActionContextProps: any;
export const pluginChatInputActions: any;
export const regChatInputAction: any;
export const pluginChatInputButtons: any;
export const regChatInputButton: any;
export const regSocketEventListener: (item: {
eventName: string;
eventFn: (...args: any[]) => void;
}) => void;
export const pluginColorScheme: any;
export const regPluginColorScheme: any;
export const pluginInspectServices: any;
export const regInspectService: any;
export const pluginMessageExtraParsers: any;
export const regMessageExtraParser: any;
export const pluginRootRoute: any;
export const regPluginRootRoute: any;
export const pluginPanelActions: any;
export const regPluginPanelAction: (
action:
| {
name: string;
label: string;
icon: string;
position: 'group';
onClick: (ctx: { groupId: string; panelId: string }) => void;
}
| {
name: string;
label: string;
icon: string;
position: 'dm';
onClick: (ctx: { converseId: string }) => void;
}
) => void;
export const pluginPermission: any;
export const regPluginPermission: (permission: {
/**
* key,
* , : plugin.com.msgbyte.github.manage
*/
key: string;
/**
*
*/
title: string;
/**
*
*/
desc: string;
/**
*
*/
default: boolean;
/**
*
*/
required?: string[];
}) => void;
export const pluginGroupPanelBadges: any;
export const regGroupPanelBadge: any;
export const pluginGroupTextPanelExtraMenus: any;
export const regPluginGroupTextPanelExtraMenu: any;
export const pluginUserExtraInfo: any;
export const regUserExtraInfo: any;
export const pluginSettings: any;
export const regPluginSettings: any;
export const pluginInboxItemMap: any;
export const regPluginInboxItemMap: any;
export const pluginGroupConfigItems: any;
export const regPluginGroupConfigItem: any;
export const useGroupIdContext: () => string;
export const useGroupPanelContext: () => {
groupId: string;
panelId: string;
} | null;
export const useSocketContext: any;
}
/**
* Tailchat
*/
declare module '@capital/component' {
export const Button: any;
export const Checkbox: any;
export const Input: any;
export const Divider: any;
export const Space: any;
export const Menu: any;
export const Table: any;
export const Switch: any;
export const Tooltip: any;
/**
* @link https://ant.design/components/notification-cn/
*/
export const notification: any;
export const Empty: React.FC<
React.PropsWithChildren<{
prefixCls?: string;
className?: string;
style?: React.CSSProperties;
imageStyle?: React.CSSProperties;
image?: React.ReactNode;
description?: React.ReactNode;
}>
>;
export const Popover: any;
export const Tag: any;
export const TextArea: any;
export const Avatar: any;
export const SensitiveText: React.FC<{ className?: string; text: string }>;
export const Icon: React.FC<{ icon: string } & React.SVGProps<SVGSVGElement>>;
export const CopyableText: React.FC<{
className?: string;
style?: React.CSSProperties;
config?:
| boolean
| {
text?: string;
onCopy?: (event?: React.MouseEvent<HTMLDivElement>) => void;
icon?: React.ReactNode;
tooltips?: boolean | React.ReactNode;
format?: 'text/plain' | 'text/html';
};
}>;
export const WebFastForm: any;
export const WebMetaForm: any;
export const createMetaFormSchema: any;
export const metaFormFieldSchema: any;
export const Link: any;
export const MessageAckContainer: any;
export const BaseChatInputButton: any;
export const useChatInputActionContext: any;
export const GroupExtraDataPanel: any;
export const Image: any;
export const IconBtn: React.FC<{
icon: string;
className?: string;
iconClassName?: string;
size?: 'small' | 'middle' | 'large';
shape?: 'circle' | 'square';
title?: string;
danger?: boolean;
active?: boolean;
disabled?: boolean;
onClick?: React.MouseEventHandler<HTMLElement>;
}>;
export const PillTabs: any;
export const PillTabPane: any;
export const FullModalField: any;
export const DefaultFullModalInputEditorRender: any;
export const DefaultFullModalTextAreaEditorRender: any;
export const openModal: (
content: React.ReactNode,
props?: {
/**
*
* @default false
*/
closable?: boolean;
/**
*
*/
maskClosable?: boolean;
/**
* modal
*/
onCloseModal?: () => void;
}
) => number;
export const closeModal: any;
export const ModalWrapper: any;
export const useModalContext: any;
export const openConfirmModal: any;
export const openReconfirmModal: any;
export const Loadable: any;
export const Loading: React.FC<{
spinning: boolean;
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
}>;
export const LoadingSpinner: React.FC<{ tip?: string }>;
export const LoadingOnFirst: React.FC<{
spinning: boolean;
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
}>;
export const SidebarView: any;
export const GroupPanelSelector: any;
export const Emoji: any;
export const PortalAdd: any;
export const PortalRemove: any;
export const ErrorBoundary: any;
export const ErrorView: React.FC<{
error: Error;
}>;
export const UserAvatar: React.FC<{
userId: string;
className?: string;
style?: React.CSSProperties;
size?: 'large' | 'small' | 'default' | number;
}>;
export const UserName: React.FC<{
userId: string;
className?: string;
style?: React.CSSProperties;
}>;
export const Markdown: any;
export const MarkdownEditor: any;
export const Webview: any;
export const WebviewKeepAlive: any;
export const Card: any;
export const Problem: any;
export const JumpToButton: any;
export const JumpToGroupPanelButton: any;
export const JumpToConverseButton: any;
}
Loading…
Cancel
Save