-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathfood-count-input.event.ts
More file actions
285 lines (256 loc) · 10.2 KB
/
Copy pathfood-count-input.event.ts
File metadata and controls
285 lines (256 loc) · 10.2 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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
import {
ActionRowBuilder,
type Message,
ButtonBuilder,
type MessageReplyOptions,
ButtonStyle
} from 'discord.js';
import { COUNT_CHANNEL_NAME } from '../nm-service';
import { v4 as uuidv4 } from 'uuid';
import { type ConfigSerive, MessageService } from '../service/index';
import { Dbg, CacheUtility } from '../utility';
import { getChannelByName } from '../service/discord.service';
// status for each cached input: does it get inserted unless cancel? or does it require a confirmation?
type CacheStatusType = 'INSERT_UNLESS_CANCEL' | 'DELETE_UNLESS_CONFIRM';
const MsgReply = MessageService.createMap({
// message sent when someone posts a food count event
FOODCOUNT_INSERT: {
lbs: '',
note: '',
org: '',
date: ''
},
// message sent when a food count gets stuck in the db successfully
FOODCOUNT_INPUT_OK: {
lbs: '',
note: '',
org: '',
date: '',
seconds: ''
}
});
// this is a cache for food-count input so that we can
// give user a set period of time to cancel
// if the user cancels, this cache is deleted
// if not, it is inserted into the spreadsheet
export const FoodCountInputCache = CacheUtility<{
status: CacheStatusType;
messageInputId: string;
messageResponseId: string;
messageCountId: string;
stamp: number;
insertTimeout: null | NodeJS.Timeout;
}>('food-count');
// after a set period of time, the input is inserted. this is that time:
export const TIME_UNTIL_UPDATE = 60 * 1000; // one minute in milliseconds
/**
*
*/
export const FoodCountInputEvent =
(guildServices: ConfigSerive) => async (message: Message) => {
const { channel, author } = message as Message<true>;
if (!message.guild?.id) {
Dbg(
'FoodCountInputEvent does not happen outside of a guild channel'
);
return;
}
const {
personCoreService,
foodCountInputService,
foodCountDataService
} = await guildServices.getServicesForGuildId(message.guild?.id);
/* STAGE 1: skip the message entirely in some cases */
// if we are a bot, we do not want to process the message
if (
author.bot ||
// this does not match the published api. there it is a string: "DM"
// in any case we do not want to continue if this is a direct message
(channel.type as unknown as number) === 1
) {
return;
}
let { content } = message;
// make sure there is some actual content
// this is probably not needed since Discord does not send blanks but it's cheap so leaving it
if (!(content = content.trim())) {
return;
}
/* STAGE 2: figure out our input status */
const [
channelStatus,
inputStatus,
// did we get the date from the content, from the channel name, or just today by default?
dateStatus,
date,
parsedInputList,
parsedInputErrorList
] = await foodCountInputService.getParsedChannelAndContent(
channel.name,
content
);
// if we are not in a night or count channel
// we do not send a message, we simply get out
if (channelStatus === 'INVALID_CHANNEL') {
return;
}
// because when we have an invalid input, and we are in the count channel ...
if (inputStatus === 'INVALID' && channelStatus === 'COUNT_CHANNEL') {
// we want to tell them that they cannot put invalid content in the count channel
return;
}
// because when we have an invalid input, and we are in the night channel ...
if (inputStatus === 'INVALID' && channelStatus === 'NIGHT_CHANNEL') {
// we want to nothing, because this is probably not a count
return;
}
// because when we have only errors, and we are in the night channel ...
if (
inputStatus === 'ONLY_ERRORS' &&
channelStatus === 'NIGHT_CHANNEL'
) {
// we want to ask them nicely if they meant to do a count at all
// ? perhaps we want a a separate cache for this case and let them confirm?
return;
}
// because when we have some errors, and we are in the night channel ...
if (
inputStatus === 'OK_WITH_ERRORS' &&
channelStatus === 'NIGHT_CHANNEL'
) {
// we want to ask them nicely if they meant to do a count
// because we do not assume that we are doing a count in this case ?
// ? perhaps we want a a separate cache for this case and let them confirm?
// ? we might also want to check what kind of errors? because if they do not have lbs, maybe we assume they did not mean to count
return;
}
// because when we have only errors, and we are in the count channel ...
if (
inputStatus === 'ONLY_ERRORS' &&
channelStatus === 'COUNT_CHANNEL'
) {
// we want to show them their errors, ask if they meant to do it
return;
}
/* OK, loop over the food count input */
// ? we can do two loops, one for successful input, one for unsuccessful
for (const { lbs, org, note } of parsedInputList) {
// we need a unique id for our cache
const cacheId = uuidv4();
// now we create our insert event
const insertTimeout = setTimeout(
async () => {
// we need to make sure teh count has not been cancelled
// todo: test this
if (FoodCountInputCache.get(cacheId) == null) {
return;
}
// todo: try/catch
await foodCountDataService.appendFoodCount([
{
org,
date,
reporter: reporter?.email ?? '',
lbs,
note
}
]);
// we want to post to food-count, always, so folks know what's in the db
const countChannel = getChannelByName(
message.guild,
COUNT_CHANNEL_NAME
);
countChannel?.send(
MsgReply.FOODCOUNT_INSERT({
lbs: lbs.toString(),
note,
org,
date
})
// `*OK, posted to db:*
// ${lbs} lbs ${note ? `(${note})` : ''} from ${org} on ${date}.`
);
try {
FoodCountInputCache.delete(cacheId);
await messageReply.delete();
} catch (e) {
console.log(e);
}
},
// we give them a certain amount of time to hit cancel
TIME_UNTIL_UPDATE
);
// create our cache
FoodCountInputCache.add(cacheId, {
status: 'INSERT_UNLESS_CANCEL',
messageInputId: message.id,
messageResponseId: '',
messageCountId: '',
stamp: Date.now() / 1000,
insertTimeout
});
// our success message
const reply: MessageReplyOptions = {
content: MsgReply.FOODCOUNT_INPUT_OK({
lbs: `${lbs}`,
note,
org,
date,
seconds: `${TIME_UNTIL_UPDATE / 1000}`
}),
components: [
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
// we keep the cacheId on the custom id so we can delete it on cancel event
.setCustomId(`food-count-cancel--${cacheId}`)
.setLabel('delete')
.setStyle(ButtonStyle.Danger)
)
]
};
// we need a reference to the message
const messageReply = await message.reply(reply);
// because we want to delete this message on cancel, or when the expiration passes
// we save the reply id
FoodCountInputCache.update(cacheId, {
messageResponseId: messageReply.id
});
// get our reporter email address
const reporter = await personCoreService.getPerson({
discordId: author.id
});
}
// loop over errors and post to channel
for (const { status, lbs, org, orgFuzzy } of parsedInputErrorList) {
let content = '';
if (status === 'NO_LBS_OR_ORG') {
content = foodCountInputService.getMessageErrorNoLbsOrOrg({
messageContent: message.content
});
}
if (status === 'NO_LBS') {
content = foodCountInputService.getMessageErrorNoLbs({
org
});
}
if (status === 'NO_ORG') {
content = foodCountInputService.getMessageErrorNoOrg({
orgFuzzy,
lbs
});
}
const responseMessage = await message.reply({
content
});
// we delete crabapple message after 1 minute
// todo: make this better
setTimeout(() => {
// ? we only delete their message if they are in food count channel??
if (channelStatus === 'COUNT_CHANNEL') {
message.delete();
}
// always delete our own message
responseMessage.delete();
}, TIME_UNTIL_UPDATE);
}
};