mirror of
https://github.com/certd/certd.git
synced 2026-06-27 05:47:34 +08:00
chore: format
This commit is contained in:
@@ -1,41 +1,41 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||
|
||||
export const GROUP_TYPE_SITE = 'site';
|
||||
export const GROUP_TYPE_SITE = "site";
|
||||
|
||||
@Entity('cd_group')
|
||||
@Entity("cd_group")
|
||||
export class GroupEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'user_id', comment: '用户id' })
|
||||
@Column({ name: "user_id", comment: "用户id" })
|
||||
userId: number;
|
||||
|
||||
@Column({ name: 'name', comment: '分组名称' })
|
||||
@Column({ name: "name", comment: "分组名称" })
|
||||
name: string;
|
||||
|
||||
@Column({ name: 'icon', comment: '图标' })
|
||||
@Column({ name: "icon", comment: "图标" })
|
||||
icon: string;
|
||||
|
||||
@Column({ name: 'favorite', comment: '收藏' })
|
||||
@Column({ name: "favorite", comment: "收藏" })
|
||||
favorite: boolean;
|
||||
|
||||
@Column({ name: 'type', comment: '类型', length: 512 })
|
||||
@Column({ name: "type", comment: "类型", length: 512 })
|
||||
type: string;
|
||||
|
||||
@Column({ name: 'project_id', comment: '项目Id' })
|
||||
@Column({ name: "project_id", comment: "项目Id" })
|
||||
projectId: number;
|
||||
|
||||
@Column({
|
||||
name: 'create_time',
|
||||
comment: '创建时间',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
name: "create_time",
|
||||
comment: "创建时间",
|
||||
default: () => "CURRENT_TIMESTAMP",
|
||||
})
|
||||
createTime: Date;
|
||||
|
||||
@Column({
|
||||
name: 'update_time',
|
||||
comment: '修改时间',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
name: "update_time",
|
||||
comment: "修改时间",
|
||||
default: () => "CURRENT_TIMESTAMP",
|
||||
})
|
||||
updateTime: Date;
|
||||
}
|
||||
|
||||
@@ -12,15 +12,14 @@ export class CaptchaService {
|
||||
@Inject()
|
||||
addonGetterService: AddonGetterService;
|
||||
|
||||
|
||||
async getCaptcha(captchaAddonId?: number) {
|
||||
if (!captchaAddonId) {
|
||||
const settings = await this.sysSettingsService.getPublicSettings();
|
||||
captchaAddonId = settings.captchaAddonId ?? 0;
|
||||
}
|
||||
const addon: ICaptchaAddon = await this.addonGetterService.getAddonById(captchaAddonId, true, 0,null, {
|
||||
const addon: ICaptchaAddon = await this.addonGetterService.getAddonById(captchaAddonId, true, 0, null, {
|
||||
type: "captcha",
|
||||
name: "image"
|
||||
name: "image",
|
||||
});
|
||||
if (!addon) {
|
||||
throw new Error("验证码插件还未配置");
|
||||
@@ -28,8 +27,7 @@ export class CaptchaService {
|
||||
return await addon.getCaptcha();
|
||||
}
|
||||
|
||||
|
||||
async doValidate(opts: { form: any, must?: boolean, captchaAddonId?: number,req:CaptchaRequest }) {
|
||||
async doValidate(opts: { form: any; must?: boolean; captchaAddonId?: number; req: CaptchaRequest }) {
|
||||
if (!opts.captchaAddonId) {
|
||||
const settings = await this.sysSettingsService.getPublicSettings();
|
||||
opts.captchaAddonId = settings.captchaAddonId ?? 0;
|
||||
@@ -46,13 +44,11 @@ export class CaptchaService {
|
||||
if (!opts.form) {
|
||||
throw new Error("请输入验证码");
|
||||
}
|
||||
const res = await addon.onValidate(opts.form,opts.req);
|
||||
const res = await addon.onValidate(opts.form, opts.req);
|
||||
if (!res) {
|
||||
throw new Error("验证码错误");
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { cache, isDev, randomNumber, simpleNanoId } from '@certd/basic';
|
||||
import { AccessService, AccessSysGetter, CodeErrorException, SysSettingsService } from '@certd/lib-server';
|
||||
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { ISmsService } from '../sms/api.js';
|
||||
import { SmsServiceFactory } from '../sms/factory.js';
|
||||
import { cache, isDev, randomNumber, simpleNanoId } from "@certd/basic";
|
||||
import { AccessService, AccessSysGetter, CodeErrorException, SysSettingsService } from "@certd/lib-server";
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { ISmsService } from "../sms/api.js";
|
||||
import { SmsServiceFactory } from "../sms/factory.js";
|
||||
import { CaptchaService } from "./captcha-service.js";
|
||||
import { EmailService } from './email-service.js';
|
||||
import { CaptchaRequest } from '../../../plugins/plugin-captcha/api.js';
|
||||
import { EmailService } from "./email-service.js";
|
||||
import { CaptchaRequest } from "../../../plugins/plugin-captcha/api.js";
|
||||
|
||||
// {data: '<svg.../svg>', text: 'abcd'}
|
||||
/**
|
||||
@@ -24,32 +24,30 @@ export class CodeService {
|
||||
@Inject()
|
||||
captchaService: CaptchaService;
|
||||
|
||||
|
||||
|
||||
async checkCaptcha(body:any,req:CaptchaRequest) {
|
||||
return await this.captchaService.doValidate({form:body,req});
|
||||
async checkCaptcha(body: any, req: CaptchaRequest) {
|
||||
return await this.captchaService.doValidate({ form: body, req });
|
||||
}
|
||||
/**
|
||||
*/
|
||||
async sendSmsCode(
|
||||
phoneCode = '86',
|
||||
phoneCode = "86",
|
||||
mobile: string,
|
||||
opts?: {
|
||||
duration?: number,
|
||||
verificationType?: string,
|
||||
verificationCodeLength?: number,
|
||||
},
|
||||
duration?: number;
|
||||
verificationType?: string;
|
||||
verificationCodeLength?: number;
|
||||
}
|
||||
) {
|
||||
if (!mobile) {
|
||||
throw new Error('手机号不能为空');
|
||||
throw new Error("手机号不能为空");
|
||||
}
|
||||
|
||||
const verificationCodeLength = Math.floor(Math.max(Math.min(opts?.verificationCodeLength || 4, 8), 4));
|
||||
const verificationCodeLength = Math.floor(Math.max(Math.min(opts?.verificationCodeLength || 4, 8), 4));
|
||||
const duration = Math.floor(Math.max(Math.min(opts?.duration || 5, 15), 1));
|
||||
|
||||
const sysSettings = await this.sysSettingsService.getPrivateSettings();
|
||||
if (!sysSettings.sms?.config?.accessId) {
|
||||
throw new Error('当前站点还未配置短信');
|
||||
throw new Error("当前站点还未配置短信");
|
||||
}
|
||||
const smsType = sysSettings.sms.type;
|
||||
const smsConfig = sysSettings.sms.config;
|
||||
@@ -66,7 +64,7 @@ export class CodeService {
|
||||
phoneCode,
|
||||
});
|
||||
|
||||
const key = this.buildSmsCodeKey(phoneCode, mobile, opts?.verificationType);
|
||||
const key = this.buildSmsCodeKey(phoneCode, mobile, opts?.verificationType);
|
||||
cache.set(key, smsCode, {
|
||||
ttl: duration * 60 * 1000, //5分钟
|
||||
});
|
||||
@@ -81,38 +79,38 @@ export class CodeService {
|
||||
async sendEmailCode(
|
||||
email: string,
|
||||
opts?: {
|
||||
duration?: number,
|
||||
verificationType?: string,
|
||||
verificationCodeLength?: number,
|
||||
},
|
||||
duration?: number;
|
||||
verificationType?: string;
|
||||
verificationCodeLength?: number;
|
||||
}
|
||||
) {
|
||||
if (!email) {
|
||||
throw new Error('Email不能为空');
|
||||
throw new Error("Email不能为空");
|
||||
}
|
||||
|
||||
|
||||
const verificationCodeLength = Math.floor(Math.max(Math.min(opts?.verificationCodeLength || 4, 8), 4));
|
||||
const verificationCodeLength = Math.floor(Math.max(Math.min(opts?.verificationCodeLength || 4, 8), 4));
|
||||
const duration = Math.floor(Math.max(Math.min(opts?.duration || 5, 15), 1));
|
||||
|
||||
const code = randomNumber(verificationCodeLength);
|
||||
|
||||
|
||||
const templateData = {
|
||||
code, duration,
|
||||
code,
|
||||
duration,
|
||||
title: "验证码",
|
||||
content:`您的验证码是${code},请勿泄露`,
|
||||
notificationType: "registerCode"
|
||||
}
|
||||
if (opts?.verificationType === 'forgotPassword') {
|
||||
templateData.title = '找回密码';
|
||||
templateData.notificationType = "forgotPassword"
|
||||
content: `您的验证码是${code},请勿泄露`,
|
||||
notificationType: "registerCode",
|
||||
};
|
||||
if (opts?.verificationType === "forgotPassword") {
|
||||
templateData.title = "找回密码";
|
||||
templateData.notificationType = "forgotPassword";
|
||||
}
|
||||
await this.emailService.sendByTemplate({
|
||||
type: templateData.notificationType,
|
||||
data: templateData,
|
||||
receivers: [email],
|
||||
type: templateData.notificationType,
|
||||
data: templateData,
|
||||
receivers: [email],
|
||||
});
|
||||
|
||||
const key = this.buildEmailCodeKey(email,opts?.verificationType);
|
||||
const key = this.buildEmailCodeKey(email, opts?.verificationType);
|
||||
cache.set(key, code, {
|
||||
ttl: duration * 60 * 1000, //5分钟
|
||||
});
|
||||
@@ -122,44 +120,43 @@ export class CodeService {
|
||||
/**
|
||||
* checkSms
|
||||
*/
|
||||
async checkSmsCode(opts: { mobile: string; phoneCode: string; smsCode: string; verificationType?: string; throwError: boolean; maxErrorCount?: number }) {
|
||||
async checkSmsCode(opts: { mobile: string; phoneCode: string; smsCode: string; verificationType?: string; throwError: boolean; maxErrorCount?: number }) {
|
||||
const key = this.buildSmsCodeKey(opts.phoneCode, opts.mobile, opts.verificationType);
|
||||
return this.checkValidateCode("sms",key, opts.smsCode, opts.throwError, opts.maxErrorCount);
|
||||
|
||||
return this.checkValidateCode("sms", key, opts.smsCode, opts.throwError, opts.maxErrorCount);
|
||||
}
|
||||
|
||||
buildSmsCodeKey(phoneCode: string, mobile: string, verificationType?: string) {
|
||||
return ['sms', verificationType, phoneCode, mobile].filter(item => !!item).join(':');
|
||||
return ["sms", verificationType, phoneCode, mobile].filter(item => !!item).join(":");
|
||||
}
|
||||
|
||||
buildEmailCodeKey(email: string, verificationType?: string) {
|
||||
return ['email', verificationType, email].filter(item => !!item).join(':');
|
||||
return ["email", verificationType, email].filter(item => !!item).join(":");
|
||||
}
|
||||
checkValidateCode(type:string,key: string, userCode: string, throwError = true, maxErrorCount = 3) {
|
||||
checkValidateCode(type: string, key: string, userCode: string, throwError = true, maxErrorCount = 3) {
|
||||
// 记录异常次数key
|
||||
if (isDev() && userCode==="1234567") {
|
||||
if (isDev() && userCode === "1234567") {
|
||||
return true;
|
||||
}
|
||||
const err_num_key = key + ':err_num';
|
||||
const err_num_key = key + ":err_num";
|
||||
//验证邮件验证码
|
||||
const code = cache.get(key);
|
||||
if (code == null || code !== userCode) {
|
||||
let maxRetryCount = false;
|
||||
if (!!code && maxErrorCount > 0) {
|
||||
const err_num = cache.get(err_num_key) || 0
|
||||
if(err_num >= maxErrorCount - 1) {
|
||||
const err_num = cache.get(err_num_key) || 0;
|
||||
if (err_num >= maxErrorCount - 1) {
|
||||
maxRetryCount = true;
|
||||
cache.delete(key);
|
||||
cache.delete(err_num_key);
|
||||
} else {
|
||||
cache.set(err_num_key, err_num + 1, {
|
||||
ttl: 30 * 60 * 1000
|
||||
ttl: 30 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (throwError) {
|
||||
const label = type ==='sms' ? '手机' : '邮箱';
|
||||
throw new CodeErrorException(!maxRetryCount ? `${label}验证码错误`: `${label}验证码错误请获取新的验证码`);
|
||||
const label = type === "sms" ? "手机" : "邮箱";
|
||||
throw new CodeErrorException(!maxRetryCount ? `${label}验证码错误` : `${label}验证码错误请获取新的验证码`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -169,8 +166,8 @@ export class CodeService {
|
||||
}
|
||||
|
||||
checkEmailCode(opts: { validateCode: string; email: string; verificationType?: string; throwError: boolean; maxErrorCount?: number }) {
|
||||
const key = this.buildEmailCodeKey(opts.email, opts.verificationType);
|
||||
return this.checkValidateCode('email',key, opts.validateCode, opts.throwError, opts.maxErrorCount);
|
||||
const key = this.buildEmailCodeKey(opts.email, opts.verificationType);
|
||||
return this.checkValidateCode("email", key, opts.validateCode, opts.throwError, opts.maxErrorCount);
|
||||
}
|
||||
|
||||
compile(templateString: string) {
|
||||
@@ -183,11 +180,10 @@ export class CodeService {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
buildValidationValueKey(code:string) {
|
||||
buildValidationValueKey(code: string) {
|
||||
return `validationValue:${code}`;
|
||||
}
|
||||
setValidationValue(value:any) {
|
||||
setValidationValue(value: any) {
|
||||
const randomCode = simpleNanoId(12);
|
||||
const key = this.buildValidationValueKey(randomCode);
|
||||
cache.set(key, value, {
|
||||
@@ -195,7 +191,7 @@ export class CodeService {
|
||||
});
|
||||
return randomCode;
|
||||
}
|
||||
getValidationValue(code:string) {
|
||||
getValidationValue(code: string) {
|
||||
return cache.get(this.buildValidationValueKey(code));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import type { EmailSend, EmailSendByTemplateReq } from '@certd/pipeline';
|
||||
import { IEmailService } from '@certd/pipeline';
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import type { EmailSend, EmailSendByTemplateReq } from "@certd/pipeline";
|
||||
import { IEmailService } from "@certd/pipeline";
|
||||
|
||||
import { logger } from '@certd/basic';
|
||||
import { isComm, isPlus } from '@certd/plus-core';
|
||||
import { logger } from "@certd/basic";
|
||||
import { isComm, isPlus } from "@certd/plus-core";
|
||||
|
||||
import nodemailer from 'nodemailer';
|
||||
import { SendMailOptions } from 'nodemailer';
|
||||
import { UserSettingsService } from '../../mine/service/user-settings-service.js';
|
||||
import { AddonService, PlusService, SysEmailConf, SysInstallInfo, SysSettingsService, SysSiteInfo } from '@certd/lib-server';
|
||||
import { getEmailSettings } from '../../sys/settings/fix.js';
|
||||
import nodemailer from "nodemailer";
|
||||
import { SendMailOptions } from "nodemailer";
|
||||
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
|
||||
import { AddonService, PlusService, SysEmailConf, SysInstallInfo, SysSettingsService, SysSiteInfo } from "@certd/lib-server";
|
||||
import { getEmailSettings } from "../../sys/settings/fix.js";
|
||||
import { UserEmailSetting } from "../../mine/service/models.js";
|
||||
import { AddonGetterService } from '../../pipeline/service/addon-getter-service.js';
|
||||
import { EmailContent, ITemplateProvider } from '../../../plugins/plugin-template/api.js';
|
||||
import { AddonGetterService } from "../../pipeline/service/addon-getter-service.js";
|
||||
import { EmailContent, ITemplateProvider } from "../../../plugins/plugin-template/api.js";
|
||||
|
||||
export type EmailConfig = {
|
||||
host: string;
|
||||
@@ -43,12 +43,11 @@ export class EmailService implements IEmailService {
|
||||
@Inject()
|
||||
addonGetterService: AddonGetterService;
|
||||
@Inject()
|
||||
addonService: AddonService
|
||||
|
||||
addonService: AddonService;
|
||||
|
||||
async sendByPlus(email: EmailSend) {
|
||||
if (!isPlus()) {
|
||||
throw new Error('plus not enabled');
|
||||
throw new Error("plus not enabled");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,13 +62,13 @@ export class EmailService implements IEmailService {
|
||||
/**
|
||||
*/
|
||||
async send(email: EmailSend) {
|
||||
logger.info('sendEmail', email);
|
||||
logger.info("sendEmail", email);
|
||||
|
||||
if (!email.receivers || email.receivers.length === 0) {
|
||||
throw new Error('收件人不能为空');
|
||||
throw new Error("收件人不能为空");
|
||||
}
|
||||
|
||||
let sysTitle = 'Certd';
|
||||
let sysTitle = "Certd";
|
||||
if (isComm()) {
|
||||
const siteInfo = await this.sysSettingsService.getSetting<SysSiteInfo>(SysSiteInfo);
|
||||
if (siteInfo) {
|
||||
@@ -79,11 +78,10 @@ export class EmailService implements IEmailService {
|
||||
let subject = email.subject;
|
||||
|
||||
if (!subject) {
|
||||
logger.error(new Error('邮件标题不能为空'));
|
||||
logger.error(new Error("邮件标题不能为空"));
|
||||
subject = `邮件标题为空,请联系管理员排查`;
|
||||
}
|
||||
|
||||
|
||||
if (!subject.includes(`【${sysTitle}】`)) {
|
||||
subject = `【${sysTitle}】${subject}`;
|
||||
}
|
||||
@@ -96,27 +94,25 @@ export class EmailService implements IEmailService {
|
||||
//自动使用plus发邮件
|
||||
return await this.sendByPlus(email);
|
||||
}
|
||||
throw new Error('邮件服务器还未设置');
|
||||
throw new Error("邮件服务器还未设置");
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (emailConf.usePlus && isPlus()) {
|
||||
return await this.sendByPlus(email);
|
||||
}
|
||||
await this.sendByCustom(emailConf, email, sysTitle);
|
||||
logger.info('sendEmail complete: ', email);
|
||||
logger.info("sendEmail complete: ", email);
|
||||
}
|
||||
|
||||
private async sendByCustom(emailConfig: EmailConfig, email: EmailSend, sysTitle: string) {
|
||||
const transporter = nodemailer.createTransport(emailConfig);
|
||||
let from = `${sysTitle} <${emailConfig.sender}>`;
|
||||
if (emailConfig.sender.includes('<')) {
|
||||
if (emailConfig.sender.includes("<")) {
|
||||
from = emailConfig.sender;
|
||||
}
|
||||
const mailOptions = {
|
||||
from: from,
|
||||
to: email.receivers.join(', '), // list of receivers
|
||||
to: email.receivers.join(", "), // list of receivers
|
||||
subject: email.subject,
|
||||
text: email.content,
|
||||
html: email.html,
|
||||
@@ -129,8 +125,8 @@ export class EmailService implements IEmailService {
|
||||
await this.sendByTemplate({
|
||||
type: "common",
|
||||
data: {
|
||||
title: '测试邮件,from certd',
|
||||
content: '测试邮件,from certd',
|
||||
title: "测试邮件,from certd",
|
||||
content: "测试邮件,from certd",
|
||||
url: await this.getTestEmailUrl(),
|
||||
},
|
||||
receivers: [receiver],
|
||||
@@ -147,41 +143,41 @@ export class EmailService implements IEmailService {
|
||||
}
|
||||
|
||||
async list(userId: any) {
|
||||
const userEmailSetting = await this.settingsService.getSetting<UserEmailSetting>(userId,null, UserEmailSetting)
|
||||
const userEmailSetting = await this.settingsService.getSetting<UserEmailSetting>(userId, null, UserEmailSetting);
|
||||
return userEmailSetting.list;
|
||||
}
|
||||
|
||||
async delete(userId: any, email: string) {
|
||||
const userEmailSetting = await this.settingsService.getSetting<UserEmailSetting>(userId, null, UserEmailSetting)
|
||||
const userEmailSetting = await this.settingsService.getSetting<UserEmailSetting>(userId, null, UserEmailSetting);
|
||||
userEmailSetting.list = userEmailSetting.list.filter(item => item !== email);
|
||||
await this.settingsService.saveSetting(userId, null, userEmailSetting)
|
||||
await this.settingsService.saveSetting(userId, null, userEmailSetting);
|
||||
}
|
||||
async add(userId: any, email: string) {
|
||||
const userEmailSetting = await this.settingsService.getSetting<UserEmailSetting>(userId, null, UserEmailSetting)
|
||||
const userEmailSetting = await this.settingsService.getSetting<UserEmailSetting>(userId, null, UserEmailSetting);
|
||||
//如果已存在
|
||||
if (userEmailSetting.list.includes(email)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
userEmailSetting.list.unshift(email)
|
||||
await this.settingsService.saveSetting(userId, null, userEmailSetting)
|
||||
userEmailSetting.list.unshift(email);
|
||||
await this.settingsService.saveSetting(userId, null, userEmailSetting);
|
||||
}
|
||||
|
||||
async sendByTemplate(req: EmailSendByTemplateReq) {
|
||||
let content = null
|
||||
let content = null;
|
||||
const emailConf = await this.sysSettingsService.getSetting<SysEmailConf>(SysEmailConf);
|
||||
const template = emailConf?.templates?.[req.type]
|
||||
if (isPlus() && template && template.addonId) {
|
||||
const addon: ITemplateProvider<EmailContent> = await this.addonGetterService.getAddonById(template.addonId, true, 0,null)
|
||||
const template = emailConf?.templates?.[req.type];
|
||||
if (isPlus() && template && template.addonId) {
|
||||
const addon: ITemplateProvider<EmailContent> = await this.addonGetterService.getAddonById(template.addonId, true, 0, null);
|
||||
if (addon) {
|
||||
content = await addon.buildContent({ data: req.data })
|
||||
content = await addon.buildContent({ data: req.data });
|
||||
}
|
||||
}
|
||||
if (isPlus() && !content ) {
|
||||
if (isPlus() && !content) {
|
||||
//看看有没有通用模版
|
||||
if (emailConf?.templates?.common && emailConf?.templates?.common.addonId) {
|
||||
const addon: ITemplateProvider<EmailContent> = await this.addonGetterService.getAddonById(emailConf.templates.common.addonId, true, 0,null)
|
||||
const addon: ITemplateProvider<EmailContent> = await this.addonGetterService.getAddonById(emailConf.templates.common.addonId, true, 0, null);
|
||||
if (addon) {
|
||||
content = await addon.buildContent({ data: req.data })
|
||||
content = await addon.buildContent({ data: req.data });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,21 +185,21 @@ export class EmailService implements IEmailService {
|
||||
// 没有找到模版,使用默认模版
|
||||
if (!content) {
|
||||
try {
|
||||
const addon: ITemplateProvider<EmailContent> = await this.addonGetterService.getBlank("emailTemplate", req.type)
|
||||
content = await addon.buildDefaultContent({ data: req.data })
|
||||
const addon: ITemplateProvider<EmailContent> = await this.addonGetterService.getBlank("emailTemplate", req.type);
|
||||
content = await addon.buildDefaultContent({ data: req.data });
|
||||
} catch (e) {
|
||||
// 对应的通知类型模版可能没有注册或者开发
|
||||
}
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
const addon: ITemplateProvider<EmailContent> = await this.addonGetterService.getBlank("emailTemplate", "common")
|
||||
content = await addon.buildDefaultContent({ data: req.data })
|
||||
const addon: ITemplateProvider<EmailContent> = await this.addonGetterService.getBlank("emailTemplate", "common");
|
||||
content = await addon.buildDefaultContent({ data: req.data });
|
||||
}
|
||||
return await this.send({
|
||||
...content,
|
||||
receivers: req.receivers,
|
||||
attachments: req.attachments,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { BaseService } from '@certd/lib-server';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { merge } from 'lodash-es';
|
||||
import { GroupEntity } from '../entity/group.js';
|
||||
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { BaseService } from "@certd/lib-server";
|
||||
import { InjectEntityModel } from "@midwayjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { merge } from "lodash-es";
|
||||
import { GroupEntity } from "../entity/group.js";
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
@@ -18,7 +18,7 @@ export class GroupService extends BaseService<GroupEntity> {
|
||||
|
||||
async add(bean: any) {
|
||||
if (!bean.type) {
|
||||
throw new Error('type is required');
|
||||
throw new Error("type is required");
|
||||
}
|
||||
bean = merge(
|
||||
{
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
import { logger } from "@certd/basic"
|
||||
import { logger } from "@certd/basic";
|
||||
|
||||
export class BackTaskExecutor {
|
||||
tasks: Record<string, Record<string, BackTask>> = {}
|
||||
tasks: Record<string, Record<string, BackTask>> = {};
|
||||
|
||||
start(task: BackTask) {
|
||||
const type = task.type || 'default'
|
||||
const type = task.type || "default";
|
||||
if (!this.tasks[type]) {
|
||||
this.tasks[type] = {}
|
||||
this.tasks[type] = {};
|
||||
}
|
||||
const oldTask = this.tasks[type][task.key]
|
||||
if (oldTask ){
|
||||
const oldTask = this.tasks[type][task.key];
|
||||
if (oldTask) {
|
||||
if (oldTask.status === "running") {
|
||||
throw new Error(`任务 ${task.title} 正在运行中`)
|
||||
throw new Error(`任务 ${task.title} 正在运行中`);
|
||||
}
|
||||
this.clear(type, task.key);
|
||||
}
|
||||
this.tasks[type][task.key] = task
|
||||
this.tasks[type][task.key] = task;
|
||||
this.run(task);
|
||||
}
|
||||
|
||||
get(type: string, key: string) {
|
||||
if (!this.tasks[type]) {
|
||||
this.tasks[type] = {}
|
||||
this.tasks[type] = {};
|
||||
}
|
||||
return this.tasks[type][key]
|
||||
return this.tasks[type][key];
|
||||
}
|
||||
|
||||
removeIsEnd(type: string, key: string) {
|
||||
const task = this.tasks[type]?.[key]
|
||||
const task = this.tasks[type]?.[key];
|
||||
if (task && task.status !== "running") {
|
||||
this.clear(type, key);
|
||||
}
|
||||
}
|
||||
|
||||
clear(type: string, key: string) {
|
||||
const task = this.tasks[type]?.[key]
|
||||
const task = this.tasks[type]?.[key];
|
||||
if (task) {
|
||||
task.clearTimeout();
|
||||
delete this.tasks[type][key]
|
||||
delete this.tasks[type][key];
|
||||
}
|
||||
}
|
||||
|
||||
private async run(task: BackTask) {
|
||||
const type = task.type || 'default'
|
||||
const type = task.type || "default";
|
||||
if (task.status === "running") {
|
||||
throw new Error(`任务 ${type}—${task.key} 正在运行中`)
|
||||
throw new Error(`任务 ${type}—${task.key} 正在运行中`);
|
||||
}
|
||||
task.startTime = Date.now();
|
||||
task.clearTimeout();
|
||||
@@ -54,84 +54,78 @@ export class BackTaskExecutor {
|
||||
} catch (e) {
|
||||
logger.error(`任务 ${task.title}[${type}-${task.key}] 执行失败`, e.message);
|
||||
task.status = "failed";
|
||||
task.addError(e.message)
|
||||
task.addError(e.message);
|
||||
} finally {
|
||||
task.endTime = Date.now();
|
||||
task.status = "done";
|
||||
task.setTimeoutId(setTimeout(() => {
|
||||
this.clear(type, task.key);
|
||||
}, 24 * 60 * 60 * 1000));
|
||||
task.setTimeoutId(
|
||||
setTimeout(() => {
|
||||
this.clear(type, task.key);
|
||||
}, 24 * 60 * 60 * 1000)
|
||||
);
|
||||
delete task.run;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
export class BackTask {
|
||||
type: string;
|
||||
key: string;
|
||||
title: string;
|
||||
total: number = 0;
|
||||
current: number = 0;
|
||||
skip: number = 0;
|
||||
total = 0;
|
||||
current = 0;
|
||||
skip = 0;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
status: string = "pending";
|
||||
status = "pending";
|
||||
errors?: string[] = [];
|
||||
private _timeoutId?: NodeJS.Timeout;
|
||||
|
||||
|
||||
|
||||
private _run: (task: BackTask) => Promise<void>;
|
||||
|
||||
constructor(opts: {
|
||||
type: string,
|
||||
key: string, title: string, run: (task: BackTask) => Promise<void>
|
||||
}) {
|
||||
const { key, title, run, type } = opts
|
||||
this.type = type
|
||||
constructor(opts: { type: string; key: string; title: string; run: (task: BackTask) => Promise<void> }) {
|
||||
const { key, title, run, type } = opts;
|
||||
this.type = type;
|
||||
this.key = key;
|
||||
this.title = title;
|
||||
this._run = run;
|
||||
|
||||
Object.defineProperty(this, '_run', {
|
||||
Object.defineProperty(this, "_run", {
|
||||
enumerable: false,
|
||||
value: this._run
|
||||
})
|
||||
Object.defineProperty(this, '_timeoutId', {
|
||||
enumerable: false,
|
||||
value: null
|
||||
})
|
||||
|
||||
Object.defineProperty(this, 'progress', {
|
||||
get: ()=> {
|
||||
return this.getProgress()
|
||||
},
|
||||
enumerable: true, // 关键:设置为可枚举
|
||||
configurable: true
|
||||
value: this._run,
|
||||
});
|
||||
Object.defineProperty(this, 'successCount', {
|
||||
get: ()=> {
|
||||
return this.getSuccessCount()
|
||||
Object.defineProperty(this, "_timeoutId", {
|
||||
enumerable: false,
|
||||
value: null,
|
||||
});
|
||||
|
||||
Object.defineProperty(this, "progress", {
|
||||
get: () => {
|
||||
return this.getProgress();
|
||||
},
|
||||
enumerable: true, // 关键:设置为可枚举
|
||||
configurable: true
|
||||
})
|
||||
Object.defineProperty(this, 'errorCount', {
|
||||
get: ()=> {
|
||||
return this.getErrorCount()
|
||||
enumerable: true, // 关键:设置为可枚举
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(this, "successCount", {
|
||||
get: () => {
|
||||
return this.getSuccessCount();
|
||||
},
|
||||
enumerable: true, // 关键:设置为可枚举
|
||||
configurable: true
|
||||
})
|
||||
Object.defineProperty(this, 'skipCount', {
|
||||
get: ()=> {
|
||||
return this.getSkipCount()
|
||||
enumerable: true, // 关键:设置为可枚举
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(this, "errorCount", {
|
||||
get: () => {
|
||||
return this.getErrorCount();
|
||||
},
|
||||
enumerable: true, // 关键:设置为可枚举
|
||||
configurable: true
|
||||
})
|
||||
enumerable: true, // 关键:设置为可枚举
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(this, "skipCount", {
|
||||
get: () => {
|
||||
return this.getSkipCount();
|
||||
},
|
||||
enumerable: true, // 关键:设置为可枚举
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
async run(task: BackTask) {
|
||||
@@ -145,7 +139,6 @@ export class BackTask {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setTimeoutId(timeoutId: NodeJS.Timeout) {
|
||||
this.clearTimeout();
|
||||
this._timeoutId = timeoutId;
|
||||
@@ -155,34 +148,33 @@ export class BackTask {
|
||||
this.total = total;
|
||||
}
|
||||
incrementCurrent() {
|
||||
this.current++
|
||||
this.current++;
|
||||
}
|
||||
|
||||
addError(error: string) {
|
||||
logger.error(error)
|
||||
this.errors.push(error)
|
||||
logger.error(error);
|
||||
this.errors.push(error);
|
||||
}
|
||||
|
||||
getErrorCount() {
|
||||
return this.errors.length
|
||||
return this.errors.length;
|
||||
}
|
||||
|
||||
getSkipCount() {
|
||||
return this.skip
|
||||
return this.skip;
|
||||
}
|
||||
|
||||
getSuccessCount() {
|
||||
return this.current - this.errors.length
|
||||
return this.current - this.errors.length;
|
||||
}
|
||||
|
||||
getProgress() {
|
||||
return (this.current * 1.0 / this.total * 100.0);
|
||||
return ((this.current * 1.0) / this.total) * 100.0;
|
||||
}
|
||||
|
||||
|
||||
incrementSkip() {
|
||||
this.skip++
|
||||
this.skip++;
|
||||
}
|
||||
}
|
||||
|
||||
export const taskExecutor = new BackTaskExecutor();
|
||||
export const taskExecutor = new BackTaskExecutor();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { logger } from '@certd/basic';
|
||||
import { ISmsService, PluginInputs, SmsPluginCtx } from './api.js';
|
||||
import { AliyunAccess, AliyunClient } from '../../../plugins/plugin-lib/aliyun/index.js';
|
||||
import { logger } from "@certd/basic";
|
||||
import { ISmsService, PluginInputs, SmsPluginCtx } from "./api.js";
|
||||
import { AliyunAccess, AliyunClient } from "../../../plugins/plugin-lib/aliyun/index.js";
|
||||
|
||||
export type AliyunSmsConfig = {
|
||||
accessId: string;
|
||||
@@ -11,30 +11,30 @@ export type AliyunSmsConfig = {
|
||||
export class AliyunSmsService implements ISmsService {
|
||||
static getDefine() {
|
||||
return {
|
||||
name: 'aliyun',
|
||||
desc: '阿里云短信服务',
|
||||
name: "aliyun",
|
||||
desc: "阿里云短信服务",
|
||||
input: {
|
||||
accessId: {
|
||||
title: '阿里云授权',
|
||||
title: "阿里云授权",
|
||||
component: {
|
||||
name: 'access-selector',
|
||||
type: 'aliyun',
|
||||
name: "access-selector",
|
||||
type: "aliyun",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
signName: {
|
||||
title: '签名',
|
||||
title: "签名",
|
||||
component: {
|
||||
name: 'a-input',
|
||||
vModel: 'value',
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
codeTemplateId: {
|
||||
title: '验证码模板Id',
|
||||
title: "验证码模板Id",
|
||||
component: {
|
||||
name: 'a-input',
|
||||
vModel: 'value',
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
@@ -55,8 +55,8 @@ export class AliyunSmsService implements ISmsService {
|
||||
await aliyunClinet.init({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
endpoint: 'https://dysmsapi.aliyuncs.com',
|
||||
apiVersion: '2017-05-25',
|
||||
endpoint: "https://dysmsapi.aliyuncs.com",
|
||||
apiVersion: "2017-05-25",
|
||||
});
|
||||
const smsConfig = this.ctx.config;
|
||||
const phoneNumber = phoneCode + mobile;
|
||||
@@ -67,6 +67,6 @@ export class AliyunSmsService implements ISmsService {
|
||||
TemplateParam: `{"code":"${code}"}`,
|
||||
};
|
||||
|
||||
await aliyunClinet.request('SendSms', params);
|
||||
await aliyunClinet.request("SendSms", params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FormItemProps, IAccessService } from '@certd/pipeline';
|
||||
import { FormItemProps, IAccessService } from "@certd/pipeline";
|
||||
|
||||
export interface ISmsService {
|
||||
sendSmsCode(opts: { mobile: string; code: string; phoneCode: string }): Promise<void>;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export class SmsServiceFactory {
|
||||
static async createSmsService(type: string) {
|
||||
const cls = await this.GetClassByType(type);
|
||||
@@ -7,17 +6,17 @@ export class SmsServiceFactory {
|
||||
|
||||
static async GetClassByType(type: string) {
|
||||
switch (type) {
|
||||
case 'aliyun':
|
||||
const {AliyunSmsService} = await import("./aliyun-sms.js")
|
||||
case "aliyun":
|
||||
const { AliyunSmsService } = await import("./aliyun-sms.js");
|
||||
return AliyunSmsService;
|
||||
case 'yfysms':
|
||||
const {YfySmsService} = await import("./yfy-sms.js")
|
||||
case "yfysms":
|
||||
const { YfySmsService } = await import("./yfy-sms.js");
|
||||
return YfySmsService;
|
||||
case 'tencent':
|
||||
const {TencentSmsService} = await import("./tencent-sms.js")
|
||||
case "tencent":
|
||||
const { TencentSmsService } = await import("./tencent-sms.js");
|
||||
return TencentSmsService;
|
||||
default:
|
||||
throw new Error('不支持的短信服务类型');
|
||||
throw new Error("不支持的短信服务类型");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TencentAccess } from '../../../plugins/plugin-lib/tencent/access.js';
|
||||
import {ISmsService, PluginInputs, SmsPluginCtx} from './api.js';
|
||||
import { TencentAccess } from "../../../plugins/plugin-lib/tencent/access.js";
|
||||
import { ISmsService, PluginInputs, SmsPluginCtx } from "./api.js";
|
||||
|
||||
export type TencentSmsConfig = {
|
||||
accessId: string;
|
||||
@@ -12,53 +12,53 @@ export type TencentSmsConfig = {
|
||||
export class TencentSmsService implements ISmsService {
|
||||
static getDefine() {
|
||||
return {
|
||||
name: 'tencent',
|
||||
desc: '腾讯云短信服务',
|
||||
name: "tencent",
|
||||
desc: "腾讯云短信服务",
|
||||
input: {
|
||||
accessId: {
|
||||
title: '腾讯云授权',
|
||||
title: "腾讯云授权",
|
||||
component: {
|
||||
name: 'access-selector',
|
||||
type: 'tencent',
|
||||
name: "access-selector",
|
||||
type: "tencent",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
region: {
|
||||
title: '区域',
|
||||
value:"ap-beijing",
|
||||
title: "区域",
|
||||
value: "ap-beijing",
|
||||
component: {
|
||||
name: 'a-select',
|
||||
vModel: 'value',
|
||||
options:[
|
||||
{value:"ap-beijing",label:"华北地区(北京)"},
|
||||
{value:"ap-guangzhou",label:"华南地区(广州)"},
|
||||
{value:"ap-nanjing",label:"华东地区(南京)"},
|
||||
]
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
options: [
|
||||
{ value: "ap-beijing", label: "华北地区(北京)" },
|
||||
{ value: "ap-guangzhou", label: "华南地区(广州)" },
|
||||
{ value: "ap-nanjing", label: "华东地区(南京)" },
|
||||
],
|
||||
},
|
||||
helper:"随便选一个",
|
||||
helper: "随便选一个",
|
||||
required: true,
|
||||
},
|
||||
signName: {
|
||||
title: '签名',
|
||||
title: "签名",
|
||||
component: {
|
||||
name: 'a-input',
|
||||
vModel: 'value',
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
appId: {
|
||||
title: '应用ID',
|
||||
title: "应用ID",
|
||||
component: {
|
||||
name: 'a-input',
|
||||
vModel: 'value',
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
codeTemplateId: {
|
||||
title: '验证码模板Id',
|
||||
title: "验证码模板Id",
|
||||
component: {
|
||||
name: 'a-input',
|
||||
vModel: 'value',
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
@@ -72,13 +72,11 @@ export class TencentSmsService implements ISmsService {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
|
||||
async getClient() {
|
||||
const sdk = await import('tencentcloud-sdk-nodejs/tencentcloud/services/sms/v20210111/index.js');
|
||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/sms/v20210111/index.js");
|
||||
const client = sdk.v20210111.Client;
|
||||
const access = await this.ctx.accessService.getById<TencentAccess>(this.ctx.config.accessId);
|
||||
|
||||
|
||||
// const region = this.region;
|
||||
const clientConfig = {
|
||||
credential: {
|
||||
@@ -102,15 +100,11 @@ export class TencentSmsService implements ISmsService {
|
||||
const client = await this.getClient();
|
||||
const smsConfig = this.ctx.config;
|
||||
const params = {
|
||||
"PhoneNumberSet": [
|
||||
`+${phoneCode}${mobile}`
|
||||
],
|
||||
"SmsSdkAppId": smsConfig.appId,
|
||||
"TemplateId": smsConfig.codeTemplateId,
|
||||
"SignName": smsConfig.signName,
|
||||
"TemplateParamSet": [
|
||||
code
|
||||
]
|
||||
PhoneNumberSet: [`+${phoneCode}${mobile}`],
|
||||
SmsSdkAppId: smsConfig.appId,
|
||||
TemplateId: smsConfig.codeTemplateId,
|
||||
SignName: smsConfig.signName,
|
||||
TemplateParamSet: [code],
|
||||
};
|
||||
const ret = await client.SendSms(params);
|
||||
this.checkRet(ret);
|
||||
@@ -118,7 +112,7 @@ export class TencentSmsService implements ISmsService {
|
||||
|
||||
checkRet(ret: any) {
|
||||
if (!ret || ret.Error) {
|
||||
throw new Error('执行失败:' + ret.Error.Code + ',' + ret.Error.Message);
|
||||
throw new Error("执行失败:" + ret.Error.Code + "," + ret.Error.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { http, utils } from '@certd/basic';
|
||||
import { ISmsService, PluginInputs, SmsPluginCtx } from './api.js';
|
||||
import { YfySmsAccess } from '../../../plugins/plugin-plus/yidun/access-sms.js';
|
||||
import { http, utils } from "@certd/basic";
|
||||
import { ISmsService, PluginInputs, SmsPluginCtx } from "./api.js";
|
||||
import { YfySmsAccess } from "../../../plugins/plugin-plus/yidun/access-sms.js";
|
||||
|
||||
export type YfySmsConfig = {
|
||||
accessId: string;
|
||||
@@ -10,22 +10,22 @@ export type YfySmsConfig = {
|
||||
export class YfySmsService implements ISmsService {
|
||||
static getDefine() {
|
||||
return {
|
||||
name: 'yfysms',
|
||||
desc: '易发云短信',
|
||||
name: "yfysms",
|
||||
desc: "易发云短信",
|
||||
input: {
|
||||
accessId: {
|
||||
title: '易发云短信授权',
|
||||
title: "易发云短信授权",
|
||||
component: {
|
||||
name: 'access-selector',
|
||||
type: 'yfysms',
|
||||
name: "access-selector",
|
||||
type: "yfysms",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
signName: {
|
||||
title: '签名',
|
||||
title: "签名",
|
||||
component: {
|
||||
name: 'a-input',
|
||||
vModel: 'value',
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
@@ -44,10 +44,10 @@ export class YfySmsService implements ISmsService {
|
||||
const access = await this.ctx.accessService.getById<YfySmsAccess>(this.ctx.config.accessId);
|
||||
|
||||
const res = await http.request({
|
||||
url: 'http://sms.yfyidc.cn/sms/',
|
||||
method: 'post',
|
||||
url: "http://sms.yfyidc.cn/sms/",
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
data: {
|
||||
/**
|
||||
@@ -75,40 +75,40 @@ export class YfySmsService implements ISmsService {
|
||||
* 9 用户已封禁
|
||||
* 10 未实名认证
|
||||
*/
|
||||
let message = '';
|
||||
let message = "";
|
||||
switch (res) {
|
||||
case 1:
|
||||
message = '余额不足';
|
||||
message = "余额不足";
|
||||
break;
|
||||
case 2:
|
||||
message = '用户不存在';
|
||||
message = "用户不存在";
|
||||
break;
|
||||
case 3:
|
||||
message = 'KEY错误';
|
||||
message = "KEY错误";
|
||||
break;
|
||||
case 4:
|
||||
message = '发送失败';
|
||||
message = "发送失败";
|
||||
break;
|
||||
case 5:
|
||||
message = '签名不存在';
|
||||
message = "签名不存在";
|
||||
break;
|
||||
case 6:
|
||||
message = '签名审核未通过';
|
||||
message = "签名审核未通过";
|
||||
break;
|
||||
case 7:
|
||||
message = '当前发信短信已达到上限';
|
||||
message = "当前发信短信已达到上限";
|
||||
break;
|
||||
case 8:
|
||||
message = '有违规词';
|
||||
message = "有违规词";
|
||||
break;
|
||||
case 9:
|
||||
message = '用户已封禁';
|
||||
message = "用户已封禁";
|
||||
break;
|
||||
case 10:
|
||||
message = '未实名认证';
|
||||
message = "未实名认证";
|
||||
break;
|
||||
default:
|
||||
message = '未知错误';
|
||||
message = "未知错误";
|
||||
}
|
||||
throw new Error(`发送短信失败:${message}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user