Skip to content

Commit 9c389c9

Browse files
committed
Partial update system
A system is partially added to allow live updating of the bot from the repository.
1 parent 4940916 commit 9c389c9

File tree

7 files changed

+381
-35
lines changed

7 files changed

+381
-35
lines changed

config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"locale": "en-US",
3+
"betaEnabled": true,
34
"errorTracking": {
45
"enabled": false,
56
"sentryDSN": "",

lib/menus/update.js

Lines changed: 295 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,54 +9,319 @@ const menuName = new URL(import.meta.url).pathname.split('/').pop().replace('.js
99
const locale = client.locale.lib.menus[menuName];
1010

1111
// Exports a default function
12-
export default async (beforeError) => {
12+
export default async (beforeError, forceUpdate) => {
1313

1414
try {
1515

16+
// Stores the local configuration from the configuration file
17+
let localConfig = require('./config.json');
18+
19+
// Function to show the post-update menu
20+
async function showPostUpdateMenu() {
21+
22+
// Shows the select menu
23+
global.currentPrompt = select({
24+
message: `${locale.promptMessage}:`,
25+
choices: [
26+
{
27+
name: `🔎 ${locale.choices.checkAgain}`,
28+
value: 'checkAgain'
29+
},
30+
{
31+
name: `${ localConfig.betaEnabled ? `🏠 ${locale.choices.switchToStable}` : `🧪 ${locale.choices.switchToBeta}`}`,
32+
value: 'switchVersion'
33+
},
34+
{
35+
name: `↩️ ${locale.choices.return}`,
36+
value: 'return'
37+
}
38+
],
39+
});
40+
41+
// When the user selects an option
42+
global.currentPrompt.then(async (result) => {
43+
44+
// Switches between the options
45+
switch (result) {
46+
47+
// If the user wants to check again for updates
48+
case 'checkAgain':
49+
50+
// Shows this menu again
51+
await client.functions.menus.update();
52+
53+
// Breaks the switch
54+
break;
55+
56+
// If the user wants to change it's version
57+
case 'switchVersion':
58+
59+
// Changes the betaEnabled value
60+
localConfig.betaEnabled = localConfig.betaEnabled ? false : true;
61+
62+
// Saves the new configuration
63+
await fs.writeFileSync('./config.json', JSON.stringify(localConfig, null, 4));
64+
65+
// Shows this menu again, passing the forceUpdate parameter based on the new value
66+
await client.functions.menus.update(null, localConfig.betaEnabled ? false : true);
67+
68+
// Breaks the switch
69+
break;
70+
71+
// If the user wants to go back to the main menu
72+
case 'return':
73+
74+
// Shows the main menu
75+
await client.functions.menus.main();
76+
77+
// Breaks the switch
78+
break;
79+
};
80+
81+
// When an error occurs
82+
}).catch(async (error) => {
83+
84+
// Runs this menu again, passing the error
85+
await client.functions.menus[menuName](error);
86+
});
87+
};
88+
1689
// Cleans the console and shows the logo
1790
process.stdout.write('\u001b[2J\u001b[0;0H');
1891
await splashLogo(client.locale.lib.loaders.splashLogo);
19-
92+
2093
// Shows an error message if one occurred
2194
if (beforeError) console.log(`❌ ${locale.errorMessage}.\n`);
2295
if (beforeError && process.env.NODE_ENV !== 'production') logger.error(`${beforeError.stack}\n`);
96+
97+
try {
98+
99+
// Shows a message indicating that the bot is checking for updates
100+
console.log(`🔎 ${locale.checkingForUpdates} ...`);
23101

24-
// Shows a default text
25-
console.log('🚧 This section is currently under development.\n');
102+
// Loads the repository configuration
103+
const packageConfig = require('./package.json');
26104

27-
// Shows the select menu
28-
global.currentPrompt = select({
29-
message: `${locale.promptMessage}:`,
30-
choices: [
31-
{
32-
name: `↩️ ${locale.choices.return}`,
33-
value: 'return'
34-
}
35-
],
36-
});
105+
// Sets the latest local tag as the current version from the package
106+
let latestLocalTag = packageConfig.version;
107+
108+
// Imports the necessary libraries
109+
const simpleGit = require('simple-git');
37110

38-
// When the user selects an option
39-
global.currentPrompt.then(async (result) => {
111+
// Creates a new instance of simple-git
112+
const git = simpleGit();
40113

41-
// When the user selects an option
42-
switch (result) {
114+
// Fetches the latest tags and prunes the old tags
115+
await git.fetch(['--prune', '--tags']);
116+
117+
// Gets the remote tags list
118+
const remoteTagLines = (await git.listRemote(['--tags'])).split('\n');
119+
120+
// Maps the remote tags list to an array of tag names
121+
let remoteTags = remoteTagLines.map(line => {
122+
123+
// Extracts the tag name from the line
124+
const match = line.match(/refs\/tags\/(.+)$/);
125+
126+
// Returns the tag name or null if it doesn't exist
127+
return match ? match[1] : null;
128+
129+
// Filters out null values
130+
}).filter(tag => tag !== null);
131+
132+
// Gets the local tags list and filters out empty values
133+
const localTags = (await git.tag(['--list'])).split('\n').filter(tag => tag.trim() !== '');
134+
135+
// Filters out tags that exist locally but not remotely
136+
const tagsToDelete = localTags.filter(tag => !remoteTags.includes(tag) && tag);
137+
138+
// Deletes these non-existent tags
139+
for (const tag of tagsToDelete) await git.tag(['-d', tag]);
140+
141+
// If the beta versions are disabled, removes them from the array
142+
if (remoteTags && !localConfig.betaEnabled) remoteTags = remoteTags.filter(tag => !tag.includes('beta'));
143+
144+
// Obtains the latest remote tag
145+
const latestRemoteTag = remoteTags[remoteTags.length - 1];
146+
147+
// Compares the local and remote tags
148+
if (latestRemoteTag && latestRemoteTag.length > 0 && latestLocalTag !== latestRemoteTag) {
149+
150+
// Function to update the bot
151+
async function updateBot() {
152+
153+
// Cleans the console and shows the logo
154+
process.stdout.write('\u001b[2J\u001b[0;0H');
155+
await splashLogo(client.locale.lib.loaders.splashLogo);
156+
157+
// Shows a message indicating that the bot is checking for updates
158+
console.log(`🔄 ${await client.functions.utils.parseLocale(locale.updatingFromTo, { originVersion: `v${latestLocalTag}`, targetVersion: `v${latestRemoteTag}` })} ...`);
159+
160+
// Simulates a loading time
161+
await new Promise(resolve => setTimeout(resolve, 3000)); // Espera de 3 segundos
162+
163+
// Makes a pull to update the bot
164+
//await git.pull();
165+
166+
// Makes a checkout to the latest tag
167+
//await git.checkout(latestTag);
168+
169+
// Cleans the console and shows the logo
170+
process.stdout.write('\u001b[2J\u001b[0;0H');
171+
await splashLogo(client.locale.lib.loaders.splashLogo);
172+
173+
// Shows a message indicating that the bot has been updated
174+
console.log(`✅ ${await client.functions.utils.parseLocale(locale.updatedCorrectly, { targetVersion: `v${latestRemoteTag}` })}\n`);
175+
};
176+
177+
// Shows a message indicating that there are new updates
178+
if (!forceUpdate) {
179+
180+
// Function to obtain the release notes from the GitHub repository
181+
function getReleaseNotes(owner, repository, tagName) {
182+
183+
// Imports the necessary libraries
184+
const https = require('https');
185+
186+
// Returns a promise to obtain the release notes
187+
return new Promise((resolve, reject) => {
188+
const options = {
189+
hostname: 'api.github.com',
190+
path: `/repos/${owner}/${repository}/releases/tags/${tagName}`,
191+
method: 'GET',
192+
headers: {
193+
'User-Agent': 'NodeJS'
194+
}
195+
};
196+
197+
// Makes the request
198+
const request = https.request(options, (response) => {
199+
200+
// Stores the data
201+
let data = '';
202+
203+
// When a chunk of data is received
204+
response.on('data', (chunk) => {
205+
206+
// Stores the chunk
207+
data += chunk;
208+
});
209+
210+
// When the response ends
211+
response.on('end', () => {
212+
213+
// If the response was OK
214+
if (response.statusCode === 200) {
215+
216+
// Parses the data
217+
const jsonData = JSON.parse(data);
218+
219+
// Resolves the promise with the body of the response
220+
resolve(jsonData.body);
221+
222+
} else {
223+
224+
// Rejects the promise with the error
225+
if (response.statusCode !== 404) return reject(new Error(`Received status code ${response.statusCode}`));
226+
227+
// Resolves the promise with a default message
228+
resolve(locale.noReleaseNotes);
229+
};
230+
});
231+
});
232+
233+
// When an error occurs
234+
request.on('error', (error) => {
235+
236+
// Rejects the promise with the error
237+
reject(error);
238+
});
239+
240+
// Ends the request
241+
request.end();
242+
});
243+
};
244+
245+
// Cleans the console and shows the logo
246+
process.stdout.write('\u001b[2J\u001b[0;0H');
247+
await splashLogo(client.locale.lib.loaders.splashLogo);
248+
249+
// Shows a message indicating that there are new updates
250+
console.log(`⬇️ ${await client.functions.utils.parseLocale(locale.newVersionAvailable, { targetVersion: latestRemoteTag })}\n`);
251+
252+
// Uses a regular expression to obtain the owner and repository name from the URL
253+
const repositoryMetadata = packageConfig.repository.url.match(/github\.com\/([^\/]+)\/([^\/]+)\.git/);
254+
255+
// Stores the owner and repository name
256+
const repositoryOwner = repositoryMetadata[1];
257+
const repositoryName = repositoryMetadata[2];
258+
259+
// Obtains the release notes
260+
await getReleaseNotes(repositoryOwner, repositoryName, latestRemoteTag).then(releaseNotes => {
261+
262+
// Shows the release notes
263+
console.log(`${locale.releaseNotes}:\n${releaseNotes}\n`);
264+
265+
}).catch(error => {
266+
267+
// Shows an error message if one occurred
268+
console.log(`❌ ${locale.releaseNotesError}\n`);
43269

44-
// If the user wants to go back to the main menu
45-
case 'return':
46-
47-
// Shows the main menu
48-
await client.functions.menus.main();
270+
// Logs an error message if one occurred
271+
logger.error(error);
272+
});
273+
274+
// Shows the select menu to ask the user if wants to update the bot
275+
global.currentPrompt = select({
276+
message: `${locale.updateConfirmationPromptMessage}:`,
277+
choices: [
278+
{
279+
name: `✅ ${locale.updateConfirmationChoices.yes}`,
280+
value: 'yes'
281+
},
282+
{
283+
name: `❌ ${locale.updateConfirmationChoices.no}`,
284+
value: 'no'
285+
}
286+
],
287+
});
288+
289+
// When the user selects an option
290+
await global.currentPrompt.then(async (result) => {
291+
292+
// Cleans the console and shows the logo
293+
process.stdout.write('\u001b[2J\u001b[0;0H');
294+
await splashLogo(client.locale.lib.loaders.splashLogo);
295+
296+
// If the user wants to update the bot, updates it
297+
if (result === 'yes') await updateBot();
298+
299+
// When an error occurs
300+
}).catch(async (error) => {
301+
302+
// Runs this menu again, passing the error
303+
await client.functions.menus[menuName](error);
304+
});
305+
};
49306

50-
// Breaks the switch
51-
break;
307+
} else {
308+
309+
// Cleans the console and shows the logo
310+
process.stdout.write('\u001b[2J\u001b[0;0H');
311+
await splashLogo(client.locale.lib.loaders.splashLogo);
312+
313+
// Shows a message indicating that the bot is updated to the latest version
314+
console.log(`✅ ${locale.alreadyUpdated}: v${latestLocalTag}\n`);
52315
};
53316

54-
// When an error occurs
55-
}).catch(async (error) => {
317+
// Shows the post-update menu
318+
await showPostUpdateMenu();
319+
320+
} catch (error) {
56321

57-
// Runs this menu again, passing the error
58-
await client.functions.menus[menuName](error);
59-
});
322+
// Shows an error message if one occurred
323+
console.error("❌ An error ocurred during the update process:", error.stack);
324+
};
60325

61326
} catch (error) {
62327

locales/en-US.json

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1349,9 +1349,25 @@
13491349
"update": {
13501350
"promptMessage": "Select an option",
13511351
"choices": {
1352+
"checkAgain": "Check again",
1353+
"switchToBeta": "Switch to Beta",
1354+
"switchToStable": "Switch to Stable",
13521355
"return": "Return"
13531356
},
1354-
"errorMessage": "An unexpected error ocurred. Check the logs for more details"
1357+
"errorMessage": "An unexpected error ocurred. Check the logs for more details",
1358+
"checkingForUpdates": "Checking if there are new updates",
1359+
"updatingFromTo": "Updating the bot from {{originVersion}} to {{targetVersion}}",
1360+
"updatedCorrectly": "The bot has been correctly updated to {{targetVersion}}",
1361+
"noReleaseNotes": "There are no release notes available for this version",
1362+
"newVersionAvailable": "Version {{targetVersion}} is available!",
1363+
"releaseNotes": "Release notes",
1364+
"releaseNotesError": "An error ocurred while obtaining the release notes",
1365+
"updateConfirmationPromptMessage": "Would you like to update the bot?",
1366+
"updateConfirmationChoices": {
1367+
"yes": "Yes",
1368+
"no": "No"
1369+
},
1370+
"alreadyUpdated": "The bot is already updated to the latest version"
13551371
},
13561372
"main": {
13571373
"promptMessage": "Select an option",

0 commit comments

Comments
 (0)