@open-discord-bots/framework 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/main.d.ts +4 -0
- package/dist/api/main.js +3 -1
- package/dist/api/modules/client.d.ts +13 -12
- package/dist/api/modules/client.js +61 -56
- package/dist/api/modules/console.js +3 -1
- package/dist/api/modules/database.d.ts +1 -1
- package/dist/api/modules/fuse.d.ts +4 -2
- package/dist/api/modules/fuse.js +2 -1
- package/dist/api/modules/permission.d.ts +1 -1
- package/dist/api/modules/permission.js +1 -1
- package/dist/api/modules/post.js +3 -3
- package/dist/api/modules/responder.d.ts +3 -3
- package/dist/api/modules/responder.js +27 -27
- package/dist/api/modules/startscreen.js +2 -2
- package/dist/api/modules/state.d.ts +126 -0
- package/dist/api/modules/state.js +222 -0
- package/dist/startup/errorHandling.js +2 -0
- package/package.json +1 -1
- package/src/api/index.ts +1 -0
- package/src/api/main.ts +6 -1
- package/src/api/modules/client.ts +60 -56
- package/src/api/modules/console.ts +3 -1
- package/src/api/modules/database.ts +1 -1
- package/src/api/modules/fuse.ts +8 -5
- package/src/api/modules/permission.ts +1 -1
- package/src/api/modules/post.ts +3 -3
- package/src/api/modules/responder.ts +31 -31
- package/src/api/modules/startscreen.ts +2 -2
- package/src/api/modules/state.ts +294 -0
- package/src/startup/errorHandling.ts +3 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
///////////////////////////////////////
|
|
2
|
+
//STATE MODULE
|
|
3
|
+
///////////////////////////////////////
|
|
4
|
+
import { ODId, ODManager, ODValidId, ODSystemError, ODManagerData, ODNoGeneric, ODValidJsonType } from "./base.js"
|
|
5
|
+
import { ODClientManager } from "./client.js"
|
|
6
|
+
import { ODDebugger } from "./console.js"
|
|
7
|
+
import { ODDatabase, ODDatabaseIdConstraint } from "./database.js"
|
|
8
|
+
import * as discord from "discord.js"
|
|
9
|
+
|
|
10
|
+
/**## ODStateKey `type`
|
|
11
|
+
* The key template for message states.
|
|
12
|
+
*/
|
|
13
|
+
export interface ODStateKey {
|
|
14
|
+
/**A valid discord server/guild ID or instance. */
|
|
15
|
+
guild?:discord.Guild|string|null,
|
|
16
|
+
/**A valid discord channel ID or instance. */
|
|
17
|
+
channel:discord.Channel|string,
|
|
18
|
+
/**A valid discord message ID or instance. */
|
|
19
|
+
message:discord.Message|string,
|
|
20
|
+
/**A valid discord user ID or instance. */
|
|
21
|
+
user?:discord.User|discord.GuildMember|string|null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**## ODStateData `type`
|
|
25
|
+
* The raw data template for message states used for storing in the database.
|
|
26
|
+
*/
|
|
27
|
+
export interface ODStateData<StateData extends any> {
|
|
28
|
+
/**The linked Guild ID of this message state. */
|
|
29
|
+
guildId:string|null,
|
|
30
|
+
/**The linked Channel ID of this message state. */
|
|
31
|
+
channelId:string,
|
|
32
|
+
/**The linked Message ID of this message state. */
|
|
33
|
+
messageId:string,
|
|
34
|
+
/**The linked User ID of this message state. */
|
|
35
|
+
userId:string|null,
|
|
36
|
+
/**The creation date of this state (UNIX TIMESTAMP). */
|
|
37
|
+
createdDate:number,
|
|
38
|
+
/**The modified date of this state (UNIX TIMESTAMP). */
|
|
39
|
+
modifiedDate:number,
|
|
40
|
+
/**Optional date after which this state should be regarded as expired (UNIX TIMESTAMP). */
|
|
41
|
+
deleteAfterDate:number|null,
|
|
42
|
+
/**The state data. */
|
|
43
|
+
data:StateData
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**## ODStateSettings `type`
|
|
47
|
+
* Configurable settings for an ODState.
|
|
48
|
+
*/
|
|
49
|
+
export interface ODStateSettings {
|
|
50
|
+
/**(Default: `false`) Completely disable autodelete of message states. (unless manually activated using other settings) */
|
|
51
|
+
disableAutodelete:boolean,
|
|
52
|
+
/**(Default: `true`) Delete state on channel deletion. */
|
|
53
|
+
autodeleteOnChannelDelete:boolean,
|
|
54
|
+
/**(Default: `true`) Delete state on message deletion. */
|
|
55
|
+
autodeleteOnMessageDelete:boolean,
|
|
56
|
+
/**(Default: `true`) Delete state when the user leaves the guild/server. */
|
|
57
|
+
autodeleteOnUserLeave:boolean,
|
|
58
|
+
/**(Default: `false`) Delete state on bot restart. */
|
|
59
|
+
autodeleteOnRestart:boolean,
|
|
60
|
+
/**(Default: `true`) Delete state after 1 hour of an ephemeral message being sent. */
|
|
61
|
+
autodeleteOnEphemeral:boolean,
|
|
62
|
+
/**(Default: `null`) Delete state when unmodified/inactive for more than.... */
|
|
63
|
+
autodeleteAfterTimeout:null|{time:number,unit:"seconds"|"minutes"|"hours"|"days"},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**## ODState `class`
|
|
67
|
+
* An Open Discord state is a system for storing additional chunks of metadata for Discord messages.
|
|
68
|
+
* A system for tracking messages or linking metadata, states or progress to Discord messages (ID-based).
|
|
69
|
+
*
|
|
70
|
+
* Features automatic garbage collection to clear expired states.
|
|
71
|
+
*/
|
|
72
|
+
export class ODState<StateData extends any> extends ODManagerData {
|
|
73
|
+
/**Alias to Open Discord message states database. */
|
|
74
|
+
protected database: ODDatabase<ODDatabaseIdConstraint>
|
|
75
|
+
/**Alias to Open Discord client manager. */
|
|
76
|
+
protected client: ODClientManager
|
|
77
|
+
/**Alias to Open Discord debugger. */
|
|
78
|
+
protected debug: ODDebugger|null = null
|
|
79
|
+
/**The settings of this state. */
|
|
80
|
+
settings: ODStateSettings
|
|
81
|
+
|
|
82
|
+
constructor(id:ODValidId,client:ODClientManager,database:ODDatabase<ODDatabaseIdConstraint>,settings:Partial<ODStateSettings>){
|
|
83
|
+
super(id)
|
|
84
|
+
this.client = client
|
|
85
|
+
this.database = database
|
|
86
|
+
this.settings = {
|
|
87
|
+
disableAutodelete:false,
|
|
88
|
+
autodeleteOnChannelDelete:true,
|
|
89
|
+
autodeleteOnMessageDelete:true,
|
|
90
|
+
autodeleteOnUserLeave:true,
|
|
91
|
+
autodeleteOnRestart:false,
|
|
92
|
+
autodeleteOnEphemeral:true,
|
|
93
|
+
autodeleteAfterTimeout:null,
|
|
94
|
+
...settings
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**Use the Open Discord debugger in this manager for logs. */
|
|
99
|
+
useDebug(debug?:ODDebugger|null){
|
|
100
|
+
this.debug = debug ?? null
|
|
101
|
+
}
|
|
102
|
+
/**Init the state and start autodeleting. The client must already be logged-in for this to work. */
|
|
103
|
+
async init(){
|
|
104
|
+
if (!this.client.loggedIn) throw new ODSystemError("ODState('"+this.id.value+"').init() => The client must be logged in for states to initialize.")
|
|
105
|
+
|
|
106
|
+
//autodelete on restart
|
|
107
|
+
if (!this.settings.disableAutodelete && this.settings.autodeleteOnRestart) this.clearAllMsgStates()
|
|
108
|
+
|
|
109
|
+
//autodelete on channelDelete
|
|
110
|
+
this.client.client.on("channelDelete",async (deletedChannel) => {
|
|
111
|
+
if (this.settings.disableAutodelete || !this.settings.autodeleteOnChannelDelete) return
|
|
112
|
+
|
|
113
|
+
for (const {key,value} of await this.listMsgStates()){
|
|
114
|
+
if (value.channelId === deletedChannel.id) await this.deleteMsgStateWithRawKey(key)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
//autodelete on messageDelete
|
|
118
|
+
this.client.client.on("messageDelete",async (deletedMsg) => {
|
|
119
|
+
if (this.settings.disableAutodelete || !this.settings.autodeleteOnMessageDelete) return
|
|
120
|
+
|
|
121
|
+
for (const {key,value} of await this.listMsgStates()){
|
|
122
|
+
if (value.messageId === deletedMsg.id) await this.deleteMsgStateWithRawKey(key)
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
//autodelete on userLeave
|
|
126
|
+
this.client.client.on("guildMemberRemove",async (member) => {
|
|
127
|
+
if (this.settings.disableAutodelete || !this.settings.autodeleteOnUserLeave) return
|
|
128
|
+
if (member.guild.id !== this.client.mainServer?.id) return
|
|
129
|
+
|
|
130
|
+
for (const {key,value} of await this.listMsgStates()){
|
|
131
|
+
if (value.userId === member.id) await this.deleteMsgStateWithRawKey(key)
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
//autodelete when expired (ephemeral/timeout) every 30 seconds
|
|
136
|
+
this.purgeExpiredStates()
|
|
137
|
+
setInterval(() => {
|
|
138
|
+
this.purgeExpiredStates()
|
|
139
|
+
},30*1000)
|
|
140
|
+
|
|
141
|
+
//create a list of all channels, messages & users that still exist (and are part of states)
|
|
142
|
+
const existingChannelIds: string[] = []
|
|
143
|
+
const existingMessageIds: string[] = []
|
|
144
|
+
const existingUserIds: string[] = []
|
|
145
|
+
|
|
146
|
+
for (const {key,value} of await this.listMsgStates()){
|
|
147
|
+
if (!existingChannelIds.includes(value.channelId)){
|
|
148
|
+
//check if channel still exists
|
|
149
|
+
const channel = await this.client.fetchChannel(value.channelId)
|
|
150
|
+
if (channel) existingChannelIds.push(channel.id)
|
|
151
|
+
}
|
|
152
|
+
if (!existingMessageIds.includes(value.messageId)){
|
|
153
|
+
//check if message still exists
|
|
154
|
+
const message = await this.client.fetchChannelMessage(value.channelId,value.messageId)
|
|
155
|
+
if (message) existingMessageIds.push(message.id)
|
|
156
|
+
}
|
|
157
|
+
if (value.userId && this.client.mainServer && !existingUserIds.includes(value.userId)){
|
|
158
|
+
//check if user still exists
|
|
159
|
+
const user = await this.client.fetchGuildMember(this.client.mainServer.id,value.userId)
|
|
160
|
+
if (user) existingUserIds.push(user.id)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
//delete all states where a channel, message or user is missing (when the bot was offline)
|
|
165
|
+
for (const {key,value} of await this.listMsgStates()){
|
|
166
|
+
if (value.channelId && !existingChannelIds.includes(value.channelId)) await this.deleteMsgStateWithRawKey(key)
|
|
167
|
+
if (value.messageId && !existingMessageIds.includes(value.messageId)) await this.deleteMsgStateWithRawKey(key)
|
|
168
|
+
if (value.userId && !existingUserIds.includes(value.userId)) await this.deleteMsgStateWithRawKey(key)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**Purge all expired message states that reached a timeout or ephemeral. */
|
|
172
|
+
protected async purgeExpiredStates(){
|
|
173
|
+
for (const {key,value} of await this.listMsgStates()){
|
|
174
|
+
if (value.deleteAfterDate && value.deleteAfterDate < Date.now()) await this.deleteMsgStateWithRawKey(key)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**Transform the object-based message state key contents to a string. */
|
|
178
|
+
protected transformKey(key:ODStateKey){
|
|
179
|
+
const newGuild = (!key.guild) ? "NULL" : (typeof key.guild === "string" ? key.guild : key.guild.id)
|
|
180
|
+
const newChannel = (!key.channel) ? "NULL" : (typeof key.channel === "string" ? key.channel : key.channel.id)
|
|
181
|
+
const newMessage = (!key.message) ? "NULL" : (typeof key.message === "string" ? key.message : key.message.id)
|
|
182
|
+
const newUser = (!key.user) ? "NULL" : (typeof key.user === "string" ? key.user : key.user.id)
|
|
183
|
+
|
|
184
|
+
return `G:${newGuild},C:${newChannel},M:${newMessage},U:${newUser}`
|
|
185
|
+
}
|
|
186
|
+
/**Transform the message state data contents for storage in the database. */
|
|
187
|
+
protected transformData(key:ODStateKey,data:StateData,isEphemeral:boolean,keepCreatedDate?:number): ODStateData<StateData> {
|
|
188
|
+
const guildId = (!key.guild) ? null : (typeof key.guild === "string" ? key.guild : key.guild.id)
|
|
189
|
+
const channelId = (typeof key.channel === "string" ? key.channel : key.channel.id)
|
|
190
|
+
const messageId = (typeof key.message === "string" ? key.message : key.message.id)
|
|
191
|
+
const userId = (!key.user) ? null : (typeof key.user === "string" ? key.user : key.user.id)
|
|
192
|
+
const createdDate = keepCreatedDate ?? Date.now()
|
|
193
|
+
const modifiedDate = Date.now()
|
|
194
|
+
|
|
195
|
+
const unmodifiedTimeoutDate = this.timeoutToUnixTime()
|
|
196
|
+
const ephemeralTimeoutDate = (isEphemeral) ? Date.now()+(3600*1000) : null //delete ephemeral state after 1 hour
|
|
197
|
+
const deleteAfterDate = (unmodifiedTimeoutDate) ? unmodifiedTimeoutDate : ephemeralTimeoutDate
|
|
198
|
+
|
|
199
|
+
return {guildId,channelId,messageId,userId,createdDate,modifiedDate,deleteAfterDate,data}
|
|
200
|
+
}
|
|
201
|
+
/**Calculate milliseconds from a time + unit. */
|
|
202
|
+
protected timeoutToUnixTime(){
|
|
203
|
+
const timeout = this.settings.autodeleteAfterTimeout
|
|
204
|
+
if (!timeout) return null
|
|
205
|
+
if (timeout.unit == "seconds") return Date.now() + (timeout.time * 1000)
|
|
206
|
+
else if (timeout.unit == "minutes") return Date.now() + (timeout.time * 1000 * 60)
|
|
207
|
+
else if (timeout.unit == "hours") return Date.now() + (timeout.time * 1000 * 3600)
|
|
208
|
+
else if (timeout.unit == "days") return Date.now() + (timeout.time * 1000 * 3600 * 24)
|
|
209
|
+
else return null
|
|
210
|
+
}
|
|
211
|
+
/**Set a message state using guild, channel & message id as key. Returns `true` when overwritten. */
|
|
212
|
+
async setMsgState(key:ODStateKey,data:StateData,isEphemeral:boolean): Promise<boolean> {
|
|
213
|
+
const rawKey = this.transformKey(key)
|
|
214
|
+
|
|
215
|
+
const existingData = await this.getMsgState(key)
|
|
216
|
+
const contents = this.transformData(key,data,isEphemeral,existingData?.createdDate)
|
|
217
|
+
return await this.database.set(this.id.value,rawKey,contents)
|
|
218
|
+
}
|
|
219
|
+
/**Get a message state using guild, channel & message id as key. */
|
|
220
|
+
async getMsgState(key:ODStateKey): Promise<ODStateData<StateData>|null> {
|
|
221
|
+
const rawKey = this.transformKey(key)
|
|
222
|
+
const rawData = await this.database.get(this.id.value,rawKey)
|
|
223
|
+
if (typeof rawData !== "object") return null
|
|
224
|
+
else return rawData as ODStateData<StateData>
|
|
225
|
+
}
|
|
226
|
+
/**Delete a message state using guild, channel & message id as key. Returns `true` when deleted. */
|
|
227
|
+
async deleteMsgState(key:ODStateKey): Promise<boolean> {
|
|
228
|
+
const rawKey = this.transformKey(key)
|
|
229
|
+
return await this.database.delete(this.id.value,rawKey)
|
|
230
|
+
}
|
|
231
|
+
/**List all message states of this `ODState`. */
|
|
232
|
+
async listMsgStates(): Promise<{key:string,value:ODStateData<StateData>}[]> {
|
|
233
|
+
return ((await this.database.getCategory(this.id.value)) ?? []) as {key:string,value:ODStateData<StateData>}[]
|
|
234
|
+
}
|
|
235
|
+
/**Delete all message states from this ODState. */
|
|
236
|
+
async clearAllMsgStates(): Promise<void> {
|
|
237
|
+
for (const state of await this.database.getAll()){
|
|
238
|
+
await this.database.delete(state.category,state.key)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**Delete a message state using the raw key. Returns `true` when deleted. */
|
|
242
|
+
protected async deleteMsgStateWithRawKey(rawKey:string): Promise<boolean> {
|
|
243
|
+
return await this.database.delete(this.id.value,rawKey)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**## ODStateManagerIdConstraint `type`
|
|
248
|
+
* The constraint/layout for id mappings/interfaces of the `ODStateManager` class.
|
|
249
|
+
*/
|
|
250
|
+
export type ODStateManagerIdConstraint = Record<string,ODState<any>>
|
|
251
|
+
|
|
252
|
+
/**## ODStateManager `class`
|
|
253
|
+
* The Open Discord state manager is a system for tracking messages or linking metadata, states or progress to Discord messages (ID-based).
|
|
254
|
+
*
|
|
255
|
+
* Features automatic garbage collection to clear expired states.
|
|
256
|
+
*/
|
|
257
|
+
export class ODStateManager<IdList extends ODStateManagerIdConstraint = ODStateManagerIdConstraint> extends ODManager<ODState<any>> {
|
|
258
|
+
constructor(debug:ODDebugger){
|
|
259
|
+
super(debug,"state")
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**Init all states. */
|
|
263
|
+
async init(){
|
|
264
|
+
for (const state of this.getAll()){
|
|
265
|
+
await state.init()
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
add(data:ODState<any>, overwrite?:boolean): boolean {
|
|
270
|
+
data.useDebug(this.debug)
|
|
271
|
+
return super.add(data,overwrite)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
get<StateId extends keyof ODNoGeneric<IdList>>(id:StateId): IdList[StateId]
|
|
275
|
+
get(id:ODValidId): ODState<any>|null
|
|
276
|
+
|
|
277
|
+
get(id:ODValidId): ODState<any>|null {
|
|
278
|
+
return super.get(id)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
remove<StateId extends keyof ODNoGeneric<IdList>>(id:StateId): IdList[StateId]
|
|
282
|
+
remove(id:ODValidId): ODState<any>|null
|
|
283
|
+
|
|
284
|
+
remove(id:ODValidId): ODState<any>|null {
|
|
285
|
+
return super.remove(id)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
exists(id:keyof ODNoGeneric<IdList>): boolean
|
|
289
|
+
exists(id:ODValidId): boolean
|
|
290
|
+
|
|
291
|
+
exists(id:ODValidId): boolean {
|
|
292
|
+
return super.exists(id)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -2,6 +2,9 @@ import * as api from "../api/index.js"
|
|
|
2
2
|
import * as utilities from "../utilities/index.js"
|
|
3
3
|
|
|
4
4
|
export function loadErrorHandling(opendiscord:api.ODMain,project:api.ODProjectType){
|
|
5
|
+
//increase error stack trace
|
|
6
|
+
Error.stackTraceLimit = 50
|
|
7
|
+
|
|
5
8
|
process.on("uncaughtException",async (error,origin) => {
|
|
6
9
|
try{
|
|
7
10
|
const beforeEvent = opendiscord.events.get("onErrorHandling")
|