shortener-check.js

'use strict';

/**
 * ShortenerCheck instances check URLs to see if they match a known
 * URL shortener.
 */
class ShortenerCheck {

	constructor(service) {
		this.service = service;

		if ( this.fetch )
			this.fetch = this.service.wrapFetch(this.fetch.bind(this));
		else
			this.fetch = this.service.fetch;
	}

	/**
	 * Get an array of example URLs that this ShortenerCheck will report
	 * as shortened. Used for populating a selection field in
	 * testing clients.
	 *
	 * The default implementation checks if the ShortenerCheck class has
	 * a static array called `examples` and, if so, returns that.
	 *
	 * It is not necessary to provide examples, but examples do
	 * make testing easier.
	 *
	 * @example
	 * class MyCheck extends ShortenerCheck { };
	 * MyCheck.examples = [
	 *     {title: 'Some Page', url: 'https://example.com/'}
	 * ];
	 *
	 * @returns {ExampleURL[]|String[]|URL[]} List of URLs.
	 */
	getExamples() {
		return this.constructor.examples ?? null;
	}

	/**
	 * Check to see if the provided URLs should be considered shortened or
	 * not. This method may return a Promise, but is not required to.
	 *
	 * Rather than returning a value, this method should modify the
	 * CheckedURLs by setting `shortened` to true if necessary and by
	 * adding strings to flags if relevant.
	 *
	 * @param {Object.<string, CheckedURL>} urls The URLs to check.
	 * @returns {Promise|undefined} If a Promise is returned, the
	 * LinkService will wait until the Promise resolves to consider
	 * the URLs checked.
	 */
	check(urls) {
		throw new Error('Not Implemented');
	}

}


/**
 * SimpleShortenerCheck allows you to check single URLs at once
 * using the checkSingle method, while handling iteration over
 * each URL for you.
 *
 * @example
 *
 * const list = new Set();
 *
 * blacklist.add('bit.ly');
 *
 * class SomeShorteners extends SimpleSafetyCheck {
 *     checkSingle(url) {
 *         return blacklist.has(url.toString());
 *     }
 * }
 *
 */
export class SimpleShortenerCheck extends ShortenerCheck {

	/**
	 * Check to see if the provided URL should be considered shortened
	 * or not. Returns a truthy value if the URL is flagged as
	 * shortened.
	 *
	 * If this returns a Promise, the Promise will be awaited.
	 *
	 * @param {URL} url The URL to be tested.
	 * @returns {Promise<Boolean|String>|Boolean|String} The
	 * result of the safety check.
	 */
	checkSingle(url) {
		throw new Error('Not Implemented');
	}

	_handle(data, result) {
		if ( result )
			data.shortened = true;
	}

	check(urls) {
		if ( ! urls )
			return null;

		const promises = [];

		for (const data of Object.values(urls)) {
			const result = this.checkSingle(data.url);
			if ( result instanceof Promise )
				promises.push(result.then(r => this._handle(data, r)));
			else
				this._handle(data, result);
		}

		if ( promises.length )
			return Promise.all(promises);
	}
}

export class UrlShortenerList extends ShortenerCheck {

	constructor(service) {
		super(service);

		this.refreshData();
	}

	refreshData() {
		return new Promise(async (s,f) => {
			if ( this._refresh_waiters )
				return this._refresh_waiters.push([s,f]);

			this._refresh_waiters = [[s,f]];

			const data = await this.fetch('https://raw.githubusercontent.com/PeterDaveHello/url-shorteners/master/list')
				.then(resp => resp.ok ? resp.text() : null)
				.catch(() => null);

			// If we got data, do something about it.
			if ( data ) {
				const lines = data
					.split(/\s*\n\s*/)
					.filter(line => line.length && ! line.startsWith('#'))
					.map(line => line.toLowerCase());

				this.data = new Set(lines);
				console.log('Loaded %d shortener domains.', this.data.size);
			}

			// Call all our waiters.
			const waiters = this._refresh_waiters;
			this._refresh_waiters = null;
			for(const pair of waiters)
				pair[0]();
		});
	}

	check(urls) {
		if ( ! urls )
			return null;

		if ( ! this.data?.size )
			return this.refreshData().then(() => this.check(urls));

		for(const data of Object.values(urls)) {
			let url = data.url;
			if (!(url instanceof URL))
				url = new URL(url);

			if ( this.data.has(url.hostname.toLowerCase()) )
				data.shortened = true;
		}
	}

}


export default ShortenerCheck;