-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdeadline_watcher.js
More file actions
150 lines (132 loc) · 4.93 KB
/
deadline_watcher.js
File metadata and controls
150 lines (132 loc) · 4.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// /etc/bvl-automations/deadline_watcher.js
// Socket Mode Slack app for managing conference/grant deadlines with timezone support
require('dotenv').config({ path: '/etc/bvl-automations/.deadline_watcher.conf' });
const { App, SocketModeReceiver } = require('@slack/bolt');
const { DateTime } = require('luxon');
const fs = require('fs');
// Map common zone codes to IANA names
const ZONE_ALIASES = {
UTC: 'UTC',
PST: 'America/Los_Angeles',
AoE: 'Etc/GMT+12'
};
// Path to the JSON file storing deadlines
const DEADLINE_FILE = '/etc/bvl-automations/deadlines.json';
// Initialize Bolt app in Socket Mode
const receiver = new SocketModeReceiver({ appToken: process.env.SLACK_APP_TOKEN });
const app = new App({ token: process.env.SLACK_BOT_TOKEN, receiver });
// Helper: format YYYY-MM-DD to "DD MMM YYYY"
function formatDate(isoDate, tz) {
return DateTime.fromISO(isoDate, { zone: ZONE_ALIASES[tz] })
.toFormat('dd LLL yyyy');
}
// Load, prune, sort, and save deadlines
function loadDeadlines() {
let raw;
try {
raw = JSON.parse(fs.readFileSync(DEADLINE_FILE, 'utf8'));
} catch {
raw = [];
}
const today = DateTime.now().startOf('day');
const upcoming = raw
.map(d => ({ id: d.id, title: d.title, date: d.date, tz: d.tz || 'UTC' }))
.filter(d => DateTime.fromISO(d.date, { zone: ZONE_ALIASES[d.tz] }).startOf('day') >= today)
.sort((a, b) => DateTime.fromISO(a.date, { zone: ZONE_ALIASES[a.tz] }) - DateTime.fromISO(b.date, { zone: ZONE_ALIASES[b.tz] }));
// If pruning happened, write back full upcoming array
if (upcoming.length !== raw.length) {
fs.writeFileSync(DEADLINE_FILE, JSON.stringify(upcoming, null, 2));
}
return upcoming;
}
// Parse slash-command arguments: subcommand, id, date, optional tz, then title
function parseArgs(text) {
const parts = text.trim().split(/\s+/);
const sub = (parts[0] || 'help').toLowerCase();
const id = parts[1] || '';
const date = parts[2] || '';
let tz = 'UTC';
let idx = 3;
if (parts[3] && ZONE_ALIASES[parts[3]]) {
tz = parts[3];
idx = 4;
}
const title = parts.slice(idx).join(' ');
return { sub, id, date, tz, title };
}
// Slash command handler for /deadline
app.command('/deadline', async ({ command, ack, say, respond }) => {
await ack(); // always acknowledge first
const { sub, id, date, tz, title } = parseArgs(command.text);
// Work on the upcoming list
let list = loadDeadlines();
let msg;
let responseType = 'ephemeral';
switch (sub) {
case 'add':
if (!id || !date || !title) {
msg = ':warning: Usage: `/deadline add <id> <YYYY-MM-DD> [TZ] <Title>`';
} else if (!DateTime.fromISO(date).isValid) {
msg = ':warning: Invalid date format. Use YYYY-MM-DD.';
} else if (list.some(d => d.id.toLowerCase() === id.toLowerCase())) {
msg = `:warning: A deadline with ID *${id}* already exists.`;
} else {
list.push({ id, title, date, tz });
fs.writeFileSync(DEADLINE_FILE, JSON.stringify(list, null, 2));
msg = `:white_check_mark: Added *${title}* (${id}) on \`${formatDate(date, tz)} ${tz}\``;
}
responseType = 'in_channel';
break;
case 'remove':
case 'rm':
case 'delete':
if (!id) {
msg = ':warning: Usage: `/deadline remove <id>`';
} else if (!list.some(d => d.id.toLowerCase() === id.toLowerCase())) {
msg = `:warning: No deadline with ID *${id}* found.`;
} else {
list = list.filter(d => d.id.toLowerCase() !== id.toLowerCase());
fs.writeFileSync(DEADLINE_FILE, JSON.stringify(list, null, 2));
msg = `:wastebasket: Removed deadline *${id}*.`;
}
responseType = 'in_channel';
break;
case 'clear':
case 'reset':
list = [];
fs.writeFileSync(DEADLINE_FILE, '[]');
msg = ':wastebasket: All deadlines cleared.';
responseType = 'in_channel';
break;
case 'list':
if (!list.length) {
msg = '_No upcoming deadlines._';
} else {
const lines = list.map(d => `• *${formatDate(d.date, d.tz)} ${d.tz}*: ${d.title} (\`${d.id}\`)`);
msg = `*Upcoming Deadlines:*
${lines.join('\n')}`;
}
break;
default:
msg = '*DeadlineWatcher Commands*\n'
+ '• `/deadline add <id> <YYYY-MM-DD> [PST/UTC/AoE] <Title>` — Add a deadline, id must be unique\n'
+ '• `/deadline remove <id>` — Remove a deadline\n'
+ '• `/deadline clear` — Clear all deadlines\n'
+ '• `/deadline list` — List upcoming deadlines\n'
+ '• `/deadline help` — Show this message';
}
switch (responseType) {
case 'in_channel':
await say({ text: msg, response_type: 'in_channel' });
break;
case 'ephemeral':
default:
await respond({ text: msg, response_type: 'ephemeral' });
}
// await say({ response_type: responseType, text: msg });
});
// Start the app
(async () => {
await app.start();
console.log('⚡️ DeadlineWatcher running in Socket Mode');
})();