-
Notifications
You must be signed in to change notification settings - Fork 7
/
cli.js
456 lines (398 loc) · 15.5 KB
/
cli.js
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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
require('dotenv').config()
var readlineSync = require('readline-sync');
const mongoose = require("mongoose")
const fs = require('fs')
const envfile = require('envfile')
const argv = require('minimist')(process.argv.slice(2))
const other = require('./my_modules/other')
const updateEnv = require('./my_modules/updateenv')
async function handleCommand(command){
try {
if(command in commands){
await commands[command].func()
} else{
console.log("Invalid command... Choose from below")
DisplayCommandsList();
}
}
catch(e){
if(typeof e === "string") console.error(e)
else throw e
}
console.log("\n______complete______\n")
}
//Connects to MongoDB database
mongoose.set('strictQuery', false)
mongoose.connect(`mongodb://127.0.0.1:27017/${process.env.DATABASE_NAME || "db_powrum"}`)
.then(async ()=> {
//Handle command through process options
if(argv.c) {
await handleCommand(argv.c)
process.exit(0)
}
//Handle commands through CLI
console.log("Welcome to Powrum's admin panel")
console.log("Say 'help' for list of commands\n")
while (true){
let command = readlineSync.question('Enter command: \n> ');
await handleCommand(command)
}
})
require('./models')
const Accounts = mongoose.model("Accounts")
const Reputations = mongoose.model("Reputations")
const Messages = mongoose.model("Messages")
const PasswordResetSessions = mongoose.model("PasswordResetSessions")
const Sessions = mongoose.model("Sessions")
const Logs = mongoose.model("Logs")
const LoginHistories = mongoose.model("LoginHistories")
const Threads = mongoose.model("Threads")
const ThreadReplies = mongoose.model("ThreadReplies")
const ThreadReplyReacts = mongoose.model("ThreadReplyReacts")
const TFAs = mongoose.model("TFAs")
const PinnedThreads = mongoose.model("PinnedThreads")
const NotificationSettings = mongoose.model("NotificationSettings")
const Notifications = mongoose.model("Notifications")
const AltAccounts = mongoose.model("AltAccounts")
const ActiveUsers = mongoose.model("ActiveUsers")
const PendingEmailVerifications = mongoose.model("PendingEmailVerifications")
const commands = {
//Preset commands. Other commands are added dynamically
help: {func: function(){DisplayCommandsList(false)}, desc: "Displays a list of commands along with description"},
".help": {func: DisplayCommandsList, desc: "Displays compact list of commands"},
}
function DisplayCommandsList(compact=true){
//since commands' keys are just commands, store in the form of array here
let commandsListArray = Object.keys(commands)
//Give list of commands without description
if(compact){
let response = ""
for (index in commandsListArray){
let command = commandsListArray[index]
response += command + ", "
}
response = response.substring(0, response.length - 2) //hacky way to remove extra ", "
console.log(response)
} else{
for (index in commandsListArray){
let command = commandsListArray[index]
console.log(`* ${command} : ${commands[command].desc}`)
}
}
}
async function FetchUser(target){
let user = {}
//Assume target is UID (number)
if(!isNaN(target)) {
user.uid = target
user.acc = await Accounts.findById(target).lean()
if(user.acc) user.username = user.acc.username
else throw "Could not find an account with that UID"
}
//Assume target is username (string)
else {
user.username = target
user.acc = await Accounts.findOne({username: new RegExp(`^${user.username}$`, 'i')}).lean()
if(user.acc) user.uid = user.acc._id
else throw "Could not find an account with that username"
}
return user
}
commands.addadmin = {
desc: "Add an admin user",
func: async () => {
//Account document selection
var target = readlineSync.question('Username or UID: ')
var {acc, uid, username} = await FetchUser(target)
acc.roles = other.StringToArray(acc.roles)
if(acc.roles.includes('admin')) throw `${username} is already an admin`
console.log(`Are you sure you want to add ${username} (UID ${uid}) as an admin? They will be granted abusable powers such as the ability to delete accounts and add categories.`)
let confirm = readlineSync.question('yes/no: ')
if(confirm !== "yes") throw "Aborted admin give"
//Adds the admin role
acc.roles.push('admin')
//Updates the account in the database to reflect data
await Accounts.updateOne({_id: uid}, {roles: JSON.stringify(acc.roles)})
console.log(`${username} is now an admin`)
}
}
commands.removeadmin = {
desc: "Removes an admin user",
func: async () => {
//Account document selection
var target = readlineSync.question('Username or UID: ')
var {acc, uid, username} = await FetchUser(target);
acc.roles = other.StringToArray(acc.roles)
let index = acc.roles.indexOf("admin")
if(index === -1) return console.log(`${username} is not an admin`)
//Removes the admin role
acc.roles.splice(index, 1)
//Updates the account in the database to reflect data
await Accounts.updateOne({_id: uid}, {roles: JSON.stringify(acc.roles)})
console.log(`${username} is no longer an admin`)
}
}
commands.rateuser = {func: RateUser, desc: "Applies a rating from the bot account"}
async function RateUser(){
//Get user id to apply rating to
var target = readlineSync.question('Username or UID: ')
var {uid, username} = await FetchUser(target);
var diff = parseInt(readlineSync.question('Diff: ')) // Must be a number
var comment = readlineSync.question('Comment: ') // "What do you have to say about this user?"
//Delete an existing reputation if it exists. The following insert is basically a replacement rating
await Reputations.deleteOne({for: uid, from: 1})
//Try posting the rating
await new Reputations({
for: uid,
from: 1,
diff,
comment,
date: new Date(),
}).save()
console.log(`Gave ${diff} reputation to ${username}(${uid}) with comment ${comment}`)
}
//Deletes (any?) data associated with this user
commands.deleteuser = {func: DeleteUser, desc: "Deletes their account and all data linked to their uid"}
async function DeleteUser(){
//Get user id to apply rating to
var target = readlineSync.question('Username or UID: ')
var acc, uid, username;
if(!isNaN(target)) {
uid = target
acc = await Accounts.findById(target)
if(acc) username = acc.username
else console.log("Acc not found for that UID, but you may proceed to delete remaining associated data.")
}
else {
username = target
acc = await Accounts.findOne({username: new RegExp(`^${username}$`, 'i')})
if(acc) uid = acc._id
else throw "Could not find an account with that username"
}
console.log(`Are you sure you want to delete ${username}(${uid})'s account?`)
let confirm = readlineSync.question('yes/no: ')
if(confirm !== "yes"){
console.log("Aborted account deletion")
return
}
await Accounts.deleteOne({_id: uid})
await PasswordResetSessions.deleteOne({uid})
await Sessions.deleteMany({session: new RegExp(`"uid":${req.session.uid}[},]`)})
await Logs.deleteMany({uid})
await LoginHistories.deleteMany({uid})
await Messages.deleteMany({$or: [{from: uid}, {to: uid}]})
await TFAs.deleteOne({_id: uid})
await NotificationSettings.deleteOne({_id: uid})
await Notifications.deleteMany({$or: [{senderid: uid}, {recipientid: uid}]})
await AltAccounts.deleteOne({_id: uid})
await ActiveUsers.deleteOne({uid})
console.log("Delete their forum content too? (Threads, replies, reputation)")
let confirm2 = readlineSync.question('yes/no: ')
if(confirm2 === "yes"){
let threads = await Threads.find({uid})
//Deletes replies to their threads
for (let thread of threads){
await ThreadReplies.deleteMany({tid: thread._id})
await PinnedThreads.deleteOne({_id: thread._id})
}
//Deletes their threads
await Threads.deleteMany({uid})
//Deletes their replies on other threads
await ThreadReplies.deleteMany({uid})
await Reputations.deleteMany({$or: [{from: uid}, {for: uid}]})
await ThreadReplyReacts.deleteMany({uid})
}
else{
//Only deletes reputation given to them since they'd be unviewable anyways
await Reputations.deleteMany({for: uid})
}
console.log("Finished")
}
//Right now the online user tracker doesnt auto clear. This command will delete documents older than 15 minutes
commands.cleanonlineuserlist = {func: CleanOnlineUsersList, desc: "Deletes activeusers documents older than 15 minutes"}
async function CleanOnlineUsersList(){
await ActiveUsers.deleteMany({
time: {
$lt: Date.now() - (1000*60*15)
}
})
}
//Right now the global chats doesnt auto clean. This command will global chats older than 15 minutes
commands.cleanglobalchat = {func: CleanMessages, desc: "Deletes global chats older than 15 minutes"}
async function CleanMessages(){
await Messages.deleteMany({
time: {
$lt: Date.now() - (1000*60*15)
}
})
}
//Right now the global chats doesnt auto clean. This command will global chats older than 15 minutes
commands.fixunicode = {
desc: "Goes through text content and fixes unicode errors",
func: async() => {
/* Conversions
’ > '
Â, > delete
 > delete
*/
{
let threadIds = (await Threads.find({title: /(’|Â)/}, {_id: 1})).map(thread => thread._id)
for (let threadId of threadIds) {
let thread = await Threads.findById(threadId)
thread.title = thread.title.replace(/’/g, "'")
thread.title = thread.title.replace(/Â,?/g, "")
await thread.save()
}
let threadReplyIds = (await ThreadReplies.find({content: /(’|Â)/}, {_id: 1})).map(thread => thread._id)
for (let threadReplyId of threadReplyIds) {
let threadReply = await ThreadReplies.findById(threadReplyId)
threadReply.content = threadReply.content.replace(/’/g, "'")
threadReply.content = threadReply.content.replace(/Â,?/g, "")
await threadReply.save()
}
}
}
}
commands.upgradev2 = {
desc: "PF has made some breaking changes in v2.0.0. Run this to automatically repair database breaking changes.",
func: async () => {
let numNotifs = await PendingEmailVerifications.countDocuments()
let page = 0
while (page*10000 < numNotifs){
console.time(`page${page}`)
//Send from newest to oldest subs
let docs = await PendingEmailVerifications.find({}).sort({_id: -1}).skip(page*1000).limit(1000)
for(let doc of docs){
await Accounts.updateOne({_id: doc._id}, {
emailVerification: {
token: doc.token,
lastSent: doc.lastsent
}
})
}
console.timeEnd(`page${page}`)
page = page + 1
}
}
}
/*
commands.purgeaccounts = {func: PurgeAccounts, desc: "Deletes account older than 6 months with no valuable history"}
async function PurgeAccounts(){
//Loop through all accounts. Must paginate to preserve memory space
let numAccount = (await mysql.query("SELECT COUNT(*) FROM accounts WHERE creationdate < NOW() - INTERVAL 6 MONTH"))[0]["COUNT(*)"]
let pageCount = 1000
let pages = Math.floor(numAccount/pageCount)+1
let deleteCount = 0
console.log(`Scanning through ${numAccount} accounts`)
for(var i=0; i<pages; i++){
let page = await mysql.query("SELECT * FROM accounts WHERE creationdate < NOW() - INTERVAL 6 MONTH ORDER BY uid LIMIT ?,?", [i*pageCount, pageCount])
for(var v=0; v<page.length; v++){
//console.log(`Scanning ${page[v].username}#${page[v].uid}'s account`)
//Look for forum thread activity
let numReplies = (await mysql.query("SELECT COUNT(*) FROM thread_replies WHERE uid=?", page[v].uid))[0]["COUNT(*)"]
if(numReplies > 0) continue
//Check if they have a public reputation
let hasRep = (await mysql.query("SELECT COUNT(*) FROM reputation WHERE _for=?", page[v].uid))[0]["COUNT(*)"] > 0
if(hasRep) continue
//Check if they've sent messages
let hasMessages = await Messages.exists({$or: [{from: page[v].uid}, {to: page[v].uid}]})
if(hasMessages) {
console.log(`Skipped ${page[v].username}: Had messages`)
continue
}
//Check if they own an exploit on the front page. They're valuable outside the forum
let isExploitOwner = (await mysql.query("SELECT COUNT(*) FROM download_links WHERE uid=?", page[v].uid))[0]["COUNT(*)"] > 0
if(isExploitOwner) {
console.log(`Skipped ${page[v].username}: Owns an exploit`)
continue
}
//No valuable content found for this account. Delete them
deleteCount++
await mysql.query("DELETE FROM accounts WHERE uid=?", [page[v].uid])
await mysql.query("DELETE FROM password_reset_sessions WHERE uid=?", [page[v].uid])
await mysql.query("DELETE FROM pending_email_verifications WHERE uid=?", [page[v].uid])
await mysql.query(`DELETE FROM sessions WHERE session LIKE '%"uid":${page[v].uid}}' OR session LIKE '%"uid":"${page[v].uid}"%'`)
await mysql.query("DELETE FROM logs WHERE uid=?", [page[v].uid])
await mysql.query("DELETE FROM loginhistory WHERE uid=?", [page[v].uid])
await mysql.query("DELETE FROM threads WHERE uid=?", [page[v].uid]) //Just in case of a bug elsewhere
await mysql.query("DELETE FROM reputation WHERE _from=? OR _for=?", [page[v].uid, page[v].uid])
console.log(`Deleted ${page[v].username}#${page[v].uid}'s account`)
}
}
console.log(`Deleted ${deleteCount} accounts`)
}
*/
commands.listNonexistentThreadsWithReplies = {
func: ListNonexistentThreadsWithReplies,
desc: "Lists non-existent threads with replies"
}
async function ListNonexistentThreadsWithReplies(){
let replies = await ThreadReplies.find().sort({_id: 1}).lean()
console.log(`Looking through ${replies.length} replies`)
var nonexistentThreads = []
for(let reply of replies){
if(nonexistentThreads.includes(reply.tid)) continue
let thread = await Threads.findById(reply.tid)
if(!thread) {
nonexistentThreads.push(reply.tid)
console.log(`tid:${reply.tid} does not exist for existing trid:${reply._id}`)
}
}
console.log(`Found ${nonexistentThreads.length} non-existent threads with existing replies.`)
if(nonexistentThreads.length > 0){
console.log(`Do you want to delete all replies of these non-existent threads?`)
let confirm = readlineSync.question('yes/no: ')
if(confirm === "yes"){
console.log(`Deleting all replies tied to the ${nonexistentThreads.length} nonexistent threads.`)
for(var i=0; i<nonexistentThreads.length; i++){
await ThreadReplies.deleteMany({tid: nonexistentThreads[i]})
}
}
}
}
commands.listThreadsWithNoReplies = {
desc: "Lists threads with no replies",
func: async () => {
let threads = await Threads.find().sort({_id: 1}).lean()
console.log(`Looking through ${threads.length} threads`)
var emptyThreads = []
for(let thread of threads){
let replyCount = await ThreadReplies.countDocuments({tid: thread._id})
if(replyCount === 0) {
emptyThreads.push(thread._id)
console.log(`tid:${thread._id} has no replies`)
}
}
console.log(`Found ${emptyThreads.length} empty threads.`)
if(emptyThreads.length > 0){
console.log(`Do you want to delete all of these empty threads?`)
let confirm = readlineSync.question('yes/no: ')
if(confirm !== "yes") return
console.log(`Deleting all ${emptyThreads.length} empty threads.`)
for(var i=0; i<emptyThreads.length; i++){
await Threads.deleteOne({_id: emptyThreads[i]})
}
}
},
}
/*
commands.listThreadsWithInvalidCategory = {
func: ListThreadsWithInvalidCategory,
desc: "List threads with non-existent category",
}
async function ListThreadsWithInvalidCategory(){
let categories = (await Subcategories.find()).map(doc => doc._id)
let threads = await Threads.find()
for(let i=0; i<threads.length; i++){
for(let thread of threads){
if(categories.indexOf(thread.forum) === -1){
console.log(`Unknown category '${thread.forum}' tid:${thread.tid}, with topic: '${thread.title}'`)
if(readlineSync.question('Delete thread? (yes/no): ') === "yes"){
await mysql.query("DELETE FROM threads WHERE tid=?", thread.tid)
await mysql.query("DELETE FROM thread_replies WHERE tid=?", thread.tid)
console.log("Deleted")
}
}
}
}
*/