chg: Implemented short link generation based on website's title

This commit is contained in:
SolninjaA 2025-01-23 15:27:40 +10:00
parent 2ea81e874d
commit ac3f0da0b9
5 changed files with 147 additions and 17 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
web-ext-artifacts/**/* web-ext-artifacts/**/*
backups/**/*

View File

@ -39,6 +39,7 @@
* @property {string} chhotoHost The location of the Chhoto URL instance. * @property {string} chhotoHost The location of the Chhoto URL instance.
* @property {string} chhotoKey The API key to communicate with a Chhoto URL instance with. * @property {string} chhotoKey The API key to communicate with a Chhoto URL instance with.
* @property {string} longUrl The requested URL to shorten. * @property {string} longUrl The requested URL to shorten.
* @property {string} title The title of the website
*/ */
/** /**
@ -50,6 +51,7 @@
* @type {object} * @type {object}
* @property {number} status * @property {number} status
* @property {{success: boolean, error: boolean, shorturl: string}} json * @property {{success: boolean, error: boolean, shorturl: string}} json
* @property {string} requestedLink The link that tried to be created. This only activates, and is only accurate, if the generateWithTitle option is enabled.
*/ */
/** /**
@ -71,9 +73,10 @@
* Validates the URL, as in, checks if the protocol is allowed. * Validates the URL, as in, checks if the protocol is allowed.
* *
* @param {!URL} url * @param {!URL} url
* @returns {!URL} * @param {!string} title
* @returns {!Promise<[URL, string], Error>}
*/ */
function validateURL(url) { function validateURL(url, title) {
return browser.storage.local.get("allowedProtocols").then(({ allowedProtocols }) => { return browser.storage.local.get("allowedProtocols").then(({ allowedProtocols }) => {
// Initialize a list of protocols that are allowed if unset. // Initialize a list of protocols that are allowed if unset.
// This needs to be synced with the initialization code in options.js. // This needs to be synced with the initialization code in options.js.
@ -93,20 +96,21 @@ function validateURL(url) {
return Promise.reject(new Error(`The current page's protocol (${url.protocol}) is unsupported.`)); return Promise.reject(new Error(`The current page's protocol (${url.protocol}) is unsupported.`));
} }
// Return URL // Return URL and title
return Promise.resolve(url); return Promise.resolve([url, title]);
}); });
} }
/** /**
* Parses the URL outputted in the previous function, and gets the full link (i.e. URI included). * Parses the URL outputted in the previous function, and gets the full link (i.e. URI included).
* *
* @param {!URL} url * @param {[!URL, string]} url - Holds long URL
* title - Title of the website
* @returns {!Promise<ChhotoRequest, Error>} * @returns {!Promise<ChhotoRequest, Error>}
* If all the data was obtained, return ChhotoRequest. * If all the data was obtained, return ChhotoRequest.
* Else, return an error * Else, return an error
*/ */
function generateChhotoRequest(url) { function generateChhotoRequest([url, title]) {
return browser.storage.local.get().then((data) => { return browser.storage.local.get().then((data) => {
// If the user didn't specify an API key // If the user didn't specify an API key
if (!data.chhotoKey) { if (!data.chhotoKey) {
@ -114,12 +118,40 @@ function generateChhotoRequest(url) {
"Missing API Key. Please configure the Chhoto URL extension. See https://git.solomon.tech/solomon/Chhoto-URL-Extension#installation for more information." "Missing API Key. Please configure the Chhoto URL extension. See https://git.solomon.tech/solomon/Chhoto-URL-Extension#installation for more information."
)); ));
} }
// If the user didn't specify an API key or a host // If the user didn't specify an API key or a host
if (!data.chhotoKey || !data.chhotoHost) { if (!data.chhotoKey || !data.chhotoHost) {
return Promise.reject(new Error("Please configure the Chhoto URL extension. See https://git.solomon.tech/solomon/Chhoto-URL-Extension#installation for more information.")); return Promise.reject(new Error("Please configure the Chhoto URL extension. See https://git.solomon.tech/solomon/Chhoto-URL-Extension#installation for more information."));
} }
// Set URL and title
data.longUrl = url.href; data.longUrl = url.href;
// Make the title an empty string by default
// This will create a randomly generated string if sent to the Chhoto URL server
data.title = "";
// If "generateWithTitle" is true
if (data.generateWithTitle) {
// Get the configured word limit
let wordLimit = data.titleWordLimit;
// Format title name
// Replace all occurences of ' - ' to '-'
// Replace all occurences of ' ' to '-'
// Replace all characters except for 'a-z', '0-9', '-' and '_' with ''
let titleName = title.toLowerCase().replace(/ - /g, '-').replace(/\s+/g, '-').replace(/[^a-z0-9-_]/g, '');
// If the wordLimit is not 0, and thus limited
if (wordLimit !== "0") {
// Limit the length of the short URL to the configured number
titleName = titleName.split('-', wordLimit).join('-');
}
// Set the title
data.title = titleName;
};
// Return // Return
return Promise.resolve(data); return Promise.resolve(data);
}); });
@ -130,7 +162,7 @@ function generateChhotoRequest(url) {
* *
* @param {!ChhotoRequest} chhotoRequest An object containing all the variables * @param {!ChhotoRequest} chhotoRequest An object containing all the variables
* needed to request a shortened link from a Chhoto URL instance. * needed to request a shortened link from a Chhoto URL instance.
* @returns {!ChhotoResponse} The HTTP response from the Chhoto URL instance. * @returns {!Promise<ChhotoResponse, Error>} The HTTP response from the Chhoto URL instance, or an error
*/ */
function requestChhoto(chhotoRequest) { function requestChhoto(chhotoRequest) {
const headers = new Headers(); const headers = new Headers();
@ -146,20 +178,20 @@ function requestChhoto(chhotoRequest) {
method: 'POST', method: 'POST',
headers, headers,
body: JSON.stringify({ body: JSON.stringify({
shortlink: "", shortlink: chhotoRequest.title,
longlink: chhotoRequest.longUrl, longlink: chhotoRequest.longUrl,
}), }),
}, },
// Add the HTTP status code to the response // Add the HTTP status code, and requestedLink (see ChhotoResponse in type definitions for details) to the response
)).then(r => r.json().then(data => ({status: r.status, json: data}))) )).then(r => r.json().then(data => ({ status: r.status, json: data, requestedLink: `${chhotoRequest.chhotoHost}/${chhotoRequest.title}` })))
// If there was an error // If there was a HTTP error
// This does not activate if the Chhoto server returns a JSON response with an error
.catch(err => { .catch(err => {
// Change the error message, if there was a NetworkError // Change the error message, if there was a NetworkError
if (err.message === "NetworkError when attempting to fetch resource.") { if (err.message === "NetworkError when attempting to fetch resource.") {
err.message = "Failed to access the Chhoto URL instance. Is the instance online?" err.message = "Failed to access the Chhoto URL instance. Is the instance online?"
}; };
// Return error
return Promise.reject(new Error( return Promise.reject(new Error(
`Error: ${err.message}` `Error: ${err.message}`
)); ));
@ -180,6 +212,15 @@ function validateChhotoResponse(httpResp) {
if (httpResp.json.success) { if (httpResp.json.success) {
return httpResp.json; return httpResp.json;
} else { } else {
if (httpResp.status === 409) {
const json = {
success: true,
error: false,
shorturl: httpResp.requestedLink,
}
return json;
}
return Promise.reject(new Error( return Promise.reject(new Error(
`Error (${httpResp.status}): ${httpResp.json.reason}.` `Error (${httpResp.status}): ${httpResp.json.reason}.`
)); ));
@ -254,7 +295,7 @@ function notifyError(error) {
function generateChhoto() { function generateChhoto() {
browser.tabs browser.tabs
.query({ active: true, currentWindow: true }) .query({ active: true, currentWindow: true })
.then(tabData => validateURL(new URL(tabData[0].url))) .then(tabData => validateURL(new URL(tabData[0].url), tabData[0].title))
.then(generateChhotoRequest) .then(generateChhotoRequest)
.then(requestChhoto) .then(requestChhoto)
.then(validateChhotoResponse) .then(validateChhotoResponse)

View File

@ -57,7 +57,8 @@
flex-direction: column; flex-direction: column;
} }
#message { #message,
#message2 {
display: none; display: none;
} }
@ -135,6 +136,20 @@
</div> </div>
<p class="note">Disabling all filters will allow a link to be created from any URL.</p> <p class="note">Disabling all filters will allow a link to be created from any URL.</p>
<div class="settings checkbox">
<h2>URL generation</h2>
<label>
Create shortened links with the website's title
<input type="checkbox" id="generate-with-title">
</label>
<label id="title-word-limit-label">
Short URL word limit
<input type="input" id="title-word-limit">
<p class="note">Inputting "0" will remove the character limit</p>
</label>
<p id="message2" class="note">Inputted value is not a number.</p>
</div>
<script type="application/javascript" src="lib/browser-polyfill.min.js"></script> <script type="application/javascript" src="lib/browser-polyfill.min.js"></script>
<script type="application/javascript" src="options.js"></script> <script type="application/javascript" src="options.js"></script>
</body> </body>

View File

@ -36,6 +36,10 @@ const AllowHttpsEle = document.getElementById("allow-https");
const AllowFileEle = document.getElementById("allow-file"); const AllowFileEle = document.getElementById("allow-file");
const AllowFtpEle = document.getElementById("allow-ftp"); const AllowFtpEle = document.getElementById("allow-ftp");
const messageEle = document.getElementById("message"); const messageEle = document.getElementById("message");
const generateWithTitleEle = document.getElementById("generate-with-title");
const titleWordLimitLabelEle = document.getElementById("title-word-limit-label");
const titleWordLimitEle = document.getElementById("title-word-limit");
const message2Ele = document.getElementById("message2");
// Get the browser storage // Get the browser storage
const browserStorage = browser.storage.local; const browserStorage = browser.storage.local;
@ -117,11 +121,54 @@ for (const [ele, protocol] of allowProtocolsMapping) {
}; };
} }
function setCurrentChoice({ chhotoHost, chhotoKey, allowedProtocols }) { /*
* URL Generation Options
*/
// Generate with title
generateWithTitleEle.onclick = () => {
browserStorage.get("generateWithTitle").then(({ generateWithTitle }) => {
// Get value
generateWithTitle = generateWithTitleEle.checked;
// Set default display option
let display = "none";
// If generateWithTitle is true
if (generateWithTitle) {
// Reassign variable
display = "block";
}
// Set display and save
titleWordLimitLabelEle.style.display = display;
browserStorage.set({ generateWithTitle });
});
};
// Title character limit
titleWordLimitEle.oninput = (event) => {
if (event.type === "click") {
event.preventDefault();
};
// Get the inputted value
const titleWordLimit = titleWordLimitEle.value;
// If the inputted value is not a number (NaN)
if (isNaN(titleWordLimit)) {
message2Ele.classList.add("warning");
} else {
message2Ele.classList.remove("warning");
browserStorage.set({ titleWordLimit });
};
}
function setCurrentChoice({ chhotoHost, chhotoKey, allowedProtocols, generateWithTitle, titleWordLimit }) {
hostKeyEle.value = chhotoHost || ""; hostKeyEle.value = chhotoHost || "";
apiKeyEle.value = chhotoKey || ""; apiKeyEle.value = chhotoKey || "";
// If "allowedProtocols" is undefined, set default protocols // If "allowedProtocols" doesn't exist, set default protocols
// If the user deselects every protocol, this does not activate // If the user deselects every protocol, this does not activate
// since the value would be empty, not undefined. // since the value would be empty, not undefined.
// //
@ -132,6 +179,18 @@ function setCurrentChoice({ chhotoHost, chhotoKey, allowedProtocols }) {
browserStorage.set({ allowedProtocols: allowedProtocols }); browserStorage.set({ allowedProtocols: allowedProtocols });
} }
// If generateWithTitle is undefined, set the default value
if (generateWithTitle === undefined) {
generateWithTitle = false;
browserStorage.set({ generateWithTitle: generateWithTitle });
}
// If titleWordLimit is undefined, set the default value
if (titleWordLimit === undefined) {
titleWordLimit = "0";
browserStorage.set({ titleWordLimit: titleWordLimit });
}
// Initialize a list of protocols that are allowed if unset. This needs // Initialize a list of protocols that are allowed if unset. This needs
// to be synced with the initialization code in background.js#validateURL. // to be synced with the initialization code in background.js#validateURL.
allowedProtocols = new Set(allowedProtocols); allowedProtocols = new Set(allowedProtocols);
@ -141,6 +200,20 @@ function setCurrentChoice({ chhotoHost, chhotoKey, allowedProtocols }) {
AllowFileEle.checked = allowedProtocols.has("file:"); AllowFileEle.checked = allowedProtocols.has("file:");
AllowFtpEle.checked = allowedProtocols.has("ftp:"); AllowFtpEle.checked = allowedProtocols.has("ftp:");
generateWithTitleEle.checked = generateWithTitle;
// Set default display
let display = "none";
// If generateWithTitle is true, reassign variable
if (generateWithTitle === true) {
display = "block";
}
// Save
titleWordLimitLabelEle.style.display = display;
titleWordLimitEle.value = titleWordLimit || "0";
} }
browserStorage.get().then(setCurrentChoice, (error) => { browserStorage.get().then(setCurrentChoice, (error) => {