Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions lib/system/autoclose.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const cron = require('node-cron')
const { Function: Func } = require('@znan/wabot')

/**
* Get current time in HH:mm format
* @returns {string} Current time (e.g., "14:30")
*/
const getCurrentTime = () => {
const now = new Date()
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
}

/**
* Check if current time falls within range
* @param {string} current - Current time in HH:mm
* @param {string} start - Start time in HH:mm
* @param {string} end - End time in HH:mm
* @returns {boolean} True if current is within start-end range
*/
const isTimeInRange = (current, start, end) => {
const curr = current.split(':').map(Number)
const st = start.split(':').map(Number)
const ed = end.split(':').map(Number)

const currMin = curr[0] * 60 + curr[1]
const startMin = st[0] * 60 + st[1]
const endMin = ed[0] * 60 + ed[1]

Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The time range comparison logic does not handle cross-midnight scenarios. If a group should be closed from 23:00 to 01:00, the current implementation would incorrectly evaluate this range. When endMin is less than startMin (indicating a cross-midnight range), the logic should check if currMin is either greater than or equal to startMin OR less than endMin.

Suggested change
// Handle ranges that cross midnight (e.g., 23:00–01:00)
if (endMin < startMin) {
return currMin >= startMin || currMin < endMin
}
// Normal same-day range

Copilot uses AI. Check for mistakes.
return currMin >= startMin && currMin < endMin
}

/**
* Initialize auto group close scheduler
* Runs every minute to check and update group status
* @param {object} conn - Connection socket object
*/
const initAutoClose = (conn) => {
cron.schedule('* * * * *', async () => {
try {
if (!global.db?.groups) return

const currentTime = getCurrentTime()

for (const [jid, groupSet] of Object.entries(global.db.groups)) {
if (!groupSet.autoclose?.active || !groupSet.autoclose.start || !groupSet.autoclose.end) continue

const { start, end, isClosed } = groupSet.autoclose
const shouldBeClosed = isTimeInRange(currentTime, start, end)

// Close group if time is within range
if (shouldBeClosed && !isClosed) {
try {
await conn.groupSettingUpdate(jid, 'announcement')
groupSet.autoclose.isClosed = true
conn.reply(jid, Func.texted('bold', `🚩 Group status changed CLOSED due to autoclose set ${start}-${end}`))
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message states 'Group status changed CLOSED' but the past tense and capitalization are inconsistent with the opening message. For consistency with the corresponding open message (line 67), consider changing to 'Group status changed to CLOSED' or simply 'Group has been closed automatically'.

Suggested change
conn.reply(jid, Func.texted('bold', `🚩 Group status changed CLOSED due to autoclose set ${start}-${end}`))
conn.reply(jid, Func.texted('bold', `🚩 Group status changed to CLOSED due to autoclose set ${start}-${end}`))

Copilot uses AI. Check for mistakes.
} catch (e) {
console.log(`Error closing group ${jid}:`, e.message)
}
}
// Open group if time is outside range
else if (!shouldBeClosed && isClosed) {
try {
await conn.groupSettingUpdate(jid, 'not_announcement')
groupSet.autoclose.isClosed = false
conn.reply(jid, Func.texted('bold', `🚩 Group status changed OPEN due to autoclose set ${start}-${end}`))
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message states 'Group status changed OPEN' but would be clearer and more consistent with the closing message if it said 'Group status changed to OPEN' or 'Group has been opened automatically'.

Copilot uses AI. Check for mistakes.
} catch (e) {
console.log(`Error opening group ${jid}:`, e.message)
}
}
Comment on lines +52 to +71
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When manually opening or closing a group via the 'group' command (plugins/admin/restrict.js) or 'gcopt' command (plugins/owner/gc.js), the isClosed flag in the autoclose configuration is not updated. This can lead to state inconsistency where the scheduler may immediately re-close a manually opened group or vice versa. Consider adding logic to update groupSet.autoclose.isClosed when manual group status changes occur, or handle this scenario in the scheduler to check actual group status before making changes.

Copilot uses AI. Check for mistakes.
}
} catch (e) {
console.log('Error in auto_close scheduler:', e.message)
}
}, {
scheduled: true,
timezone: process.env.TZ || 'Asia/Jakarta'
})
Comment on lines +40 to +79
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cron job runs every minute for all groups with autoclose enabled. If there are many groups (e.g., hundreds), this could become a performance bottleneck. Consider implementing a more efficient scheduling approach, such as using separate timers for each group or batching group status changes. Additionally, the current approach may cause all groups to be processed simultaneously at the top of each minute, creating a spike in API calls to WhatsApp.

Copilot uses AI. Check for mistakes.
}

module.exports = { initAutoClose }
6 changes: 5 additions & 1 deletion lib/system/listeners.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ const cache = new NodeCache({
stdTTL: env.spam.cooldown
})
const Notifier = require('./notifier')
const { initAutoClose } = require('./autoclose')

module.exports = (conn, system) => {
const notify = new Notifier(conn.sock, false)
notify.start(15)

// Initialize auto close scheduler
initAutoClose(conn.sock)

conn.on('import', x => {
require('../../handler')(conn.sock, x, system.database)
require('./simple')(conn.sock)
Expand Down Expand Up @@ -69,7 +73,7 @@ module.exports = (conn, system) => {
if (!global.db || !global.db.groups) return
const groupSet = global.db.groups[x.jid]
if (!groupSet) return
const pic = await conn.sock.profilePictureUrl(x.member, 'image') || await Func.fetchBuffer('./src/image/default.jpg')
const pic = await conn.sock.profilePictureUrl(x.member, 'image') || await Func.fetchBuffer('./lib/assets/images/default.jpg')
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file path change from './src/image/default.jpg' to './lib/assets/images/default.jpg' appears to be unrelated to the auto group open/close feature. The directory './lib/assets/images/' does not exist in the codebase, while './src/image/default.jpg' is used consistently in other files. This change should either be removed or done in a separate PR with proper file migration.

Suggested change
const pic = await conn.sock.profilePictureUrl(x.member, 'image') || await Func.fetchBuffer('./lib/assets/images/default.jpg')
const pic = await conn.sock.profilePictureUrl(x.member, 'image') || await Func.fetchBuffer('./src/image/default.jpg')

Copilot uses AI. Check for mistakes.
if (x.action === 'add') {
const text = 'Hi @user👋\nWelcome to @subject\n\n@desc'
const txt = (groupSet && groupSet.text_welcome ? groupSet.text_welcome : text).replace('@user', `@${x.member.split`@`[0]}`).replace('@subject', x.subject || '').replace('@desc', x.groupMetadata.desc || '')
Expand Down
6 changes: 6 additions & 0 deletions lib/system/models.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ const models = {
antibot: false,
antiviewonce: false,
antitoxic: false,
autoclose: {
active: false,
start: null,
end: null,
isClosed: false
},
member: {},
expired: 0,
stay: false
Expand Down
36 changes: 31 additions & 5 deletions plugins/admin/moderation.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
help: ['welcome', 'left', 'autodetect', 'antidelete', 'antilink', 'antivirtex', 'autosticker', 'antisticker', 'antitagsw', 'antiporn', 'antitoxic', 'antibot'],
help: ['welcome', 'left', 'autodetect', 'antidelete', 'antilink', 'antivirtex', 'autosticker', 'antisticker', 'antitagsw', 'antiporn', 'antitoxic', 'antibot', 'autoclose'],
use: 'on / off',
tags: 'admin',
run: async (m, {
Expand All @@ -12,14 +12,40 @@ module.exports = {
}) => {
try {
let type = command.toLowerCase()

if (type === 'autoclose' && !setting.autoclose) {
setting.autoclose = {
active: false,
start: null,
end: null,
isClosed: false
}
}

Comment on lines +16 to +24
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initialization logic for autoclose in moderation.js duplicates the object structure already defined in lib/system/models.js (lines 41-46). If the structure needs to change in the future, it would need to be updated in multiple places. Consider extracting this to a shared function or constant, or simply rely on the model defaults being applied during database initialization.

Suggested change
if (type === 'autoclose' && !setting.autoclose) {
setting.autoclose = {
active: false,
start: null,
end: null,
isClosed: false
}
}

Copilot uses AI. Check for mistakes.
if (!isBotAdmin && /antilink|antivirtex|antitoxic|antisticker|antitagsw|antibot/.test(type)) return conn.reply(m.chat, global.status.botAdmin, m)
if (!args || !args[0]) return conn.reply(m.chat, `🚩 *Current status* : [ ${setting[type] ? 'ON' : 'OFF'} ] (Enter *On* or *Off*)`, m)
if (!args || !args[0]) {
if (type === 'autoclose') {
return conn.reply(m.chat, `🚩 *Current status* : [ ${setting.autoclose?.active ? 'ON' : 'OFF'} ] (Enter *On* or *Off*)`, m)
}
return conn.reply(m.chat, `🚩 *Current status* : [ ${setting[type] ? 'ON' : 'OFF'} ] (Enter *On* or *Off*)`, m)
}
let option = args[0].toLowerCase()
let optionList = ['on', 'off']
if (!optionList.includes(option)) return conn.reply(m.chat, `🚩 *Current status* : [ ${setting[type] ? 'ON' : 'OFF'} ] (Enter *On* or *Off*)`, m)
if (!optionList.includes(option)) {
if (type === 'autoclose') {
return conn.reply(m.chat, `🚩 *Current status* : [ ${setting.autoclose?.active ? 'ON' : 'OFF'} ] (Enter *On* or *Off*)`, m)
}
return conn.reply(m.chat, `🚩 *Current status* : [ ${setting[type] ? 'ON' : 'OFF'} ] (Enter *On* or *Off*)`, m)
}
let status = option != 'on' ? false : true
if (setting[type] == status) return conn.reply(m.chat, Func.texted('bold', `🚩 ${Func.ucword(command)} has been ${option == 'on' ? 'activated' : 'inactivated'} previously.`), m)
setting[type] = status

if (type === 'autoclose') {
if (setting.autoclose.active === status) return conn.reply(m.chat, Func.texted('bold', `🚩 ${Func.ucword(command)} has been ${option == 'on' ? 'activated' : 'inactivated'} previously.`), m)
setting.autoclose.active = status
} else {
if (setting[type] == status) return conn.reply(m.chat, Func.texted('bold', `🚩 ${Func.ucword(command)} has been ${option == 'on' ? 'activated' : 'inactivated'} previously.`), m)
setting[type] = status
}
conn.reply(m.chat, Func.texted('bold', `🚩 ${Func.ucword(command)} has been ${option == 'on' ? 'activated' : 'inactivated'} successfully.`), m)
} catch (e) {
return conn.reply(m.chat, Func.jsonFormat(e), m)
Expand Down
71 changes: 71 additions & 0 deletions plugins/admin/setautoclose.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
*
* .setautoclose 14:30-15:00 -> Set autoclose schedule
* .setautoclose off -> Disable autoclose
* .setautoclose -> Show current status
*/

module.exports = {
help: ['setautoclose'],
use: 'HH:mm-HH:mm or off',
tags: 'admin',
run: async (m, {
conn,
args,
groupSet,
Func
}) => {
try {
if (!groupSet.autoclose) {
groupSet.autoclose = {
active: false,
start: null,
end: null,
isClosed: false
}
}
Comment on lines +19 to +26
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to moderation.js, this initialization duplicates the autoclose object structure from lib/system/models.js. Consider using a shared constant or helper function to maintain consistency and reduce duplication.

Copilot uses AI. Check for mistakes.

if (!args || !args[0]) {
const status = groupSet.autoclose?.active ? '*ACTIVE*' : '*INACTIVE*'
return conn.reply(m.chat, Func.texted('bold', `🚩 Auto Close Status : ${status}`), m)
}

if (args[0].toLowerCase() === 'off') {
groupSet.autoclose.active = false
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When disabling autoclose with 'off', consider resetting the isClosed flag to false as well. If a group is currently in the closed state due to autoclose and an admin disables the feature, leaving isClosed as true could cause confusion about the intended state. It would be clearer to set isClosed to false when the feature is disabled.

Suggested change
groupSet.autoclose.active = false
groupSet.autoclose.active = false
groupSet.autoclose.isClosed = false

Copilot uses AI. Check for mistakes.
return conn.reply(m.chat, Func.texted('bold', `🚩 Auto close has been deactivated.`), m)
}

const timePattern = /^([0-1][0-9]|2[0-3]):([0-5][0-9])-([0-1][0-9]|2[0-3]):([0-5][0-9])$/
if (!timePattern.test(args[0])) {
return conn.reply(m.chat, `🚩 *Invalid format!*\n\nUsage:\n • ${Func.texted('code', '.setautoclose 14:30-15:00')}\n • ${Func.texted('code', '.setautoclose off')}\n\n*Example* : Set group close from 14:30 to 15:00 (local timezone)`, m)
}

const [startTime, endTime] = args[0].split('-')
const [startHour, startMin] = startTime.split(':').map(Number)
const [endHour, endMin] = endTime.split(':').map(Number)

const startTotalMin = startHour * 60 + startMin
const endTotalMin = endHour * 60 + endMin

if (startTotalMin >= endTotalMin) {
return conn.reply(m.chat, `🚩 *Invalid time range!*\n\nStart time must be before end time.\n\n*Example* : ${Func.texted('code', '.setautoclose 14:30-15:00')}`, m)
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The time range validation does not handle cross-midnight scenarios. For example, if a group admin wants to close from 23:00 to 01:00 (close at 11 PM and open at 1 AM), this validation would reject it since startTotalMin (1380) is greater than endTotalMin (60). Consider supporting cross-midnight time ranges or adding a clear error message explaining this limitation.

Suggested change
return conn.reply(m.chat, `🚩 *Invalid time range!*\n\nStart time must be before end time.\n\n*Example* : ${Func.texted('code', '.setautoclose 14:30-15:00')}`, m)
return conn.reply(m.chat, `🚩 *Invalid time range!*\n\nStart time must be earlier than end time *on the same day*.\nCross-midnight ranges like 23:00-01:00 are currently not supported.\n\n*Example* : ${Func.texted('code', '.setautoclose 14:30-15:00')}`, m)

Copilot uses AI. Check for mistakes.
}

groupSet.autoclose = {
active: true,
start: startTime,
end: endTime,
isClosed: false
}

conn.reply(m.chat, Func.texted('bold', `🚩 Auto close has been set.`), m)

} catch (e) {
return conn.reply(m.chat, Func.jsonFormat(e), m)
}
},
admin: true,
group: true,
botAdmin: true,
error: false
}
1 change: 1 addition & 0 deletions plugins/group/groupinfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ module.exports = {
txt += ` ◦ ${Func.switcher(groupSet.antitagsw, '[ √ ]', '[ × ]')} Anti Tag Status\n`
txt += ` ◦ ${Func.switcher(groupSet.autosticker, '[ √ ]', '[ × ]')} Auto Sticker\n`
txt += ` ◦ ${Func.switcher(groupSet.autodetect, '[ √ ]', '[ × ]')} Auto Detect\n`
txt += ` ◦ ${Func.switcher(groupSet.autoclose?.active, '[ √ ]', '[ × ]')} Auto Close | ${groupSet.autoclose?.active ? `${groupSet.autoclose.start}-${groupSet.autoclose.end}` : 'Not Set'}\n`
txt += ` ◦ ${Func.switcher(groupSet.welcome, '[ √ ]', '[ × ]')} Welcome Message\n`
txt += ` ◦ ${Func.switcher(groupSet.left, '[ √ ]', '[ × ]')} Left Message\n\n`
txt += `乂 *G R O U P - S T A T U S*\n\n`
Expand Down
Loading