群友要送黑神话悟空了,写一个Yunzai plugin来帮忙抽一下奖,公开源码了:
import plugin from '../../lib/plugins/plugin.js';
export class LuckyDraw extends plugin {
constructor() {
super({
name: '抽奖',
dsc: '自动抽奖功能',
event: 'message',
priority: 5000,
rule: [
{
reg: '^#开始报名',
fnc: 'startSignup'
},
{
reg: '^报名\\s*(.+)$',
fnc: 'signup'
},
{
reg: '^#本群中奖记录$',
fnc: 'getDrawRecords'
},
{
reg: '^#重新挂载抽奖程序$',
fnc: 'resumePendingDraws'
}
]
});
}
async resumePendingDraws() {
const keys = await redis.keys('luckyDraw:*:endTime');
const now = Date.now();
for (const key of keys) {
const groupId = key.split(':')[1];
const theme = key.split(':')[2];
const endTime = parseInt(await redis.get(key), 10);
if (now >= endTime) {
await this.startDraw(groupId, theme);
} else {
// 恢复定时器
this.reply(`恢复定时器:${theme}, 倒计时:${(endTime - now) / 1000}秒`, false);
setTimeout(() => this.startDraw(groupId, theme), endTime - now);
}
}
}
async cleanKeys(groupId, theme) {
await redis.del(`luckyDraw:${groupId}:${theme}:theme`);
await redis.del(`luckyDraw:${groupId}:${theme}:numWinners`);
await redis.del(`luckyDraw:${groupId}:${theme}:endTime`);
await redis.del(`luckyDraw:${groupId}:${theme}:participants`);
}
async startSignup() {
const groupId = this.e.group_id;
if (this.e.sender.role !== 'admin' && this.e.sender.role !== 'owner') {
return this.reply('只有管理员或群主可以开始新的报名。', false, { at: true });
}
const match = this.e.msg.match(/^#开始报名\s*(.+)\s*,中奖人数(\d+)个,限时((\d+)(分钟|小时|天)|(\d{4}年\d{1,2}月\d{1,2}日\d{1,2}:\d{2}))$/);
if (!match) {
return this.reply('指令格式错误,请使用格式:#开始报名xxx,中奖人数n个,限时时间。', false, { at: true });
}
const theme = match[1];
const numWinners = parseInt(match[2], 10);
const existingKeywordLuckyDraw = await redis.get(`luckyDraw:${groupId}:${theme}:theme`);
const existingKeywordResult = await redis.get(`luckyDrawResult:${groupId}:${theme}`);
if (existingKeywordLuckyDraw || existingKeywordResult) {
return this.reply(`关键词 "${theme}" 已被占用,请选择其他关键词。`, false, { at: true });
}
let timeLimit;
if (match[4]) {
const timeValue = parseInt(match[4], 10);
const timeUnit = match[5];
if (timeUnit === '分钟') {
timeLimit = timeValue * 60;
} else if (timeUnit === '小时') {
timeLimit = timeValue * 3600;
} else if (timeUnit === '天') {
timeLimit = timeValue * 86400;
}
} else if (match[6]) {
const dateTimeStr = match[6].replace('年', '-').replace('月', '-').replace('日', ' ');
const endDate = new Date(dateTimeStr);
const now = new Date();
timeLimit = Math.floor((endDate.getTime() - now.getTime()) / 1000);
if (timeLimit <= 0) {
return this.reply('输入的时间已经过去,请重新设置。', false, { at: true });
}
}
if (timeLimit > 86400 * 30) {
return this.reply('时间过长,请重新发布指令,限时时间不能超过30天。', false, { at: true });
}
const startTime = Date.now();
const endTime = startTime + timeLimit * 1000;
console.log(endTime);
await redis.set(`luckyDraw:${groupId}:${theme}:theme`, theme);
await redis.set(`luckyDraw:${groupId}:${theme}:numWinners`, String(numWinners));
await redis.set(`luckyDraw:${groupId}:${theme}:endTime`, String(endTime));
await redis.set(`luckyDraw:${groupId}:${theme}:participants`, JSON.stringify([]));
await this.reply([`开始报名${theme}活动!请发送“报名${theme}”参加!`, `中奖人数:${numWinners}个,`, `报名截止时间:${new Date(endTime).toLocaleString()}`], false);
await this.reply([segment.at("all")]);
setTimeout(() => this.startDraw(groupId, theme), timeLimit * 1000);
}
async signup() {
const match = this.e.msg.match(/^报名\s*(.+)$/);
if (!match) {
return this.reply('报名格式错误,请使用 “报名+关键词” 进行报名。', false, { at: true });
}
const keyword = match[1].trim();
const groupId = this.e.group_id;
console.log(`luckyDraw:${groupId}:${keyword}:theme`);
const theme = await redis.get(`luckyDraw:${groupId}:${keyword}:theme`);
console.log(theme,keyword);
if (!theme) {
return this.reply(`报名关键词错误,请使用正确的关键词。`, false, { at: true });
}
await redis.watch(`luckyDraw:${groupId}:${keyword}:participants`);
let participants = JSON.parse(await redis.get(`luckyDraw:${groupId}:${keyword}:participants`)) || [];
if (participants.includes(this.e.user_id)) {
return this.reply('你已经报名了!', false, { at: true });
}
participants.push(this.e.user_id);
await redis.multi()
.set(`luckyDraw:${groupId}:${keyword}:participants`, JSON.stringify(participants))
.exec();
await this.reply('报名成功!', false, { at: true });
}
async startDraw(groupId, theme) {
const participants = JSON.parse(await redis.get(`luckyDraw:${groupId}:${theme}:participants`)) || [];
const numWinners = parseInt(await redis.get(`luckyDraw:${groupId}:${theme}:numWinners`), 10);
if (participants.length === 0) {
await this.cleanKeys(groupId, theme);
return this.reply(`活动${theme}:没有人报名,无法抽奖。`, false);
}
if (numWinners > participants.length) {
await this.cleanKeys(groupId, theme);
return this.reply(`活动${theme}:报名人数少于抽取人数,请重新设置抽取人数。`, false);
}
const winners = [];
const participantsCopy = [...participants];
for (let i = 0; i < numWinners; i++) {
const winnerIndex = Math.floor(Math.random() * participantsCopy.length);
winners.push(participantsCopy.splice(winnerIndex, 1)[0]);
}
await this.reply([`抽奖结束!${theme}的中奖者为以下人员`]);
for (const winner of winners) {
await this.reply([segment.at(winner)]);
}
await redis.set(`luckyDrawResult:${groupId}:${theme}`, JSON.stringify(winners));
await this.cleanKeys(groupId, theme);
}
async getDrawRecords() {
const groupId = this.e.group_id;
const keys = await redis.keys(`luckyDrawResult:${groupId}:*`);
if (keys.length === 0) {
return this.reply('本群还没有任何抽奖记录。', false, { at: true });
}
const records = [];
for (const key of keys) {
const theme = key.split(':')[2];
const winners = JSON.parse(await redis.get(key));
const winnerNames = winners.map(winner => segment.at(winner));
await this.reply([`活动${theme}的中奖者为:`, ...winnerNames]);
}
await this.reply(records.join('\n'), false);
}
}
其中服务器上出了redis的不少岔子,一一记录一下:
- 为了让Navicat连接上redis服务器,除了打开ufw的6379端口之外,还要使用 iptables -I 命令在指定位置插入规则。例如,将一个允许规则插入到链的顶部::
sudo iptables -I INPUT 1 -p tcp --dport 6379 -j ACCEPT
- ReadWriteDirectories=-/etc/redis 这一行配置允许 Redis 服务对 /etc/redis 目录进行写操作,这可能导致 Redis 动态修改配置文件并覆盖 dir 配置项。
sudo vim /lib/systemd/system/redis-server.service
- 可以尝试将这行配置注释掉或删除,然后重新加载 systemd 配置并重启 Redis 服务:
sudo systemctl daemon-reload
sudo systemctl restart redis