<commit_before>import unique from 'array-unique'
import round from 'round-to'
import { bankItems } from 'gw2e-account-value/build/bank'
import { materialsItems } from 'gw2e-account-value/build/materials'
import { charactersItems } from 'gw2e-account-value/build/characters'
import { sharedInventoryItems } from 'gw2e-account-value/build/shared'
import legendaries from 'gw2e-static-data/build/legendaries'
import permanentToolIds from '../static/permanentToolIds'
import championBagIds from '../static/championBagIds'
import tonicIds from '../static/tonicIds'
import gemstoreToyIds from '../static/gemstoreToyIds'
import luckItemIds from '../static/luckIds'
import legendaryInsightItemIds from '../static/legendaryInsightItemIds'

export default function (accountData) {
  const items = allItems(accountData)

  return {
    legendaryItems: countItems(items, legendaries.map(x => x.id)),
    legendaryItemsWeapon: countItems(items, legendaries.filter(x => x.type === 'weapon').map(x => x.id)),
    legendaryItemsArmor: countItems(items, legendaries.filter(x => x.type === 'armor').map(x => x.id)),
    legendaryItemsBack: countItems(items, legendaries.filter(x => x.type === 'back').map(x => x.id)),
    fractalTonics: countItems(items, 49277),
    legendaryInsights: countLegendaryInsights(items),
    whiteMantlePortalDevices: countItems(items, 78978),
    chakEggSacks: countItems(items, 72021),
    preservedQueenBees: countItems(items, [68440, 77594]),
    ghostlyInfusions: countItems(items, [
      77366, 77274, 77303, 77310, 77316, 77394
    ]),
    baubleInfusions: countItems(items, [
      78012, 78052, 78054, 78079, 78086, 78090, 78016, 78028,
      78030, 78031, 78057, 78097
    ]),
    luminescentRefractors: countItems(items, [67375, 67370, 67372, 79647]),
    brokenSpoons: countItems(items, 74996),
    blackLionClaimTickets: weightedCountItems(items, {43992: 1, 43998: 0.1}),
    instruments: countItems(items, [43526, 42973, 44883, 66323, 42888, 36174, 68361]),
    permanentTools: countItems(items, permanentToolIds),
    chakEggs: countItems(items, 72205),
    reclaimedMetalPlates: countItems(items, 74356),
    fossilizedInsects: countItems(items, [
      66766, 66655, 66653, 66640, 66636, 66652,
      66649, 66654, 66646, 66644, 66647, 66648,
      66657, 66645, 66656, 66651, 66642
    ]),
    championBags: countItems(items, championBagIds),
    tripleTroubleChests: countItems(items, 49664),
    tequatlChests: countItems(items, 47836),
    uniqueTonics: countItems(items, tonicIds, true),
    bloodRubies: countItems(items, 79280),
    petrifiedWood: countItems(items, 79469),
    tomesOfKnowledge: countItems(items, [43741, 43766]),
    permanentContracts: countItems(items, [
      35984, 38507, 35985, 35986, 49501,
      35978, 35977, 35976, 38506, 78217, 78455
    ]),
    freshWinterberries: countItems(items, 79899),
    wintersHeartInfusions: countItems(items, [
      79959, 79957, 79978, 79994, 79943, 80063
    ]),
    kodasWarmthEnrichment: countItems(items, 79926),
    phospholuminescentInfusions: countItems(items, [
      79665, 79674, 79639, 79653, 79661, 79669
    ]),
    gemstoreToys: countItems(items, gemstoreToyIds),
    blackLionMiniatureClaimTickets: countItems(items, 78474),
    jadeShards: countItems(items, 80332),
    giftsOfExploration: countItems(items, 19677),
    giftsOfBattle: countItems(items, 19678),
    dragoniteOre: weightedCountItems(items, {46733: 1, 46732: 100}),
    bloodstoneDust: weightedCountItems(items, {46731: 1, 46730: 100}),
    empyrealFragments: weightedCountItems(items, {46735: 1, 46734: 100}),
    crystallineOre: countItems(items, [46682, 46683]),
    airshipOil: countItems(items, [69434, 76933]),
    auricDust: weightedCountItems(items, {69432: 1, 76933: 1, 73537: 30}),
    leyLineSparks: countItems(items, [69392, 76933, 74042]),
    legendarySpikes: countItems(items, 81296),
    fireOrchidBlossoms: countItems(items, 81127),
    luck: weightedCountItems(items, luckItemIds)
  }
}

// Get all the items on the account
export function allItems (accountData) {
  const items = [
    charactersItems(accountData),
    bankItems(accountData),
    materialsItems(accountData),
    sharedInventoryItems(accountData)
  ]

  // The "characters" permission is probably missing, in which case
  // we want to abort to prevent issues with multiple API keys
  if (items[0].length === 0) {
    return []
  }

  return items.reduce((a, b) => a.concat(b), [])
}

// Count how many of a list of items the user has
function countItems (items, ids, uniqueItems = false) {
  if (items.length === 0) {
    return null
  }

  // Make sure we always use an array with all ids
  ids = [].concat(ids)
  const idNumbers = ids.reduce((a, b) => a.concat(b), [])

  // Find the items matching our search ids
  items = items.filter(x => idNumbers.indexOf(x.id) !== -1)

  // Add up the amount of items the user has
  if (!uniqueItems) {
    return items.map(x => x.count).reduce((a, b) => a + b, 0)
  }

  // See how many unique items the user has
  items = unique(items.map(x => x.id))

  // Filter arrays of ids that unlock the same thing, see if they
  // appear in the items and then make sure that they only appear once
  ids
    .filter(id => typeof id !== 'number')
    .filter(id => items.find(x => id.indexOf(x) !== -1) !== undefined)
    .map(id => {
      items = items.filter(x => id.indexOf(x) === -1)
      items.push(id[0])
    })

  return items.length
}

// Weighted item counts
function weightedCountItems (items, itemList) {
  if (items.length === 0) {
    return null
  }

  let itemIds = Object.keys(itemList)
  let count = 0

  itemIds.map(id => {
    count += itemList[id] * countItems(items, parseInt(id, 10))
  })

  return round(count, 2)
}

function countLegendaryInsights (items) {
  if (!items.length) {
    return null
  }

  let {refinedIds, perfectedIds, envoyInsignia, giftOfProwess, legendaryInsight} = legendaryInsightItemIds
  let count = 0

  refinedIds.map(id => {
    let itemCount = countItems(items, id)
    count += (itemCount ? (itemCount - 1) * 25 : 0)
  })

  perfectedIds.map(id => {
    let itemCount = countItems(items, id)
    count += (itemCount ? (itemCount * 50) - 25 : 0)
  })

  count += countItems(items, [envoyInsignia, giftOfProwess]) * 25
  count += countItems(items, legendaryInsight)

  return count
}
<commit_msg>:card_index: Add new aura item infusions<commit_after>import unique from 'array-unique'
import round from 'round-to'
import { bankItems } from 'gw2e-account-value/build/bank'
import { materialsItems } from 'gw2e-account-value/build/materials'
import { charactersItems } from 'gw2e-account-value/build/characters'
import { sharedInventoryItems } from 'gw2e-account-value/build/shared'
import legendaries from 'gw2e-static-data/build/legendaries'
import permanentToolIds from '../static/permanentToolIds'
import championBagIds from '../static/championBagIds'
import tonicIds from '../static/tonicIds'
import gemstoreToyIds from '../static/gemstoreToyIds'
import luckItemIds from '../static/luckIds'
import legendaryInsightItemIds from '../static/legendaryInsightItemIds'

export default function (accountData) {
  const items = allItems(accountData)

  return {
    legendaryItems: countItems(items, legendaries.map(x => x.id)),
    legendaryItemsWeapon: countItems(items, legendaries.filter(x => x.type === 'weapon').map(x => x.id)),
    legendaryItemsArmor: countItems(items, legendaries.filter(x => x.type === 'armor').map(x => x.id)),
    legendaryItemsBack: countItems(items, legendaries.filter(x => x.type === 'back').map(x => x.id)),
    fractalTonics: countItems(items, 49277),
    legendaryInsights: countLegendaryInsights(items),
    whiteMantlePortalDevices: countItems(items, 78978),
    chakEggSacks: countItems(items, [72021, 81825]),
    preservedQueenBees: countItems(items, [68440, 77594, 81663]),
    ghostlyInfusions: countItems(items, [
      77366, 77274, 77303, 77310, 77316, 77394
    ]),
    baubleInfusions: countItems(items, [
      78012, 78052, 78054, 78079, 78086, 78090, 78016, 78028,
      78030, 78031, 78057, 78097
    ]),
    luminescentRefractors: countItems(items, [
      67375, 67370, 67372, 79647, 82013, 81810, 81877, 82055,
      81678, 81809, 81864, 81847, 81930, 81777, 81653
    ]),
    brokenSpoons: countItems(items, 74996),
    blackLionClaimTickets: weightedCountItems(items, {43992: 1, 43998: 0.1}),
    instruments: countItems(items, [43526, 42973, 44883, 66323, 42888, 36174, 68361]),
    permanentTools: countItems(items, permanentToolIds),
    chakEggs: countItems(items, 72205),
    reclaimedMetalPlates: countItems(items, 74356),
    fossilizedInsects: countItems(items, [
      66766, 66655, 66653, 66640, 66636, 66652,
      66649, 66654, 66646, 66644, 66647, 66648,
      66657, 66645, 66656, 66651, 66642
    ]),
    championBags: countItems(items, championBagIds),
    tripleTroubleChests: countItems(items, 49664),
    tequatlChests: countItems(items, 47836),
    uniqueTonics: countItems(items, tonicIds, true),
    bloodRubies: countItems(items, 79280),
    petrifiedWood: countItems(items, 79469),
    tomesOfKnowledge: countItems(items, [43741, 43766]),
    permanentContracts: countItems(items, [
      35984, 38507, 35985, 35986, 49501,
      35978, 35977, 35976, 38506, 78217, 78455
    ]),
    freshWinterberries: countItems(items, 79899),
    wintersHeartInfusions: countItems(items, [
      79959, 79957, 79978, 79994, 79943, 80063
    ]),
    kodasWarmthEnrichment: countItems(items, 79926),
    phospholuminescentInfusions: countItems(items, [
      79665, 79674, 79639, 79653, 79661, 79669
    ]),
    gemstoreToys: countItems(items, gemstoreToyIds),
    blackLionMiniatureClaimTickets: countItems(items, 78474),
    jadeShards: countItems(items, 80332),
    giftsOfExploration: countItems(items, 19677),
    giftsOfBattle: countItems(items, 19678),
    dragoniteOre: weightedCountItems(items, {46733: 1, 46732: 100}),
    bloodstoneDust: weightedCountItems(items, {46731: 1, 46730: 100}),
    empyrealFragments: weightedCountItems(items, {46735: 1, 46734: 100}),
    crystallineOre: countItems(items, [46682, 46683]),
    airshipOil: countItems(items, [69434, 76933]),
    auricDust: weightedCountItems(items, {69432: 1, 76933: 1, 73537: 30}),
    leyLineSparks: countItems(items, [69392, 76933, 74042]),
    legendarySpikes: countItems(items, 81296),
    fireOrchidBlossoms: countItems(items, 81127),
    luck: weightedCountItems(items, luckItemIds)
  }
}

// Get all the items on the account
export function allItems (accountData) {
  const items = [
    charactersItems(accountData),
    bankItems(accountData),
    materialsItems(accountData),
    sharedInventoryItems(accountData)
  ]

  // The "characters" permission is probably missing, in which case
  // we want to abort to prevent issues with multiple API keys
  if (items[0].length === 0) {
    return []
  }

  return items.reduce((a, b) => a.concat(b), [])
}

// Count how many of a list of items the user has
function countItems (items, ids, uniqueItems = false) {
  if (items.length === 0) {
    return null
  }

  // Make sure we always use an array with all ids
  ids = [].concat(ids)
  const idNumbers = ids.reduce((a, b) => a.concat(b), [])

  // Find the items matching our search ids
  items = items.filter(x => idNumbers.indexOf(x.id) !== -1)

  // Add up the amount of items the user has
  if (!uniqueItems) {
    return items.map(x => x.count).reduce((a, b) => a + b, 0)
  }

  // See how many unique items the user has
  items = unique(items.map(x => x.id))

  // Filter arrays of ids that unlock the same thing, see if they
  // appear in the items and then make sure that they only appear once
  ids
    .filter(id => typeof id !== 'number')
    .filter(id => items.find(x => id.indexOf(x) !== -1) !== undefined)
    .map(id => {
      items = items.filter(x => id.indexOf(x) === -1)
      items.push(id[0])
    })

  return items.length
}

// Weighted item counts
function weightedCountItems (items, itemList) {
  if (items.length === 0) {
    return null
  }

  let itemIds = Object.keys(itemList)
  let count = 0

  itemIds.map(id => {
    count += itemList[id] * countItems(items, parseInt(id, 10))
  })

  return round(count, 2)
}

function countLegendaryInsights (items) {
  if (!items.length) {
    return null
  }

  let {refinedIds, perfectedIds, envoyInsignia, giftOfProwess, legendaryInsight} = legendaryInsightItemIds
  let count = 0

  refinedIds.map(id => {
    let itemCount = countItems(items, id)
    count += (itemCount ? (itemCount - 1) * 25 : 0)
  })

  perfectedIds.map(id => {
    let itemCount = countItems(items, id)
    count += (itemCount ? (itemCount * 50) - 25 : 0)
  })

  count += countItems(items, [envoyInsignia, giftOfProwess]) * 25
  count += countItems(items, legendaryInsight)

  return count
}
<|endoftext|>


<commit_before>QUnit.config.updateRate = 1;

QUnit.module( "Module that mucks with time", {
	beforeEach: function() {
		this.setTimeout = window.setTimeout;
		window.setTimeout = function() {};
	},

	afterEach: function() {
		window.setTimeout = this.setTimeout;
	}
});

QUnit.test( "just a test", function( assert ) {
	assert.ok( true );
});

QUnit.test( "just a test", function( assert ) {
	assert.ok( true );
});
<commit_msg>Tests: Prepare setTimeout tests to run along the tests<commit_after>(function( window ) {

QUnit.module( "Module that mucks with time", {
	beforeEach: function() {
		this.setTimeout = window.setTimeout;
		window.setTimeout = function() {};
	},

	afterEach: function() {
		window.setTimeout = this.setTimeout;
	}
});

QUnit.test( "just a test", function( assert ) {
	assert.ok( true );
});

QUnit.test( "just a test", function( assert ) {
	assert.ok( true );
});

}( (function() {
	return this;
})() ));
<|endoftext|>


<commit_before>'use strict';
const expect = require('chai').expect;
const path = require('path');
const fs = require('fs');

const resultUrls = require('../lib/core/resultsStorage/resultUrls');
const messageMaker = require('../lib/support/messageMaker');
const filterRegistry = require('../lib/support/filterRegistry');
const intel = require('intel');
const statsHelpers = require('../lib/support/statsHelpers');

const coachRunPath = path.resolve(__dirname, 'fixtures', 'coach.run-0.json');
const coachRun = JSON.parse(fs.readFileSync(coachRunPath, 'utf8'));

const DataCollector = require('../lib/plugins/slack/dataCollector');

const defaultContextFactory = (context = {}) => {
  return Object.assign(
    {
      messageMaker,
      filterRegistry,
      intel,
      statsHelpers,
      resultUrls: resultUrls()
    },
    context
  );
};

describe('slack', () => {
  describe('summary', () => {
    const getSummary = require('../lib/plugins/slack/summary');

    it('should not hard crash without a name', () => {
      const dataCollector = new DataCollector(defaultContextFactory());
      const options = {
        browsertime: {
          browser: 'mosaic',
          iterations: 5
        }
      };
      const codeUnderTest = () =>
        getSummary(dataCollector, [], resultUrls(), undefined, options);
      expect(codeUnderTest).to.not.throw();

      const summary = codeUnderTest();
      expect(summary.summaryText).to.have.string(
        '0 pages analysed for Unknown (5 runs, Mosaic/desktop/unknown)'
      );
    });

    it('should not hard crash with undefined browsertime options', () => {
      const dataCollector = new DataCollector(defaultContextFactory());
      const options = {
        browsertime: undefined
      };
      const codeUnderTest = () =>
        getSummary(dataCollector, [], resultUrls(), '<TEST NAME>', options);
      expect(codeUnderTest).to.not.throw();

      const summary = codeUnderTest();
      expect(summary.summaryText).to.have.string(
        '0 pages analysed for <TEST NAME> (0 runs, Unknown/desktop/unknown)'
      );
    });
  });

  describe('attachments', () => {
    const getAttachments = require('../lib/plugins/slack/attachements');

    it('should always return a list', () => {
      expect(getAttachments()).to.be.an('array').that.is.empty;
    });
  });

  describe('DataCollector', () => {
    it('should have an empty list of pages', () => {
      const context = defaultContextFactory();
      const collector = new DataCollector(context);
      expect(collector.getURLs()).to.be.an('array').that.is.empty;
    });

    it('add data should add new page URL', () => {
      const context = defaultContextFactory();
      const collector = new DataCollector(context);

      collector.addDataForUrl(
        'https://fake-site.sitespeed.io',
        'coach.run',
        { coach: { pageSummary: coachRun } },
        undefined
      );

      expect(collector.getURLs()).to.eql(['https://fake-site.sitespeed.io']);
    });
  });

  describe('index.open', () => {
    const plugin = require('../lib/plugins/slack');

    it('without any valid option', () => {
      const undefinedOptions = () => {
        plugin.open(defaultContextFactory(), undefined);
      };
      expect(undefinedOptions).to.throw(
        /^Required option\(s\) .+(hookUrl|userName).+ need to be specified in namespace "slack"$/
      );

      const emptyOptions = () => {
        plugin.open(defaultContextFactory(), {});
      };
      expect(emptyOptions).to.throw(
        /^Required option\(s\) .+(hookUrl|userName).+ need to be specified in namespace "slack"$/
      );

      const undefinedSlackOptions = () => {
        plugin.open(defaultContextFactory(), { slack: undefined });
      };
      expect(undefinedSlackOptions).to.throw(
        /^Required option\(s\) .+(hookUrl|userName).+ need to be specified in namespace "slack"$/
      );

      const emptySlackOptions = () => {
        plugin.open(defaultContextFactory(), { slack: {} });
      };
      expect(emptySlackOptions).to.throw(
        /^Required option\(s\) .+(hookUrl|userName).+ need to be specified in namespace "slack"$/
      );
    });

    it('should require userName with hookUrl option', () => {
      const codeUnderTest = () => {
        plugin.open(defaultContextFactory(), {
          hookUrl: 'https://fake-slack.sitespeed.io/hook'
        });
      };
      expect(codeUnderTest).to.throw(
        /^Required option\(s\) .+(hookUrl|userName).+ need to be specified in namespace "slack"$/
      );
    });

    it('should require hookUrl with userName option', () => {
      const codeUnderTest = () => {
        plugin.open(defaultContextFactory(), {
          userName: 'Sitespeed.io'
        });
      };
      expect(codeUnderTest).to.throw(
        /^Required option\(s\) .+(hookUrl|userName).+ need to be specified in namespace "slack"$/
      );
    });

    it('should set plugin state for all valid options', () => {
      const options = Object.assign({}, plugin.config, {
        hookUrl: 'https://fake-slack.sitespeed.io/hook'
      });
      const context = defaultContextFactory();
      plugin.open(context, {
        slack: options
      });
      expect(plugin.options.slack).to.equal(options);
      expect(plugin.context).to.equal(context);
    });
  });

  describe('index.processMessage', () => {
    const Slack = require('node-slack');
    const realSend = Slack.prototype.send;

    function mockSend() {
      function mock() {
        mock.called = true;
        mock.callArgs = Array.from(arguments);
      }
      mock.called = false;
      mock.callArgs = [];
      Slack.prototype.send = mock;
      return mock;
    }

    function pluginFactory(context, extraOptions = {}) {
      const plugin = require('../lib/plugins/slack');

      const slackOptions = Object.assign({}, plugin.config, {
        hookUrl: 'https://fake-slack.sitespeed.io/hook'
      });
      const options = Object.assign(
        {
          slack: slackOptions,
          browsertime: {
            iterations: 1,
            browser: 'Chrome'
          }
        },
        extraOptions
      );
      plugin.open(context, options);
      plugin.processMessage({
        type: 'coach.pageSummary',
        url: 'http://fake-page.sitespeed.io/coach-run',
        data: coachRun,
        runIndex: undefined
      });
      return plugin;
    }

    afterEach(() => {
      Slack.prototype.send = realSend;
    });

    it('should send message for html.finished message without baseUrl', () => {
      const context = defaultContextFactory({
        name: 'Simple test'
      });
      const plugin = pluginFactory(context);
      const mock = mockSend();
      plugin.processMessage({ type: 'html.finished' });

      expect(mock.called).to.be.true;
      const params = mock.callArgs[0];
      expect(params.text).to.equal(
        '1 page analysed for Simple test (1 run, Chrome/desktop/unknown)\n*Site summary*\nPage transfer weight: N/A\n\n'
      );
    });

    it('results URL should be part of the the report if baseUrl is provided', () => {
      const context = defaultContextFactory({
        name: 'Simple test'
      });
      context.resultUrls = resultUrls(
        'https://results.sitespeed.io/absolute/path'
      );
      const plugin = pluginFactory(context);
      const mock = mockSend();
      plugin.processMessage({ type: 'html.finished' });

      expect(mock.called).to.be.true;
      const params = mock.callArgs[0];
      expect(params.text).to.equal(
        '1 page analysed for Simple test (1 run, Chrome/desktop/unknown)\n*Site summary* ' +
          '(<https://results.sitespeed.io/absolute/path/index.html |result>)\nPage transfer weight: N/A\n\n'
      );
    });

    it('should not send message for html.finished with s3 configured', () => {
      const context = defaultContextFactory({
        name: 'S3 configured'
      });
      const plugin = pluginFactory(context, {
        s3: { bucketname: 'sitespeed_data_bucket' }
      });

      const mock = mockSend();
      expect(Slack.prototype.send).to.equal(mock);

      plugin.processMessage({ type: 'html.finished' });
      expect(mock.called).to.be.false;

      plugin.processMessage({ type: 's3.finished' });
      expect(mock.called).to.be.true;
      expect(mock.callArgs[0].text).to.equal(
        '1 page analysed for S3 configured (1 run, Chrome/desktop/unknown)\n*Site summary*\nPage transfer weight: N/A\n\n'
      );
    });

    it('should not send message for html.finished with gcs configured', () => {
      const context = defaultContextFactory({
        name: 'GCS'
      });
      const plugin = pluginFactory(context, {
        gcs: {
          bucketname: 'sitespeed_data_bucket',
          projectId: 123456789,
          key: 'sitespeed-1234567890'
        }
      });

      const mock = mockSend();
      expect(Slack.prototype.send).to.equal(mock);

      plugin.processMessage({ type: 'html.finished' });
      expect(mock.called).to.be.false;

      plugin.processMessage({ type: 'gcs.finished' });
      expect(mock.called).to.be.true;
      expect(mock.callArgs[0].text).to.equal(
        '1 page analysed for GCS (1 run, Chrome/desktop/unknown)\n*Site summary*\nPage transfer weight: N/A\n\n'
      );
    });
  });
});
<commit_msg>Fix failing Slack tests<commit_after>'use strict';
const expect = require('chai').expect;
const path = require('path');
const fs = require('fs');

const resultUrls = require('../lib/core/resultsStorage/resultUrls');
const messageMaker = require('../lib/support/messageMaker');
const filterRegistry = require('../lib/support/filterRegistry');
const intel = require('intel');
const statsHelpers = require('../lib/support/statsHelpers');

const coachRunPath = path.resolve(__dirname, 'fixtures', 'coach.run-0.json');
const coachRun = JSON.parse(fs.readFileSync(coachRunPath, 'utf8'));

const DataCollector = require('../lib/plugins/slack/dataCollector');

const defaultContextFactory = (context = {}) => {
  return Object.assign(
    {
      messageMaker,
      filterRegistry,
      intel,
      statsHelpers,
      resultUrls: resultUrls()
    },
    context
  );
};

describe('slack', () => {
  describe('summary', () => {
    const getSummary = require('../lib/plugins/slack/summary');

    it('should not hard crash without a name', () => {
      const dataCollector = new DataCollector(defaultContextFactory());
      const options = {
        browsertime: {
          browser: 'mosaic',
          iterations: 5
        }
      };
      const codeUnderTest = () =>
        getSummary(dataCollector, [], resultUrls(), undefined, options);
      expect(codeUnderTest).to.not.throw();

      const summary = codeUnderTest();
      expect(summary.summaryText).to.have.string(
        '0 pages analysed for Unknown (5 runs, Mosaic/desktop/unknown)'
      );
    });

    it('should not hard crash with undefined browsertime options', () => {
      const dataCollector = new DataCollector(defaultContextFactory());
      const options = {
        browsertime: undefined
      };
      const codeUnderTest = () =>
        getSummary(dataCollector, [], resultUrls(), '<TEST NAME>', options);
      expect(codeUnderTest).to.not.throw();

      const summary = codeUnderTest();
      expect(summary.summaryText).to.have.string(
        '0 pages analysed for <TEST NAME> (0 runs, Unknown/desktop/unknown)'
      );
    });
  });

  describe('attachments', () => {
    const getAttachments = require('../lib/plugins/slack/attachements');

    it('should always return a list', () => {
      expect(getAttachments()).to.be.an('array').that.is.empty;
    });
  });

  describe('DataCollector', () => {
    it('should have an empty list of pages', () => {
      const context = defaultContextFactory();
      const collector = new DataCollector(context);
      expect(collector.getURLs()).to.be.an('array').that.is.empty;
    });

    it('add data should add new page URL', () => {
      const context = defaultContextFactory();
      const collector = new DataCollector(context);

      collector.addDataForUrl(
        'https://fake-site.sitespeed.io',
        'coach.run',
        { coach: { pageSummary: coachRun } },
        undefined
      );

      expect(collector.getURLs()).to.eql(['https://fake-site.sitespeed.io']);
    });
  });

  describe('index.open', () => {
    const plugin = require('../lib/plugins/slack');

    it('without any valid option', () => {
      const undefinedOptions = () => {
        plugin.open(defaultContextFactory(), undefined);
      };
      expect(undefinedOptions).to.throw(
        /^Required option\(s\) .+(hookUrl|userName).+ need to be specified in namespace "slack"$/
      );

      const emptyOptions = () => {
        plugin.open(defaultContextFactory(), {});
      };
      expect(emptyOptions).to.throw(
        /^Required option\(s\) .+(hookUrl|userName).+ need to be specified in namespace "slack"$/
      );

      const undefinedSlackOptions = () => {
        plugin.open(defaultContextFactory(), { slack: undefined });
      };
      expect(undefinedSlackOptions).to.throw(
        /^Required option\(s\) .+(hookUrl|userName).+ need to be specified in namespace "slack"$/
      );

      const emptySlackOptions = () => {
        plugin.open(defaultContextFactory(), { slack: {} });
      };
      expect(emptySlackOptions).to.throw(
        /^Required option\(s\) .+(hookUrl|userName).+ need to be specified in namespace "slack"$/
      );
    });

    it('should require userName with hookUrl option', () => {
      const codeUnderTest = () => {
        plugin.open(defaultContextFactory(), {
          hookUrl: 'https://fake-slack.sitespeed.io/hook'
        });
      };
      expect(codeUnderTest).to.throw(
        /^Required option\(s\) .+(hookUrl|userName).+ need to be specified in namespace "slack"$/
      );
    });

    it('should require hookUrl with userName option', () => {
      const codeUnderTest = () => {
        plugin.open(defaultContextFactory(), {
          userName: 'Sitespeed.io'
        });
      };
      expect(codeUnderTest).to.throw(
        /^Required option\(s\) .+(hookUrl|userName).+ need to be specified in namespace "slack"$/
      );
    });

    it('should set plugin state for all valid options', () => {
      const options = Object.assign({}, plugin.config, {
        hookUrl: 'https://fake-slack.sitespeed.io/hook'
      });
      const context = defaultContextFactory();
      plugin.open(context, {
        slack: options
      });
      expect(plugin.options.slack).to.equal(options);
      expect(plugin.context).to.equal(context);
    });
  });

  describe('index.processMessage', () => {
    const Slack = require('node-slack');
    const realSend = Slack.prototype.send;

    function mockSend() {
      function mock() {
        mock.called = true;
        mock.callArgs = Array.from(arguments);
      }
      mock.called = false;
      mock.callArgs = [];
      Slack.prototype.send = mock;
      return mock;
    }

    function pluginFactory(context, extraOptions = {}) {
      const plugin = require('../lib/plugins/slack');

      const slackOptions = Object.assign({}, plugin.config, {
        hookUrl: 'https://fake-slack.sitespeed.io/hook'
      });
      const options = Object.assign(
        {
          slack: slackOptions,
          browsertime: {
            iterations: 1,
            browser: 'Chrome'
          }
        },
        extraOptions
      );
      plugin.open(context, options);
      plugin.processMessage({
        type: 'coach.pageSummary',
        url: 'http://fake-page.sitespeed.io/coach-run',
        data: coachRun,
        runIndex: undefined
      });
      return plugin;
    }

    afterEach(() => {
      Slack.prototype.send = realSend;
    });

    it('should send message for html.finished message without baseUrl', () => {
      const context = defaultContextFactory({
        name: 'Simple test'
      });
      const plugin = pluginFactory(context);
      const mock = mockSend();
      plugin.processMessage({ type: 'html.finished' });

      expect(mock.called).to.be.true;
      const params = mock.callArgs[0];
      expect(params.text).to.equal(
        '1 page analysed for Simple test (1 run, Chrome/desktop/unknown)\n*Site summary*\n\n'
      );
    });

    it('results URL should be part of the the report if baseUrl is provided', () => {
      const context = defaultContextFactory({
        name: 'Simple test'
      });
      context.resultUrls = resultUrls(
        'https://results.sitespeed.io/absolute/path'
      );
      const plugin = pluginFactory(context);
      const mock = mockSend();
      plugin.processMessage({ type: 'html.finished' });

      expect(mock.called).to.be.true;
      const params = mock.callArgs[0];
      expect(params.text).to.equal(
        '1 page analysed for Simple test (1 run, Chrome/desktop/unknown)\n*Site summary* ' +
          '(<https://results.sitespeed.io/absolute/path/index.html |result>)\n\n'
      );
    });

    it('should not send message for html.finished with s3 configured', () => {
      const context = defaultContextFactory({
        name: 'S3 configured'
      });
      const plugin = pluginFactory(context, {
        s3: { bucketname: 'sitespeed_data_bucket' }
      });

      const mock = mockSend();
      expect(Slack.prototype.send).to.equal(mock);

      plugin.processMessage({ type: 'html.finished' });
      expect(mock.called).to.be.false;

      plugin.processMessage({ type: 's3.finished' });
      expect(mock.called).to.be.true;
      expect(mock.callArgs[0].text).to.equal(
        '1 page analysed for S3 configured (1 run, Chrome/desktop/unknown)\n*Site summary*\n\n'
      );
    });

    it('should not send message for html.finished with gcs configured', () => {
      const context = defaultContextFactory({
        name: 'GCS'
      });
      const plugin = pluginFactory(context, {
        gcs: {
          bucketname: 'sitespeed_data_bucket',
          projectId: 123456789,
          key: 'sitespeed-1234567890'
        }
      });

      const mock = mockSend();
      expect(Slack.prototype.send).to.equal(mock);

      plugin.processMessage({ type: 'html.finished' });
      expect(mock.called).to.be.false;

      plugin.processMessage({ type: 'gcs.finished' });
      expect(mock.called).to.be.true;
      expect(mock.callArgs[0].text).to.equal(
        '1 page analysed for GCS (1 run, Chrome/desktop/unknown)\n*Site summary*\n\n'
      );
    });
  });
});
<|endoftext|>


<commit_before>import { action, reaction, observable, computed, toJS, ObservableMap } from 'mobx';
import fb from '../service/firebase';
import userStore from './UserStore';
import uiStore from './UiStore';
import Rx from 'rxjs';

export class PeerStore {
  stunTurn = { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' }] }
  signalRef = fb.fbdb.child('signaling');
  userProfiles;
  userProfile;
  @observable smallVidRef;
  @observable largeVidRef;
  @observable calleeStream;
  @observable isCallee = false;
 /*  @observable stream = new MediaStream(); */
  @observable onTrackStreams;
  iceStorage = [];
  replyTo;
  constraints = {
    mandatory: {
      'OfferToReceiveAudio': true,
      'OfferToReceiveVideo': true
    },
    'offerToReceiveAudio': true,
    'offerToReceiveVideo': true
  }
  constructor(uiStore, userStore) {
    this.peer = new RTCPeerConnection(this.stunTurn);
    fb.fbdb.child('users').on('value', (snap) => this.userProfiles = snap.val());

    this.signalRef.on('child_added', this.recvMsg.bind(this));

   /*  Rx.Observable.fromEvent(this.peer, 'track')
      .subscribe(event => console.log(event)) */

    /* Rx.Observable.fromEvent(this.peer, 'addstream')
      .subscribe(event => {
	      console.log(event);
	      console.log('add stream finally fired');
	      this.addLgStream(event.stream);
	    }); */
    Rx.Observable.fromEvent(this.peer, 'icecandidate')
      .subscribe(evt => evt.candidate ? this.sendPeerMsg(JSON.stringify({ 'ice': evt.candidate })) : console.log('end of ice'))

    Rx.Observable.fromEvent(this.peer, 'datachannel')
      .subscribe(event => console.log(event))

    Rx.Observable.fromEvent(this.peer, 'removestream')
      .subscribe(event => console.log(event))

    /* Rx.Observable.fromEvent(this.peer, 'signalingstatechange')
      .subscribe(event => console.log(event))

    Rx.Observable.fromEvent(this.peer, 'negotiationneeded')
      .subscribe(event => console.log(event))

    Rx.Observable.fromEvent(this.peer, 'iceconnectionstatechange')
      .subscribe(event => console.log(event))

    Rx.Observable.fromEvent(this.peer, 'icegatheringstatechange')
      .subscribe(event => console.log(event)) */

      //this.peer.ontrack = this.streamAdder.bind(this);
      this.peer.ontrack = e => {
        console.log(e);
        console.log('ontrack for remote stream');
        this.addLargeVid(e.streams)
      };
  }
  @action addLargeVid(streams) {
    console.log(streams);
    this.largeVidRef.srcObject = streams;
   // this.onTrackStreams = streams[0];
  }

  getLocalVideoFeed() {
    return navigator.mediaDevices.getUserMedia({audio:true, video: true})
  }
  sendPeerMsg(data) {
    let recv = userStore.caller || Object.keys(userStore.callee)[0];
    const msg = this.signalRef.push({
      sender: userStore.currentUser.uid,
      receiver: recv,
      message: data
    })
    msg.remove();
  }
  @action setIsCallee(val) {
    this.callee = val;
  }
  makePeerConnection(email) {
    userStore.findCalleeByEmail(email)
      .then(() =>{
        uiStore.openVideo(); // might be a race condition ???
        return this.getLocalVideoFeed()
      })
      .then((stream) =>{
        this.smallVidRef.srcObject = stream;
         return stream.getTracks().forEach(track => this.peer.addTrack(track,stream))
      })
      .then(() => this.peer.createOffer(this.constraints))
      .then(offer => this.peer.setLocalDescription(offer)) //ICE candidate immediately start firing
      .then(() => this.sendPeerMsg(JSON.stringify({ 'sdp': this.peer.localDescription })))
      .catch(err => console.log(err));
  }
  @action recvMsg(data) {
    let { message, receiver, sender } = data.val();
    message = JSON.parse(message);
    if (userStore.currentUser.uid == receiver) {
// if (confirm('Except Video Call from'))
      if (message.ice) {
        console.log('ice firing off correctly')
        this.iceStorage.push(message.ice)
      } else if (message['sdp'] != undefined && message['sdp']['type'] == "offer") {
        userStore.caller = sender;
        this.peer.setRemoteDescription(new RTCSessionDescription(message.sdp))
          .then(() => {
            uiStore.openVideo(); // might be a race condition ???
            return this.getLocalVideoFeed()
          })
          .then((stream) => {
            this.smallVidRef.srcObject = stream;
            return stream.getTracks().forEach(track => this.peer.addTrack(track,stream))
          })
          .then(() => this.peer.createAnswer(this.constraints))
          .then(answer => this.peer.setLocalDescription(answer))
          .then(() => this.iceStorage.forEach(icee => this.peer.addIceCandidate(new RTCIceCandidate(icee))))
          .then(() => this.setIsCallee(true))
          .then(() => this.sendPeerMsg(JSON.stringify({ 'sdp': this.peer.localDescription })))
          .catch(err => console.log(err))
    } else if (message['sdp'] != undefined && message['sdp']['type'] == "answer") {
      //debugger;
      this.peer.setRemoteDescription(new RTCSessionDescription(message.sdp))
        //.then(() => { this.iceStorage.forEach( icee => this.peer.addIceCandidate(new RTCIceCandidate(icee)) ) })
        //.then(() =>  this.peer.getRemoteStreams()[0]);
    } else {
      console.log('fuck it')
    }
  }
}
}
const peerStore = window.peerStore = new PeerStore();
export default peerStore;
<commit_msg> ref the array element with the media stream<commit_after>import { action, reaction, observable, computed, toJS, ObservableMap } from 'mobx';
import fb from '../service/firebase';
import userStore from './UserStore';
import uiStore from './UiStore';
import Rx from 'rxjs';

export class PeerStore {
  stunTurn = { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' }] }
  signalRef = fb.fbdb.child('signaling');
  userProfiles;
  userProfile;
  @observable smallVidRef;
  @observable largeVidRef;
  @observable calleeStream;
  @observable isCallee = false;
 /*  @observable stream = new MediaStream(); */
  @observable onTrackStreams;
  iceStorage = [];
  replyTo;
  constraints = {
    mandatory: {
      'OfferToReceiveAudio': true,
      'OfferToReceiveVideo': true
    },
    'offerToReceiveAudio': true,
    'offerToReceiveVideo': true
  }
  constructor(uiStore, userStore) {
    this.peer = new RTCPeerConnection(this.stunTurn);
    fb.fbdb.child('users').on('value', (snap) => this.userProfiles = snap.val());

    this.signalRef.on('child_added', this.recvMsg.bind(this));

   /*  Rx.Observable.fromEvent(this.peer, 'track')
      .subscribe(event => console.log(event)) */

    /* Rx.Observable.fromEvent(this.peer, 'addstream')
      .subscribe(event => {
	      console.log(event);
	      console.log('add stream finally fired');
	      this.addLgStream(event.stream);
	    }); */
    Rx.Observable.fromEvent(this.peer, 'icecandidate')
      .subscribe(evt => evt.candidate ? this.sendPeerMsg(JSON.stringify({ 'ice': evt.candidate })) : console.log('end of ice'))

    Rx.Observable.fromEvent(this.peer, 'datachannel')
      .subscribe(event => console.log(event))

    Rx.Observable.fromEvent(this.peer, 'removestream')
      .subscribe(event => console.log(event))

    /* Rx.Observable.fromEvent(this.peer, 'signalingstatechange')
      .subscribe(event => console.log(event))

    Rx.Observable.fromEvent(this.peer, 'negotiationneeded')
      .subscribe(event => console.log(event))

    Rx.Observable.fromEvent(this.peer, 'iceconnectionstatechange')
      .subscribe(event => console.log(event))

    Rx.Observable.fromEvent(this.peer, 'icegatheringstatechange')
      .subscribe(event => console.log(event)) */

      //this.peer.ontrack = this.streamAdder.bind(this);
      this.peer.ontrack = e => {
        console.log(e);
        console.log('ontrack for remote stream');
        this.addLargeVid(e.streams)
      };
  }
  @action addLargeVid(streams) {
    console.log(streams);
    this.largeVidRef.srcObject = streams[0];
   // this.onTrackStreams = streams[0];
  }

  getLocalVideoFeed() {
    return navigator.mediaDevices.getUserMedia({audio:true, video: true})
  }
  sendPeerMsg(data) {
    let recv = userStore.caller || Object.keys(userStore.callee)[0];
    const msg = this.signalRef.push({
      sender: userStore.currentUser.uid,
      receiver: recv,
      message: data
    })
    msg.remove();
  }
  @action setIsCallee(val) {
    this.callee = val;
  }
  makePeerConnection(email) {
    userStore.findCalleeByEmail(email)
      .then(() =>{
        uiStore.openVideo(); // might be a race condition ???
        return this.getLocalVideoFeed()
      })
      .then((stream) =>{
        this.smallVidRef.srcObject = stream;
         return stream.getTracks().forEach(track => this.peer.addTrack(track,stream))
      })
      .then(() => this.peer.createOffer(this.constraints))
      .then(offer => this.peer.setLocalDescription(offer)) //ICE candidate immediately start firing
      .then(() => this.sendPeerMsg(JSON.stringify({ 'sdp': this.peer.localDescription })))
      .catch(err => console.log(err));
  }
  @action recvMsg(data) {
    let { message, receiver, sender } = data.val();
    message = JSON.parse(message);
    if (userStore.currentUser.uid == receiver) {
// if (confirm('Except Video Call from'))
      if (message.ice) {
        console.log('ice firing off correctly')
        this.iceStorage.push(message.ice)
      } else if (message['sdp'] != undefined && message['sdp']['type'] == "offer") {
        userStore.caller = sender;
        this.peer.setRemoteDescription(new RTCSessionDescription(message.sdp))
          .then(() => {
            uiStore.openVideo(); // might be a race condition ???
            return this.getLocalVideoFeed()
          })
          .then((stream) => {
            this.smallVidRef.srcObject = stream;
            return stream.getTracks().forEach(track => this.peer.addTrack(track,stream))
          })
          .then(() => this.peer.createAnswer(this.constraints))
          .then(answer => this.peer.setLocalDescription(answer))
          .then(() => this.iceStorage.forEach(icee => this.peer.addIceCandidate(new RTCIceCandidate(icee))))
          .then(() => this.setIsCallee(true))
          .then(() => this.sendPeerMsg(JSON.stringify({ 'sdp': this.peer.localDescription })))
          .catch(err => console.log(err))
    } else if (message['sdp'] != undefined && message['sdp']['type'] == "answer") {
      //debugger;
      this.peer.setRemoteDescription(new RTCSessionDescription(message.sdp))
        //.then(() => { this.iceStorage.forEach( icee => this.peer.addIceCandidate(new RTCIceCandidate(icee)) ) })
        //.then(() =>  this.peer.getRemoteStreams()[0]);
    } else {
      console.log('fuck it')
    }
  }
}
}
const peerStore = window.peerStore = new PeerStore();
export default peerStore;
<|endoftext|>


<commit_before>const { MessageEmbed } = require('discord.js');
const path = require('path');
const Star = require('../models/stars');
const Queue = require('./Queue');

class Starboard {
	constructor(guild) {
		this.client = guild.client;
		this.guild = guild;
		this.stars = new Map();
		this.queue = new Queue();

		Star.findAll({ where: { guildID: this.guild.id } }).then(stars => {
			for (const star of stars) {
				this.stars.set(star.messageID, star);
			}
		});
	}

	get channel() {
		const channelID = this.client.settings.get(this.guild, 'starboardChannelID');
		return this.guild.channels.get(channelID);
	}

	add(message, starredBy) {
		if (!this.channel) {
			return 'There isn\'t a starboard channel to use.';
		}

		if (message.author.id === starredBy.id) {
			return 'You can\'t star your own messages.';
		}

		const missingPerms = this.missingPermissions();
		if (missingPerms) {
			return missingPerms;
		}

		if (this.stars.has(message.id)) {
			const star = this.stars.get(message.id);
			if (star.starredBy.includes(starredBy.id)) {
				return 'You have already starred this message before; You can\'t star it again.';
			}
		}

		this.queue.add(this.addStar.bind(this, message, starredBy));
		return undefined;
	}

	async addStar(message, user) {
		if (!this.stars.has(message.id)) {
			const starboardMessage = await this.channel.send({ embed: Starboard.buildStarboardEmbed(message) });

			const star = await Star.create({
				messageID: message.id,
				authorID: message.author.id,
				channelID: message.channel.id,
				guildID: this.guild.id,
				starboardMessageID: starboardMessage.id,
				starredBy: [user.id]
			});

			this.stars.set(message.id, star);
			return;
		}

		const star = this.stars.get(message.id);
		const newStarredBy = star.starredBy.concat([user.id]);

		const starboardMessage = await this.channel.fetchMessage(star.starboardMessageID);
		await starboardMessage.edit({ embed: Starboard.buildStarboardEmbed(message, newStarredBy.length) });

		const newStar = await star.update({
			starCount: newStarredBy.length,
			starredBy: newStarredBy
		});

		this.stars.set(message.id, newStar);
	}

	remove(message, unstarredBy) {
		if (!this.channel) {
			return 'There isn\'t a starboard channel to use.';
		}

		const star = this.stars.get(message.id);
		if (!star || !star.starredBy.includes(unstarredBy.id)) {
			return 'You can\'t remove any star from this message because you never gave it one in the first place.';
		}

		const missingPerms = this.missingPermissions();
		if (missingPerms) {
			return missingPerms;
		}

		this.queue.add(this.removeStar.bind(this, message, unstarredBy));
		return undefined;
	}

	async removeStar(message, user) {
		const star = this.stars.get(message.id);

		if (message.reactions.has('⭐') && message.reactions.get('⭐').users.has(user.id)) {
			await message.reactions.get('⭐').remove(user);
		}

		const newStarredBy = star.starredBy.filter(id => id !== user.id);
		const starboardMessage = await this.channel.fetchMessage(star.starboardMessageID);

		if (newStarredBy.length) {
			await starboardMessage.edit({ embed: Starboard.buildStarboardEmbed(message, newStarredBy.length) });

			const newStar = await star.update({
				starCount: newStarredBy.length,
				starredBy: newStarredBy
			});

			this.stars.set(message.id, newStar);
		} else {
			await starboardMessage.delete();
			await star.destroy();
			this.stars.delete(message.id);
		}
	}

	destroy() {
		return Star.destroy({ where: { guildID: this.guild.id } });
	}

	missingPermissions() {
		const missingPermissions = this.client.listenerHandler.modules.get('commandBlocked').missingPermissions;
		const str = missingPermissions(this.channel, this.client.user, [
			'MANAGE_MESSAGES',
			'SEND_MESSAGES',
			'READ_MESSAGE_HISTORY'
		]);

		if (!str) return undefined;
		return `I'm missing ${str} in ${this.channel}.`;
	}

	static findAttachment(message) {
		let attachmentImage;
		const extensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp'];
		const linkRegex = /https?:\/\/(?:\w+\.)?[\w-]+\.[\w]{2,3}(?:\/[\w-_.]+)+\.(?:png|jpg|jpeg|gif|webp)/;

		if (message.attachments.some(attachment => extensions.includes(path.extname(attachment.url)))) {
			attachmentImage = message.attachments.first().url;
		}

		if (!attachmentImage) {
			const linkMatch = message.content.match(linkRegex);
			if (linkMatch && extensions.includes(path.extname(linkMatch[0]))) {
				attachmentImage = linkMatch[0];
			}
		}

		return attachmentImage;
	}

	static buildStarboardEmbed(message, starCount = 1) {
		const embed = new MessageEmbed()
			.setColor(0xFFAC33)
			.addField('Author', message.author, true)
			.addField('Channel', message.channel, true)
			.setThumbnail(message.author.displayAvatarURL())
			.setTimestamp(message.createdAt)
			.setFooter(`⭐ ${starCount}`);

		if (message.content) {
			embed.addField('Message', message.content);
		}

		const attachment = this.findAttachment(message);
		if (attachment) {
			embed.setImage(attachment);
		}

		return embed;
	}
}

module.exports = Starboard;
<commit_msg>Consistency and stuff<commit_after>const { MessageEmbed } = require('discord.js');
const path = require('path');
const Star = require('../models/stars');
const Queue = require('./Queue');

class Starboard {
	constructor(guild) {
		this.client = guild.client;
		this.guild = guild;
		this.stars = new Map();
		this.queue = new Queue();

		Star.findAll({ where: { guildID: this.guild.id } }).then(stars => {
			for (const star of stars) {
				this.stars.set(star.messageID, star);
			}
		});
	}

	get channel() {
		const channelID = this.client.settings.get(this.guild, 'starboardChannelID');
		return this.guild.channels.get(channelID);
	}

	add(message, starredBy) {
		if (!this.channel) {
			return 'There isn\'t a starboard channel to use.';
		}

		if (message.author.id === starredBy.id) {
			return 'You can\'t star your own messages.';
		}

		const missingPerms = this.missingPermissions();
		if (missingPerms) {
			return missingPerms;
		}

		if (this.stars.has(message.id)) {
			const star = this.stars.get(message.id);
			if (star.starredBy.includes(starredBy.id)) {
				return 'You have already starred this message before; You can\'t star it again.';
			}
		}

		this.queue.add(this.addStar.bind(this, message, starredBy));
		return undefined;
	}

	async addStar(message, starredBy) {
		if (!this.stars.has(message.id)) {
			const starboardMessage = await this.channel.send({ embed: Starboard.buildStarboardEmbed(message) });

			const star = await Star.create({
				messageID: message.id,
				authorID: message.author.id,
				channelID: message.channel.id,
				guildID: this.guild.id,
				starboardMessageID: starboardMessage.id,
				starredBy: [starredBy.id]
			});

			this.stars.set(message.id, star);
			return;
		}

		const star = this.stars.get(message.id);
		const newStarredBy = star.starredBy.concat([starredBy.id]);

		const starboardMessage = await this.channel.fetchMessage(star.starboardMessageID);
		await starboardMessage.edit({ embed: Starboard.buildStarboardEmbed(message, newStarredBy.length) });

		const newStar = await star.update({
			starCount: newStarredBy.length,
			starredBy: newStarredBy
		});

		this.stars.set(message.id, newStar);
	}

	remove(message, unstarredBy) {
		if (!this.channel) {
			return 'There isn\'t a starboard channel to use.';
		}

		const star = this.stars.get(message.id);
		if (!star || !star.starredBy.includes(unstarredBy.id)) {
			return 'You can\'t remove any star from this message because you never gave it one in the first place.';
		}

		const missingPerms = this.missingPermissions();
		if (missingPerms) {
			return missingPerms;
		}

		this.queue.add(this.removeStar.bind(this, message, unstarredBy));
		return undefined;
	}

	async removeStar(message, unstarredBy) {
		const star = this.stars.get(message.id);

		if (message.reactions.has('⭐') && message.reactions.get('⭐').users.has(unstarredBy.id)) {
			await message.reactions.get('⭐').remove(unstarredBy);
		}

		const newStarredBy = star.starredBy.filter(id => id !== unstarredBy.id);
		const starboardMessage = await this.channel.fetchMessage(star.starboardMessageID);

		if (newStarredBy.length) {
			await starboardMessage.edit({ embed: Starboard.buildStarboardEmbed(message, newStarredBy.length) });

			const newStar = await star.update({
				starCount: newStarredBy.length,
				starredBy: newStarredBy
			});

			this.stars.set(message.id, newStar);
			return;
		}

		await starboardMessage.delete();
		await star.destroy();
		this.stars.delete(message.id);
	}

	destroy() {
		return Star.destroy({ where: { guildID: this.guild.id } });
	}

	missingPermissions() {
		const missingPermissions = this.client.listenerHandler.modules.get('commandBlocked').missingPermissions;
		const str = missingPermissions(this.channel, this.client.user, [
			'MANAGE_MESSAGES',
			'SEND_MESSAGES',
			'READ_MESSAGE_HISTORY'
		]);

		if (!str) return undefined;
		return `I'm missing ${str} in ${this.channel}.`;
	}

	static findAttachment(message) {
		let attachmentImage;
		const extensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp'];
		const linkRegex = /https?:\/\/(?:\w+\.)?[\w-]+\.[\w]{2,3}(?:\/[\w-_.]+)+\.(?:png|jpg|jpeg|gif|webp)/;

		if (message.attachments.some(attachment => extensions.includes(path.extname(attachment.url)))) {
			attachmentImage = message.attachments.first().url;
		}

		if (!attachmentImage) {
			const linkMatch = message.content.match(linkRegex);
			if (linkMatch && extensions.includes(path.extname(linkMatch[0]))) {
				attachmentImage = linkMatch[0];
			}
		}

		return attachmentImage;
	}

	static buildStarboardEmbed(message, starCount = 1) {
		const embed = new MessageEmbed()
			.setColor(0xFFAC33)
			.addField('Author', message.author, true)
			.addField('Channel', message.channel, true)
			.setThumbnail(message.author.displayAvatarURL())
			.setTimestamp(message.createdAt)
			.setFooter(`⭐ ${starCount}`);

		if (message.content) {
			embed.addField('Message', message.content);
		}

		const attachment = this.findAttachment(message);
		if (attachment) {
			embed.setImage(attachment);
		}

		return embed;
	}
}

module.exports = Starboard;
<|endoftext|>


<commit_before>import PropTypes from 'prop-types';
import React from 'react';

/**
 * Footer layout
 */
const Footer = ( page ) => {

	const theme = page._pages[ page._ID ].theme ? page._pages[ page._ID ].theme : 'dark';

	return (
		<footer className={`uikit-grid uikit-body uikit-footer footer footer--${ theme }`} role="contentinfo">

						<img className="footer__logo" src={`/assets/img/coa${ theme === 'blue' || theme === 'dark' ? '-white' : '' }.png`}
							alt="The Australian Government coat of Arms" />

							<ul className={`footer__links uikit-link-list uikit-link-list--inline uikit-link-list--inverted`}>
								{
									page.links && page.links.map( ( link, i ) => {
										return (

												{
													page._pages[ link[0] ]
														? { page._pages[ link[0] ].title }
														: { link[0] }
												}

										)
									})
								}

							{ page._body }

	);
}

Footer.propTypes = {
	/**
	 * 	links:
	 *      -
	 *        - page1 # this needs to be page IDs, they are converted to titles later
	 *        - http://pagelink.com # External page link
	 *      -
	 *	      - page2 # this needs to be page IDs, they are converted to titles later
	 *        -		  # can be empty
	 */
	links: PropTypes.array.isRequired,

	/**
	 * _body: (text)(2)
	 */
	_body: PropTypes.node.isRequired,
};

Footer.defaultProps = {};

export default Footer;
<commit_msg>Update footer.js<commit_after>import PropTypes from 'prop-types';
import React from 'react';

/**
 * Footer layout
 */
const Footer = ( page ) => {

	const theme = page._pages[ page._ID ].theme ? page._pages[ page._ID ].theme : 'dark';

	return (
		<footer className={`uikit-grid uikit-body uikit-footer footer footer--${ theme }`} role="contentinfo">

						<img className="footer__logo" src={`/assets/img/coa${ theme === 'blue' || theme === 'dark' ? '-white' : '' }.png`}
							alt="The Australian Government coat of Arms" />

							<ul className={`footer__links uikit-link-list uikit-link-list--inline uikit-link-list--inverted`}>
								{
									page.links && page.links.map( ( link, i ) => {
										return (

												{
													page._pages[ link[0] ]
														? { page._pages[ link[0] ].title }
														: { link[0] }
												}

										)
									})
								}

							{ page._body }

	);
}

Footer.propTypes = {
	/**
	 * 	links:
	 *      -
	 *        - page1               # this needs to be page ID, they are converted to titles later
	 *        - http://pagelink.com # External page link
	 *      -
	 *	  - page2               # this needs to be page ID, they are converted to titles later
	 *        -                     # can be empty
	 */
	links: PropTypes.array.isRequired,

	/**
	 * _body: (text)(2)
	 */
	_body: PropTypes.node.isRequired,
};

Footer.defaultProps = {};

export default Footer;
<|endoftext|>


<commit_before>/*jshint node:true*/

var path = require('path');
var InRepoAddon = require('ember-cli/blueprints/in-repo-addon/index');
var stringUtil = require('ember-cli-string-utils');

module.exports = {
  description: 'Creates an Engine within the current repository.',

  locals: function(options) {
    var entity    = options.entity;
    var rawName   = entity.name;
    var name      = stringUtil.dasherize(rawName);
    var namespace = stringUtil.classify(rawName);
    return {
      name: name,
      modulePrefix: name
    };
  },

  availableOptions: [
    {
      name: 'type',
      type: [ 'routable', 'routeless' ],
      default: 'routable',
      aliases: [
        { 'routable': 'routable' },
        { 'routeless': 'routeless' }
      ]
    }
  ],

  _setupRouteBlueprint: function() {
    this.routeBlueprint = this.lookupBlueprint('route');
    this.shouldTouchRouter = this.routeBlueprint.shouldTouchRouter;
    this.shouldEntityTouchRouter = this.routeBlueprint.shouldEntityTouchRouter;
  },

  install: function(options) {
    this.options = options;
    var superResult = this._super.install.apply(this, arguments);
    this._setupRouteBlueprint();
    return superResult;
  },

  uninstall: function(options) {
    this.options = options;
    var superResult = this._super.uninstall.apply(this, arguments);
    this._setupRouteBlueprint();
    return superResult;
  },

  filesPath: function() {
    var type = this.options && this.options.type || 'routable';
    return path.join(this.path, type + '-files');
  },

  // TODO: supportsAddon normally calls `files` which means that `filesPath`
  // gets called before options get set. Likely needs to be fixed upstream.
  supportsAddon: function() {
    return false;
  },

  beforeInstall: InRepoAddon.beforeInstall,

  afterInstall: function(options) {
    options.identifier = 'mount';
    InRepoAddon.afterInstall.call(this, options);
    this.routeBlueprint.afterInstall.call(this, options);
  },

  afterUninstall: function(options) {
    options.identifier = 'mount';
    this.routeBlueprint.afterUninstall.call(this, options);
  }
};
<commit_msg>Fixes in-repo-engines on cli ^2.9.0<commit_after>/*jshint node:true*/

var path = require('path');
var InRepoAddon = require('ember-cli/blueprints/in-repo-addon/index');
var stringUtil = require('ember-cli-string-utils');

module.exports = {
  description: 'Creates an Engine within the current repository.',

  locals: function(options) {
    var entity    = options.entity;
    var rawName   = entity.name;
    var name      = stringUtil.dasherize(rawName);
    var namespace = stringUtil.classify(rawName);
    return {
      name: name,
      modulePrefix: name
    };
  },

  availableOptions: [
    {
      name: 'type',
      type: [ 'routable', 'routeless' ],
      default: 'routable',
      aliases: [
        { 'routable': 'routable' },
        { 'routeless': 'routeless' }
      ]
    }
  ],

  _setupRouteBlueprint: function() {
    this.routeBlueprint = this.lookupBlueprint('route');
    this.shouldTouchRouter = this.routeBlueprint.shouldTouchRouter;
    this.shouldEntityTouchRouter = this.routeBlueprint.shouldEntityTouchRouter;
  },

  install: function(options) {
    this.options = options;
    var superResult = this._super.install.apply(this, arguments);
    this._setupRouteBlueprint();
    return superResult;
  },

  uninstall: function(options) {
    this.options = options;
    var superResult = this._super.uninstall.apply(this, arguments);
    this._setupRouteBlueprint();
    return superResult;
  },

  filesPath: function() {
    var type = this.options && this.options.type || 'routable';
    return path.join(this.path, type + '-files');
  },

  // TODO: supportsAddon normally calls `files` which means that `filesPath`
  // gets called before options get set. Likely needs to be fixed upstream.
  supportsAddon: function() {
    return false;
  },

  beforeInstall: InRepoAddon.beforeInstall,

  afterInstall: function(options) {
    options.identifier = 'mount';
    InRepoAddon.afterInstall.call(this, options);
    this.routeBlueprint.afterInstall.call(this, options);
  },

  afterUninstall: function(options) {
    options.identifier = 'mount';
    this.routeBlueprint.afterUninstall.call(this, options);
  },

  _generatePackageJson: function(options, isInstall) {
    InRepoAddon._generatePackageJson.apply(this, arguments);
  }
};
<|endoftext|>


<commit_before>// Polygon rendering style

import {Style} from '../style';
import {StyleParser} from '../style_parser';
import gl from '../../gl/constants'; // web workers don't have access to GL context, so import all GL constants
import VertexLayout from '../../gl/vertex_layout';
import Builders from '../builders';
import Texture from '../../gl/texture';
import Geo from '../../geo';
import WorkerBroker from '../../utils/worker_broker';
import Utils from '../../utils/utils';

export var Text = Object.create(Style);

Object.assign(Text, {
    name: 'text',
    built_in: true,

    init() {
        Style.init.apply(this);

        // Base shaders
        this.vertex_shader_key = 'styles/text/text_vertex';
        this.fragment_shader_key = 'styles/text/text_fragment';

        var attribs = [
            { name: 'a_position', size: 3, type: gl.FLOAT, normalized: false },
            { name: 'a_normal', size: 3, type: gl.FLOAT, normalized: false },
            // { name: 'a_normal', size: 3, type: gl.BYTE, normalized: true }, // attrib isn't a multiple of 4!
            // { name: 'a_color', size: 3, type: gl.FLOAT, normalized: false },
            { name: 'a_color', size: 4, type: gl.UNSIGNED_BYTE, normalized: true },
            // { name: 'a_selection_color', size: 4, type: gl.FLOAT, normalized: false },
            { name: 'a_selection_color', size: 4, type: gl.UNSIGNED_BYTE, normalized: true },
            { name: 'a_layer', size: 1, type: gl.FLOAT, normalized: false },
            { name: 'a_texcoord', size: 2, type: gl.FLOAT, normalized: false }
        ];
        this.vertex_layout = new VertexLayout(attribs);

        this.texcoords = true;

        this.shaders.uniforms = {
            label_atlas: 'labels',
            u_alpha_discard: .4
        };

        if (Utils.isMainThread) {
            WorkerBroker.addTarget('Text', this);
        }

        this.texts = {}; // unique texts, keyed by tile

        this.size = 14;
    },

    setGL (gl) {
        Style.setGL.apply(this, arguments);

        this.texture = new Texture(this.gl, 'labels', { filtering: 'nearest' });

        this.canvas = document.createElement('canvas');
        this.ctx = this.canvas.getContext('2d');

        this.canvas.width = 256;
        this.canvas.height = 256;

        this.setFont({ size: 12 });

        this.texture.setCanvas(this.canvas);
    },

    // Set font style params for canvas drawing
    // TODO: un-hardcode
    setFont ({ size }) {
        this.size = size;
        this.buffer = 6; // pixel padding around text

        this.ctx.font = `${this.size}px Helvetica`;
        this.ctx.strokeStyle = 'black';
        this.ctx.fillStyle = 'white';
        this.ctx.lineWidth = 4;
        this.ctx.miterLimit = 2;
    },

    // Width and height of text based on current font style
    textSize (text) {
        return [
            Math.ceil(this.ctx.measureText(text).width) + this.buffer * 2,
            this.size + this.buffer * 2
        ];
    },

    // Draw text at specified location, adjusting for buffer and baseline
    drawText (text, [x, y]) {
        // TODO: optional stroke
        this.ctx.strokeText(text, x + this.buffer, y + this.buffer + this.size);
        this.ctx.fillText(text, x + this.buffer, y + this.buffer + this.size);
    },

    // Called on main thread from worker, to create atlas of labels for a tile
    addTexts (tile, texts) {
        this.texts[tile] = texts;

        // Find widest label and sum of all label heights
        let widest = 0, height = 0;
        for (let text in this.texts[tile]) {
            let size = this.textSize(text);

            this.texts[tile][text] = {
                size,
                position: [0, height]
             };

            if (size[0] > widest) {
                widest = size[0];
            }
            height += size[1];
        }

        // Find smallest power-of-2 texture size
        // let texture_size = Utils.fitPowerOf2(widest, height);
        let texture_size = 256;

        console.log(`text summary for tile ${tile}: ${widest} widest, ${height} total height, fits in ${texture_size}x${texture_size}px`);

        // this.canvas.width = texture_size;
        // this.canvas.height = texture_size;
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

        for (let text in this.texts[tile]) {
            let info = this.texts[tile][text];

            this.drawText(text, info.position);

            info.texcoords = Builders.getTexcoordsForSprite(
                info.position,
                info.size,
                [texture_size, texture_size]
            );
        }

        this.texture.update();

        return Promise.resolve(this.texts[tile]);
    },

    // Override
    startData () {
        let tile_data = Style.startData.apply(this);
        tile_data.queue = [];
        return tile_data;
    },

    // Override
    endData (tile_data) {
        // Count collected text
        let tile, count;
        if (tile_data.queue.length > 0) {
            tile = tile_data.queue[0][2].tile.key;
            count = Object.keys(this.texts[tile]||{}).length;
            console.log(`# texts for tile ${tile}: ${count}`);
        }
        if (!count) {
            return Promise.resolve();
        }

        return WorkerBroker.postMessage('Text', 'addTexts', tile, this.texts[tile]).then(texts => {

            // this.texcoord_scale = texcoords;
            this.texts[tile] = texts;
            // this.texcoord_scale = this.texts[tile][Object.keys(this.texts[tile])[0]].texcoords;

            tile_data.queue.forEach(q => Style.addFeature.apply(this, q));
            tile_data.queue = [];

            return Style.endData.call(this, tile_data);
        });
    },

    // Override to queue features instead of processing immediately
    addFeature (feature, rule, context, tile_data) {
        // Collect text
        let text = feature.properties.name;
        if (text) {
            let tile = context.tile.key;
            if (!this.texts[tile]) {
                this.texts[tile] = {};
            }

            this.texts[tile][text] = true;
        }

        tile_data.queue.push([feature, rule, context, tile_data]);
    },

    _parseFeature (feature, rule_style, context) {
        var style = this.feature_style;

        // style.texture = rule_style.texture;
        // style.sprite = 'test'; //rule_style.sprite;

        // style.size = rule_style.size && StyleParser.parseDistance(rule_style.size, context);
        // style.size = StyleParser.parseDistance(['256px', '256px'], context);

        // var xratio = (this.texcoord_scale[1][0] - this.texcoord_scale[0][0]) / (this.texcoord_scale[1][1] - this.texcoord_scale[0][1]);
        // style.size = StyleParser.parseDistance([`${this.size * xratio}px`, `${this.size}px`], context);

        style.text = feature.properties.name;

        let tile = context.tile.key;
        style.size = this.texts[tile][style.text].size;
        style.size = [style.size[0] * Geo.units_per_pixel, style.size[1] * Geo.units_per_pixel];

        this.texcoord_scale = this.texts[tile][style.text].texcoords;

        return style;
    },

    /**
     * A "template" that sets constant attibutes for each vertex, which is then modified per vertex or per feature.
     * A plain JS array matching the order of the vertex layout.
     */
    makeVertexTemplate(style) {
        // Placeholder values
        var color = style.color || [0, 0, 0, 1];

        // Basic attributes, others can be added (see texture UVs below)
        var template = [
            // position - x & y coords will be filled in per-vertex below
            0, 0, style.z || 0,
            // normal
            0, 0, 1,
            // color
            // TODO: automate multiplication for normalized attribs?
            color[0] * 255, color[1] * 255, color[2] * 255, color[3] * 255,
            // selection color
            style.selection_color[0] * 255, style.selection_color[1] * 255, style.selection_color[2] * 255, style.selection_color[3] * 255,
            // draw order
            style.order
        ];

        // if (this.texcoords) {
            template.push(0, 0);            // Add texture UVs to template only if needed
            // this.setTexcoordScale(style);   // Sets texcoord scale if needed (e.g. for sprite sub-area)
        // }

        return template;

    },

    buildPoints(points, style, vertex_data) {
        // if (!style.color || !style.size) {
        //     return;
        // }

        // WorkerBroker.postMessage('Text', 'addText', style.text); //.then(texcoords => {
            // this.texcoords = texcoords;
            Builders.buildQuadsForPoints(
                points,
                style.size[0] || style.size,
                style.size[1] || style.size,
                vertex_data,
                this.makeVertexTemplate(style),
                { texcoord_index: this.vertex_layout.index.a_texcoord, texcoord_scale: this.texcoord_scale }
            );
        // });
    }

});
<commit_msg>adapt text atlas size<commit_after>// Polygon rendering style

import {Style} from '../style';
import {StyleParser} from '../style_parser';
import gl from '../../gl/constants'; // web workers don't have access to GL context, so import all GL constants
import VertexLayout from '../../gl/vertex_layout';
import Builders from '../builders';
import Texture from '../../gl/texture';
import Geo from '../../geo';
import WorkerBroker from '../../utils/worker_broker';
import Utils from '../../utils/utils';

export var Text = Object.create(Style);

Object.assign(Text, {
    name: 'text',
    built_in: true,

    init() {
        Style.init.apply(this);

        // Base shaders
        this.vertex_shader_key = 'styles/text/text_vertex';
        this.fragment_shader_key = 'styles/text/text_fragment';

        var attribs = [
            { name: 'a_position', size: 3, type: gl.FLOAT, normalized: false },
            { name: 'a_normal', size: 3, type: gl.FLOAT, normalized: false },
            // { name: 'a_normal', size: 3, type: gl.BYTE, normalized: true }, // attrib isn't a multiple of 4!
            // { name: 'a_color', size: 3, type: gl.FLOAT, normalized: false },
            { name: 'a_color', size: 4, type: gl.UNSIGNED_BYTE, normalized: true },
            // { name: 'a_selection_color', size: 4, type: gl.FLOAT, normalized: false },
            { name: 'a_selection_color', size: 4, type: gl.UNSIGNED_BYTE, normalized: true },
            { name: 'a_layer', size: 1, type: gl.FLOAT, normalized: false },
            { name: 'a_texcoord', size: 2, type: gl.FLOAT, normalized: false }
        ];
        this.vertex_layout = new VertexLayout(attribs);

        this.texcoords = true;

        this.shaders.uniforms = {
            label_atlas: 'labels',
            u_alpha_discard: .4
        };

        if (Utils.isMainThread) {
            WorkerBroker.addTarget('Text', this);
        }

        this.texts = {}; // unique texts, keyed by tile

        this.size = 14;
    },

    setGL (gl) {
        Style.setGL.apply(this, arguments);

        this.texture = new Texture(this.gl, 'labels', { filtering: 'nearest' });

        this.canvas = document.createElement('canvas');
        this.ctx = this.canvas.getContext('2d');

        this.setFont({ size: 12 });

        this.texture.setCanvas(this.canvas);
    },

    // Set font style params for canvas drawing
    // TODO: un-hardcode
    setFont ({ size }) {
        this.size = size;
        this.buffer = 6; // pixel padding around text

        this.ctx.font = `${this.size}px Helvetica`;
        this.ctx.strokeStyle = 'black';
        this.ctx.fillStyle = 'white';
        this.ctx.lineWidth = 4;
        this.ctx.miterLimit = 2;
    },

    // Width and height of text based on current font style
    textSize (text) {
        return [
            Math.ceil(this.ctx.measureText(text).width) + this.buffer * 2,
            this.size + this.buffer * 2
        ];
    },

    // Draw text at specified location, adjusting for buffer and baseline
    drawText (text, [x, y]) {
        // TODO: optional stroke
        this.ctx.strokeText(text, x + this.buffer, y + this.buffer + this.size);
        this.ctx.fillText(text, x + this.buffer, y + this.buffer + this.size);
    },

    // Called on main thread from worker, to create atlas of labels for a tile
    addTexts (tile, texts) {
        this.texts[tile] = texts;

        // Find widest label and sum of all label heights
        let widest = 0, height = 0;
        for (let text in this.texts[tile]) {
            let size = this.textSize(text);

            this.texts[tile][text] = {
                size,
                position: [0, height]
             };

            if (size[0] > widest) {
                widest = size[0];
            }
            height += size[1];
        }

        // Find smallest power-of-2 texture size
        let texture_size = Utils.fitPowerOf2(widest, height);
        // let texture_size = 512;

        console.log(`text summary for tile ${tile}: ${widest} widest, ${height} total height, fits in ${texture_size}x${texture_size}px`);

        this.canvas.width = texture_size;
        this.canvas.height = texture_size;
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

        // TODO: cleanup, seems the canvas font settings need to be refreshed whenever canvas size changes
        this.setFont({ size: 12 });

        for (let text in this.texts[tile]) {
            let info = this.texts[tile][text];

            this.drawText(text, info.position);

            info.texcoords = Builders.getTexcoordsForSprite(
                info.position,
                info.size,
                [texture_size, texture_size]
            );
        }

        this.texture.update();

        return Promise.resolve(this.texts[tile]);
    },

    // Override
    startData () {
        let tile_data = Style.startData.apply(this);
        tile_data.queue = [];
        return tile_data;
    },

    // Override
    endData (tile_data) {
        // Count collected text
        let tile, count;
        if (tile_data.queue.length > 0) {
            tile = tile_data.queue[0][2].tile.key;
            count = Object.keys(this.texts[tile]||{}).length;
            console.log(`# texts for tile ${tile}: ${count}`);
        }
        if (!count) {
            return Promise.resolve();
        }

        return WorkerBroker.postMessage('Text', 'addTexts', tile, this.texts[tile]).then(texts => {

            // this.texcoord_scale = texcoords;
            this.texts[tile] = texts;
            // this.texcoord_scale = this.texts[tile][Object.keys(this.texts[tile])[0]].texcoords;

            tile_data.queue.forEach(q => Style.addFeature.apply(this, q));
            tile_data.queue = [];

            return Style.endData.call(this, tile_data);
        });
    },

    // Override to queue features instead of processing immediately
    addFeature (feature, rule, context, tile_data) {
        // Collect text
        let text = feature.properties.name;
        if (text) {
            let tile = context.tile.key;
            if (!this.texts[tile]) {
                this.texts[tile] = {};
            }

            this.texts[tile][text] = true;
        }

        tile_data.queue.push([feature, rule, context, tile_data]);
    },

    _parseFeature (feature, rule_style, context) {
        var style = this.feature_style;

        // style.texture = rule_style.texture;
        // style.sprite = 'test'; //rule_style.sprite;

        // style.size = rule_style.size && StyleParser.parseDistance(rule_style.size, context);
        // style.size = StyleParser.parseDistance(['256px', '256px'], context);

        // var xratio = (this.texcoord_scale[1][0] - this.texcoord_scale[0][0]) / (this.texcoord_scale[1][1] - this.texcoord_scale[0][1]);
        // style.size = StyleParser.parseDistance([`${this.size * xratio}px`, `${this.size}px`], context);

        style.text = feature.properties.name;

        let tile = context.tile.key;
        style.size = this.texts[tile][style.text].size;
        style.size = [style.size[0] * Geo.units_per_pixel, style.size[1] * Geo.units_per_pixel];

        this.texcoord_scale = this.texts[tile][style.text].texcoords;

        return style;
    },

    /**
     * A "template" that sets constant attibutes for each vertex, which is then modified per vertex or per feature.
     * A plain JS array matching the order of the vertex layout.
     */
    makeVertexTemplate(style) {
        // Placeholder values
        var color = style.color || [0, 0, 0, 1];

        // Basic attributes, others can be added (see texture UVs below)
        var template = [
            // position - x & y coords will be filled in per-vertex below
            0, 0, style.z || 0,
            // normal
            0, 0, 1,
            // color
            // TODO: automate multiplication for normalized attribs?
            color[0] * 255, color[1] * 255, color[2] * 255, color[3] * 255,
            // selection color
            style.selection_color[0] * 255, style.selection_color[1] * 255, style.selection_color[2] * 255, style.selection_color[3] * 255,
            // draw order
            style.order
        ];

        // if (this.texcoords) {
            template.push(0, 0);            // Add texture UVs to template only if needed
            // this.setTexcoordScale(style);   // Sets texcoord scale if needed (e.g. for sprite sub-area)
        // }

        return template;

    },

    buildPoints(points, style, vertex_data) {
        // if (!style.color || !style.size) {
        //     return;
        // }

        // WorkerBroker.postMessage('Text', 'addText', style.text); //.then(texcoords => {
            // this.texcoords = texcoords;
            Builders.buildQuadsForPoints(
                points,
                style.size[0] || style.size,
                style.size[1] || style.size,
                vertex_data,
                this.makeVertexTemplate(style),
                { texcoord_index: this.vertex_layout.index.a_texcoord, texcoord_scale: this.texcoord_scale }
            );
        // });
    }

});
<|endoftext|>


<commit_before>process.env.LOG_LEVEL = 'fatal'

const expect = require('expect')
const {createRobot} = require('probot')
const Stale = require('../lib/stale')
const notFoundError = {
  code: 404,
  status: 'Not Found',
  headers: {}
}

describe('stale', () => {
  let robot, github, logger

  beforeEach(() => {
    robot = createRobot()
    logger = robot.log

    const issueAction = expect.createSpy().andReturn(Promise.resolve(notFoundError))

    // Mock out the GitHub API
    github = {
      integrations: {
        getInstallations: expect.createSpy()
      },
      paginate: expect.createSpy(),
      issues: {
        removeLabel: issueAction,
        getLabel: expect.createSpy().andReturn(Promise.reject(notFoundError)),
        createLabel: issueAction,
        addLabels: issueAction,
        createComment: issueAction,
        edit: issueAction
      },
      search: {
        issues: issueAction
      }
    }

    // Mock out GitHub client
    robot.auth = () => Promise.resolve(github)
  })

  it('removes the stale label and ignores if it has already been removed', async () => {
    let stale = new Stale(github, {perform: true, owner: 'probot', repo: 'stale', logger})

    for (const type of ['pulls', 'issues']) {
      try {
        await stale.unmark(type, {number: 123})
      } catch (_) {
        throw new Error('Should not have thrown an error')
      }
    }
  })

  it('should limit the number of actions it takes each run', async () => {
    const staleLabel = 'stale'
    const limitPerRun = 30

    const issueCount = 40
    const staleCount = 3

    const issues = []
    for (let i = 1; i <= issueCount; i++) {
      const labels = (i <= staleCount) ? [{name: staleLabel}] : []
      issues.push({number: i, labels: labels})
    }

    const prs = []
    for (let i = 101; i <= 100 + issueCount; i++) {
      const labels = (i <= 100 + staleCount) ? [{name: staleLabel}] : []
      prs.push({number: i, labels: labels})
    }

    github.search.issues = ({q, sort, order, per_page}) => {
      let items = []
      if (q.includes('is:pr')) {
        items = items.concat(prs.slice(0, per_page))
      } else if (q.includes('is:issue')) {
        items = items.concat(issues.slice(0, per_page))
      } else {
        throw new Error('query should specify PullRequests or Issues')
      }

      if (q.includes(`-label:"${staleLabel}"`)) {
        items = items.filter(item => !item.labels.map(label => label.name).includes(staleLabel))
      } else if (q.includes(`label:"${staleLabel}"`)) {
        items = items.filter(item => item.labels.map(label => label.name).includes(staleLabel))
      }

      expect(items.length).toBeLessThanOrEqualTo(per_page)

      return Promise.resolve({
        data: {
          items: items
        }
      })
    }

    for (const type of ['pulls', 'issues']) {
      let comments = 0
      let closed = 0
      let labeledStale = 0
      github.issues.createComment = expect.createSpy().andCall(() => comments++).andReturn(Promise.resolve(notFoundError))
      github.issues.edit = ({owner, repo, number, state}) => {
        if (state === 'closed') {
          closed++
        }
      }
      github.issues.addLabels = ({owner, repo, number, labels}) => {
        if (labels.includes(staleLabel)) {
          labeledStale++
        }
      }

      // Mock out GitHub client
      robot.auth = () => Promise.resolve(github)

      const stale = new Stale(github, {perform: true, owner: 'probot', repo: 'stale', logger})
      stale.config.limitPerRun = limitPerRun
      stale.config.staleLabel = staleLabel
      stale.config.closeComment = 'closed'

      await stale.markAndSweep(type)

      expect(comments).toEqual(limitPerRun)
      expect(closed).toEqual(staleCount)
      expect(labeledStale).toEqual(limitPerRun - staleCount)
    }
  })

  it('should not close issues if closeStale is configured as false', async () => {
    let stale = new Stale(github, {perform: true, owner: 'probot', repo: 'stale', logger})
    stale.config.daysUntilClose = false
    stale.getStale = expect.createSpy().andReturn(Promise.resolve({data: {items: []}}))
    stale.getClosable = expect.createSpy()

    await stale.markAndSweep('issues')
    expect(stale.getClosable).toNotHaveBeenCalled()

    await stale.markAndSweep('pulls')
    expect(stale.getClosable).toNotHaveBeenCalled()
  })
})
<commit_msg>Revert "Setup logger to quiet down tests"<commit_after>const expect = require('expect')
const {createRobot} = require('probot')
const Stale = require('../lib/stale')
const notFoundError = {
  code: 404,
  status: 'Not Found',
  headers: {}
}

describe('stale', () => {
  let robot
  let github

  beforeEach(() => {
    robot = createRobot()

    const issueAction = expect.createSpy().andReturn(Promise.resolve(notFoundError))

    // Mock out the GitHub API
    github = {
      integrations: {
        getInstallations: expect.createSpy()
      },
      paginate: expect.createSpy(),
      issues: {
        removeLabel: issueAction,
        getLabel: expect.createSpy().andReturn(Promise.reject(notFoundError)),
        createLabel: issueAction,
        addLabels: issueAction,
        createComment: issueAction,
        edit: issueAction
      },
      search: {
        issues: issueAction
      }
    }

    // Mock out GitHub client
    robot.auth = () => Promise.resolve(github)
  })

  it('removes the stale label and ignores if it has already been removed', async () => {
    let stale = new Stale(github, {perform: true, owner: 'probot', repo: 'stale'})

    for (const type of ['pulls', 'issues']) {
      try {
        await stale.unmark(type, {number: 123})
      } catch (_) {
        throw new Error('Should not have thrown an error')
      }
    }
  })

  it('should limit the number of actions it takes each run', async () => {
    const staleLabel = 'stale'
    const limitPerRun = 30

    const issueCount = 40
    const staleCount = 3

    const issues = []
    for (let i = 1; i <= issueCount; i++) {
      const labels = (i <= staleCount) ? [{name: staleLabel}] : []
      issues.push({number: i, labels: labels})
    }

    const prs = []
    for (let i = 101; i <= 100 + issueCount; i++) {
      const labels = (i <= 100 + staleCount) ? [{name: staleLabel}] : []
      prs.push({number: i, labels: labels})
    }

    github.search.issues = ({q, sort, order, per_page}) => {
      let items = []
      if (q.includes('is:pr')) {
        items = items.concat(prs.slice(0, per_page))
      } else if (q.includes('is:issue')) {
        items = items.concat(issues.slice(0, per_page))
      } else {
        throw new Error('query should specify PullRequests or Issues')
      }

      if (q.includes(`-label:"${staleLabel}"`)) {
        items = items.filter(item => !item.labels.map(label => label.name).includes(staleLabel))
      } else if (q.includes(`label:"${staleLabel}"`)) {
        items = items.filter(item => item.labels.map(label => label.name).includes(staleLabel))
      }

      expect(items.length).toBeLessThanOrEqualTo(per_page)

      return Promise.resolve({
        data: {
          items: items
        }
      })
    }

    for (const type of ['pulls', 'issues']) {
      let comments = 0
      let closed = 0
      let labeledStale = 0
      github.issues.createComment = expect.createSpy().andCall(() => comments++).andReturn(Promise.resolve(notFoundError))
      github.issues.edit = ({owner, repo, number, state}) => {
        if (state === 'closed') {
          closed++
        }
      }
      github.issues.addLabels = ({owner, repo, number, labels}) => {
        if (labels.includes(staleLabel)) {
          labeledStale++
        }
      }

      // Mock out GitHub client
      robot.auth = () => Promise.resolve(github)

      const stale = new Stale(github, {perform: true, owner: 'probot', repo: 'stale'})
      stale.config.limitPerRun = limitPerRun
      stale.config.staleLabel = staleLabel
      stale.config.closeComment = 'closed'

      await stale.markAndSweep(type)

      expect(comments).toEqual(limitPerRun)
      expect(closed).toEqual(staleCount)
      expect(labeledStale).toEqual(limitPerRun - staleCount)
    }
  })

  it('should not close issues if closeStale is configured as false', async () => {
    let stale = new Stale(github, {perform: true, owner: 'probot', repo: 'stale', logger})
    stale.config.daysUntilClose = false
    stale.getStale = expect.createSpy().andReturn(Promise.resolve({data: {items: []}}))
    stale.getClosable = expect.createSpy()

    await stale.markAndSweep('issues')
    expect(stale.getClosable).toNotHaveBeenCalled()

    await stale.markAndSweep('pulls')
    expect(stale.getClosable).toNotHaveBeenCalled()
  })
})
<|endoftext|>


<commit_before>var bitcoin = require('bitcoinjs-lib')
var cclib = require('coloredcoinjs-lib')
var _ = require('lodash')
var Q = require('q')

var AssetValue = require('../asset').AssetValue
var AssetTarget = require('../asset').AssetTarget
var Coin = require('../coin').Coin
var toposort = require('../tx').toposort
var HistoryEntry = require('./HistoryEntry')

/**
 * @class HistoryManager
 *
 * @param {Wallet} wallet
 */
function HistoryManager(wallet) {
  this.wallet = wallet
}

/**
 * @callback HistoryManager~getEntries
 * @param {?Error} error
 * @param {HistoryEntry[]} entries
 */

/**
 * @param {HistoryManager~getEntries} cb
 */
HistoryManager.prototype.getEntries = function(cb) {
  var self = this

  Q.fcall(function() {
    return Q.ninvoke(self.wallet.getCoinQuery().includeSpent().includeUnconfirmed(), 'getCoins')

  }).then(function(coinList) {
    var txDb = self.wallet.getTxDb()
    var transactions = _.chain(coinList.getCoins())
      .pluck('txId')
      .uniq()
      .map(function(txId) {
        var entry = {
          tx: txDb.getTxById(txId),
          blockHeight: txDb.getBlockHeightByTxId(txId),
          timestamp: txDb.getTimestampByTxId(txId)
        }
        if (entry.tx === null)
          throw new Error('txId ' + txId + ' not found in txDb')

        return [entry.tx.getId(), entry]
      })
      .sortBy(function(entry) {
        return entry[1].blockHeight + entry[1].timestamp/10000000000
      })
      .value()

    var txEntries = _.zipObject(transactions)
    transactions = transactions.map(function(entry) { return entry[1].tx })

    var promise = Q()
    var entries = []
    var coins = _.zipObject(coinList.getCoins().map(function(coin) { return [coin.txId+coin.outIndex, coin] }))

    toposort(transactions).forEach(function(tx) {
      var ins = _.filter(tx.ins.map(function(input) {
        var txId = Array.prototype.reverse.call(new Buffer(input.hash)).toString('hex')
        return coins[txId+input.index]
      }))
      var outs = _.filter(tx.outs.map(function(output, index) {
        return coins[tx.getId()+index]
      }))

      var colorValues = {}
      ins.forEach(function(coin) {
        promise = promise.then(function() { return Q.ninvoke(coin, 'getMainColorValue') }).then(function(cv) {
          var cid = cv.getColorId()
          if (_.isUndefined(colorValues[cid]))
            colorValues[cid] = cv.neg()
          else
            colorValues[cid] = colorValues[cid].minus(cv)
        })
      })
      outs.forEach(function(coin) {
        promise = promise.then(function() { return Q.ninvoke(coin, 'getMainColorValue') }).then(function(cv) {
          var cid = cv.getColorId()
          if (_.isUndefined(colorValues[cid]))
            colorValues[cid] = cv
          else
            colorValues[cid] = colorValues[cid].plus(cv)
        })
      })

      var colorTargets = []
      tx.outs.forEach(function(output, index) {
        promise = promise.then(function() {
          if (!_.isUndefined(coins[tx.getId()+index]))
            return

          var script = bitcoin.Script.fromBuffer(output.script.toBuffer())
          var address = bitcoin.Address.fromOutputScript(script, self.wallet.getNetwork()).toBase58Check()
          var coin = new Coin(self.wallet.getCoinManager(), {
            txId: tx.getId(),
            outIndex: index,
            value: output.value,
            script: script,
            address: address
          })

          return Q.ninvoke(coin, 'getMainColorValue').then(function(cv) {
            colorTargets.push(new cclib.ColorTarget(address, cv))
          })
        })
      })

      promise = promise.then(function() {
        var assetValues = {}
        _.values(colorValues).forEach(function(cv) {
          var scheme = cv.getColorDefinition().getScheme()
          var assetdef = self.wallet.getAssetDefinitionManager().getByScheme(scheme)
          if (assetdef === null)
            throw new Error('asset for ColorValue ' + cv + ' not found')
          if (!_.isUndefined(assetValues[assetdef.getId()]))
            throw new Error('multi asset not supported')
          assetValues[assetdef.getId()] = new AssetValue(assetdef, cv.getValue())
        })

        var assetTargets = colorTargets.map(function(ct) {
          var scheme = ct.getColorDefinition().getScheme()
          var assetdef = self.wallet.getAssetDefinitionManager().getByScheme(scheme)
          if (assetdef === null)
            throw new Error('asset for ColorValue ' + ct.getColorValue() + ' not found')
          var assetValue = new AssetValue(assetdef, ct.getValue())
          return new AssetTarget(ct.getAddress(), assetValue)
        })

        var entryType = HistoryEntry.entryTypes.Send
        if (ins.length === 0)
          entryType = HistoryEntry.entryTypes.Receive
        if (ins.length === tx.ins.length && outs.length === tx.outs.length)
          entryType = HistoryEntry.entryTypes.PaymentToYourself

        entries.push(new HistoryEntry({
          tx: tx,
          blockHeight: txEntries[tx.getId()].blockHeight,
          timestamp: txEntries[tx.getId()].timestamp,
          values: _.values(assetValues),
          targets: assetTargets,
          entryType: entryType
        }))
      })
    })

    return promise.then(function() { return entries })

  }).done(function(entries) { cb(null, entries) }, function(error) { cb(error) })
}

module.exports = HistoryManager
<commit_msg>added targets to receive history entries<commit_after>var bitcoin = require('bitcoinjs-lib')
var cclib = require('coloredcoinjs-lib')
var _ = require('lodash')
var Q = require('q')

var AssetValue = require('../asset').AssetValue
var AssetTarget = require('../asset').AssetTarget
var Coin = require('../coin').Coin
var toposort = require('../tx').toposort
var HistoryEntry = require('./HistoryEntry')

/**
 * @class HistoryManager
 *
 * @param {Wallet} wallet
 */
function HistoryManager(wallet) {
  this.wallet = wallet
}

/**
 * @callback HistoryManager~getEntries
 * @param {?Error} error
 * @param {HistoryEntry[]} entries
 */

/**
 * @param {HistoryManager~getEntries} cb
 */
HistoryManager.prototype.getEntries = function(cb) {
  var self = this

  Q.fcall(function() {
    return Q.ninvoke(self.wallet.getCoinQuery().includeSpent().includeUnconfirmed(), 'getCoins')

  }).then(function(coinList) {
    var txDb = self.wallet.getTxDb()
    var transactions = _.chain(coinList.getCoins())
      .pluck('txId')
      .uniq()
      .map(function(txId) {
        var entry = {
          tx: txDb.getTxById(txId),
          blockHeight: txDb.getBlockHeightByTxId(txId),
          timestamp: txDb.getTimestampByTxId(txId)
        }
        if (entry.tx === null)
          throw new Error('txId ' + txId + ' not found in txDb')

        return [entry.tx.getId(), entry]
      })
      .sortBy(function(entry) {
        return entry[1].blockHeight + entry[1].timestamp/10000000000
      })
      .value()

    var txEntries = _.zipObject(transactions)
    transactions = transactions.map(function(entry) { return entry[1].tx })

    var promise = Q()
    var entries = []
    var coins = _.zipObject(coinList.getCoins().map(function(coin) { return [coin.txId+coin.outIndex, coin] }))
    var myColorTargets = [];

    toposort(transactions).forEach(function(tx) {
      var ins = _.filter(tx.ins.map(function(input) {
        var txId = Array.prototype.reverse.call(new Buffer(input.hash)).toString('hex')
        return coins[txId+input.index]
      }))
      var outs = _.filter(tx.outs.map(function(output, index) {
        return coins[tx.getId()+index]
      }))

      var colorValues = {}
      ins.forEach(function(coin) {
        promise = promise.then(function() { return Q.ninvoke(coin, 'getMainColorValue') }).then(function(cv) {
          var cid = cv.getColorId()
          if (_.isUndefined(colorValues[cid]))
            colorValues[cid] = cv.neg()
          else
            colorValues[cid] = colorValues[cid].minus(cv)
        })
      })
      outs.forEach(function(coin) {
        promise = promise.then(function() { return Q.ninvoke(coin, 'getMainColorValue') }).then(function(cv) {
          var cid = cv.getColorId()
          if (_.isUndefined(colorValues[cid]))
            colorValues[cid] = cv;
          else
            colorValues[cid] = colorValues[cid].plus(cv);
          myColorTargets.push(new cclib.ColorTarget(coin.address, cv));
        })
      })

      var colorTargets = [];
      tx.outs.forEach(function(output, index) {
        promise = promise.then(function() {
          if (!_.isUndefined(coins[tx.getId()+index]))
            return

          var script = bitcoin.Script.fromBuffer(output.script.toBuffer())
          var address = bitcoin.Address.fromOutputScript(script, self.wallet.getNetwork()).toBase58Check()
          var coin = new Coin(self.wallet.getCoinManager(), {
            txId: tx.getId(),
            outIndex: index,
            value: output.value,
            script: script,
            address: address
          })

          return Q.ninvoke(coin, 'getMainColorValue').then(function(cv) {
            colorTargets.push(new cclib.ColorTarget(address, cv))
          })
        })
      })

      promise = promise.then(function() {
        var assetValues = {}
        _.values(colorValues).forEach(function(cv) {
          var scheme = cv.getColorDefinition().getScheme()
          var assetdef = self.wallet.getAssetDefinitionManager().getByScheme(scheme)
          if (assetdef === null)
            throw new Error('asset for ColorValue ' + cv + ' not found')
          if (!_.isUndefined(assetValues[assetdef.getId()]))
            throw new Error('multi asset not supported')
          assetValues[assetdef.getId()] = new AssetValue(assetdef, cv.getValue())
        })

        var assetTargets = colorTargets.map(function(ct) {
          var scheme = ct.getColorDefinition().getScheme()
          var assetdef = self.wallet.getAssetDefinitionManager().getByScheme(scheme)
          if (assetdef === null)
            throw new Error('asset for ColorValue ' + ct.getColorValue() + ' not found')
          var assetValue = new AssetValue(assetdef, ct.getValue())
          return new AssetTarget(ct.getAddress(), assetValue)
        })

        var entryType = HistoryEntry.entryTypes.Send
        if (ins.length === 0) {
            entryType = HistoryEntry.entryTypes.Receive;
            // replace targets
            assetTargets = myColorTargets.map(function(ct) {
                    var scheme = ct.getColorDefinition().getScheme();
                    var assetdef = self.wallet.getAssetDefinitionManager().getByScheme(scheme);
                    if (assetdef === null)
                        throw new Error('asset for ColorValue ' + ct.getColorValue() + ' not found');
                    var assetValue = new AssetValue(assetdef, ct.getValue());
                    return new AssetTarget(ct.getAddress(), assetValue);
            });
        }
        if (ins.length === tx.ins.length && outs.length === tx.outs.length)
          entryType = HistoryEntry.entryTypes.PaymentToYourself

        entries.push(new HistoryEntry({
          tx: tx,
          blockHeight: txEntries[tx.getId()].blockHeight,
          timestamp: txEntries[tx.getId()].timestamp,
          values: _.values(assetValues),
          targets: assetTargets,
          entryType: entryType
        }))
      })
    })

    return promise.then(function() { return entries })

  }).done(function(entries) { cb(null, entries) }, function(error) { cb(error) })
}

module.exports = HistoryManager
<|endoftext|>