<commit_before>/* eslint-disable no-restricted-syntax */
import { env } from 'describe-type/source/@/env.js';
import slice from 'describe-type/source/@/slice.js';
import instanceOf from 'describe-type/source/is/instanceOf.js';
import ownProperty from 'describe-type/source/has/ownProperty.js';
import exotic from 'describe-type/source/is/exotic.js';
import primitive from 'describe-type/source/is/primitive.js';
import callable from 'describe-type/source/is/callable.js';
import as from 'describe-type/source/as/as.any.js';
import string from 'describe-type/source/is/string.js';
import object from 'describe-type/source/is/object.js';
import undef from 'describe-type/source/is/undef.js';
import assignStrategyDefault from './strategies/assignDefault.js';
import dotStrategyDefault from './strategies/dotDefault.js';
import proxy from './@/proxy.js';
import assign from './core/assign.js';
import normalize from './core/normalize.js';
import read from './core/read.js';
import resolve from './core/resolve.js';
import resolver from './core/resolver.js';
import write from './core/write.js';

/**
 * A global GUID counter for objects.
 */
let guid = 0;

/**
 * Mixing behaviors.
 */
const assignStrategy = assign(assignStrategyDefault);

/**
 * Create a instance of `DotCfg`.
 * @param namespace: A string containing a qualified name to identify objects from.
 * @param scope: A object that have system-wide relevance.
 * @param strategy: A function that configures the input values.
 */
function DotCfg(namespace, scope, strategy) {
	if (instanceOf(DotCfg, this)) {
		if (exotic(namespace)) {
			strategy = scope;
			scope = namespace;
			namespace = undefined;
		}
		if (primitive(scope)) {
			scope = env;
		}
		if (string(namespace)) {
			scope[namespace] = scope[namespace] || {};
			scope = scope[namespace];
		}
		this.strategy = as(Function, strategy, dotStrategyDefault);
		this.scope = normalize(scope, this.strategy);
		this.extends = proxy(assign(this.strategy), this, this.scope);
		this.namespace = as(String, namespace, `dot${guid += 1}`);
		return this;
	}
	return new DotCfg(namespace, scope, strategy);
}

/**
 * Static methods
 */
DotCfg.strategy = dotStrategyDefault;
DotCfg.assign = assignStrategy;
DotCfg.normalize = normalize;
DotCfg.resolver = resolver;
DotCfg.resolve = resolve;
DotCfg.write = write;
DotCfg.read = read;

/**
 * Public methods and properties.
 */
DotCfg.prototype = {
	constructor: DotCfg,

	/**
	 * Write in scope.
	 * @param notation: A object path.
	 * @param value: Arguments for the object.
	 * @param strategy: Arguments for the object.
	 */
	set(notation, value, strategy) {
		const fn = !undef(value) && callable(strategy) ? strategy : this.strategy;
		if (object(notation)) {
			let context;
			for (const key in notation) {
				if (ownProperty(notation, key)) {
					context = write(this.scope, key, notation[key], fn);
				}
			}
			return context;
		}
		write(this.scope, notation, value, fn);
		return this;
	},

	/**
	 * Read scope notation.
	 * @param notation: A object path.
	 * @param defaultValue: A fallback value.
	 */
	get(notation, defaultValue) {
		const value = read(this.scope, notation);
		return undef(value) ? defaultValue : value;
	},

	/**
	 * Write/Read/Delete/Update a config with strategy method if needed.
	 * @param notation: A object path.
	 * @param value: Arguments for the object.
	 * @param strategy: Arguments for the object.
	 */
	cfg(notation, value, strategy) {
		const hasArg = arguments.length > 1;
		if (!notation) {
			return this.scope;
		}
		if (notation === true) {
			return assignStrategy({}, this.scope);
		}
		if (primitive(notation)) {
			return hasArg ? this.set(notation, value, strategy) : this.get(notation);
		}
		return this.extends(notation);
	},

	/**
	 * Read safely a key containing a function or a simple property.
	 * @param notation: A object path.
	 * @param ...rest: Arguments for the object.
	 */
	res(notation) {
		return resolve(this.scope, notation, slice(arguments, 1), false);
	},
};

export default DotCfg;
<commit_msg>build: bump v1.7.1<commit_after>/* eslint-disable no-restricted-syntax */
import { env } from 'describe-type/source/@/env.js';
import slice from 'describe-type/source/@/slice.js';
import instanceOf from 'describe-type/source/is/instanceOf.js';
import ownProperty from 'describe-type/source/has/ownProperty.js';
import exotic from 'describe-type/source/is/exotic.js';
import primitive from 'describe-type/source/is/primitive.js';
import callable from 'describe-type/source/is/callable.js';
import as from 'describe-type/source/as/as.any.js';
import string from 'describe-type/source/is/string.js';
import object from 'describe-type/source/is/object.js';
import undef from 'describe-type/source/is/undef.js';
import assignStrategyDefault from './strategies/assignDefault.js';
import dotStrategyDefault from './strategies/dotDefault.js';
import proxy from './@/proxy.js';
import assign from './core/assign.js';
import normalize from './core/normalize.js';
import read from './core/read.js';
import resolve from './core/resolve.js';
import resolver from './core/resolver.js';
import write from './core/write.js';

/**
 * A global GUID counter for objects.
 */
let guid = 0;

/**
 * Mixing behaviors.
 */
const assignStrategy = assign(assignStrategyDefault);

/**
 * Create a instance of `DotCfg`.
 * @param namespace: A string containing a qualified name to identify objects from.
 * @param scope: A object that have system-wide relevance.
 * @param strategy: A function that configures the input values.
 */
function DotCfg(namespace, scope, strategy) {
	if (instanceOf(DotCfg, this)) {
		if (exotic(namespace)) {
			strategy = scope;
			scope = namespace;
			namespace = undefined;
		}
		if (primitive(scope)) {
			scope = env;
		}
		if (string(namespace)) {
			scope[namespace] = scope[namespace] || {};
			scope = scope[namespace];
		}
		this.strategy = as(Function, strategy, dotStrategyDefault);
		this.scope = normalize(scope, this.strategy);
		this.extends = proxy(assign(this.strategy), this, this.scope);
		this.namespace = as(String, namespace, `dot${guid += 1}`);
		return this;
	}
	return new DotCfg(namespace, scope, strategy);
}

/**
 * Static methods
 */
DotCfg.strategy = dotStrategyDefault;
DotCfg.assign = assignStrategy;
DotCfg.normalize = normalize;
DotCfg.resolver = resolver;
DotCfg.resolve = resolve;
DotCfg.write = write;
DotCfg.read = read;

/**
 * Public methods and properties.
 */
DotCfg.prototype = {
	constructor: DotCfg,

	/**
	 * Write in scope.
	 * @param notation: A object path.
	 * @param value: Arguments for the object.
	 * @param strategy: Arguments for the object.
	 */
	set(notation, value, strategy) {
		const fn = !undef(value) && callable(strategy) ? strategy : this.strategy;
		if (object(notation)) {
			let context;
			for (const key in notation) {
				if (ownProperty(notation, key)) {
					context = write(this.scope, key, notation[key], fn);
				}
			}
			return context;
		}
		write(this.scope, notation, value, fn);
		return this;
	},

	/**
	 * Read scope notation.
	 * @param notation: A object path.
	 * @param defaultValue: A fallback value.
	 */
	get(notation, defaultValue) {
		const value = read(this.scope, notation);
		return undef(value) ? defaultValue : value;
	},

	/**
	 * Write/Read/Delete/Update a config with strategy method if needed.
	 * @param notation: A object path.
	 * @param value: Arguments for the object.
	 * @param strategy: Arguments for the object.
	 */
	cfg(notation, value, strategy) {
		const hasArg = arguments.length > 1;
		if (!notation) {
			return this.scope;
		}
		if (notation === true) {
			return assignStrategy({}, this.scope);
		}
		if (primitive(notation)) {
			return hasArg ? this.set(notation, value, strategy) : this.get(notation);
		}
		this.extends(notation);
		return this;
	},

	/**
	 * Read safely a key containing a function or a simple property.
	 * @param notation: A object path.
	 * @param ...rest: Arguments for the object.
	 */
	res(notation) {
		return resolve(this.scope, notation, slice(arguments, 1), false);
	},
};

export default DotCfg;
<|endoftext|>


<commit_before>// @flow
import fs from 'fs';
import util from 'util';
import { duotones, getDuotoneUrls } from './util/duotone';
import getApiProxyRoutes from './routes';
import proxyApi$ from './proxy';

const API_PROXY_PLUGIN_NAME = 'api-proxy';
const API_ROUTE_PATH = '/mu_api';

/*
 * When response is sent, the plugin needs to delete any files that were
 * uploaded to the POST/PATCH endpoint
 */
const onResponse = (request, reply) => {
	const { uploads } = request.plugins.apiProxy;
	const { logger } = request.server.app;
	if (uploads.length) {
		const info = { info: uploads, req: request.raw.req };
		// $FlowFixMe - promisify not yet defined in flow-typed
		Promise.all(uploads.map(util.promisify(fs.unlink))).then(
			() => {
				logger.info(info, 'Deleted uploaded file(s)');
			},
			err => {
				logger.error(info, 'Could not delete uploaded file(s)');
			}
		);
	}
};

export const setPluginState = (request: HapiRequest, reply: HapiReply) => {
	request.plugins.apiProxy = {
		setState: reply.state, // allow plugin to proxy cookies from API
		uploads: [], // keep track of any files that were uploaded
	};

	return reply.continue();
};

export default function register(
	server: Object,
	options: { path?: string }, // optional api request pathname
	next: () => void
) {
	// supply duotone urls through `server.plugins['api-proxy'].duotoneUrls`
	server.expose(
		'duotoneUrls',
		getDuotoneUrls(duotones, server.settings.app.photo_scaler_salt)
	);
	server.ext('onPreHandler', setPluginState);

	// add a method to the `request` object that can call REST API
	server.decorate('request', 'proxyApi$', proxyApi$, { apply: true });
	// add a route that will receive query requests
	const routes = getApiProxyRoutes(options.path || API_ROUTE_PATH);
	server.route(routes);
	server.on('response', onResponse);

	next();
}

register.attributes = {
	name: API_PROXY_PLUGIN_NAME,
	version: '1.0.0',
	dependencies: [
		'requestAuth', // provides request.plugins.requestAuth.oauth_token
		'tracking', // provides request.trackApi()
		'electrode-csrf-jwt', // provides csrf protection for POST
	],
};
<commit_msg>make sure plugin state is set before auth checks<commit_after>// @flow
import fs from 'fs';
import util from 'util';
import { duotones, getDuotoneUrls } from './util/duotone';
import getApiProxyRoutes from './routes';
import proxyApi$ from './proxy';

const API_PROXY_PLUGIN_NAME = 'api-proxy';
const API_ROUTE_PATH = '/mu_api';

/*
 * When response is sent, the plugin needs to delete any files that were
 * uploaded to the POST/PATCH endpoint
 */
const onResponse = request => {
	const { uploads } = request.plugins.apiProxy;
	const { logger } = request.server.app;
	if (uploads.length) {
		const info = { info: uploads, req: request.raw.req };
		// $FlowFixMe - promisify not yet defined in flow-typed
		Promise.all(uploads.map(util.promisify(fs.unlink))).then(
			() => {
				logger.info(info, 'Deleted uploaded file(s)');
			},
			err => {
				logger.error(info, 'Could not delete uploaded file(s)');
			}
		);
	}
};

export const setPluginState = (request: HapiRequest, reply: HapiReply) => {
	request.plugins.apiProxy = {
		setState: reply.state, // allow plugin to proxy cookies from API
		uploads: [], // keep track of any files that were uploaded
	};

	return reply.continue();
};

export default function register(
	server: Object,
	options: { path?: string }, // optional api request pathname
	next: () => void
) {
	// supply duotone urls through `server.plugins['api-proxy'].duotoneUrls`
	server.expose(
		'duotoneUrls',
		getDuotoneUrls(duotones, server.settings.app.photo_scaler_salt)
	);

	// add a method to the `request` object that can call REST API
	server.decorate('request', 'proxyApi$', proxyApi$, { apply: true });
	// plugin state must be available to all routes that use `request.proxyApi$`
	server.ext('onRequest', setPluginState);
	// clean up request state once response is sent
	server.on('response', onResponse);

	// add a route that will receive query requests as querystring params
	const routes = getApiProxyRoutes(options.path || API_ROUTE_PATH);
	server.route(routes);

	next();
}

register.attributes = {
	name: API_PROXY_PLUGIN_NAME,
	version: '1.0.0',
	dependencies: [
		'requestAuth', // provides request.plugins.requestAuth.oauth_token
		'tracking', // provides request.trackApi()
		'electrode-csrf-jwt', // provides csrf protection for POST
	],
};
<|endoftext|>


<commit_before>var e = module.exports = {};

var t;

e.init = () => {
    e.category = bu.CommandType.GENERAL;
};
e.requireCtx = require;

e.isCommand = true;
e.hidden = false;
e.usage = 'feedback <feedback>';
e.alias = ['suggest', 'report'];
e.info = `This command has three different functions for varying purposes. Please do not abuse it.

**__feedback__** - give me feedback about the bot
**__suggest__** - tell me something you want to be added or changed
**__report__** - let me know about a bug you found

Thank you for your support. It means a lot to me!`;
e.longinfo = `
This command has three different functions for varying purposes. Please do not abuse it.

feedback - give me feedback about the bot

suggest - tell me something you want added or changed

report - let me know about a bug you found

Thank you for your support. It means a lot to me!
`;

e.execute = async function(msg, words) {
    if (words.length > 1) {
        let blacklist = await r.table('vars').get('blacklist');
        if (blacklist.users.indexOf(msg.author.id) > -1) {
            bu.send(msg, 'Sorry, you have been blacklisted from the use of the `feedback`, `suggest`, and `report` commands. If you wish to appeal this, please join my support guild. You can find a link by doing `b!invite`.');
            return;
        } else if (msg.guild && blacklist.guilds.indexOf(msg.guild.id) > -1) {
            bu.send(msg, 'Sorry, your guild has been blacklisted from the use of the `feedback`, `suggest`, and `report` commands. If you wish to appeal this, please join my support guild. You can find a link by doing `b!invite`.');
            return;
        }
        if (words.length > 3 && msg.author.id == bu.CAT_ID) {
            switch (words[1].toLowerCase()) {
                case 'blacklist':
                    switch (words[2].toLowerCase()) {
                        case 'guild':
                            if (blacklist.guilds.indexOf(words[3]) == -1) blacklist.guilds.push(words[3]);
                            break;
                        case 'user':
                            if (blacklist.users.indexOf(words[3]) == -1) blacklist.users.push(words[3]);
                            break;
                    }
                    break;
                case 'unblacklist':
                    let index;
                    switch (words[2].toLowerCase()) {
                        case 'guild':
                            while (index = blacklist.guilds.indexOf(words[3]) > -1) {
                                blacklist.guilds.splice(index, 1);
                            }
                            break;
                        case 'user':
                            while (index = blacklist.users.indexOf(words[3]) > -1) {
                                blacklist.users.splice(index, 1);
                            }
                            break;
                    }
                    break;
            }
            await r.table('vars').get('blacklist').replace(blacklist);
            await bu.send(msg, 'Done');
            return;
        }
        let i = 0;
        let lastSuggestion = await r.table('suggestion').orderBy({
            index: r.desc('id')
        }).limit(1).run();
        if (lastSuggestion.length > 0) i = lastSuggestion[0].id + 1;
        logger.debug(i, lastSuggestion);
        if (isNaN(i)) i = 0;
        let type, colour, channel, list;
        switch (words[0].toLowerCase()) {
            case 'suggest':
                type = 'Suggestion';
                colour = 0x1faf0c;
                channel = '195716879237644292';
                list = '57ef25d2ba874bf651e96fc1';
                break;
            case 'report':
                type = 'Bug Report';
                colour = 0xaf0c0c;
                channel = '229137183234064384';
                list = '57ef25d5d777b8b35192eff2';
                break;
            default:
                type = 'Feedback';
                colour = 0xaaaf0c;
                channel = '268859677326966784';
                list = '5876a95f090e7f5189cd089b';
                break;
        }
        if (words[0].toLowerCase() == 'suggest') type = 'Suggestion';
        else if (words[0].toLowerCase() == 'report') type = 'Bug Report';

        function trelloPost() {
            return new Promise((f, r) => {
                bu.trello.post('1/cards', {
                    name: words.slice(1).join(' '),
                    desc: `Automated ${type} added by ${bot.user.username} - CASE ${i}.\n\nAuthor: ${msg.author.username}#${msg.author.discriminator} (${msg.author.id})`,
                    due: null,
                    idList: list,
                    idLabels: '58025f0184e677fd36dbd756' + (config.general.isbeta ? ',5888db5bced82109fff170af' : '')
                }, (err, data) => {
                    if (err) {
                        r(err);
                        return;
                    }
                    f(data);
                });
            });
        }
        try {
            let data = await trelloPost();

            await bu.send(channel, {
                embed: {
                    title: type,
                    description: words.slice(1).join(' ') + `\n\n[Trello Card](${data.shortUrl})`,
                    color: colour,
                    author: {
                        name: bu.getFullName(msg.author),
                        icon_url: msg.author.avatarURL,
                        url: `https://blargbot.xyz/user/${msg.author.id}`
                    },
                    timestamp: dep.moment(msg.timestamp),
                    footer: {
                        text: 'Case ' + i + ' | ' + msg.id
                    },
                    fields: [{
                        name: msg.guild ? msg.guild.name : 'DM',
                        value: msg.guild ? msg.guild.id : 'DM',
                        inline: true
                    }, {
                        name: msg.channel.name || 'DM',
                        value: msg.channel.id,
                        inline: true
                    }]
                }
            });

            await r.table('suggestion').insert({
                id: i,
                author: msg.author.id,
                channel: msg.channel.id,
                message: words.slice(1).join(' '),
                messageid: msg.id,
                date: r.epochTime(dep.moment().unix()),
                cardId: data.id,
                cardUrl: data.shortUrl
            }).run();
            await bu.send(msg, `${type} has been sent, and has been assigned an ID ${i}! :ok_hand:\n\nYou can view your ${type.toLowerCase()} here: <${data.shortUrl}>`);
        } catch (err) {
            logger.error(err);
            await bu.send(msg, 'An error occured posting to trello. Please try again.');
        }
    }
};<commit_msg>made it possible for me to feedback again<commit_after>var e = module.exports = {};

var t;

e.init = () => {
    e.category = bu.CommandType.GENERAL;
};
e.requireCtx = require;

e.isCommand = true;
e.hidden = false;
e.usage = 'feedback <feedback>';
e.alias = ['suggest', 'report'];
e.info = `This command has three different functions for varying purposes. Please do not abuse it.

**__feedback__** - give me feedback about the bot
**__suggest__** - tell me something you want to be added or changed
**__report__** - let me know about a bug you found

Thank you for your support. It means a lot to me!`;
e.longinfo = `
This command has three different functions for varying purposes. Please do not abuse it.

feedback - give me feedback about the bot

suggest - tell me something you want added or changed

report - let me know about a bug you found

Thank you for your support. It means a lot to me!
`;

e.execute = async function(msg, words) {
    if (words.length > 1) {
        let blacklist = await r.table('vars').get('blacklist');
        if (blacklist.users.indexOf(msg.author.id) > -1) {
            bu.send(msg, 'Sorry, you have been blacklisted from the use of the `feedback`, `suggest`, and `report` commands. If you wish to appeal this, please join my support guild. You can find a link by doing `b!invite`.');
            return;
        } else if (msg.guild && blacklist.guilds.indexOf(msg.guild.id) > -1) {
            bu.send(msg, 'Sorry, your guild has been blacklisted from the use of the `feedback`, `suggest`, and `report` commands. If you wish to appeal this, please join my support guild. You can find a link by doing `b!invite`.');
            return;
        }
        if (words.length > 3 && msg.author.id == bu.CAT_ID) {
            switch (words[1].toLowerCase()) {
                case 'blacklist':
                    switch (words[2].toLowerCase()) {
                        case 'guild':
                            if (blacklist.guilds.indexOf(words[3]) == -1) blacklist.guilds.push(words[3]);
                            break;
                        case 'user':
                            if (blacklist.users.indexOf(words[3]) == -1) blacklist.users.push(words[3]);
                            break;
                    }
                    await r.table('vars').get('blacklist').replace(blacklist);
                    await bu.send(msg, 'Done');
                    break;
                case 'unblacklist':
                    let index;
                    switch (words[2].toLowerCase()) {
                        case 'guild':
                            while (index = blacklist.guilds.indexOf(words[3]) > -1) {
                                blacklist.guilds.splice(index, 1);
                            }
                            break;
                        case 'user':
                            while (index = blacklist.users.indexOf(words[3]) > -1) {
                                blacklist.users.splice(index, 1);
                            }
                            break;
                    }
                    await r.table('vars').get('blacklist').replace(blacklist);
                    await bu.send(msg, 'Done');
                    break;
            }

            return;
        }
        let i = 0;
        let lastSuggestion = await r.table('suggestion').orderBy({
            index: r.desc('id')
        }).limit(1).run();
        if (lastSuggestion.length > 0) i = lastSuggestion[0].id + 1;
        logger.debug(i, lastSuggestion);
        if (isNaN(i)) i = 0;
        let type, colour, channel, list;
        switch (words[0].toLowerCase()) {
            case 'suggest':
                type = 'Suggestion';
                colour = 0x1faf0c;
                channel = '195716879237644292';
                list = '57ef25d2ba874bf651e96fc1';
                break;
            case 'report':
                type = 'Bug Report';
                colour = 0xaf0c0c;
                channel = '229137183234064384';
                list = '57ef25d5d777b8b35192eff2';
                break;
            default:
                type = 'Feedback';
                colour = 0xaaaf0c;
                channel = '268859677326966784';
                list = '5876a95f090e7f5189cd089b';
                break;
        }
        if (words[0].toLowerCase() == 'suggest') type = 'Suggestion';
        else if (words[0].toLowerCase() == 'report') type = 'Bug Report';

        function trelloPost() {
            return new Promise((f, r) => {
                bu.trello.post('1/cards', {
                    name: words.slice(1).join(' '),
                    desc: `Automated ${type} added by ${bot.user.username} - CASE ${i}.\n\nAuthor: ${msg.author.username}#${msg.author.discriminator} (${msg.author.id})`,
                    due: null,
                    idList: list,
                    idLabels: '58025f0184e677fd36dbd756' + (config.general.isbeta ? ',5888db5bced82109fff170af' : '')
                }, (err, data) => {
                    if (err) {
                        r(err);
                        return;
                    }
                    f(data);
                });
            });
        }
        try {
            let data = await trelloPost();

            await bu.send(channel, {
                embed: {
                    title: type,
                    description: words.slice(1).join(' ') + `\n\n[Trello Card](${data.shortUrl})`,
                    color: colour,
                    author: {
                        name: bu.getFullName(msg.author),
                        icon_url: msg.author.avatarURL,
                        url: `https://blargbot.xyz/user/${msg.author.id}`
                    },
                    timestamp: dep.moment(msg.timestamp),
                    footer: {
                        text: 'Case ' + i + ' | ' + msg.id
                    },
                    fields: [{
                        name: msg.guild ? msg.guild.name : 'DM',
                        value: msg.guild ? msg.guild.id : 'DM',
                        inline: true
                    }, {
                        name: msg.channel.name || 'DM',
                        value: msg.channel.id,
                        inline: true
                    }]
                }
            });

            await r.table('suggestion').insert({
                id: i,
                author: msg.author.id,
                channel: msg.channel.id,
                message: words.slice(1).join(' '),
                messageid: msg.id,
                date: r.epochTime(dep.moment().unix()),
                cardId: data.id,
                cardUrl: data.shortUrl
            }).run();
            await bu.send(msg, `${type} has been sent, and has been assigned an ID ${i}! :ok_hand:\n\nYou can view your ${type.toLowerCase()} here: <${data.shortUrl}>`);
        } catch (err) {
            logger.error(err);
            await bu.send(msg, 'An error occured posting to trello. Please try again.');
        }
    }
};<|endoftext|>


<commit_before>var functions = require('firebase-functions');
var paypal = require('paypal-rest-sdk');
// firebase-admin SDK init
var admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
// Configure your environment
paypal.configure({
    'mode': 'sandbox', // sandbox or live
    'client_id': 'your_client_id', // replace your_client_id
    'client_secret': 'your_client_secret' // replace your_client_secret
});

exports.pay = functions.https.onRequest((req, res) => {
    // Dev
    res.setHeader('Content-Type', 'application/json');
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Headers', 'application/json, Content-Type');
    res.setHeader('Access-Control-Allow-Methods', 'POST');

    // Build PayPal payment request
    var payReq = JSON.stringify({
        intent: 'sale',
        payer: {
            payment_method: 'paypal'
        },
        // exemple url https://us-central1-<project-id>.cloudfunctions.net/process
        // replace return_url, cancel_url
        redirect_urls: {
            return_url: 'https://us-central1-<project-id>.cloudfunctions.net/process',
            cancel_url: 'http://localhost:4200/cancel'
        },
        transactions: [{
            amount: {
                total: req.body.price,
                currency: 'USD'
            },
            // This is the payment transaction description. Maximum length: 127
            description: 'Cloud Functions Firebase paypal-rest-sdk' // req.body.id
            // reference_id string .Optional. The merchant-provided ID for the purchase unit. Maximum length: 256.
            // reference_id: ''
        }]
    });

    // Initialize the payment and redirect the user
    paypal.payment.create(payReq, function (error, payment) {
        var links = {};

        if (error) {
            console.error(JSON.stringify(error));
            res.end();
        } else {
            // Capture HATEOAS links
            payment.links.forEach(function (linkObj) {
                links[linkObj.rel] = {
                    href: linkObj.href,
                    method: linkObj.method
                };
            });

            // If redirect url present, redirect user
            if (links.hasOwnProperty('approval_url')) {
                // REDIRECT USER TO links['approval_url'].href
                // console.log(links['approval_url'])
                res.set('Content-Type', 'application/json');
                res.send({
                    approval_url: links.approval_url.href
                });
            } else {
                console.error('no redirect URI present');
            }
            res.status('500').end();
        }
    });
});

// Complete the payment
exports.process = functions.https.onRequest((req, res) => {
    var paymentId = req.query.paymentId;
    var payerId = {
        payer_id: req.query.PayerID
    };

    paypal.payment.execute(paymentId, payerId, function (error, payment) {
        if (error) {
            console.error(JSON.stringify(error));
            res.redirect('http://localhost:4200/error');
        } else {
            if (payment.state == 'approved') {
                console.info('payment completed successfully');
                console.info('res.description: ', payment.transactions[0].description);
                // set paid status to True in RealTime Database
                var description = payment.transactions[0].description;
                var ref = admin.database().ref('paypal/');
                ref.push({
                    'paid': true,
                    'description': description,
                    'date': Date.now()
                }).then(r => console.info('promise: ', r));
                // replace url
                res.redirect('http://localhost:4200/success');
            } else {
                console.warn('payment.state: not approved ?');
                // replace debug url
                res.redirect('https://console.firebase.google.com/project/<project-id>/functions/logs?search=&severity=DEBUG');
            }
        }
    });
});<commit_msg>Update index.js<commit_after>var functions = require('firebase-functions');
var paypal = require('paypal-rest-sdk');
// firebase-admin SDK init
var admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
// Configure your environment
paypal.configure({
    'mode': 'sandbox', // sandbox or live
    'client_id': 'your_client_id', // replace your_client_id
    'client_secret': 'your_client_secret' // replace your_client_secret
});

exports.pay = functions.https.onRequest((req, res) => {
    // Dev
    res.setHeader('Content-Type', 'application/json');
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Headers', 'application/json, Content-Type');
    res.setHeader('Access-Control-Allow-Methods', 'POST');

    // Build PayPal payment request
    var payReq = JSON.stringify({
        intent: 'sale',
        payer: {
            payment_method: 'paypal'
        },
        // example url https://us-central1-<project-id>.cloudfunctions.net/process
        // replace return_url, cancel_url
        redirect_urls: {
            return_url: 'https://us-central1-<project-id>.cloudfunctions.net/process',
            cancel_url: 'http://localhost:4200/cancel'
        },
        transactions: [{
            amount: {
                total: req.body.price,
                currency: 'USD'
            },
            // This is the payment transaction description. Maximum length: 127
            description: 'Cloud Functions Firebase paypal-rest-sdk' // req.body.id
            // reference_id string .Optional. The merchant-provided ID for the purchase unit. Maximum length: 256.
            // reference_id: ''
        }]
    });

    // Initialize the payment and redirect the user
    paypal.payment.create(payReq, function (error, payment) {
        var links = {};

        if (error) {
            console.error(JSON.stringify(error));
            res.end();
        } else {
            // Capture HATEOAS links
            payment.links.forEach(function (linkObj) {
                links[linkObj.rel] = {
                    href: linkObj.href,
                    method: linkObj.method
                };
            });

            // If redirect url present, redirect user
            if (links.hasOwnProperty('approval_url')) {
                // REDIRECT USER TO links['approval_url'].href
                // console.log(links['approval_url'])
                res.set('Content-Type', 'application/json');
                res.send({
                    approval_url: links.approval_url.href
                });
            } else {
                console.error('no redirect URI present');
            }
            res.status('500').end();
        }
    });
});

// Complete the payment
exports.process = functions.https.onRequest((req, res) => {
    var paymentId = req.query.paymentId;
    var payerId = {
        payer_id: req.query.PayerID
    };

    paypal.payment.execute(paymentId, payerId, function (error, payment) {
        if (error) {
            console.error(JSON.stringify(error));
            res.redirect('http://localhost:4200/error');
        } else {
            if (payment.state == 'approved') {
                console.info('payment completed successfully');
                console.info('res.description: ', payment.transactions[0].description);
                // set paid status to True in RealTime Database
                var description = payment.transactions[0].description;
                var ref = admin.database().ref('paypal/');
                ref.push({
                    'paid': true,
                    'description': description,
                    'date': Date.now()
                }).then(r => console.info('promise: ', r));
                // replace url
                res.redirect('http://localhost:4200/success');
            } else {
                console.warn('payment.state: not approved ?');
                // replace debug url
                res.redirect('https://console.firebase.google.com/project/<project-id>/functions/logs?search=&severity=DEBUG');
            }
        }
    });
});
<|endoftext|>


<commit_before>/**
 * Patterns checkedflag - Add checked flag to checkbox labels and API
 * for (un)checking.
 *
 * Copyright 2012-2013 Simplon B.V. - Simplon B.V. - Wichert Akkerman
 * Copyright 2012 JC Brand
 * Copyright 2012-2013 Florian Friesdorf
 */
define([
    "jquery",
    "../registry",
    "../core/logger"
], function($, patterns, logger) {
    var log = logger.getLogger("checkedflag");

    var _ = {
        name: "checkedflag",
        trigger: "input[type=checkbox],input[type=radio],select",
        jquery_plugin: true,

        init: function($el) {
            var $forms = $();
            $el.each(function() {
                if (this.form === null)
                    return;
                var $form = $(this.form);
                if ($form.data("pat-checkedflag.reset"))
                    return;
                $form.data("pat-checkedflag.reset", true);
                $forms = $forms.add(this.form);
            });

            $el.filter("[type=checkbox]")
                .each(_.onChangeCheckbox)
                .on("change.pat-checkedflag", _.onChangeCheckbox);

            $el.filter("[type=radio]")
                .each(_.onChangeRadio)
                .on("change.pat-checkedflag", _.onChangeRadio);

            $el.filter("select:not([multiple])")
                .each(function() {
                    var $el = $(this);
                    // create parent span if not direct child of a label
                    if ($el.parent('label').length === 0) {
                        $el.wrap('<span />');
                    }
                    _.onChangeSelect.call(this);
                })
                .on("change.pat-checkedflag", _.onChangeSelect);

            $el.filter("input:disabled").each(function() {
                $(this).closest("label").addClass('disabled');
            });

            $forms.on("reset.pat-checkedflag", _.onFormReset);
        },

        destroy: function($el) {
            return $el.off(".pat-checkedflag");
        },

        // XXX: so far I was under the assumption that prop is current
        // state and attr is default and current state. Well, this
        // does not seem to be the case. I feel like doing this
        // without jquery.
        set: function($el, val, opts) {
            opts = opts || {};
            // XXX: no support for radio yet
            return $el.each(function() {
                var $el = $(this);
                if ($el.is('input[type=checkbox]')) {
                    var $input = $(this);
                    if (opts.setdefault) {
                        // XXX: implement me
                    } else {
                        // just change the current state
                        // XXX: not sure whether this is correct
                        $input.prop('checked', val);
                    }
                    _.onChangeCheckbox.call(this);
                } else if ($el.is('select:not([multiple])')) {
                    var $select = $(this);
                    if (opts.setdefault) {
                        // XXX: implement me
                    } else {
                        // just change the current state
                        $select.find('option:selected')
                            .prop('selected', false);
                        $select.find('option[value="' + val + '"]')
                            .prop('selected', true);
                    }
                    _.onChangeSelect.call(this);
                } else {
                    log.error('Unsupported element', $el[0]);
                }
            });
        },

        onFormReset: function() {
            // This event is triggered before the form is reset, and we need
            // the post-reset state to update our pattern. Use a small delay
            // to fix this.
            var form = this;
            setTimeout(function() {
                $("input[type=checkbox]", form).each(_.onChangeCheckbox);
                $("input[type=radio]", form).each(_.onChangeRadio);
                $("select:not([multiple])", form).each(_.onChangeSelect);
            }, 50);
        },

        onChangeCheckbox: function() {
            var $el = $(this),
                $label = $el.closest("label"),
                $fieldset = $el.closest("fieldset");

            if (this.checked) {
                $label.add($fieldset)
                    .removeClass("unchecked").addClass("checked");
            } else {
                $label.addClass("unchecked").removeClass("checked");
                if ($fieldset.find("input:checked").length) {
                    $fieldset.removeClass("unchecked").addClass("checked");
                } else{
                    $fieldset.addClass("unchecked").removeClass("checked");
                }
            }
        },

        onChangeRadio: function() {
            var $el = $(this),
                $label = $el.closest("label"),
                $fieldset = $el.closest("fieldset"),
                selector = 'label' +
                    ':has(input[name="' + this.name + '"]' +
                    ':not(:checked))',
                $siblings = (this.form === null) ?
                    $(selector) : $(selector, this.form);

            $siblings.removeClass("checked").addClass("unchecked");
            if (this.checked) {
                $label.add($fieldset)
                    .removeClass("unchecked").addClass("checked");
            } else {
                $label.addClass("unchecked").removeClass("checked");
                if ($fieldset.find("input:checked").length) {
                    $fieldset.removeClass("unchecked").addClass("checked");
                } else {
                    $fieldset.addClass("unchecked").removeClass("checked");
                }
            }
        },

        onChangeSelect: function() {
            var $select = $(this);
            $select.parent().attr(
                'data-option',
                $select.find('option:selected').text()
            );
        }
    };

    patterns.register(_);
    return _;
});

// jshint indent: 4, browser: true, jquery: true, quotmark: double
// vim: sw=4 expandtab
<commit_msg>Add checked flag on radioList li as well.<commit_after>/**
 * Patterns checkedflag - Add checked flag to checkbox labels and API
 * for (un)checking.
 *
 * Copyright 2012-2013 Simplon B.V. - Simplon B.V. - Wichert Akkerman
 * Copyright 2012 JC Brand
 * Copyright 2012-2013 Florian Friesdorf
 */
define([
    "jquery",
    "../registry",
    "../core/logger"
], function($, patterns, logger) {
    var log = logger.getLogger("checkedflag");

    var _ = {
        name: "checkedflag",
        trigger: "input[type=checkbox],input[type=radio],select",
        jquery_plugin: true,

        init: function($el) {
            var $forms = $();
            $el.each(function() {
                if (this.form === null)
                    return;
                var $form = $(this.form);
                if ($form.data("pat-checkedflag.reset"))
                    return;
                $form.data("pat-checkedflag.reset", true);
                $forms = $forms.add(this.form);
            });

            $el.filter("[type=checkbox]")
                .each(_.onChangeCheckbox)
                .on("change.pat-checkedflag", _.onChangeCheckbox);

            $el.filter("[type=radio]")
                .each(_.onChangeRadio)
                .on("change.pat-checkedflag", _.onChangeRadio);

            $el.filter("select:not([multiple])")
                .each(function() {
                    var $el = $(this);
                    // create parent span if not direct child of a label
                    if ($el.parent('label').length === 0) {
                        $el.wrap('<span />');
                    }
                    _.onChangeSelect.call(this);
                })
                .on("change.pat-checkedflag", _.onChangeSelect);

            $el.filter("input:disabled").each(function() {
                $(this).closest("label").addClass('disabled');
            });

            $forms.on("reset.pat-checkedflag", _.onFormReset);
        },

        destroy: function($el) {
            return $el.off(".pat-checkedflag");
        },

        // XXX: so far I was under the assumption that prop is current
        // state and attr is default and current state. Well, this
        // does not seem to be the case. I feel like doing this
        // without jquery.
        set: function($el, val, opts) {
            opts = opts || {};
            // XXX: no support for radio yet
            return $el.each(function() {
                var $el = $(this);
                if ($el.is('input[type=checkbox]')) {
                    var $input = $(this);
                    if (opts.setdefault) {
                        // XXX: implement me
                    } else {
                        // just change the current state
                        // XXX: not sure whether this is correct
                        $input.prop('checked', val);
                    }
                    _.onChangeCheckbox.call(this);
                } else if ($el.is('select:not([multiple])')) {
                    var $select = $(this);
                    if (opts.setdefault) {
                        // XXX: implement me
                    } else {
                        // just change the current state
                        $select.find('option:selected')
                            .prop('selected', false);
                        $select.find('option[value="' + val + '"]')
                            .prop('selected', true);
                    }
                    _.onChangeSelect.call(this);
                } else {
                    log.error('Unsupported element', $el[0]);
                }
            });
        },

        onFormReset: function() {
            // This event is triggered before the form is reset, and we need
            // the post-reset state to update our pattern. Use a small delay
            // to fix this.
            var form = this;
            setTimeout(function() {
                $("input[type=checkbox]", form).each(_.onChangeCheckbox);
                $("input[type=radio]", form).each(_.onChangeRadio);
                $("select:not([multiple])", form).each(_.onChangeSelect);
            }, 50);
        },

        onChangeCheckbox: function() {
            var $el = $(this),
                $label = $el.closest("label"),
                $fieldset = $el.closest("fieldset");

            if ($el.closest("ul.radioList").length)
                $label=$label.add($el.closest("li"));

            if ($el.closest("ul.radioList").length)
                $label=$label.add($el.closest("li"));

            if (this.checked) {
                $label.add($fieldset)
                    .removeClass("unchecked").addClass("checked");
            } else {
                $label.addClass("unchecked").removeClass("checked");
                if ($fieldset.find("input:checked").length) {
                    $fieldset.removeClass("unchecked").addClass("checked");
                } else{
                    $fieldset.addClass("unchecked").removeClass("checked");
                }
            }
        },

        onChangeRadio: function() {
            var $el = $(this),
                $label = $el.closest("label"),
                $fieldset = $el.closest("fieldset"),
                selector = 'label' +
                    ':has(input[name="' + this.name + '"]' +
                    ':not(:checked))',
                $siblings = (this.form === null) ?
                    $(selector) : $(selector, this.form);

            if ($el.closest("ul.radioList").length)
                $label=$label.add($el.closest("li"));

            $siblings.removeClass("checked").addClass("unchecked");
            if (this.checked) {
                $label.add($fieldset)
                    .removeClass("unchecked").addClass("checked");
            } else {
                $label.addClass("unchecked").removeClass("checked");
                if ($fieldset.find("input:checked").length) {
                    $fieldset.removeClass("unchecked").addClass("checked");
                } else {
                    $fieldset.addClass("unchecked").removeClass("checked");
                }
            }
        },

        onChangeSelect: function() {
            var $select = $(this);
            $select.parent().attr(
                'data-option',
                $select.find('option:selected').text()
            );
        }
    };

    patterns.register(_);
    return _;
});

// jshint indent: 4, browser: true, jquery: true, quotmark: double
// vim: sw=4 expandtab
<|endoftext|>


<commit_before>/**
 * Concrete bar chart/histogram implementation.
 *
 * Examples:
 * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index}
 * - {@link http://dc-js.github.com/dc.js/crime/index.html Canadian City Crime Stats}
 * @class barChart
 * @memberof dc
 * @mixes dc.stackMixin
 * @mixes dc.coordinateGridMixin
 * @example
 * // create a bar chart under #chart-container1 element using the default global chart group
 * var chart1 = dc.barChart('#chart-container1');
 * // create a bar chart under #chart-container2 element using chart group A
 * var chart2 = dc.barChart('#chart-container2', 'chartGroupA');
 * // create a sub-chart under a composite parent chart
 * var chart3 = dc.barChart(compositeChart);
 * @param {String|node|d3.selection|dc.compositeChart} parent - Any valid
 * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector}
 * specifying a dom block element such as a div; or a dom element or d3 selection.  If the bar
 * chart is a sub-chart in a {@link dc.compositeChart Composite Chart} then pass in the parent
 * composite chart instance instead.
 * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
 * Interaction with a chart will only trigger events and redraws within the chart's group.
 * @returns {dc.barChart}
 */
dc.barChart = function (parent, chartGroup) {
    var MIN_BAR_WIDTH = 1;
    var DEFAULT_GAP_BETWEEN_BARS = 2;
    var LABEL_PADDING = 3;

    var _chart = dc.stackMixin(dc.coordinateGridMixin({}));

    var _gap = DEFAULT_GAP_BETWEEN_BARS;
    var _centerBar = false;
    var _alwaysUseRounding = false;

    var _barWidth;

    dc.override(_chart, 'rescale', function () {
        _chart._rescale();
        _barWidth = undefined;
        return _chart;
    });

    dc.override(_chart, 'render', function () {
        if (_chart.round() && _centerBar && !_alwaysUseRounding) {
            dc.logger.warn('By default, brush rounding is disabled if bars are centered. ' +
                         'See dc.js bar chart API documentation for details.');
        }

        return _chart._render();
    });

    _chart.label(function (d) {
        return dc.utils.printSingleValue(d.y0 + d.y);
    }, false);

    _chart.plotData = function () {
        var layers = _chart.chartBodyG().selectAll('g.stack')
            .data(_chart.data());

        calculateBarWidth();

        layers = layers
            .enter()
                .append('g')
                .attr('class', function (d, i) {
                    return 'stack ' + '_' + i;
                })
            .merge(layers);

        var last = layers.size() - 1;
        layers.each(function (d, i) {
            var layer = d3.select(this);

            renderBars(layer, i, d);

            if (_chart.renderLabel() && last === i) {
                renderLabels(layer, i, d);
            }
        });
    };

    function barHeight (d) {
        return dc.utils.safeNumber(Math.abs(_chart.y()(d.y + d.y0) - _chart.y()(d.y0)));
    }

    function labelXPos (d) {
        var x = _chart.x()(d.x);
        if (!_centerBar) {
            x += _barWidth / 2;
        }
        if (_chart.isOrdinal() && _gap !== undefined) {
            x += _gap / 2;
        }
        return dc.utils.safeNumber(x);
    }

    function labelYPos (d) {
        var y = _chart.y()(d.y + d.y0);

        if (d.y < 0) {
            y -= barHeight(d);
        }

        return dc.utils.safeNumber(y - LABEL_PADDING);
    }

    function renderLabels (layer, layerIndex, d) {
        var labels = layer.selectAll('text.barLabel')
            .data(d.values, dc.pluck('x'));

        var labelsEnterUpdate = labels
            .enter()
                .append('text')
                .attr('class', 'barLabel')
                .attr('text-anchor', 'middle')
                .attr('x', labelXPos)
                .attr('y', labelYPos)
            .merge(labels);

        if (_chart.isOrdinal()) {
            labelsEnterUpdate.on('click', _chart.onClick);
            labelsEnterUpdate.attr('cursor', 'pointer');
        }

        dc.transition(labelsEnterUpdate, _chart.transitionDuration(), _chart.transitionDelay())
            .attr('x', labelXPos)
            .attr('y', labelYPos)
            .text(function (d) {
                return _chart.label()(d);
            });

        dc.transition(labels.exit(), _chart.transitionDuration(), _chart.transitionDelay())
            .attr('height', 0)
            .remove();
    }

    function barXPos (d) {
        var x = _chart.x()(d.x);
        if (_centerBar) {
            x -= _barWidth / 2;
        }
        if (_chart.isOrdinal() && _gap !== undefined) {
            x += _gap / 2;
        }
        return dc.utils.safeNumber(x);
    }

    function renderBars (layer, layerIndex, d) {
        var bars = layer.selectAll('rect.bar')
            .data(d.values, dc.pluck('x'));

        var enter = bars.enter()
            .append('rect')
            .attr('class', 'bar')
            .attr('fill', dc.pluck('data', _chart.getColor))
            .attr('x', barXPos)
            .attr('y', _chart.yAxisHeight())
            .attr('height', 0);

        var barsEnterUpdate = enter.merge(bars);

        if (_chart.renderTitle()) {
            enter.append('title').text(dc.pluck('data', _chart.title(d.name)));
        }

        if (_chart.isOrdinal()) {
            barsEnterUpdate.on('click', _chart.onClick);
        }

        dc.transition(barsEnterUpdate, _chart.transitionDuration(), _chart.transitionDelay())
            .attr('x', barXPos)
            .attr('y', function (d) {
                var y = _chart.y()(d.y + d.y0);

                if (d.y < 0) {
                    y -= barHeight(d);
                }

                return dc.utils.safeNumber(y);
            })
            .attr('width', _barWidth)
            .attr('height', function (d) {
                return barHeight(d);
            })
            .attr('fill', dc.pluck('data', _chart.getColor))
            .select('title').text(dc.pluck('data', _chart.title(d.name)));

        dc.transition(bars.exit(), _chart.transitionDuration(), _chart.transitionDelay())
            .attr('x', function (d) { return _chart.x()(d.x); })
            .attr('width', _barWidth * 0.9)
            .remove();
    }

    function calculateBarWidth () {
        if (_barWidth === undefined) {
            var numberOfBars = _chart.xUnitCount();

            // please can't we always use rangeBands for bar charts?
            if (_chart.isOrdinal() && _gap === undefined) {
                _barWidth = Math.floor(_chart.x().bandwidth());
            } else if (_gap) {
                _barWidth = Math.floor((_chart.xAxisLength() - (numberOfBars - 1) * _gap) / numberOfBars);
            } else {
                _barWidth = Math.floor(_chart.xAxisLength() / (1 + _chart.barPadding()) / numberOfBars);
            }

            if (_barWidth === Infinity || isNaN(_barWidth) || _barWidth < MIN_BAR_WIDTH) {
                _barWidth = MIN_BAR_WIDTH;
            }
        }
    }

    _chart.fadeDeselectedArea = function (brushSelection) {
        if (!_chart.brushOn()) {
            return;
        }
        var bars = _chart.chartBodyG().selectAll('rect.bar');

        if (_chart.isOrdinal()) {
            if (_chart.hasFilter()) {
                bars.classed(dc.constants.SELECTED_CLASS, function (d) {
                    return _chart.hasFilter(d.x);
                });
                bars.classed(dc.constants.DESELECTED_CLASS, function (d) {
                    return !_chart.hasFilter(d.x);
                });
            } else {
                bars.classed(dc.constants.SELECTED_CLASS, false);
                bars.classed(dc.constants.DESELECTED_CLASS, false);
            }
        } else {
            if (!_chart.brushIsEmpty(brushSelection)) {
                var start = brushSelection[0];
                var end = brushSelection[1];

                bars.classed(dc.constants.DESELECTED_CLASS, function (d) {
                    return d.x < start || d.x >= end;
                });
            } else {
                bars.classed(dc.constants.DESELECTED_CLASS, false);
            }
        }
    };

    /**
     * Whether the bar chart will render each bar centered around the data position on the x-axis.
     * @method centerBar
     * @memberof dc.barChart
     * @instance
     * @param {Boolean} [centerBar=false]
     * @returns {Boolean|dc.barChart}
     */
    _chart.centerBar = function (centerBar) {
        if (!arguments.length) {
            return _centerBar;
        }
        _centerBar = centerBar;
        return _chart;
    };

    dc.override(_chart, 'onClick', function (d) {
        _chart._onClick(d.data);
    });

    /**
     * Get or set the spacing between bars as a fraction of bar size. Valid values are between 0-1.
     * Setting this value will also remove any previously set {@link dc.barChart#gap gap}. See the
     * {@link https://github.com/d3/d3-scale/blob/master/README.md#scaleBand d3 docs}
     * for a visual description of how the padding is applied.
     * @method barPadding
     * @memberof dc.barChart
     * @instance
     * @param {Number} [barPadding=0]
     * @returns {Number|dc.barChart}
     */
    _chart.barPadding = function (barPadding) {
        if (!arguments.length) {
            return _chart._rangeBandPadding();
        }
        _chart._rangeBandPadding(barPadding);
        _gap = undefined;
        return _chart;
    };

    _chart._useOuterPadding = function () {
        return _gap === undefined;
    };

    /**
     * Get or set the outer padding on an ordinal bar chart. This setting has no effect on non-ordinal charts.
     * Will pad the width by `padding * barWidth` on each side of the chart.
     * @method outerPadding
     * @memberof dc.barChart
     * @instance
     * @param {Number} [padding=0.5]
     * @returns {Number|dc.barChart}
     */
    _chart.outerPadding = _chart._outerRangeBandPadding;

    /**
     * Manually set fixed gap (in px) between bars instead of relying on the default auto-generated
     * gap.  By default the bar chart implementation will calculate and set the gap automatically
     * based on the number of data points and the length of the x axis.
     * @method gap
     * @memberof dc.barChart
     * @instance
     * @param {Number} [gap=2]
     * @returns {Number|dc.barChart}
     */
    _chart.gap = function (gap) {
        if (!arguments.length) {
            return _gap;
        }
        _gap = gap;
        return _chart;
    };

    _chart.extendBrush = function (brushSelection) {
        if (brushSelection && _chart.round() && (!_centerBar || _alwaysUseRounding)) {
            brushSelection[0] = _chart.round()(brushSelection[0]);
            brushSelection[1] = _chart.round()(brushSelection[1]);
        }
        return brushSelection;
    };

    /**
     * Set or get whether rounding is enabled when bars are centered. If false, using
     * rounding with centered bars will result in a warning and rounding will be ignored.  This flag
     * has no effect if bars are not {@link dc.barChart#centerBar centered}.
     * When using standard d3.js rounding methods, the brush often doesn't align correctly with
     * centered bars since the bars are offset.  The rounding function must add an offset to
     * compensate, such as in the following example.
     * @method alwaysUseRounding
     * @memberof dc.barChart
     * @instance
     * @example
     * chart.round(function(n) { return Math.floor(n) + 0.5; });
     * @param {Boolean} [alwaysUseRounding=false]
     * @returns {Boolean|dc.barChart}
     */
    _chart.alwaysUseRounding = function (alwaysUseRounding) {
        if (!arguments.length) {
            return _alwaysUseRounding;
        }
        _alwaysUseRounding = alwaysUseRounding;
        return _chart;
    };

    function colorFilter (color, inv) {
        return function () {
            var item = d3.select(this);
            var match = item.attr('fill') === color;
            return inv ? !match : match;
        };
    }

    _chart.legendHighlight = function (d) {
        if (!_chart.isLegendableHidden(d)) {
            _chart.g().selectAll('rect.bar')
                .classed('highlight', colorFilter(d.color))
                .classed('fadeout', colorFilter(d.color, true));
        }
    };

    _chart.legendReset = function () {
        _chart.g().selectAll('rect.bar')
            .classed('highlight', false)
            .classed('fadeout', false);
    };

    dc.override(_chart, 'xAxisMax', function () {
        var max = this._xAxisMax();
        if ('resolution' in _chart.xUnits()) {
            var res = _chart.xUnits().resolution;
            max += res;
        }
        return max;
    });

    return _chart.anchor(parent, chartGroup);
};
<commit_msg>restore fadeDeselectedArea for ordinal bar charts<commit_after>/**
 * Concrete bar chart/histogram implementation.
 *
 * Examples:
 * - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index}
 * - {@link http://dc-js.github.com/dc.js/crime/index.html Canadian City Crime Stats}
 * @class barChart
 * @memberof dc
 * @mixes dc.stackMixin
 * @mixes dc.coordinateGridMixin
 * @example
 * // create a bar chart under #chart-container1 element using the default global chart group
 * var chart1 = dc.barChart('#chart-container1');
 * // create a bar chart under #chart-container2 element using chart group A
 * var chart2 = dc.barChart('#chart-container2', 'chartGroupA');
 * // create a sub-chart under a composite parent chart
 * var chart3 = dc.barChart(compositeChart);
 * @param {String|node|d3.selection|dc.compositeChart} parent - Any valid
 * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector}
 * specifying a dom block element such as a div; or a dom element or d3 selection.  If the bar
 * chart is a sub-chart in a {@link dc.compositeChart Composite Chart} then pass in the parent
 * composite chart instance instead.
 * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
 * Interaction with a chart will only trigger events and redraws within the chart's group.
 * @returns {dc.barChart}
 */
dc.barChart = function (parent, chartGroup) {
    var MIN_BAR_WIDTH = 1;
    var DEFAULT_GAP_BETWEEN_BARS = 2;
    var LABEL_PADDING = 3;

    var _chart = dc.stackMixin(dc.coordinateGridMixin({}));

    var _gap = DEFAULT_GAP_BETWEEN_BARS;
    var _centerBar = false;
    var _alwaysUseRounding = false;

    var _barWidth;

    dc.override(_chart, 'rescale', function () {
        _chart._rescale();
        _barWidth = undefined;
        return _chart;
    });

    dc.override(_chart, 'render', function () {
        if (_chart.round() && _centerBar && !_alwaysUseRounding) {
            dc.logger.warn('By default, brush rounding is disabled if bars are centered. ' +
                         'See dc.js bar chart API documentation for details.');
        }

        return _chart._render();
    });

    _chart.label(function (d) {
        return dc.utils.printSingleValue(d.y0 + d.y);
    }, false);

    _chart.plotData = function () {
        var layers = _chart.chartBodyG().selectAll('g.stack')
            .data(_chart.data());

        calculateBarWidth();

        layers = layers
            .enter()
                .append('g')
                .attr('class', function (d, i) {
                    return 'stack ' + '_' + i;
                })
            .merge(layers);

        var last = layers.size() - 1;
        layers.each(function (d, i) {
            var layer = d3.select(this);

            renderBars(layer, i, d);

            if (_chart.renderLabel() && last === i) {
                renderLabels(layer, i, d);
            }
        });
    };

    function barHeight (d) {
        return dc.utils.safeNumber(Math.abs(_chart.y()(d.y + d.y0) - _chart.y()(d.y0)));
    }

    function labelXPos (d) {
        var x = _chart.x()(d.x);
        if (!_centerBar) {
            x += _barWidth / 2;
        }
        if (_chart.isOrdinal() && _gap !== undefined) {
            x += _gap / 2;
        }
        return dc.utils.safeNumber(x);
    }

    function labelYPos (d) {
        var y = _chart.y()(d.y + d.y0);

        if (d.y < 0) {
            y -= barHeight(d);
        }

        return dc.utils.safeNumber(y - LABEL_PADDING);
    }

    function renderLabels (layer, layerIndex, d) {
        var labels = layer.selectAll('text.barLabel')
            .data(d.values, dc.pluck('x'));

        var labelsEnterUpdate = labels
            .enter()
                .append('text')
                .attr('class', 'barLabel')
                .attr('text-anchor', 'middle')
                .attr('x', labelXPos)
                .attr('y', labelYPos)
            .merge(labels);

        if (_chart.isOrdinal()) {
            labelsEnterUpdate.on('click', _chart.onClick);
            labelsEnterUpdate.attr('cursor', 'pointer');
        }

        dc.transition(labelsEnterUpdate, _chart.transitionDuration(), _chart.transitionDelay())
            .attr('x', labelXPos)
            .attr('y', labelYPos)
            .text(function (d) {
                return _chart.label()(d);
            });

        dc.transition(labels.exit(), _chart.transitionDuration(), _chart.transitionDelay())
            .attr('height', 0)
            .remove();
    }

    function barXPos (d) {
        var x = _chart.x()(d.x);
        if (_centerBar) {
            x -= _barWidth / 2;
        }
        if (_chart.isOrdinal() && _gap !== undefined) {
            x += _gap / 2;
        }
        return dc.utils.safeNumber(x);
    }

    function renderBars (layer, layerIndex, d) {
        var bars = layer.selectAll('rect.bar')
            .data(d.values, dc.pluck('x'));

        var enter = bars.enter()
            .append('rect')
            .attr('class', 'bar')
            .attr('fill', dc.pluck('data', _chart.getColor))
            .attr('x', barXPos)
            .attr('y', _chart.yAxisHeight())
            .attr('height', 0);

        var barsEnterUpdate = enter.merge(bars);

        if (_chart.renderTitle()) {
            enter.append('title').text(dc.pluck('data', _chart.title(d.name)));
        }

        if (_chart.isOrdinal()) {
            barsEnterUpdate.on('click', _chart.onClick);
        }

        dc.transition(barsEnterUpdate, _chart.transitionDuration(), _chart.transitionDelay())
            .attr('x', barXPos)
            .attr('y', function (d) {
                var y = _chart.y()(d.y + d.y0);

                if (d.y < 0) {
                    y -= barHeight(d);
                }

                return dc.utils.safeNumber(y);
            })
            .attr('width', _barWidth)
            .attr('height', function (d) {
                return barHeight(d);
            })
            .attr('fill', dc.pluck('data', _chart.getColor))
            .select('title').text(dc.pluck('data', _chart.title(d.name)));

        dc.transition(bars.exit(), _chart.transitionDuration(), _chart.transitionDelay())
            .attr('x', function (d) { return _chart.x()(d.x); })
            .attr('width', _barWidth * 0.9)
            .remove();
    }

    function calculateBarWidth () {
        if (_barWidth === undefined) {
            var numberOfBars = _chart.xUnitCount();

            // please can't we always use rangeBands for bar charts?
            if (_chart.isOrdinal() && _gap === undefined) {
                _barWidth = Math.floor(_chart.x().bandwidth());
            } else if (_gap) {
                _barWidth = Math.floor((_chart.xAxisLength() - (numberOfBars - 1) * _gap) / numberOfBars);
            } else {
                _barWidth = Math.floor(_chart.xAxisLength() / (1 + _chart.barPadding()) / numberOfBars);
            }

            if (_barWidth === Infinity || isNaN(_barWidth) || _barWidth < MIN_BAR_WIDTH) {
                _barWidth = MIN_BAR_WIDTH;
            }
        }
    }

    _chart.fadeDeselectedArea = function (brushSelection) {
        var bars = _chart.chartBodyG().selectAll('rect.bar');

        if (_chart.isOrdinal()) {
            if (_chart.hasFilter()) {
                bars.classed(dc.constants.SELECTED_CLASS, function (d) {
                    return _chart.hasFilter(d.x);
                });
                bars.classed(dc.constants.DESELECTED_CLASS, function (d) {
                    return !_chart.hasFilter(d.x);
                });
            } else {
                bars.classed(dc.constants.SELECTED_CLASS, false);
                bars.classed(dc.constants.DESELECTED_CLASS, false);
            }
        } else if (_chart.brushOn()) {
            if (!_chart.brushIsEmpty(brushSelection)) {
                var start = brushSelection[0];
                var end = brushSelection[1];

                bars.classed(dc.constants.DESELECTED_CLASS, function (d) {
                    return d.x < start || d.x >= end;
                });
            } else {
                bars.classed(dc.constants.DESELECTED_CLASS, false);
            }
        }
    };

    /**
     * Whether the bar chart will render each bar centered around the data position on the x-axis.
     * @method centerBar
     * @memberof dc.barChart
     * @instance
     * @param {Boolean} [centerBar=false]
     * @returns {Boolean|dc.barChart}
     */
    _chart.centerBar = function (centerBar) {
        if (!arguments.length) {
            return _centerBar;
        }
        _centerBar = centerBar;
        return _chart;
    };

    dc.override(_chart, 'onClick', function (d) {
        _chart._onClick(d.data);
    });

    /**
     * Get or set the spacing between bars as a fraction of bar size. Valid values are between 0-1.
     * Setting this value will also remove any previously set {@link dc.barChart#gap gap}. See the
     * {@link https://github.com/d3/d3-scale/blob/master/README.md#scaleBand d3 docs}
     * for a visual description of how the padding is applied.
     * @method barPadding
     * @memberof dc.barChart
     * @instance
     * @param {Number} [barPadding=0]
     * @returns {Number|dc.barChart}
     */
    _chart.barPadding = function (barPadding) {
        if (!arguments.length) {
            return _chart._rangeBandPadding();
        }
        _chart._rangeBandPadding(barPadding);
        _gap = undefined;
        return _chart;
    };

    _chart._useOuterPadding = function () {
        return _gap === undefined;
    };

    /**
     * Get or set the outer padding on an ordinal bar chart. This setting has no effect on non-ordinal charts.
     * Will pad the width by `padding * barWidth` on each side of the chart.
     * @method outerPadding
     * @memberof dc.barChart
     * @instance
     * @param {Number} [padding=0.5]
     * @returns {Number|dc.barChart}
     */
    _chart.outerPadding = _chart._outerRangeBandPadding;

    /**
     * Manually set fixed gap (in px) between bars instead of relying on the default auto-generated
     * gap.  By default the bar chart implementation will calculate and set the gap automatically
     * based on the number of data points and the length of the x axis.
     * @method gap
     * @memberof dc.barChart
     * @instance
     * @param {Number} [gap=2]
     * @returns {Number|dc.barChart}
     */
    _chart.gap = function (gap) {
        if (!arguments.length) {
            return _gap;
        }
        _gap = gap;
        return _chart;
    };

    _chart.extendBrush = function (brushSelection) {
        if (brushSelection && _chart.round() && (!_centerBar || _alwaysUseRounding)) {
            brushSelection[0] = _chart.round()(brushSelection[0]);
            brushSelection[1] = _chart.round()(brushSelection[1]);
        }
        return brushSelection;
    };

    /**
     * Set or get whether rounding is enabled when bars are centered. If false, using
     * rounding with centered bars will result in a warning and rounding will be ignored.  This flag
     * has no effect if bars are not {@link dc.barChart#centerBar centered}.
     * When using standard d3.js rounding methods, the brush often doesn't align correctly with
     * centered bars since the bars are offset.  The rounding function must add an offset to
     * compensate, such as in the following example.
     * @method alwaysUseRounding
     * @memberof dc.barChart
     * @instance
     * @example
     * chart.round(function(n) { return Math.floor(n) + 0.5; });
     * @param {Boolean} [alwaysUseRounding=false]
     * @returns {Boolean|dc.barChart}
     */
    _chart.alwaysUseRounding = function (alwaysUseRounding) {
        if (!arguments.length) {
            return _alwaysUseRounding;
        }
        _alwaysUseRounding = alwaysUseRounding;
        return _chart;
    };

    function colorFilter (color, inv) {
        return function () {
            var item = d3.select(this);
            var match = item.attr('fill') === color;
            return inv ? !match : match;
        };
    }

    _chart.legendHighlight = function (d) {
        if (!_chart.isLegendableHidden(d)) {
            _chart.g().selectAll('rect.bar')
                .classed('highlight', colorFilter(d.color))
                .classed('fadeout', colorFilter(d.color, true));
        }
    };

    _chart.legendReset = function () {
        _chart.g().selectAll('rect.bar')
            .classed('highlight', false)
            .classed('fadeout', false);
    };

    dc.override(_chart, 'xAxisMax', function () {
        var max = this._xAxisMax();
        if ('resolution' in _chart.xUnits()) {
            var res = _chart.xUnits().resolution;
            max += res;
        }
        return max;
    });

    return _chart.anchor(parent, chartGroup);
};
<|endoftext|>


<commit_before>import { parseResponse } from "./http.js"

const mentionsRegExp = /\B@([a-zA-Z][a-zA-Z0-9_-]{0,17})/g
const urlsRegExp = /\b(https?:\/\/[\-A-Za-z0-9+&@#\/%?=~_|!:,\.;]*[\-A-Za-z0-9+&@#\/%=~_|])/gi
const imageExtRegExp = /(\.gif|\.jpg|\.jpeg|\.png|\.avif|\.apng|\.webp|\.bmp|\.ico|\.tif|\.tiff|\.svg)$/
const videoExtRegExp = /(\.mp4|\.webm|\.3gp|\.mov)$/
const audioExtRegExp = /(\.wav|\.mp3|\.aac|\.ogg|\.flac)$/

export function isLocalhost() {
    return ["localhost", "127.0.0.1"].includes(window.location.hostname)
}

export function isObject(x) {
    return typeof x === "object" && x !== null
}

export function isPlainObject(x) {
    return isObject(x) && !Array.isArray(x)
}

export function smartTrim(s) {
    return s
        .split("\n")
        .map(s => s.replace(/(\s)+/g, "$1").trim())
        .join("\n")
        .replace(/(\n){2,}/g, "$1$1")
        .trim()
}

/**
 * @param {string} s
 */
export function escapeHTML(s) {
    return s
        .replace(/&/g, "&")
        .replace(/</g, "<")
        .replace(/>/g, ">")
        .replace(/"/g, """)
        .replace(/"/g, "'")
}

/**
 * @param {string} s
 */
export function linkify(s) {
    return s
        .replace(mentionsRegExp, '@$1')
        .replace(urlsRegExp, '$1')
}

/**
 * @param {string|Date} date
 */
export function ago(date) {
    if (!(date instanceof Date)) {
        date = new Date(date)
    }
    const now = new Date()
    let diff = (now.getTime() - date.getTime()) / 1000
    if (diff <= 60) {
        return "Just now"
    } else if ((diff /= 60) < 60) {
        return (diff | 0) + "m"
    } else if ((diff /= 60) < 24) {
        return (diff | 0) + "h"
    } else if ((diff /= 24) < 7) {
        return (diff | 0) + "d"
    }
    let text = String(date).split(" ")[1] + " " + date.getDate()
    if (now.getFullYear() !== date.getFullYear()) {
        text += ", " + date.getFullYear()
    }
    return text
}

/**
 * @param {Node} oldNode
 * @param {Node} newNode
 */
export function replaceNode(oldNode, newNode) {
    oldNode.parentNode.insertBefore(newNode, oldNode)
    oldNode.parentNode.removeChild(oldNode)
    return newNode
}

/**
 * @param {string} html
 */
export function el(html) {
    const template = document.createElement("template")
    template.innerHTML = html
    return template.content.childElementCount === 1
        ? template.content.firstElementChild
        : template.content
}

/**
 * @param {ParentNode} el
 */
export async function collectMedia(el) {
    const media = /** @type {Node[]} */ ([])
    for (const link of Array.from(el.querySelectorAll("a"))) {
        if (link.hostname === window.location.hostname && link.pathname.startsWith("/users/")) {
            continue
        }

        const youtubeVideoID = findYouTubeVideoID(link.href)
        if (youtubeVideoID !== null) {
            const img = document.createElement("img")
            img.src = `https://img.youtube.com/vi/${youtubeVideoID}/0.jpg`
            // img.crossOrigin = ""
            img.width = 540
            img.height = 304
            const a = document.createElement("a")
            a.href = link.href
            a.target = "_blank"
            a.rel = "noopener"
            a.className = "media-item youtube-video-wrapper"
            a.appendChild(img)
            a.onclick = ev => {
                ev.preventDefault()
                ev.stopImmediatePropagation()

                const iframe = document.createElement("iframe")
                iframe.src = "https://www.youtube.com/embed/" + encodeURIComponent(youtubeVideoID) + "?autoplay=1"
                iframe.setAttribute("frameborder", "0")
                iframe.allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
                iframe.allowFullscreen = true
                iframe.className = "media-item youtube"
                a.insertAdjacentElement("afterend", iframe)
                a.remove()
            }
            media.push(a)
            continue
        }

        const coubVideoID = findCoubVideoID(link.href)
        if (coubVideoID !== null) {
            const iframe = document.createElement("iframe")
            iframe.src = "https://coub.com/embed/" + encodeURIComponent(coubVideoID) + "?muted=false&autostart=false&originalSize=true&startWithHD=true"
            iframe.setAttribute("frameborder", "0")
            iframe.allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
            iframe.allowFullscreen = true
            iframe.className = "media-item coub"
            media.push(iframe)
            continue
        }

        if (isSoundCloudURL(link.href)) {
            const iframe = await getSoundCloudIframe(link.href)
            iframe.className = "media-item soundcloud"
            media.push(iframe)
            continue
        }

        const mt = await mediaType(link.href)
        if (mt === "image") {
            const img = document.createElement("img")
            img.src = link.href
            const a = document.createElement("a")
            a.href = link.href
            a.target = "_blank"
            a.rel = "noopener"
            a.className = "media-item"
            a.appendChild(img)
            media.push(a)
            continue
        }

        if (mt === "audio") {
            const audio = document.createElement("audio")
            audio.src = link.href
            audio.controls = true
            audio.loop = true
            audio.volume = 0.5
            audio.autoplay = false
            audio.className = "media-item"
            media.push(audio)
            continue
        }

        if (mt === "video") {
            const video = document.createElement("video")
            video.src = link.href
            video.controls = true
            video.loop = true
            video.volume = 0.5
            video.autoplay = false
            video.muted = true
            video.className = "media-item"
            media.push(video)
            continue
        }
    }
    return media
}

/**
 * @param {string} url
 */
async function mediaType(url) {
    if (imageExtRegExp.test(url)) {
        return "image"
    }

    if (videoExtRegExp.test(url)) {
        return "video"
    }

    if (audioExtRegExp.test(url)) {
        return "audio"
    }

    const endpoint = "/api/proxy?target=" + encodeURIComponent(url)
    return fetch(endpoint, { method: "HEAD", redirect: "follow" }).then(res => {
        const ct = res.headers.get("Content-Type")
        if (ct.startsWith("image/")) {
            return "image"
        }
        if (ct.startsWith("video/")) {
            return "video"
        }
        if (ct.startsWith("audio/")) {
            return "audio"
        }

        return ""
    }).catch(_ => "")
}

/***
 * @param {string} href
 * @returns {string|null}
 */
function findYouTubeVideoID(href) {
    try {
        const url = new URL(href)
        if ((url.hostname === "www.youtube.com" || url.hostname === "m.youtube.com") && url.pathname === "/watch" && url.searchParams.has("v")) {
            return decodeURIComponent(url.searchParams.get("v"))
        }

        if (url.hostname === "youtu.be" && url.pathname.startsWith("/") && url.pathname !== "/") {
            return decodeURIComponent(url.pathname.substr(1))
        }

        if (url.hostname === "www.youtube.com" && url.pathname.startsWith("/embed/") && url.pathname !== "/embed/") {
            return decodeURIComponent(url.pathname.substr(7))
        }
    } catch (_) { }
    return null
}

function findCoubVideoID(href) {
    try {
        const url = new URL(href)
        if (url.hostname !== "coub.com") {
            return null
        }

        const parts = url.pathname.split("/")
        // /view/{id}
        // /embed/{id}
        if (parts.length === 3 && (parts[1] === "view" || parts[1] === "embed")) {
            return decodeURIComponent(parts[2])
        }
    } catch (_) { }
    return null
}

function isSoundCloudURL(href) {
    try {
        const url = new URL(href)
        if (url.hostname === "soundcloud.com") {
            return true
        }

        // /{username}/{slug}
        const parts = url.pathname.split("/")
        return parts.length == 3
    } catch (_) { }
    return false
}

function getSoundCloudIframe(url) {
    return fetch(`https://soundcloud.com/oembed?format=json&url=${encodeURIComponent(url)}`).then(parseResponse).then(body => {
        const tmpl = document.createElement("template")
        tmpl.innerHTML = body.html
        return tmpl.content.querySelector("iframe")
    })
}

/**
 * @param {ArrayBuffer} buff
 * @returns {string}
 */
export function arrayBufferToBase64(buff) {
    return btoa(
        new Uint8Array(buff)
            .reduce((data, byte) => data + String.fromCharCode(byte), "")
    )
        .replace(/\+/g, "-")
        .replace(/\//g, "_")
        .replace(/=/g, "")
}

/**
 * @param {string} s
 * @returns {ArrayBuffer}
 */
export function base64ToArrayBuffer(s) {
    return Uint8Array.from(atob(s), c => c.charCodeAt(0))
}
<commit_msg>web/static: imgur<commit_after>import { parseResponse } from "./http.js"

const mentionsRegExp = /\B@([a-zA-Z][a-zA-Z0-9_-]{0,17})/g
const urlsRegExp = /\b(https?:\/\/[\-A-Za-z0-9+&@#\/%?=~_|!:,\.;]*[\-A-Za-z0-9+&@#\/%=~_|])/gi
const imageExtRegExp = /(\.gif|\.jpg|\.jpeg|\.png|\.avif|\.apng|\.webp|\.bmp|\.ico|\.tif|\.tiff|\.svg)$/
const videoExtRegExp = /(\.mp4|\.webm|\.3gp|\.mov)$/
const audioExtRegExp = /(\.wav|\.mp3|\.aac|\.ogg|\.flac)$/

export function isLocalhost() {
    return ["localhost", "127.0.0.1"].includes(window.location.hostname)
}

export function isObject(x) {
    return typeof x === "object" && x !== null
}

export function isPlainObject(x) {
    return isObject(x) && !Array.isArray(x)
}

export function smartTrim(s) {
    return s
        .split("\n")
        .map(s => s.replace(/(\s)+/g, "$1").trim())
        .join("\n")
        .replace(/(\n){2,}/g, "$1$1")
        .trim()
}

/**
 * @param {string} s
 */
export function escapeHTML(s) {
    return s
        .replace(/&/g, "&")
        .replace(/</g, "<")
        .replace(/>/g, ">")
        .replace(/"/g, """)
        .replace(/"/g, "'")
}

/**
 * @param {string} s
 */
export function linkify(s) {
    return s
        .replace(mentionsRegExp, '@$1')
        .replace(urlsRegExp, '$1')
}

/**
 * @param {string|Date} date
 */
export function ago(date) {
    if (!(date instanceof Date)) {
        date = new Date(date)
    }
    const now = new Date()
    let diff = (now.getTime() - date.getTime()) / 1000
    if (diff <= 60) {
        return "Just now"
    } else if ((diff /= 60) < 60) {
        return (diff | 0) + "m"
    } else if ((diff /= 60) < 24) {
        return (diff | 0) + "h"
    } else if ((diff /= 24) < 7) {
        return (diff | 0) + "d"
    }
    let text = String(date).split(" ")[1] + " " + date.getDate()
    if (now.getFullYear() !== date.getFullYear()) {
        text += ", " + date.getFullYear()
    }
    return text
}

/**
 * @param {Node} oldNode
 * @param {Node} newNode
 */
export function replaceNode(oldNode, newNode) {
    oldNode.parentNode.insertBefore(newNode, oldNode)
    oldNode.parentNode.removeChild(oldNode)
    return newNode
}

/**
 * @param {string} html
 */
export function el(html) {
    const template = document.createElement("template")
    template.innerHTML = html
    return template.content.childElementCount === 1
        ? template.content.firstElementChild
        : template.content
}

/**
 * @param {ParentNode} el
 */
export async function collectMedia(el) {
    const media = /** @type {Node[]} */ ([])
    for (const link of Array.from(el.querySelectorAll("a"))) {
        if (link.hostname === window.location.hostname && link.pathname.startsWith("/users/")) {
            continue
        }

        const imgurID = findImgurID(link.href)
        if (imgurID !== null) {
            const img = document.createElement("img")
            // TODO: better detect imgur image extension.
            img.src = "https://i.imgur.com/" + encodeURIComponent(imgurID) + ".png"
            const a = document.createElement("a")
            a.href = link.href
            a.target = "_blank"
            a.rel = "noopener"
            a.className = "media-item imgur"
            a.appendChild(img)
            media.push(a)
            continue
        }

        const youtubeVideoID = findYouTubeVideoID(link.href)
        if (youtubeVideoID !== null) {
            const img = document.createElement("img")
            img.src = `https://img.youtube.com/vi/${youtubeVideoID}/0.jpg`
            // img.crossOrigin = ""
            img.width = 540
            img.height = 304
            const a = document.createElement("a")
            a.href = link.href
            a.target = "_blank"
            a.rel = "noopener"
            a.className = "media-item youtube-video-wrapper"
            a.appendChild(img)
            a.onclick = ev => {
                ev.preventDefault()
                ev.stopImmediatePropagation()

                const iframe = document.createElement("iframe")
                iframe.src = "https://www.youtube.com/embed/" + encodeURIComponent(youtubeVideoID) + "?autoplay=1"
                iframe.setAttribute("frameborder", "0")
                iframe.allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
                iframe.allowFullscreen = true
                iframe.className = "media-item youtube"
                a.insertAdjacentElement("afterend", iframe)
                a.remove()
            }
            media.push(a)
            continue
        }

        const coubVideoID = findCoubVideoID(link.href)
        if (coubVideoID !== null) {
            const iframe = document.createElement("iframe")
            iframe.src = "https://coub.com/embed/" + encodeURIComponent(coubVideoID) + "?muted=false&autostart=false&originalSize=true&startWithHD=true"
            iframe.setAttribute("frameborder", "0")
            iframe.allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
            iframe.allowFullscreen = true
            iframe.className = "media-item coub"
            media.push(iframe)
            continue
        }

        if (isSoundCloudURL(link.href)) {
            const iframe = await getSoundCloudIframe(link.href)
            iframe.className = "media-item soundcloud"
            media.push(iframe)
            continue
        }

        const mt = await mediaType(link.href)
        if (mt === "image") {
            const img = document.createElement("img")
            img.src = link.href
            const a = document.createElement("a")
            a.href = link.href
            a.target = "_blank"
            a.rel = "noopener"
            a.className = "media-item"
            a.appendChild(img)
            media.push(a)
            continue
        }

        if (mt === "audio") {
            const audio = document.createElement("audio")
            audio.src = link.href
            audio.controls = true
            audio.loop = true
            audio.volume = 0.5
            audio.autoplay = false
            audio.className = "media-item"
            media.push(audio)
            continue
        }

        if (mt === "video") {
            const video = document.createElement("video")
            video.src = link.href
            video.controls = true
            video.loop = true
            video.volume = 0.5
            video.autoplay = false
            video.muted = true
            video.className = "media-item"
            media.push(video)
            continue
        }
    }
    return media
}

/**
 * @param {string} url
 */
async function mediaType(url) {
    if (imageExtRegExp.test(url)) {
        return "image"
    }

    if (videoExtRegExp.test(url)) {
        return "video"
    }

    if (audioExtRegExp.test(url)) {
        return "audio"
    }

    const endpoint = "/api/proxy?target=" + encodeURIComponent(url)
    return fetch(endpoint, { method: "HEAD", redirect: "follow" }).then(res => {
        const ct = res.headers.get("Content-Type")
        if (ct.startsWith("image/")) {
            return "image"
        }
        if (ct.startsWith("video/")) {
            return "video"
        }
        if (ct.startsWith("audio/")) {
            return "audio"
        }

        return ""
    }).catch(_ => "")
}

function findImgurID(href) {
    try {
        const url = new URL(href)
        if (url.hostname !== "imgur.com") {
            return null
        }

        const parts = url.pathname.split("/")
        if (parts.length === 2 && !parts[1].includes(".")) {
            return parts[1]
        }
    } catch (_) { }
    return null
}

/***
 * @param {string} href
 * @returns {string|null}
 */
function findYouTubeVideoID(href) {
    try {
        const url = new URL(href)
        if ((url.hostname === "www.youtube.com" || url.hostname === "m.youtube.com") && url.pathname === "/watch" && url.searchParams.has("v")) {
            return decodeURIComponent(url.searchParams.get("v"))
        }

        if (url.hostname === "youtu.be" && url.pathname.startsWith("/") && url.pathname !== "/") {
            return decodeURIComponent(url.pathname.substr(1))
        }

        if (url.hostname === "www.youtube.com" && url.pathname.startsWith("/embed/") && url.pathname !== "/embed/") {
            return decodeURIComponent(url.pathname.substr(7))
        }
    } catch (_) { }
    return null
}

function findCoubVideoID(href) {
    try {
        const url = new URL(href)
        if (url.hostname !== "coub.com") {
            return null
        }

        const parts = url.pathname.split("/")
        // /view/{id}
        // /embed/{id}
        if (parts.length === 3 && (parts[1] === "view" || parts[1] === "embed")) {
            return decodeURIComponent(parts[2])
        }
    } catch (_) { }
    return null
}

function isSoundCloudURL(href) {
    try {
        const url = new URL(href)
        if (url.hostname === "soundcloud.com") {
            return true
        }

        // /{username}/{slug}
        const parts = url.pathname.split("/")
        return parts.length == 3
    } catch (_) { }
    return false
}

function getSoundCloudIframe(url) {
    return fetch(`https://soundcloud.com/oembed?format=json&url=${encodeURIComponent(url)}`).then(parseResponse).then(body => {
        const tmpl = document.createElement("template")
        tmpl.innerHTML = body.html
        return tmpl.content.querySelector("iframe")
    })
}

/**
 * @param {ArrayBuffer} buff
 * @returns {string}
 */
export function arrayBufferToBase64(buff) {
    return btoa(
        new Uint8Array(buff)
            .reduce((data, byte) => data + String.fromCharCode(byte), "")
    )
        .replace(/\+/g, "-")
        .replace(/\//g, "_")
        .replace(/=/g, "")
}

/**
 * @param {string} s
 * @returns {ArrayBuffer}
 */
export function base64ToArrayBuffer(s) {
    return Uint8Array.from(atob(s), c => c.charCodeAt(0))
}
<|endoftext|>


<commit_before>/**
 * @fileOverview force atlas 2
 * @author shiwu.wyy@antfin.com
 */
const G6 = require('@antv/g6');
const Util = G6.Util;
const Worker = require('./layout.worker');

// import StaticsWorker from 'worker-loader?inline!./statistics.worker.js';
class Layout {
  constructor(options) {
    Util.mix(this, {
      /**
       * width
       * @type  {number}
       */
      width: null,

      /**
       * height
       * @type  {number}
       */
      height: null,

      /**
       * the center of the layout
       * @type  {object}
       */
      center: null,

      /**
       * the parameter for repulsive forces,
       * it will scale the layout but won't change the layout
       * larger the kr, looser the layout
       * @type  {number}
       */
      kr: 10,

      /**
       * the parameter for gravity forces
       * @type  {number}
       */
      kg: 1.0,

      /**
       * modes:
       * 'normal' for normal using
       * 'linlog' for closer clusters.
       * @type  {string}
       */
      mode: 'normal',

      /**
       * whether preventing the node overlapping
       * @type  {boolean}
       */
      prev_overlapping: false,

      /**
       * whether active the dissuade hub mode
       * true: grant authorities (nodes with a high indegree)
       * a more central position than hubs (nodes with a high outdegree)
       * @type  {boolean}
       */
      dissuade_hubs: false,

      /**
       * whether active the barnes hut optimization on computing repulsive forces
       * @type  {boolean}
       */
      barnes_hut: false,

      /**
       * the max iteration number
       * @type  {number}
       */
      max_iteration: 1500,

      /**
       * control the global velocity
       * defualt: 0.1(gephi)
       * @type  {number}
       */
      ks: 0.1,

      /**
       * the max global velocity
       * @type  {number}
       */
      ksmax: 10,

      /**
       * the tolerance for the global swinging
       * @type  {number}
       */
      tao: 0.1
    }, options);
  }
  // execute the layout
  execute() {
    let {
      graph,
      nodes,
      edges,
      kr,
      kg,
      mode,
      prev_overlapping,
      dissuade_hubs,
      barnes_hut,
      max_iteration,
      ks,
      ksmax,
      tao
    } = this;

    if (!barnes_hut && nodes.length > 300) barnes_hut = true;
    else if (barnes_hut && nodes.length <= 300) barnes_hut = false;

    const width = this.width ? this.width : graph.getWidth();
    const height = this.height ? this.height : graph.getHeight();
    const center = this.center ? this.center : {
      x: width / 2,
      y: height / 2
    };
    // the whidth of each nodes
    const widths = [];
    const size = nodes.length;
    for (let i = 0; i < size; i += 1) {
      widths[i] = graph.getNodes()[i].getBBox().maxX;
    }
    const obj = {
      nodes,
      edges,
      kr,
      kg,
      mode,
      prev_overlapping,
      dissuade_hubs,
      barnes_hut,
      max_iteration,
      ks,
      ksmax,
      tao,
      center,
      widths
    };

    const worker = new Worker();// { type: 'module' }
    worker.postMessage(obj);
    worker.onmessage = function(event) {
      this.nodes = event.data;
      const graph_nodes = graph.getNodes();
      for (let i = 0; i < size; i += 1) {
        const model = graph_nodes[i].getModel();
        model.x = this.nodes[i].x;
        model.y = this.nodes[i].y;
      }
      graph.changeLayout();
    };
  }
}
module.exports = Layout;
<commit_msg>fix: webworker, fitview after layout<commit_after>/**
 * @fileOverview force atlas 2
 * @author shiwu.wyy@antfin.com
 */
const G6 = require('@antv/g6');
const Util = G6.Util;
const Worker = require('./layout.worker');

// import StaticsWorker from 'worker-loader?inline!./statistics.worker.js';
class Layout {
  constructor(options) {
    Util.mix(this, {
      /**
       * width
       * @type  {number}
       */
      width: null,

      /**
       * height
       * @type  {number}
       */
      height: null,

      /**
       * the center of the layout
       * @type  {object}
       */
      center: null,

      /**
       * the parameter for repulsive forces,
       * it will scale the layout but won't change the layout
       * larger the kr, looser the layout
       * @type  {number}
       */
      kr: 10,

      /**
       * the parameter for gravity forces
       * @type  {number}
       */
      kg: 1.0,

      /**
       * modes:
       * 'normal' for normal using
       * 'linlog' for closer clusters.
       * @type  {string}
       */
      mode: 'normal',

      /**
       * whether preventing the node overlapping
       * @type  {boolean}
       */
      prev_overlapping: false,

      /**
       * whether active the dissuade hub mode
       * true: grant authorities (nodes with a high indegree)
       * a more central position than hubs (nodes with a high outdegree)
       * @type  {boolean}
       */
      dissuade_hubs: false,

      /**
       * whether active the barnes hut optimization on computing repulsive forces
       * @type  {boolean}
       */
      barnes_hut: false,

      /**
       * the max iteration number
       * @type  {number}
       */
      max_iteration: 1500,

      /**
       * control the global velocity
       * defualt: 0.1(gephi)
       * @type  {number}
       */
      ks: 0.1,

      /**
       * the max global velocity
       * @type  {number}
       */
      ksmax: 10,

      /**
       * the tolerance for the global swinging
       * @type  {number}
       */
      tao: 0.1
    }, options);
  }
  // execute the layout
  execute() {
    let {
      graph,
      nodes,
      edges,
      kr,
      kg,
      mode,
      prev_overlapping,
      dissuade_hubs,
      barnes_hut,
      max_iteration,
      ks,
      ksmax,
      tao
    } = this;

    if (!barnes_hut && nodes.length > 300) barnes_hut = true;
    else if (barnes_hut && nodes.length <= 300) barnes_hut = false;

    const width = this.width ? this.width : graph.getWidth();
    const height = this.height ? this.height : graph.getHeight();
    const center = this.center ? this.center : {
      x: width / 2,
      y: height / 2
    };
    // the whidth of each nodes
    const widths = [];
    const size = nodes.length;
    for (let i = 0; i < size; i += 1) {
      widths[i] = graph.getNodes()[i].getBBox().maxX;
    }
    const obj = {
      nodes,
      edges,
      kr,
      kg,
      mode,
      prev_overlapping,
      dissuade_hubs,
      barnes_hut,
      max_iteration,
      ks,
      ksmax,
      tao,
      center,
      widths
    };

    const worker = new Worker();// { type: 'module' }
    worker.postMessage(obj);
    worker.onmessage = function(event) {
      this.nodes = event.data;
      const graph_nodes = graph.getNodes();
      for (let i = 0; i < size; i += 1) {
        const model = graph_nodes[i].getModel();
        model.x = this.nodes[i].x;
        model.y = this.nodes[i].y;
      }
      graph.changeLayout();
      const fitView = graph.get('fitView');
      fitView && graph.setFitView(fitView);
    };
  }
}
module.exports = Layout;
<|endoftext|>