<commit_before>const bg = require('./common.js');
const _ = require('lodash');
const common = require('../common/common.js');
const constants = require('../common/constants.js');
const ga = require('../common/ga.js');
const spData = require('../common/sp-data.js');

const spCm = require('./sp-cm.js');
const spRta = require('./sp-rta.js');

const entityDailySyncEvent = {};

function checkEntityId(entityId) {
    if (bg.isUnset(entityId)) {
        throw new Error('invalid entityId=' + entityId);
    }
}

function mkEvent() {
    const event = {};
    event.promise = new Promise((resolve, reject) => {
        event.set = val => {
            resolve(val);
            event.set = () => { /* ignore multiple sets */ };
        };
        event.error = e => {
            reject(e);
            event.error = () => { /* ignore multiple errors */ };
        };
    });

    return event;
}

function getEntitySyncEvent(entityId) {
    if (!entityDailySyncEvent[entityId]) {
        const event = mkEvent(`entity ${entityId}`);
        entityDailySyncEvent[entityId] = event;
        if (bg.hasSyncedToday('sp'))
            event.set();
    }
    return entityDailySyncEvent[entityId];
}

const collectorCache = {};

async function getCollectorImpl(domain, entityId, scope, probeFn) {
    const cacheTag = `${entityId}:${scope}`;
    if (collectorCache[cacheTag]) {
        return collectorCache[cacheTag];
    }

    let collector = null;
    for (const c of [spCm(domain, entityId), spRta(domain, entityId)]) {
        if (await probeFn(c)) {
            collector = c;
            break;
        }
        console.log('Probe failed for', domain, entityId, c.name);
    }
    if (!collector) {
        throw new Error(`No valid collectors for ${domain} ${cacheTag}`);
    }

    collectorCache[cacheTag] = collector;
    console.log('Using collector', domain, cacheTag, collector.name);
    ga.mga('event', 'collector-domain', domain, collector.name);

    return collector;
}

function getCollector(domain, entityId) {
    return getCollectorImpl(domain, entityId, 'general', c => c.probe());
}

function getCollectorForKeywordUpdate(domain, entityId, keywordOpts) {
    return getCollectorImpl(domain, entityId, 'keywordUpdate', c => c.probeKeywordUpdate(keywordOpts));
}

async function dataGather(req) {
    // We want to make sure that we at least attempt to sync every single
    // domain, but any exceptions we encounter should be propagated so that we
    // don't record this as a success.
    let deferredException = null;

    let entities = bg.getEntityIds();

    // if we are responding to a client request, always sync that entityId first
    if (req && req.entityId) {
        entities.unshift({ domain: req.domain, entityId: req.entityId });
        entities = _.uniqBy(entities, x => x.entityId);
    }

    for (const { domain, entityId } of entities) {
        if (bg.isUnset(entityId))
            continue;

        try {
            const collector = await getCollector(domain, entityId);

            // Store entity metadata locally if we have all relevant fields
            const entityMetadata = bg.setEntityId(entityId, { collector: collector.name });
            if (entityMetadata.name && entityMetadata.collector && entityMetadata.domain) {
                await storeEntityMetadata(entityMetadata);
            }

            const campaignIds = await requestCampaignData(collector);
            const adGroups = await getAdGroups(entityId);
            const summaries = await getCampaignSummaries({ entityId });

            await bg.parallelQueue(campaignIds, async function(campaignId) {
                campaignId = spData.stripPrefix(campaignId);
                const summary = summaries.find(x => x.campaignId == campaignId);
                if (!spData.isRunning(summary)) {
                    return;
                }

                const adGroupItem = adGroups.find(x => x.campaignId == campaignId);
                let adGroupId = null;
                if (adGroupItem) {
                    adGroupId = adGroupItem.adGroupId;
                }
                else {
                    console.log('requesting adGroupId for', collector.entityId, campaignId);
                    adGroupId = await collector.getAdGroupId(campaignId);
                    if (adGroupId)
                        await storeAdGroupMetadata({ entityId: collector.entityId, adGroupId, campaignId });
                }

                if (adGroupId) {
                    if (!(summary && summary.asin)) {
                        await requestCampaignMetadata(collector, campaignId, spData.stripPrefix(adGroupId));
                    }
                    await requestKeywordData(collector, campaignId, spData.stripPrefix(adGroupId));
                }
            });
        }
        catch (ex) {
            if (!bg.handleServerErrors(ex, "sp.dataGather")) {
                ga.merror(ex, `context: domain ${domain}, entityId ${entityId}`);
            }
            deferredException = ex;
        }
    }

    if (deferredException) {
        throw deferredException;
    }
}

const getAllowedCampaigns = bg.cache.coMemo(async function({ entityId }) {
    checkEntityId(entityId);
    const allowed = await bg.ajax(`${bg.serviceUrl}/api/data/${entityId}/allowed`, {
        method: 'GET',
        responseType: 'json'
    });

    if (allowed.length)
        return allowed;

    return bg.ajax(`${bg.serviceUrl}/api/data/${entityId}/allowed`, {
        method: 'GET',
        responseType: 'json'
    });
}, { maxAge: 30000 });

const getCampaignSummaries = bg.cache.coMemo(async function({ entityId }) {
    checkEntityId(entityId);

    await summaryReady(entityId);
    return bg.ajax(`${bg.serviceUrl}/api/data/${entityId}/summary`, {
        method: 'GET',
        responseType: 'json'
    });
}, { maxAge: 30000 });

async function summaryReady(entityId) {
    const syncEvent = getEntitySyncEvent(entityId);
    await syncEvent.promise;
}

async function requestCampaignData(collector) {
    const syncEvent = getEntitySyncEvent(collector.entityId);
    let campaignIds = [];

    try {
        console.log('requesting campaign data for', collector.entityId);
        const missing = await getMissingDates(collector.entityId);
        const days = new Set(missing.missingDays);
        const now = Date.now();

        if (missing.needLifetime) {
            const data = await collector.getLifetimeCampaignData();
            await storeLifetimeCampaignData(collector.entityId, now, data);
        }

        if (days.size) {
            const latestDay = Math.max(...days.values());
            const latestData = await collector.getDailyCampaignData(latestDay);
            await storeDailyCampaignData(collector.entityId, latestDay, latestData);

            campaignIds = latestData.map(x => x.campaignId);
            const status = await collector.getCampaignStatus(campaignIds);
            await storeCampaignStatus(collector.entityId, now, status);
            syncEvent.set();

            days.delete(latestDay);
            await bg.parallelQueue(days.values(), async date => {
                const data = await collector.getDailyCampaignData(date);
                await storeDailyCampaignData(collector.entityId, date, data);
            });
        }
    }
    finally {
        syncEvent.set();
    }

    return campaignIds;
}

async function requestCampaignMetadata(collector, campaignId, adGroupId) {
    const asin = await collector.getCampaignAsin(campaignId, adGroupId);
    if (asin) {
        await storeCampaignMetadata(collector.entityId, campaignId, asin);
    }
}

async function requestKeywordData(collector, campaignId, adGroupId) {
    let timestamp = Date.now();

    console.log('requesting keyword data for', collector.entityId, campaignId, adGroupId);
    const data = await collector.getKeywordData(campaignId, adGroupId);

    if (data && data.length) {
        await storeKeywordData(collector.entityId, adGroupId, timestamp, data);
    }
}

function storeDailyCampaignData(entityId, timestamp, data) {
    if (!(data && data.length))
        return Promise.resolve();

    return bg.ajax(`${bg.serviceUrl}/api/campaignData/${entityId}?timestamp=${timestamp}`, {
        method: 'PUT',
        jsonData: data,
        contentType: 'application/json',
    });
}

function storeLifetimeCampaignData(entityId, timestamp, data) {
    if (!(data && data.length))
        return Promise.resolve();

    return bg.ajax(`${bg.serviceUrl}/api/data/${entityId}?timestamp=${timestamp}`, {
        method: 'PUT',
        jsonData: data,
        contentType: 'application/json',
    });
}

function storeCampaignStatus(entityId, timestamp, data) {
    return bg.ajax(`${bg.serviceUrl}/api/campaignStatus/${entityId}?timestamp=${timestamp}`, {
        method: 'PUT',
        jsonData: data,
        contentType: 'application/json',
    });
}

async function storeKeywordData(entityId, adGroupId, timestamp, data) {
    // Chop the large keyword list into small, bite-sized chunks for easier
    // digestion on the server.
    for (const chunk of common.pageArray(data, 50)) {
        await bg.ajax(`${bg.serviceUrl}/api/keywordData/${entityId}/${adGroupId}?timestamp=${timestamp}`, {
            method: 'PUT',
            jsonData: chunk,
            contentType: 'application/json',
        });
    }
}

function getMissingDates(entityId) {
    return bg.ajax(`https://${constants.hostname}/api/campaignData/${entityId}/missingDates`, {
        method: 'GET',
        responseType: 'json',
    });
}

const getCampaignHistory = bg.cache.coMemo(function(entityId, campaignId) { // TODO: date ranges, etc.
    checkEntityId(entityId);
    return bg.ajax(`${bg.serviceUrl}/api/data/${entityId}/${campaignId}`, {
        method: 'GET',
        responseType: 'json'
    });
});

const getAllCampaignData = bg.cache.coMemo(function({ entityId, startTimestamp, endTimestamp }) {
    checkEntityId(entityId);
    return bg.ajax(`${bg.serviceUrl}/api/data/${entityId}?startTimestamp=${startTimestamp}&endTimestamp=${endTimestamp}`, {
        method: 'GET',
        responseType: 'json'
    });
});

const getDataHistory = bg.cache.coMemo(async function({ entityId, campaignId }) { // TODO: date ranges, etc.
    const snapshots = await getCampaignHistory(entityId, campaignId);
    return common.convertSnapshotsToDeltas(snapshots);
});

const getAggregateCampaignHistory = bg.cache.coMemo(async function({ entityId, campaignIds }) {
    let aggregate = [];
    await bg.parallelQueue(campaignIds, async function(campaignId) {
        try {
            let history = await getDataHistory({ entityId, campaignId });
            aggregate = aggregate.concat(...history);
        }
        catch (ex) {
            if (bg.handleServerErrors(ex) == 'notAllowed') {
                // swallow this
            }
            throw ex;
        }
    });

    return aggregate.sort((a, b) => a.timestamp - b.timestamp);
});

const getKeywordData = bg.cache.coMemo(async function({ domain, entityId, campaignId, adGroupId }) {
    checkEntityId(entityId);
    const url = `${bg.serviceUrl}/api/keywordData/${entityId}/${adGroupId}`;
    const opts = {
        method: 'GET',
        responseType: 'json',
    };
    let data = await bg.ajax(url, opts);

    if (!data || data.length == 0) {
        // Possibly this is the first time we've ever seen this campaign. If so,
        // let's query Amazon and populate our own servers, and then come back.
        // This is very slow but should usually only happen once.

        const collector = await getCollector(domain, entityId);
        try {
            await requestKeywordData(collector, campaignId, adGroupId);
            data = await bg.ajax(url, opts);
        }
        catch (ex) {
            ga.merror(ex, `context: domain ${domain}, entityId ${entityId}, collector ${collector.name}`);
        }
    }

    return data;
});

const getAggregateKeywordData = bg.cache.coMemo(function({ domain, entityId, adGroupIds }) {
    return bg.parallelQueue(adGroupIds, async function(adGroupId) {
        try {
            return await getKeywordData({ domain, entityId, adGroupId });
        }
        catch (ex) {
            if (bg.handleServerErrors(ex) == 'notAllowed') {
                // swallow this
            }
            throw ex;
        }
    });
});

function storeCampaignMetadata(entityId, campaignId, asin) {
    checkEntityId(entityId);
    return bg.ajax(`${bg.serviceUrl}/api/campaignMetadata/${entityId}/${campaignId}`, {
        method: 'PUT',
        jsonData: { asin },
    });
}

function storeAdGroupMetadata({ entityId, adGroupId, campaignId }) {
    checkEntityId(entityId);
    return bg.ajax(`${bg.serviceUrl}/api/adGroupMetadata/${entityId}/${adGroupId}`, {
        method: 'PUT',
        jsonData: { campaignId },
    });
}

function storeEntityMetadata(metadata) {
    const entityId = metadata.entityId;
    checkEntityId(entityId);
    return bg.ajax(`${bg.serviceUrl}/api/meta/${entityId}`, {
        method: 'PUT',
        jsonData: metadata,
    });
}

function getAdGroups(entityId) {
    checkEntityId(entityId);
    return bg.ajax(`${bg.serviceUrl}/api/adGroups/${entityId}`, {
        method: 'GET',
        responseType: 'json',
    });
}

async function updateKeyword({ domain, entityId, keywords, operation, dataValues }) {
    const timestamp = Date.now();

    const probeKw = keywords.shift();
    const collector = await getCollectorForKeywordUpdate(domain, entityId, { operation, dataValues, keyword: probeKw });

    keywords.push(probeKw);
    const successes = await collector.updateKeywords({ keywords, operation, dataValues });

    for (const page of common.pageArray(successes, 50)) {
        await bg.ajax(`${bg.serviceUrl}/api/keywordData/${entityId}?timestamp=${timestamp}`, {
            method: 'PATCH',
            jsonData: { operation, dataValues, keywordIds: page },
            responseType: 'json',
        });
    }

    if (successes.length)
        getKeywordData.clear();

    // TODO: in the case that we have a lot of these (bulk update), implement
    // progress feedback.
    return { success: successes.length == keywords.length };
}

async function addKeywords({ domain, entityId, keywords, adGroupId, bid }) {
    const collector = await getCollector(domain, entityId);

    const timestamp = Date.now();
    const result = await collector.addKeywords({ keywords, adGroupId, bid });
    for (const page of common.pageArray(result.ok, 50)) {
        await bg.ajax(`${bg.serviceUrl}/api/keywordData/${entityId}?timestamp=${timestamp}`, {
            method: 'PATCH',
            jsonData: { operation: 'ADD', adGroupId, bid, keywordIds: page },
            responseType: 'json',
        });
    }

    return result;
}

function setBrandName({ entityId, brandName }) {
    bg.setEntityId(entityId, { name: brandName });
}

module.exports = {
    name: 'sp',
    dataGather,
    getAllowedCampaigns,
    getCampaignSummaries,
    getAllCampaignData,
    getDataHistory,
    getAggregateCampaignHistory,
    getKeywordData,
    getAggregateKeywordData,
    storeAdGroupMetadata,
    updateKeyword,
    addKeywords,
    setBrandName,
};
<commit_msg>corrected the arguments to the PATCH+ADD to conform to the pattern used by others<commit_after>const bg = require('./common.js');
const _ = require('lodash');
const common = require('../common/common.js');
const constants = require('../common/constants.js');
const ga = require('../common/ga.js');
const spData = require('../common/sp-data.js');

const spCm = require('./sp-cm.js');
const spRta = require('./sp-rta.js');

const entityDailySyncEvent = {};

function checkEntityId(entityId) {
    if (bg.isUnset(entityId)) {
        throw new Error('invalid entityId=' + entityId);
    }
}

function mkEvent() {
    const event = {};
    event.promise = new Promise((resolve, reject) => {
        event.set = val => {
            resolve(val);
            event.set = () => { /* ignore multiple sets */ };
        };
        event.error = e => {
            reject(e);
            event.error = () => { /* ignore multiple errors */ };
        };
    });

    return event;
}

function getEntitySyncEvent(entityId) {
    if (!entityDailySyncEvent[entityId]) {
        const event = mkEvent(`entity ${entityId}`);
        entityDailySyncEvent[entityId] = event;
        if (bg.hasSyncedToday('sp'))
            event.set();
    }
    return entityDailySyncEvent[entityId];
}

const collectorCache = {};

async function getCollectorImpl(domain, entityId, scope, probeFn) {
    const cacheTag = `${entityId}:${scope}`;
    if (collectorCache[cacheTag]) {
        return collectorCache[cacheTag];
    }

    let collector = null;
    for (const c of [spCm(domain, entityId), spRta(domain, entityId)]) {
        if (await probeFn(c)) {
            collector = c;
            break;
        }
        console.log('Probe failed for', domain, entityId, c.name);
    }
    if (!collector) {
        throw new Error(`No valid collectors for ${domain} ${cacheTag}`);
    }

    collectorCache[cacheTag] = collector;
    console.log('Using collector', domain, cacheTag, collector.name);
    ga.mga('event', 'collector-domain', domain, collector.name);

    return collector;
}

function getCollector(domain, entityId) {
    return getCollectorImpl(domain, entityId, 'general', c => c.probe());
}

function getCollectorForKeywordUpdate(domain, entityId, keywordOpts) {
    return getCollectorImpl(domain, entityId, 'keywordUpdate', c => c.probeKeywordUpdate(keywordOpts));
}

async function dataGather(req) {
    // We want to make sure that we at least attempt to sync every single
    // domain, but any exceptions we encounter should be propagated so that we
    // don't record this as a success.
    let deferredException = null;

    let entities = bg.getEntityIds();

    // if we are responding to a client request, always sync that entityId first
    if (req && req.entityId) {
        entities.unshift({ domain: req.domain, entityId: req.entityId });
        entities = _.uniqBy(entities, x => x.entityId);
    }

    for (const { domain, entityId } of entities) {
        if (bg.isUnset(entityId))
            continue;

        try {
            const collector = await getCollector(domain, entityId);

            // Store entity metadata locally if we have all relevant fields
            const entityMetadata = bg.setEntityId(entityId, { collector: collector.name });
            if (entityMetadata.name && entityMetadata.collector && entityMetadata.domain) {
                await storeEntityMetadata(entityMetadata);
            }

            const campaignIds = await requestCampaignData(collector);
            const adGroups = await getAdGroups(entityId);
            const summaries = await getCampaignSummaries({ entityId });

            await bg.parallelQueue(campaignIds, async function(campaignId) {
                campaignId = spData.stripPrefix(campaignId);
                const summary = summaries.find(x => x.campaignId == campaignId);
                if (!spData.isRunning(summary)) {
                    return;
                }

                const adGroupItem = adGroups.find(x => x.campaignId == campaignId);
                let adGroupId = null;
                if (adGroupItem) {
                    adGroupId = adGroupItem.adGroupId;
                }
                else {
                    console.log('requesting adGroupId for', collector.entityId, campaignId);
                    adGroupId = await collector.getAdGroupId(campaignId);
                    if (adGroupId)
                        await storeAdGroupMetadata({ entityId: collector.entityId, adGroupId, campaignId });
                }

                if (adGroupId) {
                    if (!(summary && summary.asin)) {
                        await requestCampaignMetadata(collector, campaignId, spData.stripPrefix(adGroupId));
                    }
                    await requestKeywordData(collector, campaignId, spData.stripPrefix(adGroupId));
                }
            });
        }
        catch (ex) {
            if (!bg.handleServerErrors(ex, "sp.dataGather")) {
                ga.merror(ex, `context: domain ${domain}, entityId ${entityId}`);
            }
            deferredException = ex;
        }
    }

    if (deferredException) {
        throw deferredException;
    }
}

const getAllowedCampaigns = bg.cache.coMemo(async function({ entityId }) {
    checkEntityId(entityId);
    const allowed = await bg.ajax(`${bg.serviceUrl}/api/data/${entityId}/allowed`, {
        method: 'GET',
        responseType: 'json'
    });

    if (allowed.length)
        return allowed;

    return bg.ajax(`${bg.serviceUrl}/api/data/${entityId}/allowed`, {
        method: 'GET',
        responseType: 'json'
    });
}, { maxAge: 30000 });

const getCampaignSummaries = bg.cache.coMemo(async function({ entityId }) {
    checkEntityId(entityId);

    await summaryReady(entityId);
    return bg.ajax(`${bg.serviceUrl}/api/data/${entityId}/summary`, {
        method: 'GET',
        responseType: 'json'
    });
}, { maxAge: 30000 });

async function summaryReady(entityId) {
    const syncEvent = getEntitySyncEvent(entityId);
    await syncEvent.promise;
}

async function requestCampaignData(collector) {
    const syncEvent = getEntitySyncEvent(collector.entityId);
    let campaignIds = [];

    try {
        console.log('requesting campaign data for', collector.entityId);
        const missing = await getMissingDates(collector.entityId);
        const days = new Set(missing.missingDays);
        const now = Date.now();

        if (missing.needLifetime) {
            const data = await collector.getLifetimeCampaignData();
            await storeLifetimeCampaignData(collector.entityId, now, data);
        }

        if (days.size) {
            const latestDay = Math.max(...days.values());
            const latestData = await collector.getDailyCampaignData(latestDay);
            await storeDailyCampaignData(collector.entityId, latestDay, latestData);

            campaignIds = latestData.map(x => x.campaignId);
            const status = await collector.getCampaignStatus(campaignIds);
            await storeCampaignStatus(collector.entityId, now, status);
            syncEvent.set();

            days.delete(latestDay);
            await bg.parallelQueue(days.values(), async date => {
                const data = await collector.getDailyCampaignData(date);
                await storeDailyCampaignData(collector.entityId, date, data);
            });
        }
    }
    finally {
        syncEvent.set();
    }

    return campaignIds;
}

async function requestCampaignMetadata(collector, campaignId, adGroupId) {
    const asin = await collector.getCampaignAsin(campaignId, adGroupId);
    if (asin) {
        await storeCampaignMetadata(collector.entityId, campaignId, asin);
    }
}

async function requestKeywordData(collector, campaignId, adGroupId) {
    let timestamp = Date.now();

    console.log('requesting keyword data for', collector.entityId, campaignId, adGroupId);
    const data = await collector.getKeywordData(campaignId, adGroupId);

    if (data && data.length) {
        await storeKeywordData(collector.entityId, adGroupId, timestamp, data);
    }
}

function storeDailyCampaignData(entityId, timestamp, data) {
    if (!(data && data.length))
        return Promise.resolve();

    return bg.ajax(`${bg.serviceUrl}/api/campaignData/${entityId}?timestamp=${timestamp}`, {
        method: 'PUT',
        jsonData: data,
        contentType: 'application/json',
    });
}

function storeLifetimeCampaignData(entityId, timestamp, data) {
    if (!(data && data.length))
        return Promise.resolve();

    return bg.ajax(`${bg.serviceUrl}/api/data/${entityId}?timestamp=${timestamp}`, {
        method: 'PUT',
        jsonData: data,
        contentType: 'application/json',
    });
}

function storeCampaignStatus(entityId, timestamp, data) {
    return bg.ajax(`${bg.serviceUrl}/api/campaignStatus/${entityId}?timestamp=${timestamp}`, {
        method: 'PUT',
        jsonData: data,
        contentType: 'application/json',
    });
}

async function storeKeywordData(entityId, adGroupId, timestamp, data) {
    // Chop the large keyword list into small, bite-sized chunks for easier
    // digestion on the server.
    for (const chunk of common.pageArray(data, 50)) {
        await bg.ajax(`${bg.serviceUrl}/api/keywordData/${entityId}/${adGroupId}?timestamp=${timestamp}`, {
            method: 'PUT',
            jsonData: chunk,
            contentType: 'application/json',
        });
    }
}

function getMissingDates(entityId) {
    return bg.ajax(`https://${constants.hostname}/api/campaignData/${entityId}/missingDates`, {
        method: 'GET',
        responseType: 'json',
    });
}

const getCampaignHistory = bg.cache.coMemo(function(entityId, campaignId) { // TODO: date ranges, etc.
    checkEntityId(entityId);
    return bg.ajax(`${bg.serviceUrl}/api/data/${entityId}/${campaignId}`, {
        method: 'GET',
        responseType: 'json'
    });
});

const getAllCampaignData = bg.cache.coMemo(function({ entityId, startTimestamp, endTimestamp }) {
    checkEntityId(entityId);
    return bg.ajax(`${bg.serviceUrl}/api/data/${entityId}?startTimestamp=${startTimestamp}&endTimestamp=${endTimestamp}`, {
        method: 'GET',
        responseType: 'json'
    });
});

const getDataHistory = bg.cache.coMemo(async function({ entityId, campaignId }) { // TODO: date ranges, etc.
    const snapshots = await getCampaignHistory(entityId, campaignId);
    return common.convertSnapshotsToDeltas(snapshots);
});

const getAggregateCampaignHistory = bg.cache.coMemo(async function({ entityId, campaignIds }) {
    let aggregate = [];
    await bg.parallelQueue(campaignIds, async function(campaignId) {
        try {
            let history = await getDataHistory({ entityId, campaignId });
            aggregate = aggregate.concat(...history);
        }
        catch (ex) {
            if (bg.handleServerErrors(ex) == 'notAllowed') {
                // swallow this
            }
            throw ex;
        }
    });

    return aggregate.sort((a, b) => a.timestamp - b.timestamp);
});

const getKeywordData = bg.cache.coMemo(async function({ domain, entityId, campaignId, adGroupId }) {
    checkEntityId(entityId);
    const url = `${bg.serviceUrl}/api/keywordData/${entityId}/${adGroupId}`;
    const opts = {
        method: 'GET',
        responseType: 'json',
    };
    let data = await bg.ajax(url, opts);

    if (!data || data.length == 0) {
        // Possibly this is the first time we've ever seen this campaign. If so,
        // let's query Amazon and populate our own servers, and then come back.
        // This is very slow but should usually only happen once.

        const collector = await getCollector(domain, entityId);
        try {
            await requestKeywordData(collector, campaignId, adGroupId);
            data = await bg.ajax(url, opts);
        }
        catch (ex) {
            ga.merror(ex, `context: domain ${domain}, entityId ${entityId}, collector ${collector.name}`);
        }
    }

    return data;
});

const getAggregateKeywordData = bg.cache.coMemo(function({ domain, entityId, adGroupIds }) {
    return bg.parallelQueue(adGroupIds, async function(adGroupId) {
        try {
            return await getKeywordData({ domain, entityId, adGroupId });
        }
        catch (ex) {
            if (bg.handleServerErrors(ex) == 'notAllowed') {
                // swallow this
            }
            throw ex;
        }
    });
});

function storeCampaignMetadata(entityId, campaignId, asin) {
    checkEntityId(entityId);
    return bg.ajax(`${bg.serviceUrl}/api/campaignMetadata/${entityId}/${campaignId}`, {
        method: 'PUT',
        jsonData: { asin },
    });
}

function storeAdGroupMetadata({ entityId, adGroupId, campaignId }) {
    checkEntityId(entityId);
    return bg.ajax(`${bg.serviceUrl}/api/adGroupMetadata/${entityId}/${adGroupId}`, {
        method: 'PUT',
        jsonData: { campaignId },
    });
}

function storeEntityMetadata(metadata) {
    const entityId = metadata.entityId;
    checkEntityId(entityId);
    return bg.ajax(`${bg.serviceUrl}/api/meta/${entityId}`, {
        method: 'PUT',
        jsonData: metadata,
    });
}

function getAdGroups(entityId) {
    checkEntityId(entityId);
    return bg.ajax(`${bg.serviceUrl}/api/adGroups/${entityId}`, {
        method: 'GET',
        responseType: 'json',
    });
}

async function updateKeyword({ domain, entityId, keywords, operation, dataValues }) {
    const timestamp = Date.now();

    const probeKw = keywords.shift();
    const collector = await getCollectorForKeywordUpdate(domain, entityId, { operation, dataValues, keyword: probeKw });

    keywords.push(probeKw);
    const successes = await collector.updateKeywords({ keywords, operation, dataValues });

    for (const page of common.pageArray(successes, 50)) {
        await bg.ajax(`${bg.serviceUrl}/api/keywordData/${entityId}?timestamp=${timestamp}`, {
            method: 'PATCH',
            jsonData: { operation, dataValues, keywordIds: page },
            responseType: 'json',
        });
    }

    if (successes.length)
        getKeywordData.clear();

    // TODO: in the case that we have a lot of these (bulk update), implement
    // progress feedback.
    return { success: successes.length == keywords.length };
}

async function addKeywords({ domain, entityId, keywords, adGroupId, bid }) {
    const collector = await getCollector(domain, entityId);

    const timestamp = Date.now();
    const result = await collector.addKeywords({ keywords, adGroupId, bid });
    for (const page of common.pageArray(result.ok, 50)) {
        await bg.ajax(`${bg.serviceUrl}/api/keywordData/${entityId}?timestamp=${timestamp}`, {
            method: 'PATCH',
            jsonData: { operation: 'ADD', adGroupId, keywordIds: page, dataValues: { bid } },
            responseType: 'json',
        });
    }

    return result;
}

function setBrandName({ entityId, brandName }) {
    bg.setEntityId(entityId, { name: brandName });
}

module.exports = {
    name: 'sp',
    dataGather,
    getAllowedCampaigns,
    getCampaignSummaries,
    getAllCampaignData,
    getDataHistory,
    getAggregateCampaignHistory,
    getKeywordData,
    getAggregateKeywordData,
    storeAdGroupMetadata,
    updateKeyword,
    addKeywords,
    setBrandName,
};
<|endoftext|>


<commit_before>var express = require('express'),
    http = require('http'),
    path = require('path')
    io = require('socket.io');

var app = express();

app.configure(function() {
    app.set('port', process.env.PORT || 3000);
    app.use(express.static(path.join(__dirname, 'public')));
});

app.configure('development', function() {
    app.use(express.errorHandler());
});

// Set up express
var server = http.createServer(app).listen(app.get('port'), function(){
    console.log("Express server listening on port " + app.get('port'));
});

// Set up socket.io
var io = require('socket.io').listen(server);

// Handle socket traffic
io.sockets.on('connection', function (socket) {
    {}
    // Set the nickname property for a given client
    socket.on('nick', function(nick) {
        socket.set('nickname', nick);
    });

    // Relay chat data to all clients
    socket.on('chat', function(data) {
        socket.get('nickname', function(err, nick) {
            var nickname = err ? 'Anonymous' : nick;

            var payload = {
                message: data.message,
                nick: nickname
            };

            socket.emit('chat',payload);
            socket.broadcast.emit('chat', payload);
        });
    });
});<commit_msg>Exercise 5<commit_after>// This does not work like the Finished example - does not print the results of the chat to screen

var express = require('express'),
    http = require('http'),
    path = require('path')
    io = require('socket.io');

var app = express();

app.configure(function() {
    app.set('port', process.env.PORT || 3000);
    app.use(express.static(path.join(__dirname, 'public')));
});

app.configure('development', function() {
    app.use(express.errorHandler());
});

// Set up express
var server = http.createServer(app).listen(app.get('port'), function(){
    console.log("Express server listening on port " + app.get('port'));
});

// Set up socket.io
var io = require('socket.io').listen(server);

// Handle socket traffic
io.sockets.on('connection', function (socket) {
    {}
    // Set the nickname property for a given client
    socket.on('nick', function(nick) {
        socket.set('nickname', nick);
    });

    // Relay chat data to all clients
    socket.on('chat', function(data) {
        socket.get('nickname', function(err, nick) {
            var nickname = err ? 'Anonymous' : nick;

            var payload = {
                message: data.message,
                nick: nickname
            };

            socket.emit('chat',payload);
            socket.broadcast.emit('chat', payload);
        });
    });
});<|endoftext|>


<commit_before>'use strict';
/*eslint no-restricted-modules: [0]*/

let color = require('../config/color');
let moment = require('moment');

let BR = '
';
let SPACE = ' ';
let profileColor = '#24678d';
let trainersprites = [1, 2, 101, 102, 169, 170, 265, 266, 168];

/**
 * Profile constructor.
 *
 * @param {Boolean} isOnline
 * @param {Object|String} user - if isOnline then Object else String
 * @param {String} image
 */
function Profile(isOnline, user, image) {
	this.isOnline = isOnline || false;
	this.user = user || null;
	this.image = image;

	this.username = Tools.escapeHTML(this.isOnline ? this.user.name : this.user);
	this.url = Config.avatarurl || '';
}

/**
 * Create an bold html tag element.
 *
 * Example:
 * createFont('Hello World!');
 * => 'Hello World!'
 *
 * @param {String} color
 * @param {String} text
 * @return {String}
 */
function bold(text) {
	return '' + text + '';
}

/**
 * Create an font html tag element.
 *
 * Example:
 * createFont('Hello World!', 'blue');
 * => 'Hello World!'
 *
 * @param {String} color
 * @param {String} text
 * @return {String}
 */
function font(color, text) {
	return '' + text + '';
}

/**
 * Create an img tag element.
 *
 * Example:
 * createImg('phil.png');
 * => ''
 *
 * @param {String} link
 * @return {String}
 */
function img(link) {
	return '';
}

/**
 * Create a font html element wrap around by a bold html element.
 * Uses to `profileColor` as a color.
 * Adds a colon at the end of the text and a SPACE at the end of the element.
 *
 * Example:
 * label('Name');
 * => 'Name: '
 *
 * @param {String} text
 * @return {String}
 */
function label(text) {
	return bold(font(profileColor, text + ':')) + SPACE;
}

function currencyName(amount) {
	let name = " buck";
	return amount === 1 ? name : name + "s";
}

Profile.prototype.avatar = function () {
	if (this.isOnline) {
		if (typeof this.image === 'string') return img(this.url + ':' + Config.port + '/avatars/' + this.image);
		return img('http://play.pokemonshowdown.com/sprites/trainers/' + this.image + '.png');
	}
	for (let name in Config.customAvatars) {
		if (this.username === name) {
			return img(this.url + ':' + Config.port + '/avatars/' + Config.customAvatars[name]);
		}
	}
	let selectedSprite = trainersprites[Math.floor(Math.random() * trainersprites.length)];
	return img('http://play.pokemonshowdown.com/sprites/trainers/' + selectedSprite + '.png');
};

Profile.prototype.buttonAvatar = function () {
	let css = 'border:none;background:none;padding:0;float:left;';
	return '<button style="' + css + '" name="parseCommand" value="/user ' + this.username + '">' + this.avatar() + "</button>";
};

Profile.prototype.group = function () {
	if (this.isOnline && this.user.group === ' ') return label('Group') + 'Regular User';
	if (this.isOnline) return label('Group') + Config.groups[this.user.group].name;
	for (let name in Users.usergroups) {
		if (toId(this.username) === name) {
			return label('Group') + Config.groups[Users.usergroups[name].charAt(0)].name;
		}
	}
	return label('Group') + 'Regular User';
};

Profile.prototype.money = function (amount) {
	return label('Money') + amount + currencyName(amount);
};

Profile.prototype.name = function () {
	return label('Name') + bold(font(color(toId(this.username)), this.username));
};

Profile.prototype.seen = function (timeAgo) {
	if (this.isOnline) return label('Last Seen') + font('#2ECC40', 'Currently Online');
	if (!timeAgo) return label('Last Seen') + 'Never';
	return label('Last Seen') + moment(timeAgo).fromNow();
};

Profile.prototype.vip = function (user) {
	if (isVip(user)) return font('#6390F0', '(VIP User)');
	return '';

};

Profile.prototype.rstaff = function (user) {
	if (isRSTAFF(user)) return font('#6390F0', '(Retired Staff)');
	return '';

};

Profile.prototype.title = function () {
	let title = Db('TitleDB').get(toId(toId(this.user)));
	if (typeof title !== 'undefined' && title !== null)  return ' (' + Tools.escapeHTML(title[1]) + ')';
	return '';
};

Profile.prototype.show = function (callback) {
	let userid = toId(this.username);

	return this.buttonAvatar() +
		SPACE + this.name() + SPACE + this.title() + BR +
		SPACE + this.group() + SPACE + this.vip(userid) + SPACE + this.rstaff(userid) + BR +
		SPACE + this.money(Db('money').get(userid, 0)) + BR +
		SPACE + this.seen(Db('seen').get(userid)) +
		'
';
};

exports.commands = {
	profile: function (target, room, user) {
		if (!this.runBroadcast()) return;
		if (target.length >= 19) return this.sendReply("Usernames are required to be less than 19 characters long.");
		let targetUser = this.targetUserOrSelf(target);
		let profile;
		if (!targetUser) {
			profile = new Profile(false, target);
		} else {
			profile = new Profile(true, targetUser, targetUser.avatar);
		}
		this.sendReplyBox(profile.show());
	},

	customtitle: function (target, room, user) {
		let parts = target.split(',');
		let cmd = parts[0].trim().toLowerCase();
		let userid, targetUser;
		let title = [];
		switch (cmd) {
		case 'set':
			if (!this.can('lock') && !this.can('vip')) return false;
			let reg = /^\w+$/;
			userid = toId(parts[1]);
			targetUser = Users.getExact(userid);
			if (!userid) return this.sendReply("You didn't specify a user.");
			if (!Users.get(targetUser)) return this.errorReply('The target user is not online.');
			if (targetUser.length >= 19) return this.sendReply("Usernames are required to be less than 19 characters long.");
			if (targetUser.length < 3) return this.sendReply("Usernames are required to be greater than 2 characters long.");
			if (!reg.test(parts[2].trim())) return this.sendReply("Enter only alphanumeric characters for the eg. ff80b3");
			if (parts.length < 4) return this.sendReply("Invalid command. Valid commands are `/customtitle set, user, color, title`.");
			if (toId(targetUser) !== toId(user) && !this.can('lock')) return this.sendReply("You must be staff to set other people their custom title.");
			title[0] = parts[2].trim();
			title[1] = Tools.escapeHTML(parts.slice(3).join(",").trim());
			if (title[1].length > 30) return this.errorReply("Custom titles cannot be longer than 30 characters.");
			Db('TitleDB').set(toId(userid), title);
			Users.get(userid).popup('|modal||html|ATTENTION!
 You have received a custom title from ' + Tools.escapeHTML(user.name) + ': ' + ' ' + Tools.escapeHTML(title[1]) + '');
			this.sendReply("Usertitle set.");
			break;
		default:
			return this.sendReply("Invalid command. Valid commands are `/customtitle set, user, color, title`.");
		}
	},
	profilehelp: ["/profile -	Shows information regarding user's name, group, money, and when they were last seen."],
};
<commit_msg>Update profile.js<commit_after>'use strict';
/*eslint no-restricted-modules: [0]*/

let color = require('../config/color');
let moment = require('moment');

let BR = '
';
let SPACE = ' ';
let profileColor = '#24678d';
let trainersprites = [1, 2, 101, 102, 169, 170, 265, 266, 168];

/**
 * Profile constructor.
 *
 * @param {Boolean} isOnline
 * @param {Object|String} user - if isOnline then Object else String
 * @param {String} image
 */
function Profile(isOnline, user, image) {
	this.isOnline = isOnline || false;
	this.user = user || null;
	this.image = image;

	this.username = Tools.escapeHTML(this.isOnline ? this.user.name : this.user);
	this.url = Config.avatarurl || '';
}

/**
 * Create an bold html tag element.
 *
 * Example:
 * createFont('Hello World!');
 * => 'Hello World!'
 *
 * @param {String} color
 * @param {String} text
 * @return {String}
 */
function bold(text) {
	return '' + text + '';
}

/**
 * Create an font html tag element.
 *
 * Example:
 * createFont('Hello World!', 'blue');
 * => 'Hello World!'
 *
 * @param {String} color
 * @param {String} text
 * @return {String}
 */
function font(color, text) {
	return '' + text + '';
}

/**
 * Create an img tag element.
 *
 * Example:
 * createImg('phil.png');
 * => ''
 *
 * @param {String} link
 * @return {String}
 */
function img(link) {
	return '';
}

/**
 * Create a font html element wrap around by a bold html element.
 * Uses to `profileColor` as a color.
 * Adds a colon at the end of the text and a SPACE at the end of the element.
 *
 * Example:
 * label('Name');
 * => 'Name: '
 *
 * @param {String} text
 * @return {String}
 */
function label(text) {
	return bold(font(profileColor, text + ':')) + SPACE;
}

function currencyName(amount) {
	let name = " buck";
	return amount === 1 ? name : name + "s";
}

Profile.prototype.avatar = function () {
	if (this.isOnline) {
		if (typeof this.image === 'string') return img(this.url + ':' + Config.port + '/avatars/' + this.image);
		return img('http://play.pokemonshowdown.com/sprites/trainers/' + this.image + '.png');
	}
	for (let name in Config.customAvatars) {
		if (this.username === name) {
			return img(this.url + ':' + Config.port + '/avatars/' + Config.customAvatars[name]);
		}
	}
	let selectedSprite = trainersprites[Math.floor(Math.random() * trainersprites.length)];
	return img('http://play.pokemonshowdown.com/sprites/trainers/' + selectedSprite + '.png');
};

Profile.prototype.buttonAvatar = function () {
	let css = 'border:none;background:none;padding:0;float:left;';
	return '<button style="' + css + '" name="parseCommand" value="/user ' + this.username + '">' + this.avatar() + "</button>";
};

Profile.prototype.group = function () {
	if (this.isOnline && this.user.group === ' ') return label('Group') + 'Regular User';
	if (this.isOnline) return label('Group') + Config.groups[this.user.group].name;
	for (let name in Users.usergroups) {
		if (toId(this.username) === name) {
			return label('Group') + Config.groups[Users.usergroups[name].charAt(0)].name;
		}
	}
	return label('Group') + 'Regular User';
};

Profile.prototype.money = function (amount) {
	return label('Money') + amount + currencyName(amount);
};

Profile.prototype.name = function () {
	return label('Name') + bold(font(color(toId(this.username)), this.username));
};

Profile.prototype.seen = function (timeAgo) {
	if (this.isOnline) return label('Last Seen') + font('#2ECC40', 'Currently Online');
	if (!timeAgo) return label('Last Seen') + 'Never';
	return label('Last Seen') + moment(timeAgo).fromNow();
};

Profile.prototype.vip = function (user) {
	if (isVip(user)) return font('#6390F0', '(VIP User)');
	return '';

};

Profile.prototype.rstaff = function (user) {
	if (isRSTAFF(user)) return font('#6390F0', '(Retired Staff)');
	return '';

};

Profile.prototype.contributor = function (user) {
	if (isCONTRIBUTOR(user)) return font('#6390F0', '(Contributor)');
	return '';

};

Profile.prototype.title = function () {
	let title = Db('TitleDB').get(toId(toId(this.user)));
	if (typeof title !== 'undefined' && title !== null)  return ' (' + Tools.escapeHTML(title[1]) + ')';
	return '';
};

Profile.prototype.show = function (callback) {
	let userid = toId(this.username);

	return this.buttonAvatar() +
		SPACE + this.name() + SPACE + this.title() + BR +
		SPACE + this.group() + SPACE + this.vip(userid) + SPACE + this.rstaff(userid) + SPACE + this.contributor(userid) + BR +
		SPACE + this.money(Db('money').get(userid, 0)) + BR +
		SPACE + this.seen(Db('seen').get(userid)) +
		'
';
};

exports.commands = {
	profile: function (target, room, user) {
		if (!this.runBroadcast()) return;
		if (target.length >= 19) return this.sendReply("Usernames are required to be less than 19 characters long.");
		let targetUser = this.targetUserOrSelf(target);
		let profile;
		if (!targetUser) {
			profile = new Profile(false, target);
		} else {
			profile = new Profile(true, targetUser, targetUser.avatar);
		}
		this.sendReplyBox(profile.show());
	},

	customtitle: function (target, room, user) {
		let parts = target.split(',');
		let cmd = parts[0].trim().toLowerCase();
		let userid, targetUser;
		let title = [];
		switch (cmd) {
		case 'set':
			if (!this.can('lock') && !this.can('vip')) return false;
			let reg = /^\w+$/;
			userid = toId(parts[1]);
			targetUser = Users.getExact(userid);
			if (!userid) return this.sendReply("You didn't specify a user.");
			if (!Users.get(targetUser)) return this.errorReply('The target user is not online.');
			if (targetUser.length >= 19) return this.sendReply("Usernames are required to be less than 19 characters long.");
			if (targetUser.length < 3) return this.sendReply("Usernames are required to be greater than 2 characters long.");
			if (!reg.test(parts[2].trim())) return this.sendReply("Enter only alphanumeric characters for the eg. ff80b3");
			if (parts.length < 4) return this.sendReply("Invalid command. Valid commands are `/customtitle set, user, color, title`.");
			if (toId(targetUser) !== toId(user) && !this.can('lock')) return this.sendReply("You must be staff to set other people their custom title.");
			title[0] = parts[2].trim();
			title[1] = Tools.escapeHTML(parts.slice(3).join(",").trim());
			if (title[1].length > 30) return this.errorReply("Custom titles cannot be longer than 30 characters.");
			Db('TitleDB').set(toId(userid), title);
			Users.get(userid).popup('|modal||html|ATTENTION!
 You have received a custom title from ' + Tools.escapeHTML(user.name) + ': ' + ' ' + Tools.escapeHTML(title[1]) + '');
			this.sendReply("Usertitle set.");
			break;
		default:
			return this.sendReply("Invalid command. Valid commands are `/customtitle set, user, color, title`.");
		}
	},
	profilehelp: ["/profile -	Shows information regarding user's name, group, money, and when they were last seen."],
};
<|endoftext|>


<commit_before>'use strict';
/*eslint no-restricted-modules: [0]*/

let color = require('../config/color');
let moment = require('moment');
let request = require('request');

let BR = '
';
let SPACE = ' ';
let profileColor = '#24678d';
let trainersprites = [1, 2, 101, 102, 169, 170, 265, 266, 168];

/**
 * Profile constructor.
 *
 * @param {Boolean} isOnline
 * @param {Object|String} user - if isOnline then Object else String
 * @param {String} image
 */
function Profile(isOnline, user, image) {
	this.isOnline = isOnline || false;
	this.user = user || null;
	this.image = image;

	this.username = Tools.escapeHTML(this.isOnline ? this.user.name : this.user);
	this.url = Config.avatarurl || 'http://188.221.150.7';
}

/**
 * Fetch the Users regdate.
 *
 * Example:
 * Regdate('Audinory');
 * => 'May 19, 2016.'
 *
 */

function Regdate(getDate, user) {
	let profileAdd = "";
	this.user = user || null;
	this.getDate = request('http://pokemonshowdown.com/users/' + username, function (error, response, body) {
			if (error && response.statusCode !== 200) {
				profileAdd = " Unregistered";
			}
			let regdate = body.split('')[1].split('')[0].replace(/(|<\/em>)/g, '');
			if (regdate === '(Unregistered)') {
				profileAdd = " Unregistered";
			} else if (regdate === '(Account disabled)') {
				profileAdd = " Account disabled.";
			} else {
				profileAdd = regdate.slice(7) + ".";
			}
		}.bind(this));
}

/**
 * Create an bold html tag element.
 *
 * Example:
 * createFont('Hello World!');
 * => 'Hello World!'
 *
 * @param {String} color
 * @param {String} text
 * @return {String}
 */
function bold(text) {
	return '' + text + '';
}

/**
 * Create an font html tag element.
 *
 * Example:
 * createFont('Hello World!', 'blue');
 * => 'Hello World!'
 *
 * @param {String} color
 * @param {String} text
 * @return {String}
 */
function font(color, text) {
	return '' + text + '';
}

/**
 * Create an img tag element.
 *
 * Example:
 * createImg('phil.png');
 * => ''
 *
 * @param {String} link
 * @return {String}
 */
function img(link) {
	return '';
}

/**
 * Create a font html element wrap around by a bold html element.
 * Uses to `profileColor` as a color.
 * Adds a colon at the end of the text and a SPACE at the end of the element.
 *
 * Example:
 * label('Name');
 * => 'Name: '
 *
 * @param {String} text
 * @return {String}
 */

function label(text) {
	return bold(font(profileColor, text + ':')) + SPACE;
}

function currencyName(amount) {
	let name = " buck";
	return amount === 1 ? name : name + "s";
}

Profile.prototype.avatar = function () {
	if (this.isOnline) {
		if (typeof this.image === 'string') return img(this.url + ':' + Config.port + '/avatars/' + this.image);
		return img('http://play.pokemonshowdown.com/sprites/trainers/' + this.image + '.png');
	}
	for (let name in Config.customAvatars) {
		if (this.username === name) {
			return img(this.url + ':' + Config.port + '/avatars/' + Config.customAvatars[name]);
		}
	}
	let selectedSprite = trainersprites[Math.floor(Math.random() * trainersprites.length)];
	return img('http://play.pokemonshowdown.com/sprites/trainers/' + selectedSprite + '.png');
};

Profile.prototype.buttonAvatar = function () {
	let css = 'border:none;background:none;padding:0;float:left;';
	return '<button style="' + css + '" name="parseCommand" value="/user ' + this.username + '">' + this.avatar() + "</button>";
};

Profile.prototype.group = function () {
	if (this.isOnline && this.user.group === ' ') return label('Group') + 'Regular User';
	if (this.isOnline) return label('Group') + Config.groups[this.user.group].name;
	for (let name in Users.usergroups) {
		if (toId(this.username) === name) {
			return label('Group') + Config.groups[Users.usergroups[name].charAt(0)].name;
		}
	}
	return label('Group') + 'Regular User';
};

Profile.prototype.money = function (amount) {
	return label('Money') + amount + currencyName(amount);
};

Profile.prototype.name = function () {
	return label('Name') + bold(font(color(toId(this.username)), this.username));
};

Profile.prototype.seen = function (timeAgo) {
	if (this.isOnline) return label('Last Seen') + font('#2ECC40', 'Currently Online');
	if (!timeAgo) return label('Last Seen') + 'Never';
	return label('Last Seen') + moment(timeAgo).fromNow();
};

Profile.prototype.regdate = function (Regdate) {
	return label('Registered: ') + bold(font(toId(this.regdate)));
};

Profile.prototype.show = function (callback) {
	let userid = toId(this.username);

	return this.buttonAvatar() +
		SPACE + this.name() + BR +
		SPACE + this.group() + BR +
		SPACE + this.money(Db('money').get(userid, 0)) + BR +
		SPACE + this.seen(Db('seen').get(userid))
		SPACE + this.regdate.get(userid) +
		'
';
};

exports.commands = {
	profile: function (target, room, user) {
		if (!this.runBroadcast()) return;
		if (target.length >= 19) return this.sendReply("Usernames are required to be less than 19 characters long.");
		let targetUser = this.targetUserOrSelf(target);
		let profile;
		if (!targetUser) {
			profile = new Profile(false, target);
		} else {
			profile = new Profile(true, targetUser, targetUser.avatar);
		}
		this.sendReplyBox(profile.show());
	},
	profilehelp: ["/profile -	Shows information regarding user's name, group, money, and when they were last seen."],
};
<commit_msg>Fix Spacing for chat <commit_after>'use strict';
/*eslint no-restricted-modules: [0]*/

let color = require('../config/color');
let moment = require('moment');
let request = require('request');

let BR = '
';
let SPACE = ' ';
let profileColor = '#24678d';
let trainersprites = [1, 2, 101, 102, 169, 170, 265, 266, 168];

/**
 * Profile constructor.
 *
 * @param {Boolean} isOnline
 * @param {Object|String} user - if isOnline then Object else String
 * @param {String} image
 */
function Profile(isOnline, user, image) {
	this.isOnline = isOnline || false;
	this.user = user || null;
	this.image = image;

	this.username = Tools.escapeHTML(this.isOnline ? this.user.name : this.user);
	this.url = Config.avatarurl || 'http://188.221.150.7';
}

/**
 * Fetch the Users regdate.
 *
 * Example:
 * Regdate('Audinory');
 * => 'May 19, 2016.'
 *
 */

function Regdate(getDate, user) {
	let profileAdd = "";
	this.user = user || null;
	this.getDate = request('http://pokemonshowdown.com/users/' + username, function (error, response, body) {
			if (error && response.statusCode !== 200) {
				profileAdd = " Unregistered";
			}
			let regdate = body.split('')[1].split('')[0].replace(/(|<\/em>)/g, '');
			if (regdate === '(Unregistered)') {
				profileAdd = " Unregistered";
			} else if (regdate === '(Account disabled)') {
				profileAdd = " Account disabled.";
			} else {
				profileAdd = regdate.slice(7) + ".";
			}
		}.bind(this));
}

/**
 * Create an bold html tag element.
 *
 * Example:
 * createFont('Hello World!');
 * => 'Hello World!'
 *
 * @param {String} color
 * @param {String} text
 * @return {String}
 */
function bold(text) {
	return '' + text + '';
}

/**
 * Create an font html tag element.
 *
 * Example:
 * createFont('Hello World!', 'blue');
 * => 'Hello World!'
 *
 * @param {String} color
 * @param {String} text
 * @return {String}
 */
function font(color, text) {
	return '' + text + '';
}

/**
 * Create an img tag element.
 *
 * Example:
 * createImg('phil.png');
 * => ''
 *
 * @param {String} link
 * @return {String}
 */
function img(link) {
	return '';
}

/**
 * Create a font html element wrap around by a bold html element.
 * Uses to `profileColor` as a color.
 * Adds a colon at the end of the text and a SPACE at the end of the element.
 *
 * Example:
 * label('Name');
 * => 'Name: '
 *
 * @param {String} text
 * @return {String}
 */

function label(text) {
	return bold(font(profileColor, text + ':')) + SPACE;
}

function currencyName(amount) {
	let name = " buck";
	return amount === 1 ? name : name + "s";
}

Profile.prototype.avatar = function () {
	if (this.isOnline) {
		if (typeof this.image === 'string') return img(this.url + ':' + Config.port + '/avatars/' + this.image);
		return img('http://play.pokemonshowdown.com/sprites/trainers/' + this.image + '.png');
	}
	for (let name in Config.customAvatars) {
		if (this.username === name) {
			return img(this.url + ':' + Config.port + '/avatars/' + Config.customAvatars[name]);
		}
	}
	let selectedSprite = trainersprites[Math.floor(Math.random() * trainersprites.length)];
	return img('http://play.pokemonshowdown.com/sprites/trainers/' + selectedSprite + '.png');
};

Profile.prototype.buttonAvatar = function () {
	let css = 'border:none;background:none;padding:0;float:left;';
	return '<button style="' + css + '" name="parseCommand" value="/user ' + this.username + '">' + this.avatar() + "</button>";
};

Profile.prototype.group = function () {
	if (this.isOnline && this.user.group === ' ') return label('Group') + 'Regular User';
	if (this.isOnline) return label('Group') + Config.groups[this.user.group].name;
	for (let name in Users.usergroups) {
		if (toId(this.username) === name) {
			return label('Group') + Config.groups[Users.usergroups[name].charAt(0)].name;
		}
	}
	return label('Group') + 'Regular User';
};

Profile.prototype.money = function (amount) {
	return label('Money') + amount + currencyName(amount);
};

Profile.prototype.name = function () {
	return label('Name') + bold(font(color(toId(this.username)), this.username));
};

Profile.prototype.seen = function (timeAgo) {
	if (this.isOnline) return label('Last Seen') + font('#2ECC40', 'Currently Online');
	if (!timeAgo) return label('Last Seen') + 'Never';
	return label('Last Seen') + moment(timeAgo).fromNow();
};

Profile.prototype.regdate = function (Regdate) {
	return label('Registered: ') + bold(font(toId(this.regdate)));
};

Profile.prototype.show = function (callback) {
	let userid = toId(this.username);

	return this.buttonAvatar() +
		SPACE + this.name() + BR +
		SPACE + this.group() + BR +
		SPACE + this.money(Db('money').get(userid, 0)) + BR +
		SPACE + this.seen(Db('seen').get(userid))
		SPACE + this.regdate.get(userid) +
		'
';
};

exports.commands = {
	profile: function (target, room, user) {
		if (!this.runBroadcast()) return;
		if (target.length >= 19) return this.sendReply("Usernames are required to be less than 19 characters long.");
		let targetUser = this.targetUserOrSelf(target);
		let profile;
		if (!targetUser) {
			profile = new Profile(false, target);
		} else {
			profile = new Profile(true, targetUser, targetUser.avatar);
		}
		this.sendReply('<div class="infobox" style="width:100%; height: 85px;' + profile.show() + '
');
	},
	profilehelp: ["/profile -	Shows information regarding user's name, group, money, and when they were last seen."],
};
<|endoftext|>


<commit_before>/* globals browser, catcher, log */

"use strict";

var global = this;

this.selectorLoader = (function() {
  const exports = {};

  // These modules are loaded in order, first standardScripts, then optionally onboardingScripts, and then selectorScripts
  // The order is important due to dependencies
  const standardScripts = [
    "build/buildSettings.js",
    "log.js",
    "catcher.js",
    "assertIsTrusted.js",
    "background/selectorLoader.js",
    "selector/callBackground.js",
    "selector/util.js"
  ];

  const selectorScripts = [
    "clipboard.js",
    "makeUuid.js",
    "build/shot.js",
    "randomString.js",
    "domainFromUrl.js",
    "build/inlineSelectionCss.js",
    "selector/documentMetadata.js",
    "selector/ui.js",
    "selector/shooter.js",
    "selector/uicontrol.js"
  ];

  // These are loaded on request (by the selector worker) to activate the onboarding:
  const onboardingScripts = [
    "build/onboardingCss.js",
    "build/onboardingHtml.js",
    "onboarding/slides.js"
  ];

  exports.unloadIfLoaded = function(tabId) {
    return browser.tabs.executeScript(tabId, {
      code: "this.selectorLoader && this.selectorLoader.unloadModules()",
      runAt: "document_start"
    }).then(result => {
      return result && result[0];
    });
  };

  exports.testIfLoaded = function(tabId) {
    return browser.tabs.executeScript(tabId, {
      code: "!!this.selectorLoader",
      runAt: "document_start"
    }).then(result => {
      return result && result[0];
    });
  };

  exports.loadModules = function(tabId, hasSeenOnboarding) {
    if (hasSeenOnboarding) {
      return executeModules(tabId, standardScripts.concat(selectorScripts));
    }
    return executeModules(tabId, standardScripts.concat(onboardingScripts).concat(selectorScripts));
  };

  function executeModules(tabId, scripts) {
    let lastPromise = Promise.resolve(null);
    scripts.forEach((file) => {
      lastPromise = lastPromise.then(() => {
        return browser.tabs.executeScript(tabId, {
          file,
          runAt: "document_end"
        })
          .catch((error) => {
            log.error("error in script:", file, error);
            error.scriptName = file;
            throw error;
          })
      })
    });
    return lastPromise.then(() => {
      log.debug("finished loading scripts:", scripts.join(" "));
    },
    (error) => {
      exports.unloadIfLoaded(tabId);
      catcher.unhandled(error);
      throw error;
    });
  }

  exports.unloadModules = function() {
    const watchFunction = catcher.watchFunction;
    let allScripts = standardScripts.concat(onboardingScripts).concat(selectorScripts);
    const moduleNames = allScripts.map((filename) =>
      filename.replace(/^.*\//, "").replace(/\.js$/, ""));
    moduleNames.reverse();
    for (let moduleName of moduleNames) {
      let moduleObj = global[moduleName];
      if (moduleObj && moduleObj.unload) {
        try {
          watchFunction(moduleObj.unload)();
        } catch (e) {
          // ignore (watchFunction handles it)
        }
      }
      delete global[moduleName];
    }
    return true;
  };

  exports.toggle = function(tabId, hasSeenOnboarding) {
    return exports.unloadIfLoaded(tabId)
      .then(wasLoaded => {
        if (!wasLoaded) {
          exports.loadModules(tabId, hasSeenOnboarding);
        }
        return !wasLoaded;
      })
  };

  return exports;
})();
null;
<commit_msg>Fix testIfLoaded so it can see when a load is in progress (#2762)<commit_after>/* globals browser, catcher, log */

"use strict";

var global = this;

this.selectorLoader = (function() {
  const exports = {};

  // These modules are loaded in order, first standardScripts, then optionally onboardingScripts, and then selectorScripts
  // The order is important due to dependencies
  const standardScripts = [
    "build/buildSettings.js",
    "log.js",
    "catcher.js",
    "assertIsTrusted.js",
    "background/selectorLoader.js",
    "selector/callBackground.js",
    "selector/util.js"
  ];

  const selectorScripts = [
    "clipboard.js",
    "makeUuid.js",
    "build/shot.js",
    "randomString.js",
    "domainFromUrl.js",
    "build/inlineSelectionCss.js",
    "selector/documentMetadata.js",
    "selector/ui.js",
    "selector/shooter.js",
    "selector/uicontrol.js"
  ];

  // These are loaded on request (by the selector worker) to activate the onboarding:
  const onboardingScripts = [
    "build/onboardingCss.js",
    "build/onboardingHtml.js",
    "onboarding/slides.js"
  ];

  exports.unloadIfLoaded = function(tabId) {
    return browser.tabs.executeScript(tabId, {
      code: "this.selectorLoader && this.selectorLoader.unloadModules()",
      runAt: "document_start"
    }).then(result => {
      return result && result[0];
    });
  };

  exports.testIfLoaded = function(tabId) {
    if (loadingTabs.has(tabId)) {
      return true;
    }
    return browser.tabs.executeScript(tabId, {
      code: "!!this.selectorLoader",
      runAt: "document_start"
    }).then(result => {
      return result && result[0];
    });
  };

  let loadingTabs = new Set();

  exports.loadModules = function(tabId, hasSeenOnboarding) {
    let promise;
    loadingTabs.add(tabId);
    if (hasSeenOnboarding) {
      promise = executeModules(tabId, standardScripts.concat(selectorScripts));
    } else {
      promise = executeModules(tabId, standardScripts.concat(onboardingScripts).concat(selectorScripts));
    }
    return promise.then((result) => {
      loadingTabs.delete(tabId);
      return result;
    }, (error) => {
      loadingTabs.delete(tabId);
      throw error;
    });
  };

  function executeModules(tabId, scripts) {
    let lastPromise = Promise.resolve(null);
    scripts.forEach((file) => {
      lastPromise = lastPromise.then(() => {
        return browser.tabs.executeScript(tabId, {
          file,
          runAt: "document_end"
        })
          .catch((error) => {
            log.error("error in script:", file, error);
            error.scriptName = file;
            throw error;
          })
      })
    });
    return lastPromise.then(() => {
      log.debug("finished loading scripts:", scripts.join(" "));
    },
    (error) => {
      exports.unloadIfLoaded(tabId);
      catcher.unhandled(error);
      throw error;
    });
  }

  exports.unloadModules = function() {
    const watchFunction = catcher.watchFunction;
    let allScripts = standardScripts.concat(onboardingScripts).concat(selectorScripts);
    const moduleNames = allScripts.map((filename) =>
      filename.replace(/^.*\//, "").replace(/\.js$/, ""));
    moduleNames.reverse();
    for (let moduleName of moduleNames) {
      let moduleObj = global[moduleName];
      if (moduleObj && moduleObj.unload) {
        try {
          watchFunction(moduleObj.unload)();
        } catch (e) {
          // ignore (watchFunction handles it)
        }
      }
      delete global[moduleName];
    }
    return true;
  };

  exports.toggle = function(tabId, hasSeenOnboarding) {
    return exports.unloadIfLoaded(tabId)
      .then(wasLoaded => {
        if (!wasLoaded) {
          exports.loadModules(tabId, hasSeenOnboarding);
        }
        return !wasLoaded;
      })
  };

  return exports;
})();
null;
<|endoftext|>


<commit_before>const HTMLLinkElement = function

  // http://w3c.github.io/webcomponents/spec/imports/#h-interface-import

(tag) {

  const
    proxy = {}

  , link = document.querySelector // use CSS :any ?
      ('link#'+tag+'[rel=import], link[href*='+tag+'][rel=import]')

  , register = (event, handler) =>
      (HTMLImports && !!! HTMLImports.useNative)
        // https://github.com/webcomponents/html-imports#htmlimports
        ? HTMLImports.whenReady ( _ => handler ({ target: link }) ) // eww
        : link.addEventListener (event, handler)

    Object
      .defineProperties (proxy, {

        'addEventListener': {
          writable: false,

          value: function (event, handler) {
            !!! link
              ? handler ({ target: proxy })
              : register (event, handler)
          }
        }

      , 'onerror': // TODO: definition for onerror
          { set (handler) {} }
      })

  return proxy
}

class TokenList {

  constructor (node) {

    const
      tokenize = node =>
        (node.text = node.textContent)

        && node.textContent
            .match (/{(\w+|#)}/g)
              .map (symbol => symbol.match (/(\w+|#)/g) [0])
              .map (insert (node))

    , insert = token =>
        symbol =>
          (this [symbol] = this [symbol] || [])
            && this [symbol].push (token)

    this
      .sift (node)
      .map  (tokenize)
  }

  sift (node) {

    const
      nodes = []

    , visit = node =>
        node.nodeType === Node.TEXT_NODE
          ? TEXT_NODE (node)
            && NodeFilter.FILTER_ACCEPT // Accept TEXT_NODEs

          : ELEMENT_NODE (node.attributes)
            && NodeFilter.FILTER_REJECT // reject ELEMENT_NODEs

    , TEXT_NODE = node =>
        /{(\w+|#)}/.test (node.textContent)

    , ELEMENT_NODE = attributes =>
        Array
          .from (attributes)
          .filter (attr => /{(\w+|#)}/g.test (attr.value))
          .map (attribute => nodes.push (attribute))

    , walker =
        document.createNodeIterator
          (node, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, visit)
          // by default breaks on template YAY! 🎉

    while
      (node = walker.nextNode ())
        nodes.push (node)

    return nodes
  }

  bind (context) {

    const
      replace = symbol =>
        this [symbol]
          .map (token => token.textContent = token.text)

        && this [symbol]
          .map (replacement (symbol))

    , replacement = symbol =>
        item => // thunk
          item.textContent = item.textContent
            .replace ('{'+symbol+'}', context [symbol])

    Object
      .keys (this)

      .filter
        (key => context [key] !== undefined)

      .map (replace)
  }

//zip (...elements) {

//  const
//    lock = (zipper, row) => [...zipper, ...row]

//  , pair = teeth => // http://english.stackexchange.com/questions/121601/pair-or-couple
//      (tooth, position) => // thunk
//        [tooth, teeth [position]]

//  return elements [1]
//    .map (pair (elements [0]))
//    .reduce (lock)
//}

//slice (text, tokens = []) {

//  const
//    match    = /({\w+})/g // stored regex is faster https://jsperf.com/regexp-indexof-perf
//  , replace  = token => (collect (token), '✂')
//  , collect  = token => tokens.push (token)
//  , sections = text
//      .replace (match, replace)
//        .split ('✂')

//  return zip (tokens, sections)
//     .filter (element => element)
//        .map (element => new Text (element))
//}
}

// INTERESTING! Converting `Template` to a class increases size by ~16 octets

const HTMLTemplateElement = Template = function (name) {

  return Object.assign
    (document.querySelector ('template[name='+name+']'), { bind } )

  function bind (context) {
    contexts = [].concat ( ... [context] )

    let
      clone
    , template = document.createElement ('template')

    void (this.dependents || [])
      .map (dependent => dependent.remove ())

    this.dependents = []

    template.innerHTML =
    contexts.map ((context, index) => {
      context = (typeof context  === 'object') ? context : { self: context }
      context ['#'] = index

      clone  = this.cloneNode (true)

      void (new TokenList (clone.content))
        .bind (context)

      return clone.innerHTML
    })
    .join ('')

    this.dependents.push (...template.content.childNodes)
    this.after ( template.content )

    return this
  }
}

const EventTarget = Element => // why buble

  // DOM Levels
  // (https://developer.mozilla.org/fr/docs/DOM_Levels)
  //
  // WHATWG Living Standard HTML5 EventTarget
  // https://dom.spec.whatwg.org/#eventtarget
  //
  // MDN EventTarget
  // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
  //
  // DOM Level 3 EventTarget
  // https://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-EventTarget
  //
  // DOM Level 2 EventTarget
  // (AKA Str🎱  W3C #fockery) ➡  https://annevankesteren.nl/2016/01/film-at-11
  // 😕  https://w3c.github.io/uievents/DOM3-Events.html#interface-EventTarget
  //❓❓ https://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html
  // https://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-EventTarget
  // Within https://w3c.github.io/uievents/#conf-interactive-ua
  // EventTarget links to WHATWG - https://dom.spec.whatwg.org/#eventtarget

(class extends Element {

  connectedCallback () {

    new HTMLLinkElement
      (this.tagName.toLowerCase ())
        .addEventListener
          ('load', this.onimport.bind (this))
  }

  listen (event, listener = this [event])

    // MDN EventTarget.addEventListener
    // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
    //
    // WHATWG Living Standard EventTarget.addEventListener
    // https://dom.spec.whatwg.org/#dom-eventtarget-removeeventlistener
    //
    // DOM Level 2 EventTarget.addEventListener
    // https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget-addEventListener

    { this.addEventListener (event, listener) }

//ignore (event, listener = 'on' + this [event])
//  // MDN EventTarget.removeEventListener
//  // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
//  //
//  // WHATWG Living Standard EventTarget.removeEventListener
//  // https://dom.spec.whatwg.org/#dom-eventtarget-removeeventlistener
//  //
//  // DOM Level 2 EventTarget.removeEventListener
//  // https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget-removeEventListener

//  { this.removeEventListener (event, listener) }

//dispatch (event)
//  // MDN EventTarget.dispatchEvent
//  // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent
//  //
//  // WHATWG Living Standard EventTarget.dispatchEvent
//  // https://dom.spec.whatwg.org/#dom-eventtarget-dispatchevent
//  //
//  // DOM Level 2 EventTarget.dispatchEvent
//  //  https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget-dispatchEvent

//  { }
})

const ParentNode = Element =>

  // DOM Levels
  // (https://developer.mozilla.org/fr/docs/DOM_Levels)
  //
  // Living Standard HTML5 ParentNode
  // https://dom.spec.whatwg.org/#parentnode
  //
  // MDN ParentNode
  // https://developer.mozilla.org/en-US/docs/Web/API/ParentNode
  //
  // ElementTraversal interface
  // https://www.w3.org/TR/ElementTraversal/#interface-elementTraversal

(class extends Element {

  // http://jsfiddle.net/zaqtg/10
  // https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
  // https://developer.mozilla.org/en-US/docs/Web/API/NodeIterator
  // https://www.w3.org/TR/DOM-Level-2-Traversal-Range/traversal.html
  // https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter
  // NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT

  selectAll (selector)
    { return this.querySelectorAll (selector) }

  select (selector)
    // watch out for clobbering `HTMLInputElement.select ()`
    // https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/select
    { return this.selectAll (selector) [0] }

})

//function comb
//  // ElementTraversal interface
//  // https://www.w3.org/TR/ElementTraversal/#interface-elementTraversal
//
//(parent) {
//  if (parent.hasChildNodes())
//    for (let node = parent.firstChild; node; node = node.nextSibling)
//      comb (node)
//}

const GlobalEventHandlers = Element =>

  // DOM Levels
  // (https://developer.mozilla.org/fr/docs/DOM_Levels)
  //
  // Living Standard HTML5 GlobalEventHandlers
  // https://html.spec.whatwg.org/multipage/webappapis.html#globaleventhandlers
  //
  // MDN GlobalEventHandlers
  // https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers
  //
  // MDN on* Events
  // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Event_handlers
  //
  // DOM Level 0
  // This event handling model was introduced by Netscape Navigator,
  // and remains the most cross-browser model as of 2005
  // https://en.wikipedia.org/wiki/DOM_events#DOM_Level_0#DOM_Level_0
  //
  // Inline Model
  // https://en.wikipedia.org/wiki/DOM_events#DOM_Level_0#Inline_model
  //
  // Traditional Model
  // https://en.wikipedia.org/wiki/DOM_events#Traditional_model
  //
  // Traditional Registration
  // http://www.quirksmode.org/js/events_tradmod.html

(class extends Element {

  onimport (event) {

    const
      document =
        event.target.import

    , template = document &&
        document.querySelector ('template')

    template &&
      this.clone (template)

    // dispatch `import`
    // and captured from `EventTarget`
    Element.onconnect &&
      Element.onconnect.call (this)

    // dispatch `render`
    // and captured from `EventTarget`
    this.render ()
  }

  register (events = event => /^on/.exec (event)) {

    const
      mirror = handler =>
        (this [handler] === null) && // ensure W3C on event
          (this [handler] = Element [handler].bind (this))

    Object // mirror class events to element
      .getOwnPropertyNames (Element)
      .filter (events)
      .map (mirror)

    const
      nodes = // CSS :not negation https://developer.mozilla.org/en-US/docs/Web/CSS/:not
        // How can we select elements with on* attribute? (i.e. <... onclick=foo onblur=bar>)
        // If we can do this we can only retrieve the elements that have a traditional inline event.
        // This is theoretically more performant as most elements won't need traditional event registration.
        ':not(script):not(template):not(style):not(link)' // remove metadata elements

    , children =
        Array.from (this.querySelectorAll (nodes))

    , registered =
        node =>
          Array
            .from (node.attributes)
            .map (attr => attr.name)
            .filter (events)
            .length > 0

    , reflect =
        node =>
          Array
            .from (node.attributes)
            .map  (attr => attr.name)
            .filter (events)
            .map (reflection (node))

    , reflection =
        node => // closure
          event =>
            (node [event] = handle (node [event]))

    , handle =
        (handler, [_, event] = (/{\s*(\w+)\s*}/.exec (handler) || [])) =>
          event
            && Element [event]
            && Element [event].bind (this)
            || handler // existing handler
            || null  // default for W3C on* event handlers

    void [this]
      .concat (children)
      .filter (registered)
      .map    (reflect)
  }
})

const Component = Element => // why buble

  // exotic object - https://github.com/whatwg/html/issues/1704

( class extends // interfaces
  ( EventTarget ( ParentNode ( GlobalEventHandlers ( Element ))))
{

  constructor () { super ()

    this.context = {}

    // dispatch `initialize`
    // and captured from `EventTarget`
    this.initialize
      && this.initialize ()

    this.tokens = new TokenList (this)
  }

  render () {

    this.tokens
      .bind (this)

    Array
      .from // templates with `name` attribute
        (this.selectAll ('template[name]'))

      .map
        (template => template.getAttribute ('name'))

      .map
        (name => (new Template (name)).bind (this [name]))

    this.register ()

    // dispatch `idle`
    // and captured from `EventTarget`
    Element.onidle &&
      Element.onidle.call (this) // TODO: Migrate to `EventTarget`
  }

  // This doesn't go here. Perhaps SlotList / Template / TokenList (in that order)
  clone (template) {

    const
      fragment = template.content
        .cloneNode (true)

    , slots =
        Array.from (fragment.querySelectorAll ('slot'))

    , replace = replacement =>
        slots
          .filter (match (replacement))
          .map (exchange (replacement))

    , match = replacement =>
        slot =>
          replacement.getAttribute ('slot')
            === slot.getAttribute  ('name')

    , exchange = replacement =>
        slot =>
          slot
            // prefer to use replaceWith however support is sparse
            // https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/replaceWith
            // using `Node.parentNode` & `Node.replaceChid` as is defined in (ancient) W3C DOM Level 1,2,3
            .parentNode
            .replaceChild (replacement, slot)

    Array // map attributes from template
      .from (template.attributes)
      .map  (attribute => this.setAttribute (attribute.name, attribute.value))

    Array // map slots from template
      .from (this.querySelectorAll ('[slot]'))
      .map  (replace)

    this.innerHTML = template.innerHTML
  }

})

const ElementPrototype = window.Element.prototype // see bottom of this file

const Element = function
  (tag, CustomElementRegistry = window.customElements )

  //https://gist.github.com/allenwb/53927e46b31564168a1d
  // https://github.com/w3c/webcomponents/issues/587#issuecomment-271031208
  // https://github.com/w3c/webcomponents/issues/587#issuecomment-254017839

{
  return function (Element) { // https://en.wikipedia.org/wiki/Higher-order_function

    CustomElementRegistry.define
      ( ...tag, Component (Element))
  }
}

// Assign `window.Element.prototype` in case of feature checking on `Element`
Element.prototype = ElementPrototype
  // http://2ality.com/2013/09/window.html
  // http://tobyho.com/2013/03/13/window-prop-vs-global-var
  // https://github.com/webcomponents/webcomponentsjs/blob/master/webcomponents-es5-loader.js#L19

<commit_msg>Convert function to arrows to shed weight<commit_after>const HTMLLinkElement = function

  // http://w3c.github.io/webcomponents/spec/imports/#h-interface-import

(tag) {

  const
    proxy = {}

  , link = document.querySelector // use CSS :any ?
      ('link#'+tag+'[rel=import], link[href*='+tag+'][rel=import]')

  , register = (event, handler) =>
      (HTMLImports && !!! HTMLImports.useNative)
        // https://github.com/webcomponents/html-imports#htmlimports
        ? HTMLImports.whenReady ( _ => handler ({ target: link }) ) // eww
        : link.addEventListener (event, handler)

    Object
      .defineProperties (proxy, {

        'addEventListener': {
          writable: false,

          value: function (event, handler) {
            !!! link
              ? handler ({ target: proxy })
              : register (event, handler)
          }
        }

      , 'onerror': // TODO: definition for onerror
          { set (handler) {} }
      })

  return proxy
}

class TokenList {

  constructor (node) {

    const
      tokenize = node =>
        (node.text = node.textContent)

        && node.textContent
            .match (/{(\w+|#)}/g)
              .map (symbol => symbol.match (/(\w+|#)/g) [0])
              .map (insert (node))

    , insert = token =>
        symbol =>
          (this [symbol] = this [symbol] || [])
            && this [symbol].push (token)

    this
      .sift (node)
      .map  (tokenize)
  }

  sift (node) {

    const
      nodes = []

    , visit = node =>
        node.nodeType === Node.TEXT_NODE
          ? TEXT_NODE (node)
            && NodeFilter.FILTER_ACCEPT // Accept TEXT_NODEs

          : ELEMENT_NODE (node.attributes)
            && NodeFilter.FILTER_REJECT // reject ELEMENT_NODEs

    , TEXT_NODE = node =>
        /{(\w+|#)}/.test (node.textContent)

    , ELEMENT_NODE = attributes =>
        Array
          .from (attributes)
          .filter (attr => /{(\w+|#)}/g.test (attr.value))
          .map (attribute => nodes.push (attribute))

    , walker =
        document.createNodeIterator
          (node, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, visit)
          // by default breaks on template YAY! 🎉

    while
      (node = walker.nextNode ())
        nodes.push (node)

    return nodes
  }

  bind (context) {

    const
      replace = symbol =>
        this [symbol]
          .map (token => token.textContent = token.text)

        && this [symbol]
          .map (replacement (symbol))

    , replacement = symbol =>
        item => // thunk
          item.textContent = item.textContent
            .replace ('{'+symbol+'}', context [symbol])

    Object
      .keys (this)

      .filter
        (key => context [key] !== undefined)

      .map (replace)
  }

//zip (...elements) {

//  const
//    lock = (zipper, row) => [...zipper, ...row]

//  , pair = teeth => // http://english.stackexchange.com/questions/121601/pair-or-couple
//      (tooth, position) => // thunk
//        [tooth, teeth [position]]

//  return elements [1]
//    .map (pair (elements [0]))
//    .reduce (lock)
//}

//slice (text, tokens = []) {

//  const
//    match    = /({\w+})/g // stored regex is faster https://jsperf.com/regexp-indexof-perf
//  , replace  = token => (collect (token), '✂')
//  , collect  = token => tokens.push (token)
//  , sections = text
//      .replace (match, replace)
//        .split ('✂')

//  return zip (tokens, sections)
//     .filter (element => element)
//        .map (element => new Text (element))
//}
}

// INTERESTING! Converting `Template` to a class increases size by ~16 octets

const HTMLTemplateElement = Template = function (name) {

  return Object.assign
    (document.querySelector ('template[name='+name+']'), { bind } )

  function bind (context) {
    contexts = [].concat ( ... [context] )

    let
      clone
    , template = document.createElement ('template')

    void (this.dependents || [])
      .map (dependent => dependent.remove ())

    this.dependents = []

    template.innerHTML =
    contexts.map ((context, index) => {
      context = (typeof context  === 'object') ? context : { self: context }
      context ['#'] = index

      clone  = this.cloneNode (true)

      void (new TokenList (clone.content))
        .bind (context)

      return clone.innerHTML
    })
    .join ('')

    this.dependents.push (...template.content.childNodes)
    this.after ( template.content )

    return this
  }
}

const EventTarget = Element => // why buble

  // DOM Levels
  // (https://developer.mozilla.org/fr/docs/DOM_Levels)
  //
  // WHATWG Living Standard HTML5 EventTarget
  // https://dom.spec.whatwg.org/#eventtarget
  //
  // MDN EventTarget
  // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
  //
  // DOM Level 3 EventTarget
  // https://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-EventTarget
  //
  // DOM Level 2 EventTarget
  // (AKA Str🎱  W3C #fockery) ➡  https://annevankesteren.nl/2016/01/film-at-11
  // 😕  https://w3c.github.io/uievents/DOM3-Events.html#interface-EventTarget
  //❓❓ https://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html
  // https://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-EventTarget
  // Within https://w3c.github.io/uievents/#conf-interactive-ua
  // EventTarget links to WHATWG - https://dom.spec.whatwg.org/#eventtarget

(class extends Element {

  connectedCallback () {

    new HTMLLinkElement
      (this.tagName.toLowerCase ())
        .addEventListener
          ('load', this.onimport.bind (this))
  }

  listen (event, listener = this [event])

    // MDN EventTarget.addEventListener
    // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
    //
    // WHATWG Living Standard EventTarget.addEventListener
    // https://dom.spec.whatwg.org/#dom-eventtarget-removeeventlistener
    //
    // DOM Level 2 EventTarget.addEventListener
    // https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget-addEventListener

    { this.addEventListener (event, listener) }

//ignore (event, listener = 'on' + this [event])
//  // MDN EventTarget.removeEventListener
//  // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
//  //
//  // WHATWG Living Standard EventTarget.removeEventListener
//  // https://dom.spec.whatwg.org/#dom-eventtarget-removeeventlistener
//  //
//  // DOM Level 2 EventTarget.removeEventListener
//  // https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget-removeEventListener

//  { this.removeEventListener (event, listener) }

//dispatch (event)
//  // MDN EventTarget.dispatchEvent
//  // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent
//  //
//  // WHATWG Living Standard EventTarget.dispatchEvent
//  // https://dom.spec.whatwg.org/#dom-eventtarget-dispatchevent
//  //
//  // DOM Level 2 EventTarget.dispatchEvent
//  //  https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget-dispatchEvent

//  { }
})

const ParentNode = Element =>

  // DOM Levels
  // (https://developer.mozilla.org/fr/docs/DOM_Levels)
  //
  // Living Standard HTML5 ParentNode
  // https://dom.spec.whatwg.org/#parentnode
  //
  // MDN ParentNode
  // https://developer.mozilla.org/en-US/docs/Web/API/ParentNode
  //
  // ElementTraversal interface
  // https://www.w3.org/TR/ElementTraversal/#interface-elementTraversal

(class extends Element {

  // http://jsfiddle.net/zaqtg/10
  // https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
  // https://developer.mozilla.org/en-US/docs/Web/API/NodeIterator
  // https://www.w3.org/TR/DOM-Level-2-Traversal-Range/traversal.html
  // https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter
  // NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT

  selectAll (selector)
    { return this.querySelectorAll (selector) }

  select (selector)
    // watch out for clobbering `HTMLInputElement.select ()`
    // https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/select
    { return this.selectAll (selector) [0] }

})

//function comb
//  // ElementTraversal interface
//  // https://www.w3.org/TR/ElementTraversal/#interface-elementTraversal
//
//(parent) {
//  if (parent.hasChildNodes())
//    for (let node = parent.firstChild; node; node = node.nextSibling)
//      comb (node)
//}

const GlobalEventHandlers = Element =>

  // DOM Levels
  // (https://developer.mozilla.org/fr/docs/DOM_Levels)
  //
  // Living Standard HTML5 GlobalEventHandlers
  // https://html.spec.whatwg.org/multipage/webappapis.html#globaleventhandlers
  //
  // MDN GlobalEventHandlers
  // https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers
  //
  // MDN on* Events
  // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Event_handlers
  //
  // DOM Level 0
  // This event handling model was introduced by Netscape Navigator,
  // and remains the most cross-browser model as of 2005
  // https://en.wikipedia.org/wiki/DOM_events#DOM_Level_0#DOM_Level_0
  //
  // Inline Model
  // https://en.wikipedia.org/wiki/DOM_events#DOM_Level_0#Inline_model
  //
  // Traditional Model
  // https://en.wikipedia.org/wiki/DOM_events#Traditional_model
  //
  // Traditional Registration
  // http://www.quirksmode.org/js/events_tradmod.html

(class extends Element {

  onimport (event) {

    const
      document =
        event.target.import

    , template = document &&
        document.querySelector ('template')

    template &&
      this.clone (template)

    // dispatch `import`
    // and captured from `EventTarget`
    Element.onconnect &&
      Element.onconnect.call (this)

    // dispatch `render`
    // and captured from `EventTarget`
    this.render ()
  }

  register (events = event => /^on/.exec (event)) {

    const
      mirror = handler =>
        (this [handler] === null) && // ensure W3C on event
          (this [handler] = Element [handler].bind (this))

    Object // mirror class events to element
      .getOwnPropertyNames (Element)
      .filter (events)
      .map (mirror)

    const
      nodes = // CSS :not negation https://developer.mozilla.org/en-US/docs/Web/CSS/:not
        // How can we select elements with on* attribute? (i.e. <... onclick=foo onblur=bar>)
        // If we can do this we can only retrieve the elements that have a traditional inline event.
        // This is theoretically more performant as most elements won't need traditional event registration.
        ':not(script):not(template):not(style):not(link)' // remove metadata elements

    , children =
        Array.from (this.querySelectorAll (nodes))

    , registered =
        node =>
          Array
            .from (node.attributes)
            .map (attr => attr.name)
            .filter (events)
            .length > 0

    , reflect =
        node =>
          Array
            .from (node.attributes)
            .map  (attr => attr.name)
            .filter (events)
            .map (reflection (node))

    , reflection =
        node => // closure
          event =>
            (node [event] = handle (node [event]))

    , handle =
        (handler, [_, event] = (/{\s*(\w+)\s*}/.exec (handler) || [])) =>
          event
            && Element [event]
            && Element [event].bind (this)
            || handler // existing handler
            || null  // default for W3C on* event handlers

    void [this]
      .concat (children)
      .filter (registered)
      .map    (reflect)
  }
})

const Component = Element => // why buble

  // exotic object - https://github.com/whatwg/html/issues/1704

( class extends // interfaces
  ( EventTarget ( ParentNode ( GlobalEventHandlers ( Element ))))
{

  constructor () { super ()

    this.context = {}

    // dispatch `initialize`
    // and captured from `EventTarget`
    this.initialize
      && this.initialize ()

    this.tokens = new TokenList (this)
  }

  render () {

    this.tokens
      .bind (this)

    Array
      .from // templates with `name` attribute
        (this.selectAll ('template[name]'))

      .map
        (template => template.getAttribute ('name'))

      .map
        (name => (new Template (name)).bind (this [name]))

    this.register ()

    // dispatch `idle`
    // and captured from `EventTarget`
    Element.onidle &&
      Element.onidle.call (this) // TODO: Migrate to `EventTarget`
  }

  // This doesn't go here. Perhaps SlotList / Template / TokenList (in that order)
  clone (template) {

    const
      fragment = template.content
        .cloneNode (true)

    , slots =
        Array.from (fragment.querySelectorAll ('slot'))

    , replace = replacement =>
        slots
          .filter (match (replacement))
          .map (exchange (replacement))

    , match = replacement =>
        slot =>
          replacement.getAttribute ('slot')
            === slot.getAttribute  ('name')

    , exchange = replacement =>
        slot =>
          slot
            // prefer to use replaceWith however support is sparse
            // https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/replaceWith
            // using `Node.parentNode` & `Node.replaceChid` as is defined in (ancient) W3C DOM Level 1,2,3
            .parentNode
            .replaceChild (replacement, slot)

    Array // map attributes from template
      .from (template.attributes)
      .map  (attribute => this.setAttribute (attribute.name, attribute.value))

    Array // map slots from template
      .from (this.querySelectorAll ('[slot]'))
      .map  (replace)

    this.innerHTML = template.innerHTML
  }

})

const ElementPrototype = window.Element.prototype // see bottom of this file

const Element =
  (tag, CustomElementRegistry = window.customElements ) =>

    //https://gist.github.com/allenwb/53927e46b31564168a1d
    // https://github.com/w3c/webcomponents/issues/587#issuecomment-271031208
    // https://github.com/w3c/webcomponents/issues/587#issuecomment-254017839

    Element => // https://en.wikipedia.org/wiki/Higher-order_function
      CustomElementRegistry.define
        ( ...tag, Component (Element))

// Assign `window.Element.prototype` in case of feature checking on `Element`
Element.prototype = ElementPrototype
  // http://2ality.com/2013/09/window.html
  // http://tobyho.com/2013/03/13/window-prop-vs-global-var
  // https://github.com/webcomponents/webcomponentsjs/blob/master/webcomponents-es5-loader.js#L19

<|endoftext|>