@@ -56,7 +56,6 @@ module.exports = {
5656 . addIntegerOption ( option =>
5757 option . setName ( 'percent' )
5858 . setDescription ( 'The minimum percent players need to get a record on this level (list percent)' ) ) )
59-
6059 . addSubcommand ( subcommand =>
6160 subcommand
6261 . setName ( 'submit' )
@@ -127,7 +126,52 @@ module.exports = {
127126 option . setName ( 'level2' )
128127 . setDescription ( 'The name of the second level' )
129128 . setAutocomplete ( true )
130- . setRequired ( true ) ) ) ,
129+ . setRequired ( true ) ) )
130+ . addSubcommand ( subcommand =>
131+ subcommand
132+ . setName ( 'edit' )
133+ . setDescription ( 'Edit a level\'s info' )
134+ . addStringOption ( option =>
135+ option . setName ( 'level' )
136+ . setDescription ( 'The name of the level to edit' )
137+ . setAutocomplete ( true )
138+ . setRequired ( true ) )
139+ . addStringOption ( option =>
140+ option . setName ( 'levelname' )
141+ . setDescription ( 'The name of the level to place' ) )
142+ . addIntegerOption ( option =>
143+ option . setName ( 'position' )
144+ . setDescription ( 'The position to place the level at' ) )
145+ . addIntegerOption ( option =>
146+ option . setName ( 'difficulty' )
147+ . setDescription ( 'The tier the level is in (1-10, see the list website for details)' ) )
148+ . addIntegerOption ( option =>
149+ option . setName ( 'id' )
150+ . setDescription ( 'The GD ID of the level to place' ) )
151+ . addStringOption ( option =>
152+ option . setName ( 'uploader' )
153+ . setDescription ( 'The name of the person who uploaded the level on GD' ) )
154+ . addStringOption ( option =>
155+ option . setName ( 'verifier' )
156+ . setDescription ( 'The name of the verifier' ) )
157+ . addStringOption ( option =>
158+ option . setName ( 'verification' )
159+ . setDescription ( 'The link to the level\'s verification video' ) )
160+ . addStringOption ( option =>
161+ option . setName ( 'songname' )
162+ . setDescription ( 'The name of this level\'s song' ) )
163+ . addStringOption ( option =>
164+ option . setName ( 'songlink' )
165+ . setDescription ( 'The NONG link for this level, if any.' ) )
166+ . addStringOption ( option =>
167+ option . setName ( 'creators' )
168+ . setDescription ( 'The list of the creators of the level, each separated by a comma' ) )
169+ . addStringOption ( option =>
170+ option . setName ( 'password' )
171+ . setDescription ( 'The GD password of the level to place' ) )
172+ . addIntegerOption ( option =>
173+ option . setName ( 'percent' )
174+ . setDescription ( 'The minimum percent players need to get a record on this level (list percent)' ) ) ) ,
131175 async autocomplete ( interaction ) {
132176 const focused = interaction . options . getFocused ( ) ;
133177 const { cache } = require ( '../../index.js' ) ;
@@ -224,6 +268,218 @@ module.exports = {
224268 return ;
225269 } else if ( interaction . options . getSubcommand ( ) === 'submit' ) {
226270 return await interaction . editReply ( 'Not implemented yet' ) ;
271+ } else if ( interaction . options . getSubcommand ( ) === 'edit' ) {
272+ const { db, cache } = require ( '../../index.js' ) ;
273+ const { octokit } = require ( '../../index.js' ) ;
274+ const level = interaction . options . getString ( 'level' ) || null ;
275+ const levelname = interaction . options . getString ( 'levelname' ) || null ;
276+ const id = interaction . options . getInteger ( 'id' ) || null ;
277+ const uploaderName = interaction . options . getString ( 'uploader' ) || null ;
278+ const verifierName = interaction . options . getString ( 'verifier' ) || null ;
279+ const verification = interaction . options . getString ( 'verification' ) || null ;
280+ const password = interaction . options . getString ( 'password' ) || null ;
281+ const rawCreators = interaction . options . getString ( 'creators' ) || null ;
282+ const creatorNames = rawCreators ? rawCreators . split ( ',' ) : [ ] ;
283+ const percent = interaction . options . getInteger ( 'percent' ) || null ;
284+ const difficulty = interaction . options . getInteger ( 'difficulty' ) || null ;
285+ const songName = interaction . options . getString ( 'songname' ) || null ;
286+ const songLink = interaction . options . getString ( 'songlink' ) || null ;
287+
288+ const levelToEdit = await cache . levels . findOne ( { where : { filename : level } } ) ;
289+ const filename = levelToEdit . filename ;
290+ let fileResponse ;
291+ try {
292+ fileResponse = await octokit . rest . repos . getContent ( {
293+ owner : githubOwner ,
294+ repo : githubRepo ,
295+ path : githubDataPath + `/${ filename } .json` ,
296+ branch : githubBranch ,
297+ } ) ;
298+ } catch ( fetchError ) {
299+ logger . info ( `Couldn't fetch ${ filename } .json: \n${ fetchError } ` ) ;
300+ return await interaction . editReply ( `:x: Couldn't fetch ${ filename } .json: \n${ fetchError } ` ) ;
301+ }
302+
303+ let parsedData ;
304+ try {
305+ parsedData = JSON . parse ( Buffer . from ( fileResponse . data . content , 'base64' ) . toString ( 'utf-8' ) ) ;
306+ } catch ( parseError ) {
307+ logger . info ( `Unable to parse data fetched from ${ filename } :\n${ parseError } ` ) ;
308+ return await interaction . editReply ( `:x: Unable to parse data fetched from ${ filename } :\n${ parseError } ` ) ;
309+ }
310+
311+ if ( levelname !== null ) parsedData . name = levelname ;
312+ if ( id !== null ) parsedData . id = id ;
313+ if ( uploaderName !== null ) parsedData . author = uploaderName ;
314+ if ( verifierName !== null ) parsedData . verifier = verifierName ;
315+ if ( verification !== null ) parsedData . verification = verification ;
316+ if ( password !== null ) parsedData . password = password ;
317+ if ( creatorNames . length > 0 ) parsedData . creators = creatorNames ;
318+ if ( percent !== null ) parsedData . percentToQualify = percent ;
319+ if ( difficulty !== null ) parsedData . difficulty = difficulty ;
320+ if ( songName !== null ) parsedData . song = songName ;
321+ if ( songLink !== null ) parsedData . songLink = songLink ;
322+
323+ let existing = true ;
324+
325+ if ( levelname !== null || id !== null || uploaderName !== null || verifierName !== null || verification !== null || password !== null || creatorNames . length > 0 || percent !== null || difficulty !== null || songName !== null || songLink !== null ) existing = false ;
326+
327+ if ( existing ) {
328+ return await interaction . editReply ( 'You didn\'t change anything' ) ;
329+ }
330+ await interaction . editReply ( "Committing..." ) ;
331+
332+ // not sure why it needs to be done this way but :shrug:
333+ let changes = [ ] ;
334+ changes . push ( {
335+ path : githubDataPath + `/${ filename } .json` ,
336+ content : JSON . stringify ( parsedData , null , '\t' ) ,
337+ } )
338+
339+ const changePath = githubDataPath + `/${ filename } .json`
340+ const content = JSON . stringify ( parsedData ) ;
341+
342+ const debugStatus = await db . infos . findOne ( { where : { name : 'commitdebug' } } ) ;
343+ if ( ! debugStatus || ! debugStatus . status ) {
344+ let commitSha ;
345+ try {
346+ // Get the SHA of the latest commit from the branch
347+ const { data : refData } = await octokit . git . getRef ( {
348+ owner : githubOwner ,
349+ repo : githubRepo ,
350+ ref : `heads/${ githubBranch } ` ,
351+ } ) ;
352+ commitSha = refData . object . sha ;
353+ } catch ( getRefError ) {
354+ logger . info ( `Something went wrong while fetching the latest commit SHA:\n${ getRefError } ` ) ;
355+ await db . messageLocks . destroy ( { where : { discordid : interaction . message . id } } ) ;
356+ return await interaction . editReply ( ':x: Something went wrong while commiting the records to github, please try again later (getRefError)' ) ;
357+ }
358+ let treeSha ;
359+ try {
360+ // Get the commit using its SHA
361+ const { data : commitData } = await octokit . git . getCommit ( {
362+ owner : githubOwner ,
363+ repo : githubRepo ,
364+ commit_sha : commitSha ,
365+ } ) ;
366+ treeSha = commitData . tree . sha ;
367+ } catch ( getCommitError ) {
368+ logger . info ( `Something went wrong while fetching the latest commit:\n${ getCommitError } ` ) ;
369+ await db . messageLocks . destroy ( { where : { discordid : interaction . message . id } } ) ;
370+ return await interaction . editReply ( ':x: Something went wrong while commiting the records to github, please try again later (getCommitError)' ) ;
371+ }
372+
373+ let newTree ;
374+ try {
375+ // Create a new tree with the changes
376+ newTree = await octokit . git . createTree ( {
377+ owner : githubOwner ,
378+ repo : githubRepo ,
379+ base_tree : treeSha ,
380+ tree : changes . map ( change => ( {
381+ path : change . path ,
382+ mode : '100644' ,
383+ type : 'blob' ,
384+ content : change . content ,
385+ } ) ) ,
386+ } ) ;
387+ } catch ( createTreeError ) {
388+ logger . info ( `Something went wrong while creating a new tree:\n${ createTreeError } ` ) ;
389+ await db . messageLocks . destroy ( { where : { discordid : interaction . message . id } } ) ;
390+ return await interaction . editReply ( ':x: Something went wrong while commiting the records to github, please try again later (createTreeError)' ) ;
391+ }
392+
393+ let newCommit ;
394+ try {
395+ // Create a new commit with this tree
396+ newCommit = await octokit . git . createCommit ( {
397+ owner : githubOwner ,
398+ repo : githubRepo ,
399+ message : `Updated info for ${ levelToEdit . name } )` ,
400+ tree : newTree . data . sha ,
401+ parents : [ commitSha ] ,
402+ } ) ;
403+ } catch ( createCommitError ) {
404+ logger . info ( `Something went wrong while creating a new commit:\n${ createCommitError } ` ) ;
405+ await db . messageLocks . destroy ( { where : { discordid : interaction . message . id } } ) ;
406+ return await interaction . editReply ( ':x: Something went wrong while commiting the records to github, please try again later (createCommitError)' ) ;
407+ }
408+
409+ try {
410+ // Update the branch to point to the new commit
411+ await octokit . git . updateRef ( {
412+ owner : githubOwner ,
413+ repo : githubRepo ,
414+ ref : `heads/${ githubBranch } ` ,
415+ sha : newCommit . data . sha ,
416+ } ) ;
417+ } catch ( updateRefError ) {
418+ logger . info ( `Something went wrong while updating the branch :\n${ updateRefError } ` ) ;
419+ await db . messageLocks . destroy ( { where : { discordid : interaction . message . id } } ) ;
420+ return await interaction . editReply ( ':x: Something went wrong while commiting the records to github, please try again later (updateRefError)' ) ;
421+ }
422+ logger . info ( `Successfully created commit on ${ githubBranch } (record addition): ${ newCommit . data . sha } ` ) ;
423+ await interaction . editReply ( "This record has been added!" ) ;
424+ } else {
425+ let updatedFiles = 0 ;
426+ let i = 1 ;
427+ // Get file SHA
428+ let fileSha ;
429+ try {
430+ const response = await octokit . repos . getContent ( {
431+ owner : githubOwner ,
432+ repo : githubRepo ,
433+ path : changePath ,
434+ } ) ;
435+ fileSha = response . data . sha ;
436+ } catch ( error ) {
437+ logger . info ( `Error fetching ${ changePath } SHA:\n${ error } ` ) ;
438+ erroredRecords . push ( `All from ${ changePath } ` ) ;
439+ return await interaction . editReply ( `:x: Couldn't fetch data from ${ changePath } ` ) ;
440+ i ++ ;
441+
442+ }
443+
444+ try {
445+ await octokit . repos . createOrUpdateFileContents ( {
446+ owner : githubOwner ,
447+ repo : githubRepo ,
448+ path : changePath ,
449+ message : `Updated ${ changePath } (${ interaction . user . tag } )` ,
450+ content : Buffer . from ( content ) . toString ( 'base64' ) ,
451+ sha : fileSha ,
452+ } ) ;
453+ logger . info ( `Updated ${ changePath } (${ interaction . user . tag } ` ) ;
454+ } catch ( error ) {
455+ logger . info ( `Failed to update ${ changePath } (${ interaction . user . tag } ):\n${ error } ` ) ;
456+ erroredRecords . push ( `All from ${ changePath } ` ) ;
457+ await interaction . editReply ( `:x: Couldn't update the file ${ changePath } , skipping...` ) ;
458+ }
459+ updatedFiles ++ ;
460+ i ++ ;
461+
462+ let detailedErrors = '' ;
463+ for ( const err of erroredRecords ) detailedErrors += `\n${ err } ` ;
464+
465+ const replyEmbed = new EmbedBuilder ( )
466+ . setColor ( 0x8fce00 )
467+ . setTitle ( ':white_check_mark: Commit successful' )
468+ . setDescription ( `Successfully updated ${ updatedFiles } / files` )
469+ . addFields (
470+ { name : 'Duplicates found:' , value : `**${ duplicateRecords } **` , inline : true } ,
471+ { name : 'Errors:' , value : `${ erroredRecords . length } ` , inline : true } ,
472+ { name : 'Detailed Errors:' , value : ( detailedErrors . length == 0 ? 'None' : detailedErrors ) } ,
473+ )
474+ . setTimestamp ( ) ;
475+ await interaction . message . delete ( ) ;
476+ await interaction . editReply ( ':white_check_mark: The record has been accepted' ) ;
477+ }
478+
479+ logger . info ( `${ interaction . user . tag } (${ interaction . user . id } ) submitted ${ interaction . options . getString ( 'levelname' ) } for ${ interaction . options . getString ( 'username' ) } ` ) ;
480+ // Reply
481+ await interaction . editReply ( `:white_check_mark: ${ levelToEdit . name } has been edited successfully` ) ;
482+ return ;
227483 } else if ( interaction . options . getSubcommand ( ) === 'move' ) {
228484 const { db, octokit } = require ( '../../index.js' ) ;
229485
0 commit comments