[
  {
    "path": ".circleci/config.yml",
    "content": "# Javascript Node CircleCI 2.0 configuration file\n#\n# Check https://circleci.com/docs/2.0/language-javascript/ for more details\n#\nversion: 2\njobs:\n  build:\n    docker:\n      # specify the version you desire here\n      - image: circleci/node:10.13.0-jessie\n\n      # Specify service dependencies here if necessary\n      # CircleCI maintains a library of pre-built images\n      # documented at https://circleci.com/docs/2.0/circleci-images/\n      # - image: circleci/mongo:3.4.4\n\n    working_directory: ~/emojme\n\n    steps:\n      - checkout\n\n      # Download and cache dependencies\n      - restore_cache:\n          keys:\n          - v1-dependencies-{{ checksum \"package.json\" }}\n          # fallback to using the latest cache if no exact match is found\n          - v1-dependencies-\n\n      - run: npm install\n\n      - save_cache:\n          paths:\n            - node_modules\n          key: v1-dependencies-{{ checksum \"package.json\" }}\n\n      # run tests!\n      - run: npm test\n"
  },
  {
    "path": ".eslintignore",
    "content": "docs/\n"
  },
  {
    "path": ".eslintrc.json",
    "content": "{\n    \"extends\": \"airbnb-base\",\n    \"rules\": {\n      \"no-console\": \"off\",\n      \"no-plusplus\": \"off\",\n      \"no-param-reassign\": \"off\",\n      \"linebreak-style\": \"off\",\n      \"prefer-destructuring\": \"off\",\n      \"guard-for-in\": \"off\",\n      \"no-restricted-syntax\": \"off\",\n      \"no-cond-assign\": \"off\",\n      \"no-multi-assign\": \"off\",\n      \"max-len\": [\n        \"error\", 100, 2, {\n          \"ignoreUrls\": true,\n          \"ignoreComments\": true,\n          \"ignoreRegExpLiterals\": true,\n          \"ignoreStrings\": true,\n          \"ignoreTemplateLiterals\": true\n        }\n      ]\n    },\n    \"env\": {\n      \"commonjs\": true,\n      \"node\": true,\n      \"mocha\": true\n    }\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Script results\nbuild/\nlog/*\n\n# Secret inputs\n.env\n.config\n\n# node projects amirite\nnode_modules/\npackage-lock.json\nemojme@*\n\n# Moving files around\n*.old\n\n# OSX Garb\n.DS_Store\n"
  },
  {
    "path": ".jsdoc.json",
    "content": "{\n\t\"tags\": {\n\t\t\"allowUnknownTags\": true,\n\t\t\"dictionaries\": [\"jsdoc\"]\n\t},\n\t\"source\": {\n\t\t\"include\": [\".\", \"lib\", \"package.json\", \"README.md\"],\n\t\t\"includePattern\": \".js$\",\n\t\t\"excludePattern\": \"(node_modules/|docs|spec)\"\n\t},\n\t\"plugins\": [\n\t\t\"plugins/markdown\"\n\t\t],\n\t\t\"templates\": {\n\t\t\t\"referenceTitle\": \"emojme\",\n\t\t\t\"disableSort\": false,\n\t\t\t\"collapse\": true\n\t\t},\n\t\t\"opts\": {\n\t\t\t\"destination\": \"./docs/\",\n\t\t\t\"encoding\": \"utf8\",\n\t\t\t\"private\": true,\n\t\t\t\"recurse\": true,\n\t\t\t\"template\": \"./node_modules/jsdoc-template\"\n\t\t}\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# 2.0.0\n* Require cookie tokens and cookies >:[\n  * All operations that previously required a (subdomain, token) tuple now require a (subdomain, token, cookie) tuple.\n    * This means the addition of a `--cookie` command line argument.\n    * cookie is also now the third ordered argument in emojme module methods.\n  * Check the readme for how to collect a cookie.\n  * alias --subdomain to --domain for kicks\n  * Reduce adminList request rate slightly to dodge rate limiting.\n  * AuthPairs are now AuthTuples as they represent subdomain, token, and cookie.\n\n# 1.9.1\n* Add Emojme chrome extension to README\n* Resolve (#59), sanitizing user names for disk interaction\n\n# 1.9.0\n* Clean up README readablility\n* Add `--since` option to download, user-stats, and sync\n* Add `--dry-run` option to emojme sync\n\n# 1.8.1\n* Add `--lite` option to emojme favorites.\n  * Does not download complete adminList\n  * returns only emoji name and usage count in `favoriteEmojiAdminList`\n* adds a little more documentation around `allowCollisions`\n\n# 1.8.0\n* Add confusingly named `allowCollisions` to `add` and `upload` endpoints alongside existing `avoidCollisions` param\n  * When set, no adminList will be pre-fetched to prevent collisions. This allows uploads to execute much faster, but with \"untrusted\" uploads it could cause many more errors and therefore rate limiting.\n  * In a future major version: `avoidCollisions` will be renamed to more-accurate `preventCollisions` and `allowCollisions` will be negated and renamed to `avoidCollisison`. For the time being, we don't need a 2.0 / breaking change.\n\n# 1.7.0\n* Add /client.boot endpoint accessor\n* Add emojme favorites function to find a user's favorite emoji\n  * This comprises the content of the `Frequently Used` emoji box\n  * Also includes personal emoji usage counts (!?)\n\n# 1.6.3\n* Update README to reflect slack's new api_token location\n* Fix rate limiting for good this time\n* Resolve npm audit vulnerability\n\n# 1.6.0\n* Implement rate limiting\n  * rate varies depending on endpoint\n  * Can be overridden but new environment variables\n    * SLACK_REQUEST_CONCURRENCY\n    * SLACK_REQUEST_RATE\n    * SLACK_REQUEST_WINDOW\n  * Add naive backoff logic\n* Add timestamps to logs\n\n# 1.5.1\n* Resolve npm audit vulnerability\n\n# 1.5.0\n* Rework logging to be less noisy and more organized.\n   * Use Winston\n   * log warning and worse to the console\n   * log everything to log/combined.log\n   * Add verbosity control\n* Fix bug related to incorrect upload summary output\n\n# 1.4.0\n* Revamp download\n  * `--save` can no longer be called with 'all' (but that never worked)\n  * `--save-all-by-user` added to save all emoji by all users into build/$subdomain/$user\n  * `--save-all` added to save all emoji to build/$subdomain\n* Add jsdoc documentation, available at https://jackellenberger.github.io/emojme\n* Configure circle ci\n* Clarify what a user token should look like\n\n# 1.3.3\n* Fix bug preventing correct package contents from being uploaded to npm\n* Fix bug preventing empty slack instances from adding and syncing emoji\n\n# 1.3.2\n* Add keywords, bin, etc to package.json\n* Add module usage instructions to readme\n\n\n# 1.3.1\n* Create CHANGELOG.md\n* Allow certain required `Add` params to be nulled out by providing an empty string\n  * For example, `add --src 'source.jpg' --name ''` will act identically to `add --src 'source.jpg'`\n  * This resolves an issue where adding multiple emoji of different shapes (i.e. new vs alias vs default named new) could become misaligned\n* Add emojiList to output of `user-stats` for consistency and ease of use\n* Add `<action>Cli` methods to ease testing.\n* Resolve issue where repeated invocation of cli from a single process could pollute commander args\n"
  },
  {
    "path": "README.md",
    "content": "# [emojme](https://github.com/jackellenberger/emojme) - [Documentation](https://jackellenberger.github.io/emojme)\n\n## Table of Contents\n* [Project Overview](#what-it-is)\n* [Breaking Changes](#breaking-changes)\n    * [2.0.0](#2-0-0)\n* [Requirements](#requirements)\n* [Installation](#installation)\n    * [Getting a slack token](#finding-a-slack-token)\n    * [Getting a slack cookie](#finding-a-slack-cookie)\n* [Usage](#usage)\n    * [Command Line](#usage)\n    * [Module](#module)\n* [Build directory output](#build-directory-output)\n* [A closer look at options](#a-closer-look-at-options)\n* [Add vs Upload](#whats-the-difference-between-add-and-upload)\n* [CLI Examples](#cli-examples)\n    * [Download](#emojme-download)\n    * [Add](#emojme-add)\n    * [Upload](#emojme-upload)\n    * [Sync](#emojme-sync)\n    * [User Stats](#emojme-user-stats)\n    * [Favorites](#emojme-favorites)\n* [Pro Moves](#pro-moves-promoves)\n    * [Rate Limiting](#rate-limiting-and-you)\n    * [FAQ](#faq)\n* [Other Projects of Note](#inspirations)\n\n## What it is\nEmojme is a set of tools to manage your Slack emoji, either directly from the command line or from within your own Javascript project.\n\nPrimary features are:\n* Uploading new emoji\n    * Individually, by passing a file or url\n    * In bulk, by passing a json \"adminList\" or a yaml \"emojipack\" file\n    * To one or many slack instances at once\n* Download existing emoji\n    * From one or many slack instances\n    * Download all emoji\n    * Download some emoji\n* Sync emoji between mulitple slack instance\n    * One to one, one to many, many to one, or many to many\n* Analyze emoji authorship\n    * Who makes the most emoji in your slack instance?\n* Analyze emoji usage\n    * Which emoji do you use most?\n\njsdocs are available at [https://jackellenberger.github.io/emojme](https://jackellenberger.github.io/emojme). Read em.\n\n## Breaking Changes\n\n### 2.0.0\n\nRemoves support for easy breazy beautiful user token auth, adds support for grumble grumble cookie token + cookie auth. Slack made me do it I swear. What does it mean for you?\n- Whenever you wrote or used an emojme method with a signature like `method(domain, token, options)`, you will now need `method(domain, token, cookie, options)`.\n- Whenever you were calling the CLI with a pattern like `emojme command --subdomain $SUBDOMAIN --token $TOKEN`, you will now need `emojme command --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE`.\n- Read on for examples and instructions on how to collect your cookie from the jar.\n\n\n## Requirements\n\nTo use emojme you don't need a bot or a workspace admin account. In fact, ~only regular [**user tokens**](https://api.slack.com/docs/token-types#user) work~ only *cookie* tokens work, in combination with shortlived browser tokens, and getting both isn't _quite_ as easy as getting other types of tokens. Limitations are:\n* Cookie tokens can be grabbed from any logged in slack webpage by following [these instructions](#finding-a-slack-token).\n* Auth Cookies are grabbed with even more difficulty, again from logged in slack pages, following [these instructions](#finding-a-slack-cookie).\n* All actions taken through Emojme can be linked back to your user account. That might be bad, but no one has yelled at me yet.\n* Cookie tokens are cycled at inditerminate times, and cannot (to my knowledge) be cycled manually. Ditto for the cookies themselves. **DO NOT LOSE CONTROL OF YOUR COOKIES**. Any project that uses emojme should have tokens passed in through environment variables and should not store them in source control.\n  * Update July 2021: If you are have been using an automated system to scrape User Tokens, you are pretty much hosed. The cookies now required are [Http Only](https://owasp.org/www-community/HttpOnly) and can't be easily (or at all?) accessed via javascript.\n\n## Installation\n\n### Command Line\n\nVia npm\n```bash\n$ (nvm use 10 || nvm install 10) && npm install emojme\n$ npx emojme [command] [options]\n```\n\nVia github\n\n```bash\n$ git clone https://github.com/jackellenberger/emojme.git\n$ cd emojme\n$ node ./emojme [command] [options]\n```\n\nIn order to use either feature, you will need both a Token and a Cookie each for every target subdomain (e.g. my-subdomain.slack.com). You can of course use your own methods for achieving this, but (and I will repeat this), the [Emojme: Emoji Anywhere](https://chrome.google.com/webstore/detail/emojme-emoji-anywhere/nbnaglaclijdfidbinlcnfdbikpbdkog?hl=en-US) Chrome Extension makes it very much easier than anything else, at only minor risk to your personal security. But hey if I were gonna steal your slack creds I'd do it in an alley with a knife or something, not in broad daylight. Its source is also [available on github](https://github.com/jackellenberger/emojme-emoji-anywhere) if you don't enjoy pre-rolls.\n\n### Finding a slack token\n\nUpdate July 2021: Slack has switched away from using questionably rotated user tokens to using \"cookie tokens\" and an associated short lived cookie. Smart, but we're smarter. User Tokens were of the format `xox[sp]-(\\w{12}|\\w{10})-(\\w{12}|\\w{11})-\\w{12}-\\w{64}` but *will no longer work* in most cases (and i'm too lazy to determine which). If use see an auth error, this is probably the reason. Cookie tokens follow a similar form, but note the `c`: `xoxc-(\\w{12}|\\w{10})-(\\w{12}|\\w{11})-\\w{12}-\\w{64}`.\n\nAs of emojme@2.0.0, support for cookie tokens has been added and is the recommended way of interacting with this dumb tool. The [emojme chrome extension](https://chrome.google.com/webstore/detail/emojme-emoji-anywhere/nbnaglaclijdfidbinlcnfdbikpbdkog?hl=en-US) provides a relatively ergonomic way to capture these along with a matching cookie. \n\nUpdate August 2022: ~It doesn't appear that the boot data that previously persisted on the page sticks around, and with it getting cleaned up there's no longer a \"just run this js one liner\" to my knowledge - if you know one, submit a PR! Put it right here -> [here](#cookie-token-one-liner) <-, with your name (@mootari), and feel free to take this wedge of cheese as payment 💨.\n\n#### Cookie Token One-liner\n\nTo extract the Slack cookie token, run the following script in your devtools console while being logged into your Slack team:\n```js\nJSON.parse(localStorage.localConfig_v2).teams[document.location.pathname.match(/^\\/client\\/([TE][A-Z0-9]+)/)[1]].token\n```\n\nThanks again to @mootari for finding this (and all of `localStorage.localConfig_v2`!)\n\n#### Finding a slack cookie\n\nAs cookies are now required, so too is this section. Slack's auth cookie, as far as I can tell, is the `d` cookie, which is unfortunately HttpOnly meaning it cannot be accessed via javascript. It can, however, be accessed with a little creativity.\n\nChrome's (and presumably any modern browser's) cookies API does allow for HttpConly cookies to be accessed, but require the user's explicit approval but way of an extension. [Emojme: Emoji Anywhere](https://github.com/jackellenberger/emojme-emoji-anywhere) is such an extension, and is [available in the chrome web store](https://chrome.google.com/webstore/detail/emojme-emoji-anywhere/nbnaglaclijdfidbinlcnfdbikpbdkog?hl=en-US) (or of course can be loaded from source if you want to take your life in your own hands). Clicking the extension icon > `Get Slack Token and Cookie` will land you with what I am calling a \"auth blob\", which you can then pass to emojme via the `--auth-json` argument.\n\n![So easy! So Fun! With just one chrome extension!](/images/emojme-chrome-extension.jpg)\n\nYou may also pull the `d` cookie with your fleshy human hands, if you so desire. Open up your browser's developer tools, then Application menu > Cookies > d, and copy the string out for yourself. With this method, it will be easier to specify individual `--subdomain --token --cookie` flags.\n\n![I have an MFA in drawing with a mouse](/images/how-to-get-a-cookie.jpg)\n\n## Usage\n\nEmojme can be used either as a command line tool or as a node module to be mixed in with your existing projects.\n\nComplete CLI flags can be found in [USAGe.md](USAGE.md), but each command takes the `--help` option.\n\n### Module\n\nIn your project's directory\n```bash\nnpm install --save emojme\n```\n\nIn your project\n\n```node\nvar emojme = require('emojme');\n\n// emojme-download\nvar downloadOptions = {\n  save: ['username_1', 'username_2'], // Download the emoji source files for these two users\n  bustCache: true, // make sure this data is fresh\n  output: true // download the adminList to ./build\n};\nvar downloadResults = await emojme.download('mySubdomain', 'myToken', 'myCookie', downloadOptions);\nconsole.log(downloadResults);\n/*\n  {\n    mySubdomain: {\n      emojiList: [\n        { name: 'emoji-from-mySubdomain', ... },\n        ...\n      ],\n      saveResults: [\n        './build/mySubdomain/username_1/an_emoji.jpg',\n        './build/mySubdomain/username_1/another_emoji.gif',\n        ... all of username_1's emoji\n        './build/mySubdomain/username_2/some_emoji.jpg',\n        './build/mySubdomain/username_2/some_other_emoji.gif',\n        ... all of username_2's emoji\n      ]\n    }\n  }\n*/\n\n// emojme-upload\nvar uploadOptions = {\n  src: './emoji-list.json', // upload all the emoji in this json array of objects\n  avoidCollisions: true, // append '-1' or similar if we try to upload a dupe\n  prefix: 'new-' // prepend every emoji in src with \"new-\", e.g. \"emoji\" becomes \"new-emoji\"\n};\nvar uploadResults = await emojme.upload('mySubdomain', 'myToken', 'myCookie', uploadOptions);\nconsole.log(uploadResults);\n/*\n  {\n    mySubdomain: {\n      collisions: [\n        { name: an-emoji-that-already-exists-in-mySubdomain ... }\n      ],\n      emojiList: [\n        { name: emoji-from-emoji-list-json ... },\n        { name: emoji-from-emoji-list-json ... },\n        ...\n      ]\n    }\n  }\n*/\n\n// emojme-add\nvar addOptions = {\n  src: ['./emoji1.jpg', 'http://example.com/emoji2.png'], // upload these two images\n  name: ['myLocalEmoji', 'myOnlineEmoji'], // call them these two names\n  bustCache: false, // don't bother redownloading existing emoji\n  avoidCollisions: true, // if there are similarly named emoji, change my new emoji names\n  output: false // don't write any files\n};\nvar subdomains = ['mySubdomain1', 'mySubdomain2'] // can add one or multiple\nvar tokens = ['myToken1', 'myToken2'] // can add one or multiple\nvar addResults = await emojme.add(subdomains, tokens, addOptions);\nconsole.log(addResults);\n/*\n  {\n    mySubomain1: {\n      collisions: [], // only defined if avoidCollisons = false\n      emojiList: [\n        { name: 'myLocalEmoji', ... },\n        { name: 'myOnlineEmoji', ... },\n      ]\n    },\n    mySubomain2: {\n      collisions: [], // only defined if avoidCollisons = false\n      emojiList: [\n        { name: 'myLocalEmoji', ... },\n        { name: 'myOnlineEmoji', ... },\n      ]\n    }\n  }\n*/\n\n// emojme-sync\nvar syncOptions = {\n  srcSubdomains: ['srcSubdomain'], // copy all emoji from srcSubdomain...\n  srcTokens: ['srcToken'],\n  dstSubdomains: ['dstSubdomain1', 'dstSubdomain2'], // ...to dstSubdomain1 and dstSubdomain2\n  dstTokens: ['dstToken1', 'dstToken2'],\n  bustCache: true // get fresh lists to make sure we're not doing more lifting than we have to\n};\nvar syncResults = await emojme.sync(null, null, syncOptions);\nconsole.log(syncResults);\n/*\n  {\n    dstSubdomain1: {\n      emojiList: [\n        { name: emoji-1-from-srcSubdomain ... },\n        { name: emoji-2-from-srcSubdomain ... }\n      ]\n    },\n    dstSubdomain2: {\n      emojiList: [\n        { name: emoji-1-from-srcSubdomain ... },\n        { name: emoji-2-from-srcSubdomain ... }\n      ]\n    }\n  }\n*/\n\n//emojme-user-stats\nvar userStatsOptions = {\n  user: ['username_1', 'username_2'] // get me some info on these two users\n};\nvar userStatsResults = await emojme.userStats('mySubdomain', 'myToken', 'myCookie', userStatsOptions);\nconsole.log(userStatsResults);\n/*\n  {\n    mySubdomain: {\n      userStatsResults: [\n        {\n          user: 'username_1',\n          userEmoji: [{ all username_1's emoji }],\n          subdomain: mySubdomain,\n          originalCount: x,\n          aliasCount: y,\n          totalCount: x + y,\n          percentage: (x + y) / mySubdomain's total emoji count\n        },\n        {\n          user: 'username_2',\n          userEmoji: [{ all username_2's emoji }],\n          subdomain: mySubdomain,\n          originalCount: x,\n          aliasCount: y,\n          totalCount: x + y,\n          percentage: (x + y) / mySubdomain's total emoji count\n        }\n      ]\n    }\n  }\n*/\n\n//emojme-favorites\nvar favoritesResult = await emojme.favorites('mySubdomain', 'myToken', 'myCookie', {});\nconsole.log(favoritesResult);\n/*\n  {\n    mySubdomain: {\n      favoritesResult: {\n          user: '{myToken's user}',\n          favoriteEmoji: [\n             emojiName,\n             ...\n          ],\n          favoriteEmojiAdminList: [\n            {emojiName}: {adminList-style emoji object, with additional `usage` value}\n            ...\n          ],\n        }\n    }\n  }\n*/\n```\n\n## Build directory output\n\nOkay you've run it, now what? Where are my dang emoji?\n\n* Diagnostic info and intermediate results are written to the build directory. Some might come in handy!\n* `build/$SUBDOMAIN.emojiUploadErrors.json` will give you a json of emoji that failed to upload and why. Use it to reattempt an upload! Generated from `upload` and `sync` calls.\n* `build/$SUBDOMAIN.adminList.json` is the \"master list\" of a subdomain's emoji. Generated from `download` and `sync` calls.\n* `build/$USER.$SUBDOMAIN.adminList.json` is all the emoji created by a user. Generated from `user-stats` calls.\n* `build/diff.to-$SUBDOMAIN.from-$SUBDOMAINLIST.adminList.json` contains all emoji present in $SUBDOMAINLIST but not in $SUBDOMAIN. Generated from `sync` calls.\n\n\n## A closer look at options\n* Universal options:\n  * **requires** at least one `--subdomain`/`--token`/`--cookie` **auth tuple**. Can accept multiple auth tuples.\n    * exception: sync can use a source/destination pattern, see below.\n  * _optional_: `--bust-cache` will force a redownload of emoji adminlist. If not supplied, a redownload is forced every  24 hours.\n  * _optional_: `--no-output` will prevent writing of files in the ./build directory. It does not currently suppres stdout.\n\n* `download`\n  * **requires** at least one `--subdomain`/`--token`/`--cookie` **auth tuple**. Can accept multiple auth tuples.\n  * _optional_: `--save $user` will save actual emoji data for the specified user, rather than just adminList json. Find the emoji in ./build/subdomain/user/\n  * _optional_: `--bust-cache` will force a redownload of emoji adminlist. If not supplied, a redownload is forced every  24 hours.\n  * _optional_: `--no-output` will prevent writing of files in the ./build directory. It does not currently suppres stdout.\n  * _optional_: `--since timestamp` will only download or save emoji created after the epoch time timestamp given, e.g. `1572064302751`\n* `upload`\n  * **requires** at least one `--subdomain`/`--token`/`--cookie` **auth tuple**. Can accept multiple auth tuples.\n  * **requires** at least one `--src` source json file.\n    * Src json should contain a list of objects where each object contains a \"name\" and \"url\" for image source\n    * Src yaml should contain an `emojis` key whose value is a list of emoji objects. Each object should contain `name` and `src` if an original emoji, or `name`, `is_alias: 1`, and `alias_for` if an alias.\n    * If adding an alias, url will be ignored and \"is_alias\" should be set to \"1\", and \"alias_for\" should be the name of the emoji to be aliased.\n  * _optional_: `--no-output` will prevent writing of files in the ./build directory. It does not currently suppres stdout.\n* `add`\n  * **requires** at least one `--subdomain`/`--token`/`--cookie` **auth tuple**. Can accept multiple auth tuples.\n  * **requires** one of the following:\n      1. `--src` path of local emoji file.\n          * _optional_: `--name` name of the emoji being uploaded. If not provided, the file name will be used.\n      1. `--name` and `--alias-for` to create an alias called `$NAME` with the same image as `$ALIAS-FOR`\n  * Multiple `--src`'s or `--name`/`--alias-for` pairs may be provided, but don't mix the patterns. You'll confuse yourself.\n  * _optional_: `--no-output` will prevent writing of files in the ./build directory. It does not currently suppres stdout.\n* `user-stats`\n  * **requires** at least one `--subdomain`/`--token`/`--cookie` **auth tuple**. Can accept multiple auth tuples.\n  * With no optional parameters given, this will print the top 10 emoji contributors\n  * _optional_: one of the following:\n      1. `--top` will show the top $TOP emoji contributors\n      1. `--user` will show statistics for $USER. Can accept multiple `--user` calls.\n  * _optional_: `--bust-cache` will force a redownload of emoji adminlist. If not supplied, a redownload is forced every  24 hours.\n  * _optional_: `--no-output` will prevent writing of files in the ./build directory. It does not currently suppres stdout.\n  * _optional_: `--since timestamp` will count the author statistics of only those emoji created after the epoch time timestamp given, e.g. `1572064302751`\n* `sync`\n  * **requires** one of the following:\n      1. at least **two** `--subdomain`/`--token`/`--cookie` **auth tuple**. Can accept more than two auth tuples.\n      1. at least **one** `--src-subdomain`/`--src-token` auth tuple and at least **one** `--dst-subdomain`/`--dst-token` auth tuples for \"one way\" syncing.\n  * _optional_: `--bust-cache` will force a redownload of emoji adminlist. If not supplied, a redownload is forced every  24 hours.\n  * _optional_: `--no-output` will prevent writing of files in the ./build directory. It does not currently suppres stdout.\n  * _optional_: `--since timestamp` will count the author statistics of only those emoji created after the epoch time timestamp given, e.g. `1572064302751`\n  * _optional_: `--dry-run` download adminLists for all requested subdomains and diff them, but don't upload any new emoji. Find the diffs in `./output/to-$DST_SUBDOMAIN.from-$SRC_SUBDOMAIN.adminList.json`\n* `favorites`\n  * **requires** at least one `--subdomain`/`--token`/`--cookie` **auth tuple**. Can accept multiple auth tuples.\n  * With no optional parameters given, this will print the token's user's 10 most used emoji\n  * _optional_: `--top` _verbose cli usage only_ limits stdout to top N most used emoji\n  * _optional_: `--usage` _verbose cli usage only_ prints not only the user's favorite emoji, but also the usage numbers.\n  * _optional_: `--bust-cache` will force a redownload of emoji adminlist and boot data. If not supplied, a redownload is forced every  24 hours.\n  * _optional_: `--no-output` will prevent writing of files in the ./build directory. It does not currently suppres stdout.\n\n\n## What's the difference between `Add` and `Upload`?\n\nInput type and use case! Technically (and behind the scenes) these commands do the same thing, which is post emoji to Slack.\n\nThe difference is that `Upload` is designed to take an `adminList` (what Slack calls a list of emoji and their related metadata) in the form of a json file. You can create this json file yourself, or use the `download` command to get it from an existing slack instance. It should be a Json array of objects, where each object represents an emoji and has attributes:\n* `name` (the name of the emoji duh)\n* `url` (the source content of the emoji. either a url, a file path, or a raw `data:` string)\n* `is_alias` (either 0 for non-aliases or 1 for aliases)\n* `alias_for` (name of the emoji to alias if the emoji being uploaded is an alias)\nThere are other fields in an adminList, but no others are used at the current time.\n\n`Add` is designed to allow users to upload a single or few emoji, directly from the command line, without having to craft a json file before hand. You can create either new emojis or new aliases (but not both, for now). Each new emoji needs a `--src`, and can take a `--name`, otherwise the file name will be used. Each new alias takes a `--name` and the name of the original emoji to alias as `--alias-for`.\n\n## CLI Examples\n\nIt should be noted that there are many ways to run this project. `npx emojme add` will work when emojme is present in `node_modules` (such as when downloaded via `npm`). `node ./emojme add` and `node ./emojme-add` will work if you have cloned the repo. These examples will use the former construction, but feel free to do whatever.\n\n### emojme download\n\n* Download all emoji from subdomain\n  * `npx emojme download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE`\n  * creates `./build/$SUBDOMAIN.adminList.json` containing url references to all emoji, but not the files themselves.\n\n* Download all emoji from subdomain using an authjson\n  * `npx emojme download --auth-json '{\"token\":\"$TOKEN\",\"domain\":\"$SUBDOMAIN\",\"cookie\":\"$COOKIE\"}'`\n  * creates `./build/$SUBDOMAIN.adminList.json` containing url references to all emoji, but not the files themselves.\n\n* Download all emoji from multiple subdomains\n  * `npx emojme download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2`\n  * creates `./build/$SUBDOMAIN1.adminList.json` and `./build/$SUBDOMAIN2.adminList.json`\n\n* download source content for emoji made by $USER1 and $USER2 in $SUBDOMAIN\n  * `npx emojme download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --save $USER1 --save $USER2`\n  * This will create directories `./build/$SUBDOMAIN/$USER1/` and `./build/$SUBDOMAIN/$USER2/`, each containing that user's raw emoji image files\n\n* download source content for all emoji in $SUBDOMAIN, grouping by user\n  * `npx emojme download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --save-all`\n  * This will create directories `./build/$SUBDOMAIN/$USER/` for each user in $SUBDOMAIN that has created an emoji\n\n### emojme add\n\n* add $FILE as :$NAME: and $URL as :$NAME2: to subdomain\n    * `npx emojme add --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src $FILE --name $NAME --src $URL --name $NAME2`\n\n* in $SUBDOMAIN1 and $SUBDOMAIN2, alias $ORIGINAL to $NAME\n    * `npx emojme add --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 ---subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2 --alias-for '$ORIGINAL' --name '$NAME'`\n\n* Alias :$ORIGINAL: as :$NAME:, and if :$NAME: exists, alias as :$NAME-1: instead\n    * `npx emojme add --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --name $NAME --alias_for $ORIGINAL --avoid-collisions`\n    * This has some amount of intelligence to it - if $ORIGINAL uses `_`'s, the alias will be `$ORIGINAL_1`, if the original has hyphens it will use hyphens, and if `-1` already exists it will use `-2`, etc.\n\n### emojme upload\n\n* upload emoji from source json to subdomain\n    * `npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src './myfile.json'`\n\n* upload emoji from source emojipacks yaml to subdomain\n    * `npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src './emojipacks.yaml'`\n\n* upload emoji from source json to multiple subdomains\n    * `npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2 --src './myfile.json'`\n\n* upload emoji from source json to subdomain, with each emoji being prefixed by $PREFIX\n    * `npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src './myfile.json' --prefix '$PREFIX'`\n\n* upload emoji from source json to subdomain, with each emoji being suffixed if it conficts with an existing emoji\n    * `npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src './myfile.json' --avoid-collisions`\n\n### emojme-sync\n\n* sync emoji so that $SUBDOMAIN1 and $SUBDOMAIN2 have the same emoji*\n    * <sup>*the same emoji names, that is. If :hi: is different on the two subdomains they will remain different</sup>\n    * `npx emojme sync --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 --subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2`\n\n* sync emoji so that $SUBDOMAIN1, $SUBDOMAIN2, and $SUBDOMAIN3 have the same emoji\n    * `npx emojme sync --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 --subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2 --subdomain $SUBDOMAIN3 --token $TOKEN3 --cookie $COOKIE3`\n\n* sync emoji from $SUBDOMAIN1 to $SUBDOMAIN2, so that $SUBDOMAIN1's emoji are a subset of $SUBDOMAIN2's emoji\n    * `npx emojme sync --src-subdomain $SUBDOMAIN1 --src-token $TOKEN1 --dst-subdomain $SUBDOMAIN2 --dst-token $TOKEN2`\n\n* sync emoji from $SUBDOMAIN1 to $SUBDOMAIN2 and $SUBDOMAIN3\n    * `npx emojme sync --src-subdomain $SUBDOMAIN1 --src-token $TOKEN1 --dst-subdomain $SUBDOMAIN2 --dst-token $TOKEN2 --dst-subdomain $SUBDOMAIN3 --dst-token $TOKEN3`\n\n* sync emoji from $SUBDOMAIN1 and $SUBDOMAIN2 to $SUBDOMAIN3\n    * `npx emojme sync --src-subdomain $SUBDOMAIN1 --src-token $TOKEN1 --src-subdomain $SUBDOMAIN2 --src-token $TOKEN2 --dst-subdomain $SUBDOMAIN3 --dst-token $TOKEN3`\n\n### emojme user stats\n\nThese commands all write files to the build directory, but become more immediately useful with the `--verbose` flag.\n\n* get author statistics for user $USER (emoji upload count, etc)\n    * `npx emojme user-stats --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --user $USER --verbose`\n    * This will create json file `./build/$USER.$SUBDOMAIN.adminList.json`\n\n* get user statistics for multiple users\n    * `npx emojme user-stats --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --user $USER --user $USER2 --user $USER3`\n    * This will create json files `./build/$USERX.$SUBDOMAIN.adminList.json` for each user passed\n\n* get user statistics for top $N contributors\n    * `npx emojme user-stats --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --top $N`\n    * Defaults to top 10 users.\n\n### emojme-favorites\n\n* Print the token's user's top 20 most used emoji\n    * `npx emojme favorites --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 --top 20 --verbose`\n\n* Print the usage numbers for the user's top 10 most used emoji\n    * `npx emojme favorites --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 --usage --verbose`\n\n\n## Pro Moves :promoves:\n\n### Creating a json file from a directory of images\nYou can use the script below to create a json file that will include all images in a directory. Make sure your directory only has files that end in gif, png, jpg, or jpeg. It will output a file called `emoji.json`.\n\n```\nbrew install jq\n./create-json.sh $PATH\n```\n\n\n### Getting a list of single attributes from an adminList json:\n\nHey try this with $ATTRIBUTE of \"url\". You might need all those urls!\n\n```\ncat $ADMINLIST.json | jq '.[] | .[\"$ATTRIBUTE\"]'\n```\n\n### Rate limiting and you\n\nSlack [threatened to release](https://api.slack.com/changelog/2018-03-great-rate-limits) then [released](https://api.slack.com/docs/rate-limits) rate limiting rules across its new api endpoints, and the rollout has included their undocumented endpoints now as well. As such, Emojme is going to slow down :capysad: Another nail in the coffin of making this a useful slackbot.\n\nThough it is unpublished, I have on good authority that `/emoji.adminList` is Tier 3 (when paginated) and `/emoji.add` is Tier 2, so emojme now has a \"fast part\" and a \"slow part\" respectively.\n\nI'm not one to judge how a person uses their own credentials, so there is a work around for those looking to get a bit more personal with the Slack networking infra team; Use the following environment variables to override my conservative defaults:\n```sh\n# How many requests to make at a time. Higher numbers are faster (as long as the other two params allow) and more prone to trip Slack's \"hey that's not a burst that's a malicous user\" alarm\nSLACK_REQUEST_CONCURRENCY\n# How many requests are to be sent per unit time. This is the real control of speed, the higher the more likely you are to be rate limited.\nSLACK_REQUEST_RATE\n# The unit of time, in ms. The lower the number the faster.\nSLACK_REQUEST_WINDOW\n\n# So, an example that has 10 in-flight requests at a time at a maximum rate of 200 requests per minute would be:\nSLACK_REQUEST_CONCURRENCY=10 \\\nSLACK_REQUEST_RATE=200 \\\nSLACK_REQUEST_WINDOW=60000 \\\nnode emojme-download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --save-all --bust-cache\n```\n\nI have tried my darndest to make the slack client in this project 429 tolerant, but after a few ignored 429's Slack gets mean and says you can't try again, so have fun dealing with that.\n\n### FAQ\n\n* I'm getting `invalid_auth` errors? huh???\n  * See #60. Essentially, Slack has gotten wise to our whole \"you can use a token for arbitrary lengths of time because Slack doesn't want to rotate them often and log us out of active sessions, or deal with zombie sessions that are authed with out of date tokens\". They've switched from using User Tokens (xoxs-) to Cookie Tokens (xoxc-), in combination with a cookie that is shortlived. Very clever, but we are more cleverer. We'll just rip off that cookie and pass it through the same way we were doing the token. It'll be a pain, but only as insecure as it was before.\n\n* I don't see any progress when I run a cli command\n  * Do you have `--verbose` in your command? that's pretty useful.\n\n* My network requests are slow and jerky\n  * That's how we gotta live under [rate limiting](#rate-limiting-and-you). To speed things up, try the env vars that are listed, but things might not go well. To make things less jerkey, knock down the concurrency so requests are more serial and there is no down time between bursts.\n\n* I just want to upload this thing fast, but I have to download 20k emoji to upload one?\n  * Nope! That is the normal behavior to not anger slack - we do more easy GET's to avoid some troublesome POSTs, but you can turn that off. Just add `--allow-collisions` (or `{collsions: true}`) to your upload request.\n\n## Contributing\n\nContribute! I'm garbo at js (and it's js's fault), so feel free to jump inand clean up, add features, and make the project live. I would recommend:\n\n* Add tests\n* Make your change\n* Run tests `npm run test` or `npm run test:unit && npm run test:integration`\n  * pro move: add a `debugger;` and use `it.only`, then `npm inspect node_modules/mocha/bin/_mocha spec/...` to debug a failing test.\n* Run end to end tests (requires a real slack instance) `npm run test:e2e -- --subdomain $YOUR_REAL_SUBDOMAIN --token $YOUR_REAL_TOKEN`\n* Lint\n* Regenerate docs, if necessary\n\n## Inspirations\n* [emojipacks](https://github.com/lambtron/emojipacks) is my OG. It mostly worked but seems rather undermaintained.\n* [neutral-face-emoji-tools](https://github.com/Fauntleroy/neutral-face-emoji-tools) is a fantastic tool that has enabled me to make enough emoji that this tool became necessary.\n\n## Stupid ways to use this stupid library!\n* https://github.com/jackellenberger/allmyemojichildren\n* https://github.com/guyfedwards/emoji\n* https://github.com/jackellenberger/emojme-hubot-plugin\n* https://github.com/jackellenberger/emojme-emoji-anywhere\n* https://github.com/jackellenberger/infinite-emoji-discord-bot\n"
  },
  {
    "path": "USAGE.md",
    "content": "# Commands\n```\nUsage: emojme [options] [command]\n\nOptions:\n  -V, --version  output the version number\n  -h, --help     output usage information\n\nCommands:\n  download       download all emoji from given subdomain\n  upload         upload source emoji to given subdomain\n  add            upload source emoji to given subdomain\n  user-stats     get emoji statistics for given user on given subdomain\n  sync           get emoji statistics for given user on given subdomain\n  favorites      get favorite emoji and personal emoji usage statistics\n  help [cmd]     display help for [cmd]\n```\n\n## emojme download\n```\nUsage: emojme-download [options]\n\nOptions:\n  -s, --subdomain <value>  slack subdomain. Can be specified multiple times, paired with respective token. (default: [])\n  -d, --domain <value>     alias for --subdomain (default: [])\n  -t, --token <value>      slack cookie token. ususaly starts xoxc-... Can be specified multiple times, paired with respective subdomains. User tokens (xoxs-...) are no longer supported. :( (default: [])\n  -c, --cookie <value>     slack cookie. paired with respective subdomains and tokens. (default: [])\n  -a --auth-json <value>   A json-string containing keys \"domain\", \"token\", and \"cookie\", as generated by the Emojme: Emoji Anywhere Chrome Extension. Can be used as a replacement for or in leu of subdomain, token, and cookie options. (default: [])\n  --bust-cache             force a redownload of all cached info.\n  --no-output              prevent writing of files in build/ and log/\n  --since <value>          only consider emoji since the given epoch timestamp\n  --verbose                log debug messages to console\n  --save <user>            save all of <user>'s emoji to disk at build/$subdomain/$user (default: [])\n  --save-all               save all emoji from all users to disk at build/$subdomain\n  --save-all-by-user       save all emoji from all users to disk at build/$subdomain/$user\n  -h, --help               output usage information\n```\n\n## emojme upload\n```\nUsage: emojme-upload [options]\n\nOptions:\n  -s, --subdomain <value>  slack subdomain. Can be specified multiple times, paired with respective token. (default: [])\n  -d, --domain <value>     alias for --subdomain (default: [])\n  -t, --token <value>      slack cookie token. ususaly starts xoxc-... Can be specified multiple times, paired with respective subdomains. User tokens (xoxs-...) are no longer supported. :( (default: [])\n  -c, --cookie <value>     slack cookie. paired with respective subdomains and tokens. (default: [])\n  -a --auth-json <value>   A json-string containing keys \"domain\", \"token\", and \"cookie\", as generated by the Emojme: Emoji Anywhere Chrome Extension. Can be used as a replacement for or in leu of subdomain, token, and cookie options. (default: [])\n  --bust-cache             force a redownload of all cached info.\n  --no-output              prevent writing of files in build/ and log/\n  --since <value>          only consider emoji since the given epoch timestamp\n  --verbose                log debug messages to console\n  --allow-collisions       do not cull collisions ever, upload everything just as it is and accept the collisions. This will be faster for known-good uploads, more rate-limiting prone for untrusted uploads.\n  --avoid-collisions       instead of culling collisions, rename the emoji to be uploaded \"intelligently\"\n  --prefix <value>         prefix all emoji to be uploaded with <value>\n  --src <value>            source file(s) for emoji json or yaml you'd like to upload\n  -h, --help               output usage information\n```\n\n## emojme add\n```\nUsage: emojme-add [options]\n\nOptions:\n  -s, --subdomain <value>  slack subdomain. Can be specified multiple times, paired with respective token. (default: [])\n  -d, --domain <value>     alias for --subdomain (default: [])\n  -t, --token <value>      slack cookie token. ususaly starts xoxc-... Can be specified multiple times, paired with respective subdomains. User tokens (xoxs-...) are no longer supported. :( (default: [])\n  -c, --cookie <value>     slack cookie. paired with respective subdomains and tokens. (default: [])\n  -a --auth-json <value>   A json-string containing keys \"domain\", \"token\", and \"cookie\", as generated by the Emojme: Emoji Anywhere Chrome Extension. Can be used as a replacement for or in leu of subdomain, token, and cookie options. (default: [])\n  --bust-cache             force a redownload of all cached info.\n  --no-output              prevent writing of files in build/ and log/\n  --since <value>          only consider emoji since the given epoch timestamp\n  --verbose                log debug messages to console\n  --allow-collisions       do not cull collisions ever, upload everything just as it is and accept the collisions. This will be faster for known-good uploads, more rate-limiting prone for untrusted uploads.\n  --avoid-collisions       instead of culling collisions, rename the emoji to be uploaded \"intelligently\"\n  --prefix <value>         prefix all emoji to be uploaded with <value>\n  --src <value>            source image/gif/#content for emoji you'd like to upload (default: null)\n  --name <value>           name of the emoji from --src that you'd like to upload (default: null)\n  --alias-for <value>      name of the emoji you'd like --name to be an alias of. Specifying this will negate --src (default: null)\n  -h, --help               output usage information\n```\n\n## emojme user-stats\n```\nUsage: emojme-user-stats [options]\n\nOptions:\n  -s, --subdomain <value>  slack subdomain. Can be specified multiple times, paired with respective token. (default: [])\n  -d, --domain <value>     alias for --subdomain (default: [])\n  -t, --token <value>      slack cookie token. ususaly starts xoxc-... Can be specified multiple times, paired with respective subdomains. User tokens (xoxs-...) are no longer supported. :( (default: [])\n  -c, --cookie <value>     slack cookie. paired with respective subdomains and tokens. (default: [])\n  -a --auth-json <value>   A json-string containing keys \"domain\", \"token\", and \"cookie\", as generated by the Emojme: Emoji Anywhere Chrome Extension. Can be used as a replacement for or in leu of subdomain, token, and cookie options. (default: [])\n  --bust-cache             force a redownload of all cached info.\n  --no-output              prevent writing of files in build/ and log/\n  --since <value>          only consider emoji since the given epoch timestamp\n  --verbose                log debug messages to console\n  --user <value>           slack user you'd like to get stats on. Can be specified multiple times for multiple users. (default: null)\n  --top <value>            the top n users you'd like user emoji statistics on (default: 10)\n  -h, --help               output usage information\n```\n\n## emojme sync\n```\nUsage: emojme-sync [options]\n\nOptions:\n  -s, --subdomain <value>  slack subdomain. Can be specified multiple times, paired with respective token. (default: [])\n  -d, --domain <value>     alias for --subdomain (default: [])\n  -t, --token <value>      slack cookie token. ususaly starts xoxc-... Can be specified multiple times, paired with respective subdomains. User tokens (xoxs-...) are no longer supported. :( (default: [])\n  -c, --cookie <value>     slack cookie. paired with respective subdomains and tokens. (default: [])\n  -a --auth-json <value>   A json-string containing keys \"domain\", \"token\", and \"cookie\", as generated by the Emojme: Emoji Anywhere Chrome Extension. Can be used as a replacement for or in leu of subdomain, token, and cookie options. (default: [])\n  --bust-cache             force a redownload of all cached info.\n  --no-output              prevent writing of files in build/ and log/\n  --since <value>          only consider emoji since the given epoch timestamp\n  --verbose                log debug messages to console\n  --src-subdomain [value]  subdomain from which to draw emoji for one way sync (default: null)\n  --src-token [value]      token with which to draw emoji for one way sync (default: null)\n  --src-cookie [value]     cookie with which to draw emoji for one way sync (default: null)\n  --dst-subdomain [value]  subdomain to which to emoji will be added is one way sync (default: null)\n  --dst-token [value]      token with which emoji will be added for one way sync (default: null)\n  --dst-cookie [value]     cookie with which emoji will be added for one way sync (default: null)\n  --dry-run                if set to true, nothing will be uploaded or synced\n  -h, --help               output usage information\n```\n\n## emojme favorites\n```\nUsage: emojme-favorites [options]\n\nOptions:\n  -s, --subdomain <value>  slack subdomain. Can be specified multiple times, paired with respective token. (default: [])\n  -d, --domain <value>     alias for --subdomain (default: [])\n  -t, --token <value>      slack cookie token. ususaly starts xoxc-... Can be specified multiple times, paired with respective subdomains. User tokens (xoxs-...) are no longer supported. :( (default: [])\n  -c, --cookie <value>     slack cookie. paired with respective subdomains and tokens. (default: [])\n  -a --auth-json <value>   A json-string containing keys \"domain\", \"token\", and \"cookie\", as generated by the Emojme: Emoji Anywhere Chrome Extension. Can be used as a replacement for or in leu of subdomain, token, and cookie options. (default: [])\n  --bust-cache             force a redownload of all cached info.\n  --no-output              prevent writing of files in build/ and log/\n  --since <value>          only consider emoji since the given epoch timestamp\n  --verbose                log debug messages to console\n  --top <value>            (verbose cli only) the top n favorites you'd like to see (default: 10)\n  --usage                  (verbose cli only) print emoji usage of favorites in addition to their names\n  --lite                   do not attempt to marry favorites with complete adminlist content. Results will contain only emoji name and usage count.\n  -h, --help               output usage information\n```\n"
  },
  {
    "path": "create-json.sh",
    "content": "ls $1 | jq -R \"reduce . as \\$i ({}; {\\\"src\\\": (\\\"$1/\\\" + \\$i), \\\"name\\\": (\\$i | sub(\\\".png\\\"; \\\"\\\") | sub(\\\".gif\\\"; \\\"\\\") | sub(\\\".jpg\\\"; \\\"\\\") | sub(\\\".jpeg\\\"; \\\"\\\"))})\" | jq -s '.' > emoji.json"
  },
  {
    "path": "docs/emojme-add.js.html",
    "content": "\n\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\n  <title>\n      emojme-add.js - Documentation\n  </title>\n\n  <link href=\"https://www.braintreepayments.com/images/favicon-ccda0b14.png\" rel=\"icon\" type=\"image/png\">\n\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/highlight.min.js\"></script>\n  <script>hljs.initHighlightingOnLoad();</script>\n\n  <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js\"></script>\n\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/prettify-tomorrow.css\">\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/jsdoc-default.css\">\n\n  \n\n  <!-- start Mixpanel -->\n  <script type=\"text/javascript\">(function(e,a){if(!a.__SV){var b=window;try{var c,l,i,j=b.location,g=j.hash;c=function(a,b){return(l=a.match(RegExp(b+\"=([^&]*)\")))?l[1]:null};g&&c(g,\"state\")&&(i=JSON.parse(decodeURIComponent(c(g,\"state\"))),\"mpeditor\"===i.action&&(b.sessionStorage.setItem(\"_mpcehash\",g),history.replaceState(i.desiredHash||\"\",e.title,j.pathname+j.search)))}catch(m){}var k,h;window.mixpanel=a;a._i=[];a.init=function(b,c,f){function e(b,a){var c=a.split(\".\");2==c.length&&(b=b[c[0]],a=c[1]);b[a]=function(){b.push([a].concat(Array.prototype.slice.call(arguments,\n  0)))}}var d=a;\"undefined\"!==typeof f?d=a[f]=[]:f=\"mixpanel\";d.people=d.people||[];d.toString=function(b){var a=\"mixpanel\";\"mixpanel\"!==f&&(a+=\".\"+f);b||(a+=\" (stub)\");return a};d.people.toString=function(){return d.toString(1)+\".people (stub)\"};k=\"disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user\".split(\" \");\n  for(h=0;h<k.length;h++)e(d,k[h]);a._i.push([b,c,f])};a.__SV=1.2;b=e.createElement(\"script\");b.type=\"text/javascript\";b.async=!0;b.src=\"undefined\"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:\"file:\"===e.location.protocol&&\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\".match(/^\\/\\//)?\"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\":\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\";c=e.getElementsByTagName(\"script\")[0];c.parentNode.insertBefore(b,c)}})(document,window.mixpanel||[]);\n  mixpanel.init(\"1919205b2da72e4da3b9b6639b444d59\");</script>\n  <!-- end Mixpanel -->\n</head>\n\n<body>\n  <svg style=\"display: none;\">\n    <defs>\n      <symbol id=\"linkIcon\" fill=\"#706d77\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n          <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n          <path d=\"M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z\"/>\n      </symbol>\n    </defs>\n  </svg>\n\n  <input type=\"checkbox\" id=\"nav-trigger\" class=\"nav-trigger\" />\n  <label for=\"nav-trigger\" class=\"navicon-button x\">\n    <div class=\"navicon\"></div>\n  </label>\n\n  <label for=\"nav-trigger\" class=\"overlay\"></label>\n\n  <div class=\"top-nav-wrapper\">\n    <ul>\n      <li >\n        <a href=\"index.html\">\n          \n            <svg fill=\"#6D6D6D\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n              <path d=\"M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z\"/>\n              <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n            </svg>\n          \n          \n        </a>\n      </li>\n\n      \n\n    </ul>\n  </div>\n\n  <nav>\n    <h3 class=\"reference-title\">\n      emojme\n    </h3>\n\n    <h3>Modules</h3><ul><li id=\"add-nav\"><a href=\"module-add.html\">add</a><ul class='methods'><li data-type=\"method\" id=\"add-add-nav\"><a href=\"module-add.html#~add\">add</a></li></ul></li><li id=\"download-nav\"><a href=\"module-download.html\">download</a><ul class='methods'><li data-type=\"method\" id=\"download-download-nav\"><a href=\"module-download.html#~download\">download</a></li></ul></li><li id=\"favorites-nav\"><a href=\"module-favorites.html\">favorites</a><ul class='methods'><li data-type=\"method\" id=\"favorites-favorites-nav\"><a href=\"module-favorites.html#~favorites\">favorites</a></li></ul></li><li id=\"sync-nav\"><a href=\"module-sync.html\">sync</a><ul class='methods'><li data-type=\"method\" id=\"sync-sync-nav\"><a href=\"module-sync.html#~sync\">sync</a></li></ul></li><li id=\"upload-nav\"><a href=\"module-upload.html\">upload</a><ul class='methods'><li data-type=\"method\" id=\"upload-upload-nav\"><a href=\"module-upload.html#~upload\">upload</a></li></ul></li><li id=\"userStats-nav\"><a href=\"module-userStats.html\">userStats</a><ul class='methods'><li data-type=\"method\" id=\"userStats-userStats-nav\"><a href=\"module-userStats.html#~userStats\">userStats</a></li></ul></li></ul>\n  </nav>\n\n  <div id=\"main\">\n    \n      <h1 class=\"page-title\">\n        emojme-add.js\n      </h1>\n    \n\n    \n      \n\n<section>\n  <article>\n    <pre class=\"prettyprint source linenums\"><code>const _ = require('lodash');\nconst commander = require('commander');\n\nconst EmojiAdminList = require('./lib/emoji-admin-list');\nconst EmojiAdd = require('./lib/emoji-add');\n\nconst FileUtils = require('./lib/util/file-utils');\nconst Helpers = require('./lib/util/helpers');\nconst Cli = require('./lib/util/cli');\n/** @module add */\n\n/**\n * The add response object, like other response objects, is organized by input subdomain.\n * @typedef {object} addResponseObject\n * @property {object} subdomain each subdomain passed in to add will appear as a key in the response\n * @property {emojiList[]} subdomain.emojiList the list of emoji added to `subdomain`, with each element reflecting the parameters passed in to `add`\n * @property {emojiList[]} subdomain.collisions if `options.avoidCollisions` is `false`, emoji that cannot be uploaded due to existing conflicting emoji names will exist here\n */\n\n/**\n * Add emoji described by parameters within options to the specified subdomain(s).\n *\n * Note that options can accept both aliases and original emoji at the same time, but ordering can get complicated and honestly I'd skip it if I were you. For each emoji, make sure that every descriptor (src, name, aliasFor) has a value, using `null`s for fields that are not relevant to the current emoji.\n *\n * @async\n * @param {string|string[]} subdomains a single or list of subdomains to add emoji to. Must match respectively to `token`s and `cookie`s.\n * @param {string|string[]} tokens a single or list of tokens to add emoji to. Must match respectively to `subdomain`s and `cookie`s.\n * @param {string|string[]} cookies a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to `subdomain`s and `token`s.\n * @param {object} options contains singleton or arrays of emoji descriptors.\n * @param {string|string[]} [options.src] source image files for the emoji to be added. If no corresponding `options.name` is given, the filename will be used\n * @param {string|string[]} [options.name] names of the emoji to be added, overriding filenames if given, and becoming the alias name if an `options.aliasFor` is given\n * @param {string|string[]} [options.aliasFor] names of emoji to be aliased to `options.name`\n * @param {boolean} [options.allowCollisions] if `true`, emoji being uploaded will not be checked against existing emoji. This will take less time up front but may cause more errors.\n * @param {boolean} [options.avoidCollisions] if `true`, emoji being added will be renamed to not collide with existing emoji. See {@link lib/util/helpers.avoidCollisions} for logic and details // TODO: fix this link, maybe link to tests which has better examples\n * @param {string} [options.prefix] string to prefix all emoji being uploaded\n * @param {boolean} [options.bustCache] if `true`, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making `options.avoidCollisions` more accurate\n * @param {boolean} [options.output] if `false`, no files will be written during execution. Prevents saving of adminList for future use\n *\n * @returns {Promise&lt;addResponseObject>} addResponseObject result object\n *\n * @example\nvar addOptions = {\n  src: ['./emoji1.jpg', 'http://example.com/emoji2.png'], // upload these two images\n  name: ['myLocalEmoji', 'myOnlineEmoji'], // call them these two names\n  bustCache: false, // don't bother redownloading existing emoji\n  avoidCollisions: true, // if there are similarly named emoji, change my new emoji names\n  output: false // don't write any files\n};\nvar subdomains = ['mySubdomain1', 'mySubdomain2'] // can add one or multiple\nvar tokens = ['myToken1', 'myToken2'] // can add one or multiple\nvar cookies = ['myCookie1', 'myCookie2'] // can add one or multiple\nvar addResults = await emojme.add(subdomains, tokens, cookies, addOptions);\nconsole.log(userStatsResults);\n// {\n//   mySubomain1: {\n//     collisions: [], // only defined if avoidCollisons = false\n//     emojiList: [\n//       { name: 'myLocalEmoji', ... },\n//       { name: 'myOnlineEmoji', ... },\n//     ]\n//   },\n//   mySubomain2: {\n//     collisions: [], // only defined if avoidCollisons = false\n//     emojiList: [\n//       { name: 'myLocalEmoji', ... },\n//       { name: 'myOnlineEmoji', ... },\n//     ]\n//   }\n// }\n */\nasync function add(subdomains, tokens, cookies, options) {\n  subdomains = Helpers.arrayify(subdomains);\n  tokens = Helpers.arrayify(tokens);\n  cookies = Helpers.arrayify(cookies);\n  options = options || {};\n  const aliases = Helpers.arrayify(options.aliasFor);\n  const names = Helpers.arrayify(options.name);\n  const sources = Helpers.arrayify(options.src);\n  let inputEmoji = []; let name; let alias; let\n    source;\n\n  while (aliases.length || sources.length) {\n    name = names.shift();\n    if (source = sources.shift()) {\n      inputEmoji.push({\n        is_alias: 0,\n        url: source,\n        name: name || source.match(/(?:.*\\/)?(.*).(jpg|jpeg|png|gif)/)[1],\n      });\n    } else {\n      alias = aliases.shift();\n      inputEmoji.push({\n        is_alias: 1,\n        alias_for: alias,\n        name,\n      });\n    }\n  }\n\n  if (names.length || _.find(inputEmoji, ['name', undefined])) {\n    return Promise.reject(new Error('Invalid input. Either not all inputs have been consumed, or not all emoji are well formed. Consider simplifying input, or padding input with `null` values.'));\n  }\n\n  const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies);\n\n  const addPromises = authTuples.map(async (authTuple) => {\n    let emojiToUpload = []; let\n      collisions = [];\n\n    if (options.prefix) {\n      inputEmoji = Helpers.applyPrefix(inputEmoji, options.prefix);\n    }\n\n    if (options.allowCollisions) {\n      emojiToUpload = inputEmoji;\n    } else {\n      const existingEmojiList = await new EmojiAdminList(...authTuple, options.output)\n        .get(options.bustCache);\n      const existingNameList = existingEmojiList.map(e => e.name);\n\n      if (options.avoidCollisions) {\n        inputEmoji = Helpers.avoidCollisions(inputEmoji, existingEmojiList);\n      }\n\n      [collisions, emojiToUpload] = _.partition(inputEmoji,\n        emoji => existingNameList.includes(emoji.name));\n    }\n\n    const emojiAdd = new EmojiAdd(...authTuple);\n    return emojiAdd.upload(emojiToUpload).then((uploadResult) => {\n      if (uploadResult.errorList &amp;&amp; uploadResult.errorList.length > 1 &amp;&amp; options.output) {\n        FileUtils.writeJson(`./build/${this.subdomain}.emojiUploadErrors.json`, uploadResult.errorList);\n      }\n      return Object.assign({}, uploadResult, { collisions });\n    });\n  });\n\n  return Helpers.formatResultsHash(await Promise.all(addPromises));\n}\n\nfunction addCli() {\n  const program = new commander.Command();\n\n  Cli.requireAuth(program);\n  Cli.allowIoControl(program);\n  Cli.allowEmojiAlterations(program)\n    .option('--src &lt;value>', 'source image/gif/#content for emoji you\\'d like to upload', Cli.list, null)\n    .option('--name &lt;value>', 'name of the emoji from --src that you\\'d like to upload', Cli.list, null)\n    .option('--alias-for &lt;value>', 'name of the emoji you\\'d like --name to be an alias of. Specifying this will negate --src', Cli.list, null)\n    .parse(process.argv);\n\n  Cli.unpackAuthJson(program);\n\n  return add(program.subdomain, program.token, program.cookie, {\n    src: program.src,\n    name: program.name,\n    aliasFor: program.aliasFor,\n    bustCache: program.bustCache,\n    allowCollisions: program.allowCollisions,\n    avoidCollisions: program.avoidCollisions,\n    prefix: program.prefix,\n    output: program.output,\n  });\n}\n\n\nif (require.main === module) {\n  addCli();\n}\n\nmodule.exports = {\n  add,\n  addCli,\n};\n</code></pre>\n  </article>\n</section>\n\n    \n\n\n  </div>\n\n  <br class=\"clear\">\n\n  <footer>\n    Documentation generated by <a href=\"https://github.com/jsdoc3/jsdoc\">JSDoc 3.5.5</a>\n  </footer>\n\n  <script src=\"scripts/linenumber.js\"></script>\n  <script src=\"scripts/pagelocation.js\"></script>\n\n  \n  \n</body>\n</html>\n"
  },
  {
    "path": "docs/emojme-download.js.html",
    "content": "\n\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\n  <title>\n      emojme-download.js - Documentation\n  </title>\n\n  <link href=\"https://www.braintreepayments.com/images/favicon-ccda0b14.png\" rel=\"icon\" type=\"image/png\">\n\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/highlight.min.js\"></script>\n  <script>hljs.initHighlightingOnLoad();</script>\n\n  <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js\"></script>\n\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/prettify-tomorrow.css\">\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/jsdoc-default.css\">\n\n  \n\n  <!-- start Mixpanel -->\n  <script type=\"text/javascript\">(function(e,a){if(!a.__SV){var b=window;try{var c,l,i,j=b.location,g=j.hash;c=function(a,b){return(l=a.match(RegExp(b+\"=([^&]*)\")))?l[1]:null};g&&c(g,\"state\")&&(i=JSON.parse(decodeURIComponent(c(g,\"state\"))),\"mpeditor\"===i.action&&(b.sessionStorage.setItem(\"_mpcehash\",g),history.replaceState(i.desiredHash||\"\",e.title,j.pathname+j.search)))}catch(m){}var k,h;window.mixpanel=a;a._i=[];a.init=function(b,c,f){function e(b,a){var c=a.split(\".\");2==c.length&&(b=b[c[0]],a=c[1]);b[a]=function(){b.push([a].concat(Array.prototype.slice.call(arguments,\n  0)))}}var d=a;\"undefined\"!==typeof f?d=a[f]=[]:f=\"mixpanel\";d.people=d.people||[];d.toString=function(b){var a=\"mixpanel\";\"mixpanel\"!==f&&(a+=\".\"+f);b||(a+=\" (stub)\");return a};d.people.toString=function(){return d.toString(1)+\".people (stub)\"};k=\"disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user\".split(\" \");\n  for(h=0;h<k.length;h++)e(d,k[h]);a._i.push([b,c,f])};a.__SV=1.2;b=e.createElement(\"script\");b.type=\"text/javascript\";b.async=!0;b.src=\"undefined\"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:\"file:\"===e.location.protocol&&\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\".match(/^\\/\\//)?\"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\":\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\";c=e.getElementsByTagName(\"script\")[0];c.parentNode.insertBefore(b,c)}})(document,window.mixpanel||[]);\n  mixpanel.init(\"1919205b2da72e4da3b9b6639b444d59\");</script>\n  <!-- end Mixpanel -->\n</head>\n\n<body>\n  <svg style=\"display: none;\">\n    <defs>\n      <symbol id=\"linkIcon\" fill=\"#706d77\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n          <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n          <path d=\"M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z\"/>\n      </symbol>\n    </defs>\n  </svg>\n\n  <input type=\"checkbox\" id=\"nav-trigger\" class=\"nav-trigger\" />\n  <label for=\"nav-trigger\" class=\"navicon-button x\">\n    <div class=\"navicon\"></div>\n  </label>\n\n  <label for=\"nav-trigger\" class=\"overlay\"></label>\n\n  <div class=\"top-nav-wrapper\">\n    <ul>\n      <li >\n        <a href=\"index.html\">\n          \n            <svg fill=\"#6D6D6D\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n              <path d=\"M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z\"/>\n              <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n            </svg>\n          \n          \n        </a>\n      </li>\n\n      \n\n    </ul>\n  </div>\n\n  <nav>\n    <h3 class=\"reference-title\">\n      emojme\n    </h3>\n\n    <h3>Modules</h3><ul><li id=\"add-nav\"><a href=\"module-add.html\">add</a><ul class='methods'><li data-type=\"method\" id=\"add-add-nav\"><a href=\"module-add.html#~add\">add</a></li></ul></li><li id=\"download-nav\"><a href=\"module-download.html\">download</a><ul class='methods'><li data-type=\"method\" id=\"download-download-nav\"><a href=\"module-download.html#~download\">download</a></li></ul></li><li id=\"favorites-nav\"><a href=\"module-favorites.html\">favorites</a><ul class='methods'><li data-type=\"method\" id=\"favorites-favorites-nav\"><a href=\"module-favorites.html#~favorites\">favorites</a></li></ul></li><li id=\"sync-nav\"><a href=\"module-sync.html\">sync</a><ul class='methods'><li data-type=\"method\" id=\"sync-sync-nav\"><a href=\"module-sync.html#~sync\">sync</a></li></ul></li><li id=\"upload-nav\"><a href=\"module-upload.html\">upload</a><ul class='methods'><li data-type=\"method\" id=\"upload-upload-nav\"><a href=\"module-upload.html#~upload\">upload</a></li></ul></li><li id=\"userStats-nav\"><a href=\"module-userStats.html\">userStats</a><ul class='methods'><li data-type=\"method\" id=\"userStats-userStats-nav\"><a href=\"module-userStats.html#~userStats\">userStats</a></li></ul></li></ul>\n  </nav>\n\n  <div id=\"main\">\n    \n      <h1 class=\"page-title\">\n        emojme-download.js\n      </h1>\n    \n\n    \n      \n\n<section>\n  <article>\n    <pre class=\"prettyprint source linenums\"><code>const commander = require('commander');\n\nconst EmojiAdminList = require('./lib/emoji-admin-list');\n\nconst Cli = require('./lib/util/cli');\nconst Helpers = require('./lib/util/helpers');\n/** @module download */\n\n/**\n * The download response object, like other response objects, is organized by input subdomain.\n * @typedef {object} downloadResponseObject\n * @property {object} subdomain each subdomain passed in to add will appear as a key in the response\n * @property {emojiList[]} subdomain.emojiList the list of emoji downloaded from `subdomain`\n * @property {string[]} subdomain.saveResults an array of paths for emoji that have been downloaded. note that all users that have been passed with `options.save` will be grouped together here.\n */\n\n/**\n * Download the list of custom emoji that have been added to the given slack instances, by default saving a json of all available relevant data. Optionally save the source images for a given user.\n *\n * @async\n * @param {string|string[]} subdomains a single or list of subdomains from which to download emoji. Must match respectively to `token`s and `cookie`s.\n * @param {string|string[]} tokens a single or list of tokens with which to authenticate. Must match respectively to `subdomain`s and `cookie`s.\n * @param {string|string[]} cookies a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to `subdomain`s and `token`s.\n * @param {object} options contains singleton or arrays of emoji descriptors.\n * @param {string|string[]} [options.save] A user name or array of user names whose emoji source images will be saved. All emoji source images are linked to in the default adminList, but passing a user name here will save that user's emoji to build/&lt;subdomain>/&lt;username>\n * @param {boolean} [options.saveAll] if `true`, download all emoji on slack instance from all users to disk in a single location.\n * @param {boolean} [options.saveAllByUser] if `true`, download all emoji on slack instance from all users to disk, organized into directories by user.\n * @param {boolean} [options.bustCache] if `true`, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making `options.avoidCollisions` more accurate\n * @param {boolean} [options.output] if `false`, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files\n * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file.\n *\n * @returns {Promise&lt;downloadResponseObject>} downloadResponseObject result object\n *\n * @example\nvar downloadOptions = {\n  save: ['username_1', 'username_2'], // Download the emoji source files for these two users\n  bustCache: true, // make sure this data is fresh\n  output: true // download the adminList to ./build\n};\nvar downloadResults = await emojme.download('mySubdomain', 'myToken', 'myCookie', downloadOptions);\nconsole.log(downloadResults);\n// {\n//   mySubdomain: {\n//     emojiList: [\n//       { name: 'emoji-from-mySubdomain', ... },\n//       ...\n//     ],\n//     saveResults: [\n//       './build/mySubdomain/username_1/an_emoji.jpg',\n//       './build/mySubdomain/username_1/another_emoji.gif',\n//       ... all of username_1's emoji\n//       './build/mySubdomain/username_2/some_emoji.jpg',\n//       './build/mySubdomain/username_2/some_other_emoji.gif',\n//       ... all of username_2's emoji\n//     ]\n//   }\n// }\n */\nasync function download(subdomains, tokens, cookies, options) {\n  subdomains = Helpers.arrayify(subdomains);\n  tokens = Helpers.arrayify(tokens);\n  cookies = Helpers.arrayify(cookies);\n  options = options || {};\n\n  const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies);\n\n  const downloadPromises = authTuples.map(async (authTuple) => {\n    const subdomain = authTuple[0];\n    let saveResults = [];\n\n    const adminList = new EmojiAdminList(...authTuple, options.output);\n    const emojiList = await adminList.get(options.bustCache, options.since);\n    if ((options.save &amp;&amp; options.save.length) || options.saveAll || options.saveAllByUser) {\n      saveResults = saveResults.concat(await EmojiAdminList.save(emojiList, subdomain, {\n        save: options.save, saveAll: options.saveAll, saveAllByUser: options.saveAllByUser,\n      }));\n    }\n\n    return { emojiList, subdomain, saveResults };\n  });\n\n  return Helpers.formatResultsHash(await Promise.all(downloadPromises));\n}\nfunction downloadCli() {\n  const program = new commander.Command();\n\n  Cli.requireAuth(program);\n  Cli.allowIoControl(program)\n    .option('--save &lt;user>', 'save all of &lt;user>\\'s emoji to disk at build/$subdomain/$user', Cli.list, [])\n    .option('--save-all', 'save all emoji from all users to disk at build/$subdomain')\n    .option('--save-all-by-user', 'save all emoji from all users to disk at build/$subdomain/$user')\n    .parse(process.argv);\n  Cli.unpackAuthJson(program);\n\n  return download(program.subdomain, program.token, program.cookie, {\n    save: program.save,\n    saveAll: program.saveAll,\n    saveAllByUser: program.saveAllByUser,\n    bustCache: program.bustCache,\n    output: program.output,\n    since: program.since,\n  });\n}\n\nif (require.main === module) {\n  downloadCli();\n}\n\nmodule.exports = {\n  download,\n  downloadCli,\n};\n</code></pre>\n  </article>\n</section>\n\n    \n\n\n  </div>\n\n  <br class=\"clear\">\n\n  <footer>\n    Documentation generated by <a href=\"https://github.com/jsdoc3/jsdoc\">JSDoc 3.5.5</a>\n  </footer>\n\n  <script src=\"scripts/linenumber.js\"></script>\n  <script src=\"scripts/pagelocation.js\"></script>\n\n  \n  \n</body>\n</html>\n"
  },
  {
    "path": "docs/emojme-favorites.js.html",
    "content": "\n\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\n  <title>\n      emojme-favorites.js - Documentation\n  </title>\n\n  <link href=\"https://www.braintreepayments.com/images/favicon-ccda0b14.png\" rel=\"icon\" type=\"image/png\">\n\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/highlight.min.js\"></script>\n  <script>hljs.initHighlightingOnLoad();</script>\n\n  <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js\"></script>\n\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/prettify-tomorrow.css\">\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/jsdoc-default.css\">\n\n  \n\n  <!-- start Mixpanel -->\n  <script type=\"text/javascript\">(function(e,a){if(!a.__SV){var b=window;try{var c,l,i,j=b.location,g=j.hash;c=function(a,b){return(l=a.match(RegExp(b+\"=([^&]*)\")))?l[1]:null};g&&c(g,\"state\")&&(i=JSON.parse(decodeURIComponent(c(g,\"state\"))),\"mpeditor\"===i.action&&(b.sessionStorage.setItem(\"_mpcehash\",g),history.replaceState(i.desiredHash||\"\",e.title,j.pathname+j.search)))}catch(m){}var k,h;window.mixpanel=a;a._i=[];a.init=function(b,c,f){function e(b,a){var c=a.split(\".\");2==c.length&&(b=b[c[0]],a=c[1]);b[a]=function(){b.push([a].concat(Array.prototype.slice.call(arguments,\n  0)))}}var d=a;\"undefined\"!==typeof f?d=a[f]=[]:f=\"mixpanel\";d.people=d.people||[];d.toString=function(b){var a=\"mixpanel\";\"mixpanel\"!==f&&(a+=\".\"+f);b||(a+=\" (stub)\");return a};d.people.toString=function(){return d.toString(1)+\".people (stub)\"};k=\"disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user\".split(\" \");\n  for(h=0;h<k.length;h++)e(d,k[h]);a._i.push([b,c,f])};a.__SV=1.2;b=e.createElement(\"script\");b.type=\"text/javascript\";b.async=!0;b.src=\"undefined\"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:\"file:\"===e.location.protocol&&\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\".match(/^\\/\\//)?\"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\":\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\";c=e.getElementsByTagName(\"script\")[0];c.parentNode.insertBefore(b,c)}})(document,window.mixpanel||[]);\n  mixpanel.init(\"1919205b2da72e4da3b9b6639b444d59\");</script>\n  <!-- end Mixpanel -->\n</head>\n\n<body>\n  <svg style=\"display: none;\">\n    <defs>\n      <symbol id=\"linkIcon\" fill=\"#706d77\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n          <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n          <path d=\"M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z\"/>\n      </symbol>\n    </defs>\n  </svg>\n\n  <input type=\"checkbox\" id=\"nav-trigger\" class=\"nav-trigger\" />\n  <label for=\"nav-trigger\" class=\"navicon-button x\">\n    <div class=\"navicon\"></div>\n  </label>\n\n  <label for=\"nav-trigger\" class=\"overlay\"></label>\n\n  <div class=\"top-nav-wrapper\">\n    <ul>\n      <li >\n        <a href=\"index.html\">\n          \n            <svg fill=\"#6D6D6D\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n              <path d=\"M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z\"/>\n              <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n            </svg>\n          \n          \n        </a>\n      </li>\n\n      \n\n    </ul>\n  </div>\n\n  <nav>\n    <h3 class=\"reference-title\">\n      emojme\n    </h3>\n\n    <h3>Modules</h3><ul><li id=\"add-nav\"><a href=\"module-add.html\">add</a><ul class='methods'><li data-type=\"method\" id=\"add-add-nav\"><a href=\"module-add.html#~add\">add</a></li></ul></li><li id=\"download-nav\"><a href=\"module-download.html\">download</a><ul class='methods'><li data-type=\"method\" id=\"download-download-nav\"><a href=\"module-download.html#~download\">download</a></li></ul></li><li id=\"favorites-nav\"><a href=\"module-favorites.html\">favorites</a><ul class='methods'><li data-type=\"method\" id=\"favorites-favorites-nav\"><a href=\"module-favorites.html#~favorites\">favorites</a></li></ul></li><li id=\"sync-nav\"><a href=\"module-sync.html\">sync</a><ul class='methods'><li data-type=\"method\" id=\"sync-sync-nav\"><a href=\"module-sync.html#~sync\">sync</a></li></ul></li><li id=\"upload-nav\"><a href=\"module-upload.html\">upload</a><ul class='methods'><li data-type=\"method\" id=\"upload-upload-nav\"><a href=\"module-upload.html#~upload\">upload</a></li></ul></li><li id=\"userStats-nav\"><a href=\"module-userStats.html\">userStats</a><ul class='methods'><li data-type=\"method\" id=\"userStats-userStats-nav\"><a href=\"module-userStats.html#~userStats\">userStats</a></li></ul></li></ul>\n  </nav>\n\n  <div id=\"main\">\n    \n      <h1 class=\"page-title\">\n        emojme-favorites.js\n      </h1>\n    \n\n    \n      \n\n<section>\n  <article>\n    <pre class=\"prettyprint source linenums\"><code>const _ = require('lodash');\nconst commander = require('commander');\nconst util = require('util');\n\nconst ClientBoot = require('./lib/client-boot');\nconst EmojiAdminList = require('./lib/emoji-admin-list');\n\nconst logger = require('./lib/logger');\nconst Cli = require('./lib/util/cli');\nconst FileUtils = require('./lib/util/file-utils');\nconst Helpers = require('./lib/util/helpers');\n/** @module favorites */\n\n/**\n * The user-specific favorites response object, like other response objects, is organized by input subdomain.\n * @typedef {object} favoritesResponseObject\n * @property {object} subdomain each subdomain passed in to add will appear as a key in the response\n * @property {string} subdomain.favoritesResult.user the username associated with the given cookie token\n * @property {string[]} subdomain.favoritesResult.favoriteEmoji the list of 'favorite' emoji as deemed by slack, in desc sorted order\n * @property {object[]} subdomain.favoritesResult.favoriteEmojiAdminList an array of emoji objects, as organized by emojiAdminList\n */\n\n/**\n * Get the contents of the \"Frequenly Used\" box for your specified user\n *\n * @async\n * @param {string|string[]} subdomains a single or list of subdomains from which to analyze emoji. Must match respectively to `token`s and `cookie`s.\n * @param {string|string[]} tokens a single or list of tokens to add emoji to. Must match respectively to `subdomain`s and `cookie`s.\n * @param {string|string[]} cookies a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to `subdomain`s and `token`s.\n * @param {object} options contains options on what to present\n * @param {Number} [options.lite] do not attempt to marry favorites with complete adminlist content. Results will contain only emoji name and usage count.\n * @param {Number} [options.top] (verbose cli only) count of top n emoji contriubtors you would like to retrieve user statistics on\n * @param {Number} [options.usage] (verbose cli only) print not just the list of favorite emoji, but their usage count\n * @param {boolean} [options.bustCache] if `true`, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making `options.avoidCollisions` more accurate\n * @param {boolean} [options.output] if `false`, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files\n * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file.\n *\n * @returns {Promise&lt;favoritesResponseObject>} fovoritesResponseObject result object\n *\n * @example\nvar favoritesResult = await emojme.favorites('mySubdomain', 'myToken', 'myCookie', {});\nconsole.log(favoritesResult);\n// {\n//   mySubdomain: {\n//     favoritesResult: {\n//         user: '{myToken's user}',\n//         favoriteEmoji: [\n//            emojiName,\n//            ...\n//         ],\n//         favoriteEmojiAdminList: [\n//           {emojiName}: {adminList-style emoji object, with additional `usage` value}\n//           ...\n//         ],\n//       }\n//   }\n// }\n */\nasync function favorites(subdomains, tokens, cookies, options) {\n  subdomains = Helpers.arrayify(subdomains);\n  tokens = Helpers.arrayify(tokens);\n  cookies = Helpers.arrayify(cookies);\n  options = options || {};\n\n  const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies);\n\n  const favoritesPromises = authTuples.map(async (authTuple) => {\n    let emojiList = [];\n    if (!options.lite) {\n      const emojiAdminList = new EmojiAdminList(...authTuple, options.output);\n      emojiList = await emojiAdminList.get(options.bustCache);\n    }\n\n    const bootClient = new ClientBoot(...authTuple, options.output);\n    const bootData = await bootClient.get(options.bustCache);\n    const user = ClientBoot.extractName(bootData);\n    const favoriteEmojiUsage = ClientBoot.extractEmojiUse(bootData);\n    const favoriteEmojiList = favoriteEmojiUsage.map(e => e.name);\n    const favoriteEmojiAdminList = _.reduce(favoriteEmojiUsage, (acc, usageObj) => {\n      acc.push({\n        [usageObj.name]: {\n          ...EmojiAdminList.find(emojiList, usageObj.name),\n          usage: usageObj.usage,\n        },\n      });\n      return acc;\n    }, []);\n\n    const result = {\n      user,\n      subdomain: bootClient.subdomain,\n      favoriteEmoji: favoriteEmojiList,\n      favoriteEmojiAdminList,\n    };\n\n    const safeUserName = FileUtils.sanitize(result.user);\n    if (options.output) FileUtils.writeJson(`./build/${safeUserName}.${bootClient.subdomain}.favorites.json`, result.favoriteEmojiAdminList, null, 3);\n\n    const topNFavorites = util.inspect(\n      (options.usage ? favoriteEmojiList : favoriteEmojiUsage)\n        .slice(0, options.top),\n    );\n    logger.info(`[${bootClient.subdomain}] Favorite emoji for ${result.user}: ${topNFavorites}`);\n\n    return { subdomain: bootClient.subdomain, favoritesResult: result };\n  });\n\n  return Helpers.formatResultsHash(_.flatten(await Promise.all(favoritesPromises)));\n}\n\nfunction favoritesCli() {\n  const program = new commander.Command();\n\n  Cli.requireAuth(program);\n  Cli.allowIoControl(program);\n  program\n    .option('--top &lt;value>', '(verbose cli only) the top n favorites you\\'d like to see', 10)\n    .option('--usage', '(verbose cli only) print emoji usage of favorites in addition to their names', false)\n    .option('--lite', 'do not attempt to marry favorites with complete adminlist content. Results will contain only emoji name and usage count.', false)\n    .parse(process.argv);\n  Cli.unpackAuthJson(program);\n\n  return favorites(program.subdomain, program.token, program.cookie, {\n    top: program.top,\n    usage: program.usage,\n    lite: program.lite,\n    bustCache: program.bustCache,\n    output: program.output,\n  }).catch((err) => {\n    console.error('An error occurred: ', err);\n  });\n}\n\nif (require.main === module) {\n  favoritesCli();\n}\n\nmodule.exports = {\n  favorites,\n  favoritesCli,\n};\n</code></pre>\n  </article>\n</section>\n\n    \n\n\n  </div>\n\n  <br class=\"clear\">\n\n  <footer>\n    Documentation generated by <a href=\"https://github.com/jsdoc3/jsdoc\">JSDoc 3.5.5</a>\n  </footer>\n\n  <script src=\"scripts/linenumber.js\"></script>\n  <script src=\"scripts/pagelocation.js\"></script>\n\n  \n  \n</body>\n</html>\n"
  },
  {
    "path": "docs/emojme-sync.js.html",
    "content": "\n\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\n  <title>\n      emojme-sync.js - Documentation\n  </title>\n\n  <link href=\"https://www.braintreepayments.com/images/favicon-ccda0b14.png\" rel=\"icon\" type=\"image/png\">\n\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/highlight.min.js\"></script>\n  <script>hljs.initHighlightingOnLoad();</script>\n\n  <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js\"></script>\n\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/prettify-tomorrow.css\">\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/jsdoc-default.css\">\n\n  \n\n  <!-- start Mixpanel -->\n  <script type=\"text/javascript\">(function(e,a){if(!a.__SV){var b=window;try{var c,l,i,j=b.location,g=j.hash;c=function(a,b){return(l=a.match(RegExp(b+\"=([^&]*)\")))?l[1]:null};g&&c(g,\"state\")&&(i=JSON.parse(decodeURIComponent(c(g,\"state\"))),\"mpeditor\"===i.action&&(b.sessionStorage.setItem(\"_mpcehash\",g),history.replaceState(i.desiredHash||\"\",e.title,j.pathname+j.search)))}catch(m){}var k,h;window.mixpanel=a;a._i=[];a.init=function(b,c,f){function e(b,a){var c=a.split(\".\");2==c.length&&(b=b[c[0]],a=c[1]);b[a]=function(){b.push([a].concat(Array.prototype.slice.call(arguments,\n  0)))}}var d=a;\"undefined\"!==typeof f?d=a[f]=[]:f=\"mixpanel\";d.people=d.people||[];d.toString=function(b){var a=\"mixpanel\";\"mixpanel\"!==f&&(a+=\".\"+f);b||(a+=\" (stub)\");return a};d.people.toString=function(){return d.toString(1)+\".people (stub)\"};k=\"disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user\".split(\" \");\n  for(h=0;h<k.length;h++)e(d,k[h]);a._i.push([b,c,f])};a.__SV=1.2;b=e.createElement(\"script\");b.type=\"text/javascript\";b.async=!0;b.src=\"undefined\"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:\"file:\"===e.location.protocol&&\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\".match(/^\\/\\//)?\"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\":\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\";c=e.getElementsByTagName(\"script\")[0];c.parentNode.insertBefore(b,c)}})(document,window.mixpanel||[]);\n  mixpanel.init(\"1919205b2da72e4da3b9b6639b444d59\");</script>\n  <!-- end Mixpanel -->\n</head>\n\n<body>\n  <svg style=\"display: none;\">\n    <defs>\n      <symbol id=\"linkIcon\" fill=\"#706d77\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n          <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n          <path d=\"M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z\"/>\n      </symbol>\n    </defs>\n  </svg>\n\n  <input type=\"checkbox\" id=\"nav-trigger\" class=\"nav-trigger\" />\n  <label for=\"nav-trigger\" class=\"navicon-button x\">\n    <div class=\"navicon\"></div>\n  </label>\n\n  <label for=\"nav-trigger\" class=\"overlay\"></label>\n\n  <div class=\"top-nav-wrapper\">\n    <ul>\n      <li >\n        <a href=\"index.html\">\n          \n            <svg fill=\"#6D6D6D\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n              <path d=\"M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z\"/>\n              <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n            </svg>\n          \n          \n        </a>\n      </li>\n\n      \n\n    </ul>\n  </div>\n\n  <nav>\n    <h3 class=\"reference-title\">\n      emojme\n    </h3>\n\n    <h3>Modules</h3><ul><li id=\"add-nav\"><a href=\"module-add.html\">add</a><ul class='methods'><li data-type=\"method\" id=\"add-add-nav\"><a href=\"module-add.html#~add\">add</a></li></ul></li><li id=\"download-nav\"><a href=\"module-download.html\">download</a><ul class='methods'><li data-type=\"method\" id=\"download-download-nav\"><a href=\"module-download.html#~download\">download</a></li></ul></li><li id=\"favorites-nav\"><a href=\"module-favorites.html\">favorites</a><ul class='methods'><li data-type=\"method\" id=\"favorites-favorites-nav\"><a href=\"module-favorites.html#~favorites\">favorites</a></li></ul></li><li id=\"sync-nav\"><a href=\"module-sync.html\">sync</a><ul class='methods'><li data-type=\"method\" id=\"sync-sync-nav\"><a href=\"module-sync.html#~sync\">sync</a></li></ul></li><li id=\"upload-nav\"><a href=\"module-upload.html\">upload</a><ul class='methods'><li data-type=\"method\" id=\"upload-upload-nav\"><a href=\"module-upload.html#~upload\">upload</a></li></ul></li><li id=\"userStats-nav\"><a href=\"module-userStats.html\">userStats</a><ul class='methods'><li data-type=\"method\" id=\"userStats-userStats-nav\"><a href=\"module-userStats.html#~userStats\">userStats</a></li></ul></li></ul>\n  </nav>\n\n  <div id=\"main\">\n    \n      <h1 class=\"page-title\">\n        emojme-sync.js\n      </h1>\n    \n\n    \n      \n\n<section>\n  <article>\n    <pre class=\"prettyprint source linenums\"><code>const _ = require('lodash');\nconst commander = require('commander');\n\nconst EmojiAdminList = require('./lib/emoji-admin-list');\nconst EmojiAdd = require('./lib/emoji-add');\n\nconst Cli = require('./lib/util/cli');\nconst FileUtils = require('./lib/util/file-utils');\nconst Helpers = require('./lib/util/helpers');\n/** @module sync */\n\n/**\n * The sync response object, like other response objects, is organized by input subdomain.\n * @typedef {object} syncResponseObject\n * @property {object} subdomain each subdomain passed in to add will appear as a key in the response\n * @property {emojiList[]} subdomain.emojiList the list of emoji added to `subdomain`, with each element an emoji pulled from either `srcSubdomain` or `subdomains` less the subdomain in question.\n */\n\n/**\n * Sync emoji between slack subdomains\n *\n * Sync can be executed in either a \"one way\" or \"n way\" configuration, and both configurations can have a variable number of sources and destinations. In a \"one way\" configuration, all emoji from all source subdomains will be added to all destination subdomains\" and can be set by specifying `srcSubdomains` and `dstSubdomains`. In an \"n way\" configuration, every subdomain given is treated as the destination for every emoji in every other subdomain.\n *\n * @async\n * @param {string|string[]|null} subdomains Two ore more subdomains that you wish to have the same emoji pool\n * @param {string|string[]|null} tokens cookie tokens corresponding to the given subdomains\n * @param {string|string[]|null} cookies User cookies corresponding to the given subdomains\n * @param {object} options contains src* and dst* information for \"one way\" sync configuration. Either specify `subdomains` and `tokens`, or `srcSubdomains`, `srcTokens`, `dstSubdomains`, and `dstTokens`, not both.\n * @param {string|string[]} [options.srcSubdomains] slack instances from which to draw emoji. No additions will be made to these subdomains\n * @param {string|string[]} [options.srcTokens] tokens for the slack instances from which to draw emoji\n * @param {string|string[]} [options.srcCookies] cookies auth cookies for the slack instances from which to draw emoji\n * @param {string|string[]} [options.dstSubdomains] slack instances in which all source emoji will be deposited. None of `dstSubdomain`'s emoji will end up in `srcSubdomain`\n * @param {string|string[]} [options.dstTokens] tokens for the slack instances where emoji will be deposited\n * @param {string|string[]} [options.dstCookies] cookies auth cookies for the slack instances from which to draw emoji\n * @param {boolean} [options.bustCache] if `true`, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making `options.avoidCollisions` more accurate\n * @param {boolean} [options.output] if `false`, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files\n * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file.\n *\n * @returns {Promise&lt;syncResponseObject>} syncResponseObject result object\n *\n * @example\nvar syncOptions = {\n  srcSubdomains: ['srcSubdomain'], // copy all emoji from srcSubdomain...\n  srcTokens: ['srcToken'],\n  srcCookies: ['srcCookie'],\n  dstSubdomains: ['dstSubdomain1', 'dstSubdomain2'], // ...to dstSubdomain1 and dstSubdomain2\n  dstTokens: ['dstToken1', 'dstToken2'],\n  dstCookies: ['dstCookie1', 'dstCookie2'],\n  bustCache: true // get fresh lists to make sure we're not doing more lifting than we have to\n};\nvar syncResults = await emojme.sync(null, null, syncOptions);\nconsole.log(syncResults);\n// {\n//   dstSubdomain1: {\n//     emojiList: [\n//       { name: emoji-1-from-srcSubdomain ... },\n//       { name: emoji-2-from-srcSubdomain ... }\n//     ]\n//   },\n//   dstSubdomain2: {\n//     emojiList: [\n//       { name: emoji-1-from-srcSubdomain ... },\n//       { name: emoji-2-from-srcSubdomain ... }\n//     ]\n//   }\n// }\n */\nasync function sync(subdomains, tokens, cookies, options) {\n  let diffs;\n  subdomains = Helpers.arrayify(subdomains);\n  tokens = Helpers.arrayify(tokens);\n  cookies = Helpers.arrayify(cookies);\n  options = options || {};\n\n  const [authTuples, srcPairs, dstPairs] = Helpers.zipAuthTuples(\n    subdomains,\n    tokens,\n    cookies,\n    options,\n  );\n\n  if (subdomains.length > 0) {\n    const emojiLists = await Promise.all(\n      authTuples.map(async authTuple => new EmojiAdminList(...authTuple, options.output)\n        .get(options.bustCache, options.since)),\n    );\n\n    diffs = EmojiAdminList.diff(emojiLists, subdomains);\n  } else if (srcPairs &amp;&amp; dstPairs) {\n    const srcDstPromises = [srcPairs, dstPairs].map(pairs => Promise.all(\n      pairs.map(async pair => new EmojiAdminList(...pair, options.output)\n        .get(options.bustCache, options.since)),\n    ));\n\n    const [srcEmojiLists, dstEmojiLists] = await Promise.all(srcDstPromises);\n    diffs = EmojiAdminList.diff(\n      srcEmojiLists, options.srcSubdomains, dstEmojiLists, options.dstSubdomains,\n    );\n  } else {\n    throw new Error('Invalid Input');\n  }\n\n  const uploadedDiffPromises = diffs.map((diffObj) => {\n    const pathSlug = `to-${diffObj.dstSubdomain}.from-${diffObj.srcSubdomains.join('-')}`;\n    if (options.output) FileUtils.writeJson(`./build/${pathSlug}.emojiAdminList.json`, diffObj.emojiList);\n    if (options.dryRun) return { subdomain: diffObj.dstSubdomain, emojiList: diffObj.emojiList };\n\n    const emojiAdd = new EmojiAdd(diffObj.dstSubdomain, _.find(\n      authTuples,\n      [0, diffObj.dstSubdomain],\n    )[1], options.output);\n    return emojiAdd.upload(diffObj.emojiList).then((results) => {\n      if (results.errorList &amp;&amp; results.errorList.length > 0 &amp;&amp; options.output) FileUtils.writeJson(`./build/${pathSlug}.emojiUploadErrors.json`, results.errorList);\n      return results;\n    });\n  });\n\n  return Helpers.formatResultsHash(await Promise.all(uploadedDiffPromises));\n}\n\nfunction syncCli() {\n  const program = new commander.Command();\n\n  Cli.requireAuth(program);\n  Cli.allowIoControl(program)\n    .option('--src-subdomain [value]', 'subdomain from which to draw emoji for one way sync', Cli.list, null)\n    .option('--src-token [value]', 'token with which to draw emoji for one way sync', Cli.list, null)\n    .option('--src-cookie [value]', 'cookie with which to draw emoji for one way sync', Cli.list, null)\n    .option('--dst-subdomain [value]', 'subdomain to which to emoji will be added is one way sync', Cli.list, null)\n    .option('--dst-token [value]', 'token with which emoji will be added for one way sync', Cli.list, null)\n    .option('--dst-cookie [value]', 'cookie with which emoji will be added for one way sync', Cli.list, null)\n    // Notice that this is missing --force and --prefix. These have been\n    // deemed TOO POWERFUL for mortal usage. If you _really_ want that\n    // power, you can download then upload the adminlist you retrieve.\n    .option('--dry-run', 'if set to true, nothing will be uploaded or synced', false)\n    .parse(process.argv);\n\n  Cli.unpackAuthJson(program);\n\n  return sync(program.subdomain, program.token, program.cookie, {\n    srcSubdomains: program.srcSubdomain,\n    srcTokens: program.srcToken,\n    srcCookies: program.srcCookie,\n    dstSubdomains: program.dstSubdomain,\n    dstTokens: program.dstToken,\n    dstCookies: program.dstCookie,\n    bustCache: program.bustCache,\n    output: program.output,\n    since: program.since,\n    dryRun: program.dryRun,\n  });\n}\n\nif (require.main === module) {\n  syncCli();\n}\n\nmodule.exports = {\n  sync,\n  syncCli,\n};\n</code></pre>\n  </article>\n</section>\n\n    \n\n\n  </div>\n\n  <br class=\"clear\">\n\n  <footer>\n    Documentation generated by <a href=\"https://github.com/jsdoc3/jsdoc\">JSDoc 3.5.5</a>\n  </footer>\n\n  <script src=\"scripts/linenumber.js\"></script>\n  <script src=\"scripts/pagelocation.js\"></script>\n\n  \n  \n</body>\n</html>\n"
  },
  {
    "path": "docs/emojme-upload.js.html",
    "content": "\n\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\n  <title>\n      emojme-upload.js - Documentation\n  </title>\n\n  <link href=\"https://www.braintreepayments.com/images/favicon-ccda0b14.png\" rel=\"icon\" type=\"image/png\">\n\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/highlight.min.js\"></script>\n  <script>hljs.initHighlightingOnLoad();</script>\n\n  <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js\"></script>\n\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/prettify-tomorrow.css\">\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/jsdoc-default.css\">\n\n  \n\n  <!-- start Mixpanel -->\n  <script type=\"text/javascript\">(function(e,a){if(!a.__SV){var b=window;try{var c,l,i,j=b.location,g=j.hash;c=function(a,b){return(l=a.match(RegExp(b+\"=([^&]*)\")))?l[1]:null};g&&c(g,\"state\")&&(i=JSON.parse(decodeURIComponent(c(g,\"state\"))),\"mpeditor\"===i.action&&(b.sessionStorage.setItem(\"_mpcehash\",g),history.replaceState(i.desiredHash||\"\",e.title,j.pathname+j.search)))}catch(m){}var k,h;window.mixpanel=a;a._i=[];a.init=function(b,c,f){function e(b,a){var c=a.split(\".\");2==c.length&&(b=b[c[0]],a=c[1]);b[a]=function(){b.push([a].concat(Array.prototype.slice.call(arguments,\n  0)))}}var d=a;\"undefined\"!==typeof f?d=a[f]=[]:f=\"mixpanel\";d.people=d.people||[];d.toString=function(b){var a=\"mixpanel\";\"mixpanel\"!==f&&(a+=\".\"+f);b||(a+=\" (stub)\");return a};d.people.toString=function(){return d.toString(1)+\".people (stub)\"};k=\"disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user\".split(\" \");\n  for(h=0;h<k.length;h++)e(d,k[h]);a._i.push([b,c,f])};a.__SV=1.2;b=e.createElement(\"script\");b.type=\"text/javascript\";b.async=!0;b.src=\"undefined\"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:\"file:\"===e.location.protocol&&\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\".match(/^\\/\\//)?\"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\":\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\";c=e.getElementsByTagName(\"script\")[0];c.parentNode.insertBefore(b,c)}})(document,window.mixpanel||[]);\n  mixpanel.init(\"1919205b2da72e4da3b9b6639b444d59\");</script>\n  <!-- end Mixpanel -->\n</head>\n\n<body>\n  <svg style=\"display: none;\">\n    <defs>\n      <symbol id=\"linkIcon\" fill=\"#706d77\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n          <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n          <path d=\"M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z\"/>\n      </symbol>\n    </defs>\n  </svg>\n\n  <input type=\"checkbox\" id=\"nav-trigger\" class=\"nav-trigger\" />\n  <label for=\"nav-trigger\" class=\"navicon-button x\">\n    <div class=\"navicon\"></div>\n  </label>\n\n  <label for=\"nav-trigger\" class=\"overlay\"></label>\n\n  <div class=\"top-nav-wrapper\">\n    <ul>\n      <li >\n        <a href=\"index.html\">\n          \n            <svg fill=\"#6D6D6D\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n              <path d=\"M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z\"/>\n              <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n            </svg>\n          \n          \n        </a>\n      </li>\n\n      \n\n    </ul>\n  </div>\n\n  <nav>\n    <h3 class=\"reference-title\">\n      emojme\n    </h3>\n\n    <h3>Modules</h3><ul><li id=\"add-nav\"><a href=\"module-add.html\">add</a><ul class='methods'><li data-type=\"method\" id=\"add-add-nav\"><a href=\"module-add.html#~add\">add</a></li></ul></li><li id=\"download-nav\"><a href=\"module-download.html\">download</a><ul class='methods'><li data-type=\"method\" id=\"download-download-nav\"><a href=\"module-download.html#~download\">download</a></li></ul></li><li id=\"favorites-nav\"><a href=\"module-favorites.html\">favorites</a><ul class='methods'><li data-type=\"method\" id=\"favorites-favorites-nav\"><a href=\"module-favorites.html#~favorites\">favorites</a></li></ul></li><li id=\"sync-nav\"><a href=\"module-sync.html\">sync</a><ul class='methods'><li data-type=\"method\" id=\"sync-sync-nav\"><a href=\"module-sync.html#~sync\">sync</a></li></ul></li><li id=\"upload-nav\"><a href=\"module-upload.html\">upload</a><ul class='methods'><li data-type=\"method\" id=\"upload-upload-nav\"><a href=\"module-upload.html#~upload\">upload</a></li></ul></li><li id=\"userStats-nav\"><a href=\"module-userStats.html\">userStats</a><ul class='methods'><li data-type=\"method\" id=\"userStats-userStats-nav\"><a href=\"module-userStats.html#~userStats\">userStats</a></li></ul></li></ul>\n  </nav>\n\n  <div id=\"main\">\n    \n      <h1 class=\"page-title\">\n        emojme-upload.js\n      </h1>\n    \n\n    \n      \n\n<section>\n  <article>\n    <pre class=\"prettyprint source linenums\"><code>const _ = require('lodash');\nconst fs = require('graceful-fs');\nconst commander = require('commander');\n\nconst EmojiAdminList = require('./lib/emoji-admin-list');\nconst EmojiAdd = require('./lib/emoji-add');\n\nconst Cli = require('./lib/util/cli');\nconst FileUtils = require('./lib/util/file-utils');\nconst Helpers = require('./lib/util/helpers');\n/** @module upload */\n\n/**\n * The upload response object, like other response objects, is organized by input subdomain.\n * @typedef {object} syncResponseObject\n * @property {object} subdomain each subdomain passed in to add will appear as a key in the response\n * @property {emojiList[]} subdomain.emojiList the list of emoji added to `subdomain`, with each element an emoji pulled from either `srcSubdomain` or `subdomains` less the subdomain in question.\n * @property {emojiList[]} subdomain.collisions if `options.avoidCollisions` is `false`, emoji that cannot be uploaded due to existing conflicting emoji names will exist here\n */\n\n/**\n * The required format of a json file that can be used as the `options.src` for {@link upload}\n *\n * To see an example, use {@link download}, then look at `buidl/*.adminList.json`\n *\n * @typedef {Array} jsonEmojiListFormat\n * @property {Array} emojiList\n * @property {object} emojiList.emojiObject\n * @property {string} emojiList.emojiObject.name the name of the emoji\n * @property {1|0} emojiList.emojiObject.is_alias whether or not the emoji is an alias. If `1`, `alias_for` is require and `url` is ignored. If `0` vice versa\n * @property {string} emojiList.emojiObject.alias_for the name of the emoji this emoji is apeing\n * @property {string} emojiList.emojiObject.url the remote url or local path of the emoji\n * @property {string} emojiList.emojiObject.user_display_name the name of the emoji creator\n *\n * @example\n * [\n *   {\n *      \"name\": \"a_giving_lovely_generous_individual\",\n *      \"is_alias\": 1,\n *      \"alias_for\": \"caleb\"\n *   },\n *   {\n *     \"name\": \"gooddoggy\",\n *     \"is_alias\": 0,\n *     \"alias_for\": null,\n *     \"url\": \"https://emoji.slack-edge.com/T3T9KQULR/gooddoggy/849f53cf1de25f97.png\"\n *   }\n * ]\n */\n\n/**\n * The required format of a yaml file that can be used as the `options.src` for {@link upload}\n * @typedef {object} yamlEmojiListFormat\n * @property {object} topLevelYaml all keys execpt for `emojis` are ignored\n * @property {Array} emojis the array of emoji objects\n * @property {object} emojis.emojiObject\n * @property {string} emojis.emojiObject.name the name of the emoji\n * @property {string} emojis.emojiObject.src alias for `name`\n * @property {1|0} emojis.emojiObject.is_alias whether or not the emoji is an alias. If `1`, `alias_for` is require and `url` is ignored. If `0` vice versa\n * @property {string} emojis.emojiObject.alias_for the name of the emoji this emoji is apeing\n * @property {string} emojis.emojiObject.url the remote url or local path of the emoji\n * @property {string} emojis.emojiObject.user_display_name the name of the emoji creator\n *\n * @example\n *  title: animals\n *  emojis:\n *    - name: llama\n *      src: http://i.imgur.com/6bKXKUP.gif\n *    - name: alpaca\n *      src: http://i.imgur.com/c6QxTbM.gif\n */\n\n/**\n * Upload multiple emoji described by an existing list on disk, either as a json emoji admin list or emojipacks-like yaml.\n *\n * @async\n * @param {string|string[]} subdomains a single or list of subdomains from which to download emoji. Must match respectively to `token`s and `cookie`s.\n * @param {string|string[]} tokens a single or list of tokens with which to authenticate. Must match respectively to `subdomain`s and `cookie`s.\n * @param {string|string[]} cookies a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to `subdomain`s and `token`s.\n * @param {object} options contains singleton or arrays of emoji descriptors.\n * @param {string|string[]} options.src source emoji list files for the emoji to be added. Can either be in {@link jsonEmojiListFormat} or {@link yamlEmojiListFormat}\n * @param {boolean} [options.avoidCollisions] if `true`, emoji being added will be renamed to not collide with existing emoji. See {@link lib/util/helpers.avoidCollisions} for logic and details // TODO: fix this link, maybe link to tests which has better examples\n * @param {string} [options.prefix] string to prefix all emoji being uploaded\n * @param {boolean} [options.bustCache] if `true`, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making `options.avoidCollisions` more accurate\n * @param {boolean} [options.output] if `false`, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files\n * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file.\n *\n * @returns {Promise&lt;uploadResponseObject>} uploadResponseObject result object\n *\n * @example\nvar uploadOptions = {\n  src: './emoji-list.json', // upload all the emoji in this json array of objects\n  avoidCollisions: true, // append '-1' or similar if we try to upload a dupe\n  prefix: 'new-' // prepend every emoji in src with \"new-\", e.g. \"emoji\" becomes \"new-emoji\"\n};\nvar uploadResults = await emojme.upload('mySubdomain', 'myToken', 'myCookie', uploadOptions);\nconsole.log(uploadResults);\n// {\n//   mySubdomain: {\n//     collisions: [\n//       { name: an-emoji-that-already-exists-in-mySubdomain ... }\n//     ],\n//     emojiList: [\n//       { name: emoji-from-emoji-list-json ... },\n//       { name: emoji-from-emoji-list-json ... },\n//       ...\n//     ]\n//   }\n// }\n */\nasync function upload(subdomains, tokens, cookies, options) {\n  subdomains = Helpers.arrayify(subdomains);\n  tokens = Helpers.arrayify(tokens);\n  cookies = Helpers.arrayify(cookies);\n  options = options || {};\n  let inputEmoji;\n\n  // TODO this isn't handling --src file --src file correctly\n  if (Array.isArray(options.src)) {\n    inputEmoji = options.src;\n  } else if (!fs.existsSync(options.src)) {\n    throw new Error(`Emoji source file ${options.src} does not exist`);\n  } else {\n    const fileType = options.src.split('.').slice(-1)[0];\n    if (fileType.match(/yaml|yml/)) {\n      inputEmoji = FileUtils.readYaml(options.src);\n    } else if (fileType.match(/json/)) {\n      inputEmoji = FileUtils.readJson(options.src);\n    } else {\n      throw new Error(`Filetype ${fileType} is not supported`);\n    }\n  }\n\n  const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies);\n\n  const uploadPromises = authTuples.map(async (authTuple) => {\n    let emojiToUpload = []; let\n      collisions = [];\n\n    if (options.prefix) {\n      inputEmoji = Helpers.applyPrefix(inputEmoji, options.prefix);\n    }\n\n    if (options.allowCollisions) {\n      emojiToUpload = inputEmoji;\n    } else {\n      const existingEmojiList = await new EmojiAdminList(...authTuple, options.output)\n        .get(options.bustCache);\n      const existingNameList = existingEmojiList.map(e => e.name);\n\n      if (options.avoidCollisions) {\n        inputEmoji = Helpers.avoidCollisions(inputEmoji, existingEmojiList);\n      }\n\n      [collisions, emojiToUpload] = _.partition(inputEmoji,\n        emoji => existingNameList.includes(emoji.name));\n    }\n\n    const emojiAdd = new EmojiAdd(...authTuple);\n    const uploadResult = await emojiAdd.upload(emojiToUpload);\n    return Object.assign({}, uploadResult, { collisions });\n  });\n\n  return Helpers.formatResultsHash(await Promise.all(uploadPromises));\n}\n\nfunction uploadCli() {\n  const program = new commander.Command();\n\n  Cli.requireAuth(program);\n  Cli.allowIoControl(program);\n  Cli.allowEmojiAlterations(program)\n    .option('--src &lt;value>', 'source file(s) for emoji json or yaml you\\'d like to upload')\n    .parse(process.argv);\n  Cli.unpackAuthJson(program);\n\n  return upload(program.subdomain, program.token, program.cookie, {\n    src: program.src,\n    bustCache: program.bustCache,\n    allowCollisions: program.allowCollisions,\n    avoidCollisions: program.avoidCollisions,\n    prefix: program.prefix,\n    output: program.output,\n  });\n}\n\nif (require.main === module) {\n  uploadCli();\n}\n\nmodule.exports = {\n  upload,\n  uploadCli,\n};\n</code></pre>\n  </article>\n</section>\n\n    \n\n\n  </div>\n\n  <br class=\"clear\">\n\n  <footer>\n    Documentation generated by <a href=\"https://github.com/jsdoc3/jsdoc\">JSDoc 3.5.5</a>\n  </footer>\n\n  <script src=\"scripts/linenumber.js\"></script>\n  <script src=\"scripts/pagelocation.js\"></script>\n\n  \n  \n</body>\n</html>\n"
  },
  {
    "path": "docs/emojme-user-stats.js.html",
    "content": "\n\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\n  <title>\n      emojme-user-stats.js - Documentation\n  </title>\n\n  <link href=\"https://www.braintreepayments.com/images/favicon-ccda0b14.png\" rel=\"icon\" type=\"image/png\">\n\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/highlight.min.js\"></script>\n  <script>hljs.initHighlightingOnLoad();</script>\n\n  <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js\"></script>\n\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/prettify-tomorrow.css\">\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/jsdoc-default.css\">\n\n  \n\n  <!-- start Mixpanel -->\n  <script type=\"text/javascript\">(function(e,a){if(!a.__SV){var b=window;try{var c,l,i,j=b.location,g=j.hash;c=function(a,b){return(l=a.match(RegExp(b+\"=([^&]*)\")))?l[1]:null};g&&c(g,\"state\")&&(i=JSON.parse(decodeURIComponent(c(g,\"state\"))),\"mpeditor\"===i.action&&(b.sessionStorage.setItem(\"_mpcehash\",g),history.replaceState(i.desiredHash||\"\",e.title,j.pathname+j.search)))}catch(m){}var k,h;window.mixpanel=a;a._i=[];a.init=function(b,c,f){function e(b,a){var c=a.split(\".\");2==c.length&&(b=b[c[0]],a=c[1]);b[a]=function(){b.push([a].concat(Array.prototype.slice.call(arguments,\n  0)))}}var d=a;\"undefined\"!==typeof f?d=a[f]=[]:f=\"mixpanel\";d.people=d.people||[];d.toString=function(b){var a=\"mixpanel\";\"mixpanel\"!==f&&(a+=\".\"+f);b||(a+=\" (stub)\");return a};d.people.toString=function(){return d.toString(1)+\".people (stub)\"};k=\"disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user\".split(\" \");\n  for(h=0;h<k.length;h++)e(d,k[h]);a._i.push([b,c,f])};a.__SV=1.2;b=e.createElement(\"script\");b.type=\"text/javascript\";b.async=!0;b.src=\"undefined\"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:\"file:\"===e.location.protocol&&\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\".match(/^\\/\\//)?\"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\":\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\";c=e.getElementsByTagName(\"script\")[0];c.parentNode.insertBefore(b,c)}})(document,window.mixpanel||[]);\n  mixpanel.init(\"1919205b2da72e4da3b9b6639b444d59\");</script>\n  <!-- end Mixpanel -->\n</head>\n\n<body>\n  <svg style=\"display: none;\">\n    <defs>\n      <symbol id=\"linkIcon\" fill=\"#706d77\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n          <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n          <path d=\"M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z\"/>\n      </symbol>\n    </defs>\n  </svg>\n\n  <input type=\"checkbox\" id=\"nav-trigger\" class=\"nav-trigger\" />\n  <label for=\"nav-trigger\" class=\"navicon-button x\">\n    <div class=\"navicon\"></div>\n  </label>\n\n  <label for=\"nav-trigger\" class=\"overlay\"></label>\n\n  <div class=\"top-nav-wrapper\">\n    <ul>\n      <li >\n        <a href=\"index.html\">\n          \n            <svg fill=\"#6D6D6D\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n              <path d=\"M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z\"/>\n              <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n            </svg>\n          \n          \n        </a>\n      </li>\n\n      \n\n    </ul>\n  </div>\n\n  <nav>\n    <h3 class=\"reference-title\">\n      emojme\n    </h3>\n\n    <h3>Modules</h3><ul><li id=\"add-nav\"><a href=\"module-add.html\">add</a><ul class='methods'><li data-type=\"method\" id=\"add-add-nav\"><a href=\"module-add.html#~add\">add</a></li></ul></li><li id=\"download-nav\"><a href=\"module-download.html\">download</a><ul class='methods'><li data-type=\"method\" id=\"download-download-nav\"><a href=\"module-download.html#~download\">download</a></li></ul></li><li id=\"favorites-nav\"><a href=\"module-favorites.html\">favorites</a><ul class='methods'><li data-type=\"method\" id=\"favorites-favorites-nav\"><a href=\"module-favorites.html#~favorites\">favorites</a></li></ul></li><li id=\"sync-nav\"><a href=\"module-sync.html\">sync</a><ul class='methods'><li data-type=\"method\" id=\"sync-sync-nav\"><a href=\"module-sync.html#~sync\">sync</a></li></ul></li><li id=\"upload-nav\"><a href=\"module-upload.html\">upload</a><ul class='methods'><li data-type=\"method\" id=\"upload-upload-nav\"><a href=\"module-upload.html#~upload\">upload</a></li></ul></li><li id=\"userStats-nav\"><a href=\"module-userStats.html\">userStats</a><ul class='methods'><li data-type=\"method\" id=\"userStats-userStats-nav\"><a href=\"module-userStats.html#~userStats\">userStats</a></li></ul></li></ul>\n  </nav>\n\n  <div id=\"main\">\n    \n      <h1 class=\"page-title\">\n        emojme-user-stats.js\n      </h1>\n    \n\n    \n      \n\n<section>\n  <article>\n    <pre class=\"prettyprint source linenums\"><code>const _ = require('lodash');\nconst commander = require('commander');\n\nconst EmojiAdminList = require('./lib/emoji-admin-list');\n\nconst Cli = require('./lib/util/cli');\nconst FileUtils = require('./lib/util/file-utils');\nconst Helpers = require('./lib/util/helpers');\n/** @module userStats */\n\n/**\n * The user-specific userStats response object, like other response objects, is organized by input subdomain.\n * @typedef {object} userStatsResponseObject\n * @property {object} subdomain each subdomain passed in to add will appear as a key in the response\n * @property {emojiList[]} subdomain.emojiList the list of emoji downloaded from `subdomain`\n * @property {object[]} subdomain.userStatsResults an array of user stats objects\n * @property {object} subdomain.userStatsResults.userStatsObject An object containing several maybe-useful statistics, separated by user\n * @property {string} subdomain.userStatsResults.userStatsObject.user the name of the user in question\n * @property {emojiList[]} subdomain.userStatsResults.userStatsObject.userEmoji the emojiList the user authored\n * @property {string} subdomain.userStatsResults.userStatsObject.subdomain redundant :shrug:\n * @property {Number} subdomain.userStatsResults.userStatsObject.originalCount the number of original emoji the user has created\n * @property {Number} subdomain.userStatsResults.userStatsObject.aliasCount the number of emoji aliases the user has defined\n * @property {Number} subdomain.userStatsResults.userStatsObject.totalCount the number of original and aliases the user has created\n * @property {Number} subdomain.userStatsResults.userStatsObject.percentage the percentage of emoji in the given subdomain that the user is responsible for\n */\n\n/**\n * Get a few useful-ish statistics for either specific users, or the top-n emoji creators\n *\n * @async\n * @param {string|string[]} subdomains a single or list of subdomains to analyze. Must match respectively to `token`s and `cookie`s.\n * @param {string|string[]} tokens a single or list of tokens to add emoji to. Must match respectively to `subdomain`s and `cookie`s.\n * @param {string|string[]} cookies a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to `subdomain`s and `token`s.\n * @param {object} options contains options for what stats to present\n * @param {string|string[]} [options.user] user name or array of user names you would like to retrieve user statistics on. If specified, ignores `top`\n * @param {Number} [options.top] count of top n emoji contriubtors you would like to retrieve user statistics on\n * @param {boolean} [options.bustCache] if `true`, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making `options.avoidCollisions` more accurate\n * @param {boolean} [options.output] if `false`, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files\n * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file.\n *\n * @returns {Promise&lt;userStatsResponseObject>} userStatsResponseObject result object\n *\n * @example\nvar userStatsOptions = {\n  user: ['username_1', 'username_2'] // get me some info on these two users\n};\nvar userStatsResults = await emojme.userStats('mySubdomain', 'myToken', 'myCookie', userStatsOptions);\nconsole.log(userStatsResults);\n// {\n//   mySubdomain: {\n//     userStatsResults: [\n//       {\n//         user: 'username_1',\n//         userEmoji: [{ all username_1's emoji }],\n//         subdomain: mySubdomain,\n//         originalCount: x,\n//         aliasCount: y,\n//         totalCount: x + y,\n//         percentage: (x + y) / mySubdomain's total emoji count\n//       },\n//       {\n//         user: 'username_2',\n//         userEmoji: [{ all username_2's emoji }],\n//         subdomain: mySubdomain,\n//         originalCount: x,\n//         aliasCount: y,\n//         totalCount: x + y,\n//         percentage: (x + y) / mySubdomain's total emoji count\n//       }\n//     ]\n//   }\n// }\n */\nasync function userStats(subdomains, tokens, cookies, options) {\n  subdomains = Helpers.arrayify(subdomains);\n  tokens = Helpers.arrayify(tokens);\n  cookies = Helpers.arrayify(cookies);\n  const users = Helpers.arrayify(options.user);\n  options = options || {};\n\n  const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies);\n\n  const userStatsPromises = authTuples.map(async (authTuple) => {\n    const emojiAdminList = new EmojiAdminList(...authTuple, options.output);\n    const emojiList = await emojiAdminList.get(options.bustCache, options.since);\n    if (users &amp;&amp; users.length > 0) {\n      const results = EmojiAdminList.summarizeUser(emojiList, authTuple[0], users);\n      return results.map((result) => {\n        const safeUserName = FileUtils.sanitize(result.user);\n        FileUtils.writeJson(`./build/${safeUserName}.${result.subdomain}.adminList.json`, result.userEmoji, null, 3);\n        return { subdomain: authTuple[0], userStatsResults: results, emojiList };\n      });\n    }\n    const results = EmojiAdminList.summarizeSubdomain(emojiList, authTuple[0], options.top);\n    results.forEach((result) => {\n      const safeUserName = FileUtils.sanitize(result.user);\n      FileUtils.writeJson(`./build/${safeUserName}.${result.subdomain}.adminList.json`, result.userEmoji, null, 3);\n    });\n\n    return { subdomain: authTuple[0], userStatsResults: results, emojiList };\n  });\n\n  return Helpers.formatResultsHash(_.flatten(await Promise.all(userStatsPromises)));\n}\n\nfunction userStatsCli() {\n  const program = new commander.Command();\n\n  Cli.requireAuth(program);\n  Cli.allowIoControl(program)\n    .option('--user &lt;value>', 'slack user you\\'d like to get stats on. Can be specified multiple times for multiple users.', Cli.list, null)\n    .option('--top &lt;value>', 'the top n users you\\'d like user emoji statistics on', 10)\n    .parse(process.argv);\n  Cli.unpackAuthJson(program);\n\n  return userStats(program.subdomain, program.token, program.cookie, {\n    user: program.user,\n    top: program.top,\n    bustCache: program.bustCache,\n    output: program.output,\n    since: program.since,\n  });\n}\n\nif (require.main === module) {\n  userStatsCli();\n}\n\nmodule.exports = {\n  userStats,\n  userStatsCli,\n};\n</code></pre>\n  </article>\n</section>\n\n    \n\n\n  </div>\n\n  <br class=\"clear\">\n\n  <footer>\n    Documentation generated by <a href=\"https://github.com/jsdoc3/jsdoc\">JSDoc 3.5.5</a>\n  </footer>\n\n  <script src=\"scripts/linenumber.js\"></script>\n  <script src=\"scripts/pagelocation.js\"></script>\n\n  \n  \n</body>\n</html>\n"
  },
  {
    "path": "docs/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\n  <title>\n      Home - Documentation\n  </title>\n\n  <link href=\"https://www.braintreepayments.com/images/favicon-ccda0b14.png\" rel=\"icon\" type=\"image/png\">\n\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/highlight.min.js\"></script>\n  <script>hljs.initHighlightingOnLoad();</script>\n\n  <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js\"></script>\n\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/prettify-tomorrow.css\">\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/jsdoc-default.css\">\n\n  \n\n  <!-- start Mixpanel -->\n  <script type=\"text/javascript\">(function(e,a){if(!a.__SV){var b=window;try{var c,l,i,j=b.location,g=j.hash;c=function(a,b){return(l=a.match(RegExp(b+\"=([^&]*)\")))?l[1]:null};g&&c(g,\"state\")&&(i=JSON.parse(decodeURIComponent(c(g,\"state\"))),\"mpeditor\"===i.action&&(b.sessionStorage.setItem(\"_mpcehash\",g),history.replaceState(i.desiredHash||\"\",e.title,j.pathname+j.search)))}catch(m){}var k,h;window.mixpanel=a;a._i=[];a.init=function(b,c,f){function e(b,a){var c=a.split(\".\");2==c.length&&(b=b[c[0]],a=c[1]);b[a]=function(){b.push([a].concat(Array.prototype.slice.call(arguments,\n  0)))}}var d=a;\"undefined\"!==typeof f?d=a[f]=[]:f=\"mixpanel\";d.people=d.people||[];d.toString=function(b){var a=\"mixpanel\";\"mixpanel\"!==f&&(a+=\".\"+f);b||(a+=\" (stub)\");return a};d.people.toString=function(){return d.toString(1)+\".people (stub)\"};k=\"disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user\".split(\" \");\n  for(h=0;h<k.length;h++)e(d,k[h]);a._i.push([b,c,f])};a.__SV=1.2;b=e.createElement(\"script\");b.type=\"text/javascript\";b.async=!0;b.src=\"undefined\"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:\"file:\"===e.location.protocol&&\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\".match(/^\\/\\//)?\"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\":\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\";c=e.getElementsByTagName(\"script\")[0];c.parentNode.insertBefore(b,c)}})(document,window.mixpanel||[]);\n  mixpanel.init(\"1919205b2da72e4da3b9b6639b444d59\");</script>\n  <!-- end Mixpanel -->\n</head>\n\n<body>\n  <svg style=\"display: none;\">\n    <defs>\n      <symbol id=\"linkIcon\" fill=\"#706d77\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n          <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n          <path d=\"M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z\"/>\n      </symbol>\n    </defs>\n  </svg>\n\n  <input type=\"checkbox\" id=\"nav-trigger\" class=\"nav-trigger\" />\n  <label for=\"nav-trigger\" class=\"navicon-button x\">\n    <div class=\"navicon\"></div>\n  </label>\n\n  <label for=\"nav-trigger\" class=\"overlay\"></label>\n\n  <div class=\"top-nav-wrapper\">\n    <ul>\n      <li  class=\"active\" >\n        <a href=\"index.html\">\n          \n          \n            <svg fill=\"#0095dd\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n              <path d=\"M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z\"/>\n              <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n            </svg>\n          \n        </a>\n      </li>\n\n      \n\n    </ul>\n  </div>\n\n  <nav>\n    <h3 class=\"reference-title\">\n      emojme\n    </h3>\n\n    <h3>Modules</h3><ul><li id=\"add-nav\"><a href=\"module-add.html\">add</a><ul class='methods'><li data-type=\"method\" id=\"add-add-nav\"><a href=\"module-add.html#~add\">add</a></li></ul></li><li id=\"download-nav\"><a href=\"module-download.html\">download</a><ul class='methods'><li data-type=\"method\" id=\"download-download-nav\"><a href=\"module-download.html#~download\">download</a></li></ul></li><li id=\"favorites-nav\"><a href=\"module-favorites.html\">favorites</a><ul class='methods'><li data-type=\"method\" id=\"favorites-favorites-nav\"><a href=\"module-favorites.html#~favorites\">favorites</a></li></ul></li><li id=\"sync-nav\"><a href=\"module-sync.html\">sync</a><ul class='methods'><li data-type=\"method\" id=\"sync-sync-nav\"><a href=\"module-sync.html#~sync\">sync</a></li></ul></li><li id=\"upload-nav\"><a href=\"module-upload.html\">upload</a><ul class='methods'><li data-type=\"method\" id=\"upload-upload-nav\"><a href=\"module-upload.html#~upload\">upload</a></li></ul></li><li id=\"userStats-nav\"><a href=\"module-userStats.html\">userStats</a><ul class='methods'><li data-type=\"method\" id=\"userStats-userStats-nav\"><a href=\"module-userStats.html#~userStats\">userStats</a></li></ul></li></ul>\n  </nav>\n\n  <div id=\"main\">\n    \n\n    \n      \n\n\n\n    \n\n      \n\n\n  <section class=\"readme\">\n    <article>\n      <h1><a href=\"https://github.com/jackellenberger/emojme\">emojme</a> - <a href=\"https://jackellenberger.github.io/emojme\">Documentation</a></h1><h2>Table of Contents</h2><ul>\n<li><a href=\"#what-it-is\">Project Overview</a></li>\n<li><a href=\"#breaking-changes\">Breaking Changes</a><ul>\n<li><a href=\"#2-0-0\">2.0.0</a></li>\n</ul>\n</li>\n<li><a href=\"#requirements\">Requirements</a></li>\n<li><a href=\"#installation\">Installation</a><ul>\n<li><a href=\"#finding-a-slack-token\">Getting a slack token</a></li>\n<li><a href=\"#finding-a-slack-cookie\">Getting a slack cookie</a></li>\n</ul>\n</li>\n<li><a href=\"#usage\">Usage</a><ul>\n<li><a href=\"#usage\">Command Line</a></li>\n<li><a href=\"#module\">Module</a></li>\n</ul>\n</li>\n<li><a href=\"#build-directory-output\">Build directory output</a></li>\n<li><a href=\"#a-closer-look-at-options\">A closer look at options</a></li>\n<li><a href=\"#whats-the-difference-between-add-and-upload\">Add vs Upload</a></li>\n<li><a href=\"#cli-examples\">CLI Examples</a><ul>\n<li><a href=\"#emojme-download\">Download</a></li>\n<li><a href=\"#emojme-add\">Add</a></li>\n<li><a href=\"#emojme-upload\">Upload</a></li>\n<li><a href=\"#emojme-sync\">Sync</a></li>\n<li><a href=\"#emojme-user-stats\">User Stats</a></li>\n<li><a href=\"#emojme-favorites\">Favorites</a></li>\n</ul>\n</li>\n<li><a href=\"#pro-moves-promoves\">Pro Moves</a><ul>\n<li><a href=\"#rate-limiting-and-you\">Rate Limiting</a></li>\n<li><a href=\"#faq\">FAQ</a></li>\n</ul>\n</li>\n<li><a href=\"#inspirations\">Other Projects of Note</a></li>\n</ul>\n<h2>What it is</h2><p>Emojme is a set of tools to manage your Slack emoji, either directly from the command line or from within your own Javascript project.</p>\n<p>Primary features are:</p>\n<ul>\n<li>Uploading new emoji<ul>\n<li>Individually, by passing a file or url</li>\n<li>In bulk, by passing a json &quot;adminList&quot; or a yaml &quot;emojipack&quot; file</li>\n<li>To one or many slack instances at once</li>\n</ul>\n</li>\n<li>Download existing emoji<ul>\n<li>From one or many slack instances</li>\n<li>Download all emoji</li>\n<li>Download some emoji</li>\n</ul>\n</li>\n<li>Sync emoji between mulitple slack instance<ul>\n<li>One to one, one to many, many to one, or many to many</li>\n</ul>\n</li>\n<li>Analyze emoji authorship<ul>\n<li>Who makes the most emoji in your slack instance?</li>\n</ul>\n</li>\n<li>Analyze emoji usage<ul>\n<li>Which emoji do you use most?</li>\n</ul>\n</li>\n</ul>\n<p>jsdocs are available at <a href=\"https://jackellenberger.github.io/emojme\">https://jackellenberger.github.io/emojme</a>. Read em.</p>\n<h2>Breaking Changes</h2><h3>2.0.0</h3><p>Removes support for easy breazy beautiful user token auth, adds support for grumble grumble cookie token + cookie auth. Slack made me do it I swear. What does it mean for you?</p>\n<ul>\n<li>Whenever you wrote or used an emojme method with a signature like <code>method(domain, token, options)</code>, you will now need <code>method(domain, token, cookie, options)</code>.</li>\n<li>Whenever you were calling the CLI with a pattern like <code>emojme command --subdomain $SUBDOMAIN --token $TOKEN</code>, you will now need <code>emojme command --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE</code>.</li>\n<li>Read on for examples and instructions on how to collect your cookie from the jar.</li>\n</ul>\n<h2>Requirements</h2><p>To use emojme you don't need a bot or a workspace admin account. In fact, ~only regular <a href=\"https://api.slack.com/docs/token-types#user\"><strong>user tokens</strong></a> work~ only <em>cookie</em> tokens work, in combination with shortlived browser tokens, and getting both isn't <em>quite</em> as easy as getting other types of tokens. Limitations are:</p>\n<ul>\n<li>Cookie tokens can be grabbed from any logged in slack webpage by following <a href=\"#finding-a-slack-token\">these instructions</a>.</li>\n<li>Auth Cookies are grabbed with even more difficulty, again from logged in slack pages, following <a href=\"#finding-a-slack-cookie\">these instructions</a>.</li>\n<li>All actions taken through Emojme can be linked back to your user account. That might be bad, but no one has yelled at me yet.</li>\n<li>Cookie tokens are cycled at inditerminate times, and cannot (to my knowledge) be cycled manually. Ditto for the cookies themselves. <strong>DO NOT LOSE CONTROL OF YOUR COOKIES</strong>. Any project that uses emojme should have tokens passed in through environment variables and should not store them in source control.<ul>\n<li>Update July 2021: If you are have been using an automated system to scrape User Tokens, you are pretty much hosed. The cookies now required are <a href=\"https://owasp.org/www-community/HttpOnly\">Http Only</a> and can't be easily (or at all?) accessed via javascript.</li>\n</ul>\n</li>\n</ul>\n<h2>Installation</h2><h3>Command Line</h3><p>Via npm</p>\n<pre class=\"prettyprint source lang-bash\"><code>$ (nvm use 10 || nvm install 10) && npm install emojme\n$ npx emojme [command] [options]</code></pre><p>Via github</p>\n<pre class=\"prettyprint source lang-bash\"><code>$ git clone https://github.com/jackellenberger/emojme.git\n$ cd emojme\n$ node ./emojme [command] [options]</code></pre><p>In order to use either feature, you will need both a Token and a Cookie each for every target subdomain (e.g. my-subdomain.slack.com). You can of course use your own methods for achieving this, but (and I will repeat this), the <a href=\"https://chrome.google.com/webstore/detail/emojme-emoji-anywhere/nbnaglaclijdfidbinlcnfdbikpbdkog?hl=en-US\">Emojme: Emoji Anywhere</a> Chrome Extension makes it very much easier than anything else, at only minor risk to your personal security. But hey if I were gonna steal your slack creds I'd do it in an alley with a knife or something, not in broad daylight. Its source is also <a href=\"https://github.com/jackellenberger/emojme-emoji-anywhere\">available on github</a> if you don't enjoy pre-rolls.</p>\n<h3>Finding a slack token</h3><p>Update July 2021: Slack has switched away from using questionably rotated user tokens to using &quot;cookie tokens&quot; and an associated short lived cookie. Smart, but we're smarter. User Tokens were of the format <code>xox[sp]-(\\w{12}|\\w{10})-(\\w{12}|\\w{11})-\\w{12}-\\w{64}</code> but <em>will no longer work</em>. If use see an auth error, this is probably the reason. Cookie tokens follow a similar form, but not the <code>c</code>: <code>xoxc-(\\w{12}|\\w{10})-(\\w{12}|\\w{11})-\\w{12}-\\w{64}</code>.</p>\n<h4>Slack for Web</h4><p>It's easyish! Open and sign into the slack customization page, e.g. https://my.slack.com/customize, right click anywhere &gt; inspect element. Open the console and paste:</p>\n<pre class=\"prettyprint source lang-javascript\"><code>window.prompt(&quot;your api token is: &quot;, TS.boot_data.api_token)</code></pre><p>You will be prompted with your api token! This can be sped up if you find yourself doing it often by adding the following bookmarklet, becase who doesn't love a good bookmarklet?:</p>\n<pre class=\"prettyprint source\"><code>javascript:(function()%7Bwindow.prompt(%22your%20api%20token%20is%3A%20%22%2C%20TS.boot_data.api_token)%7D)()</code></pre><p>(Slack has invalidated @curtisgibby's sage advice here, but we still appreciate them)</p>\n<h4>Slack for Desktop (Has anyone tried this?)</h4><p>This is a similar process, but requires an extra step depending on your platform.</p>\n<ul>\n<li>OSX: run or add to your .bashrc: <code>export SLACK_DEVELOPER_MENU=true; open -a /Applications/Slack.app</code></li>\n<li>Windows: create a shortcut: <code>C:\\Windows\\System32\\cmd.exe /c &quot; SET SLACK_DEVELOPER_MENU=TRUE &amp;&amp; start C:\\existing\\path\\to\\slack.exe&quot;</code></li>\n<li>Linux: honestly probably the same as OSX :shrug:</li>\n</ul>\n<p>With that done and slack open, open View &gt; Developer &gt; Toggle Webapp DevTools (shortcut <code>super+option+i</code>). This will give you a chromium inspector into which you can paste</p>\n<pre class=\"prettyprint source\"><code>console.log(window.boot_data.api_token)</code></pre><h3>Finding a slack cookie</h3><p>As cookies are now required, and so too is this section. Slack's auth cookie, as far as I can tell, is the <code>d</code> cookie, which is unfortunately HttpOnly meaning it cannot be accessed via javascript. It can, however, be accessed with a little creativity.</p>\n<p>Chrome's (and presumably any modern browser's) cookies API does allow for HttpConly cookies to be accessed, but require the user's explicit approval but way of an extension. <a href=\"https://github.com/jackellenberger/emojme-emoji-anywhere\">Emojme: Emoji Anywhere</a> is such an extension, and is <a href=\"https://chrome.google.com/webstore/detail/emojme-emoji-anywhere/nbnaglaclijdfidbinlcnfdbikpbdkog?hl=en-US\">available in the chrome web store</a> (or of course can be loaded from source if you want to take your life in your own hands). Clicking the extension icon &gt; <code>Get Slack Token and Cookie</code> will land you with what I am calling a &quot;auth blob&quot;, which you can then pass to emojme via the <code>--auth-json</code> argument.</p>\n<p><img src=\"/images/emojme-chrome-extension.jpg\" alt=\"So easy! So Fun! With just one chrome extension!\"></p>\n<p>You may also pull the <code>d</code> cookie with your fleshy human hands, if you so desire. Open up your browser's developer tools, then Application menu &gt; Cookies &gt; d, and copy the string out for yourself. With this method, it will be easier to specify individual <code>--subdomain --token --cookie</code> flags.</p>\n<p><img src=\"/images/how-to-get-a-cookie.jpg\" alt=\"I have an MFA in drawing with a mouse\"></p>\n<h2>Usage</h2><p>Emojme can be used either as a command line tool or as a node module to be mixed in with your existing projects.</p>\n<p>Complete CLI flags can be found in <a href=\"USAGE.md\">USAGe.md</a>, but each command takes the <code>--help</code> option.</p>\n<h3>Module</h3><p>In your project's directory</p>\n<pre class=\"prettyprint source lang-bash\"><code>npm install --save emojme</code></pre><p>In your project</p>\n<pre class=\"prettyprint source lang-node\"><code>var emojme = require('emojme');\n\n// emojme-download\nvar downloadOptions = {\n  save: ['username_1', 'username_2'], // Download the emoji source files for these two users\n  bustCache: true, // make sure this data is fresh\n  output: true // download the adminList to ./build\n};\nvar downloadResults = await emojme.download('mySubdomain', 'myToken', 'myCookie', downloadOptions);\nconsole.log(downloadResults);\n/*\n  {\n    mySubdomain: {\n      emojiList: [\n        { name: 'emoji-from-mySubdomain', ... },\n        ...\n      ],\n      saveResults: [\n        './build/mySubdomain/username_1/an_emoji.jpg',\n        './build/mySubdomain/username_1/another_emoji.gif',\n        ... all of username_1's emoji\n        './build/mySubdomain/username_2/some_emoji.jpg',\n        './build/mySubdomain/username_2/some_other_emoji.gif',\n        ... all of username_2's emoji\n      ]\n    }\n  }\n*/\n\n// emojme-upload\nvar uploadOptions = {\n  src: './emoji-list.json', // upload all the emoji in this json array of objects\n  avoidCollisions: true, // append '-1' or similar if we try to upload a dupe\n  prefix: 'new-' // prepend every emoji in src with &quot;new-&quot;, e.g. &quot;emoji&quot; becomes &quot;new-emoji&quot;\n};\nvar uploadResults = await emojme.upload('mySubdomain', 'myToken', 'myCookie', uploadOptions);\nconsole.log(uploadResults);\n/*\n  {\n    mySubdomain: {\n      collisions: [\n        { name: an-emoji-that-already-exists-in-mySubdomain ... }\n      ],\n      emojiList: [\n        { name: emoji-from-emoji-list-json ... },\n        { name: emoji-from-emoji-list-json ... },\n        ...\n      ]\n    }\n  }\n*/\n\n// emojme-add\nvar addOptions = {\n  src: ['./emoji1.jpg', 'http://example.com/emoji2.png'], // upload these two images\n  name: ['myLocalEmoji', 'myOnlineEmoji'], // call them these two names\n  bustCache: false, // don't bother redownloading existing emoji\n  avoidCollisions: true, // if there are similarly named emoji, change my new emoji names\n  output: false // don't write any files\n};\nvar subdomains = ['mySubdomain1', 'mySubdomain2'] // can add one or multiple\nvar tokens = ['myToken1', 'myToken2'] // can add one or multiple\nvar addResults = await emojme.add(subdomains, tokens, addOptions);\nconsole.log(addResults);\n/*\n  {\n    mySubomain1: {\n      collisions: [], // only defined if avoidCollisons = false\n      emojiList: [\n        { name: 'myLocalEmoji', ... },\n        { name: 'myOnlineEmoji', ... },\n      ]\n    },\n    mySubomain2: {\n      collisions: [], // only defined if avoidCollisons = false\n      emojiList: [\n        { name: 'myLocalEmoji', ... },\n        { name: 'myOnlineEmoji', ... },\n      ]\n    }\n  }\n*/\n\n// emojme-sync\nvar syncOptions = {\n  srcSubdomains: ['srcSubdomain'], // copy all emoji from srcSubdomain...\n  srcTokens: ['srcToken'],\n  dstSubdomains: ['dstSubdomain1', 'dstSubdomain2'], // ...to dstSubdomain1 and dstSubdomain2\n  dstTokens: ['dstToken1', 'dstToken2'],\n  bustCache: true // get fresh lists to make sure we're not doing more lifting than we have to\n};\nvar syncResults = await emojme.sync(null, null, syncOptions);\nconsole.log(syncResults);\n/*\n  {\n    dstSubdomain1: {\n      emojiList: [\n        { name: emoji-1-from-srcSubdomain ... },\n        { name: emoji-2-from-srcSubdomain ... }\n      ]\n    },\n    dstSubdomain2: {\n      emojiList: [\n        { name: emoji-1-from-srcSubdomain ... },\n        { name: emoji-2-from-srcSubdomain ... }\n      ]\n    }\n  }\n*/\n\n//emojme-user-stats\nvar userStatsOptions = {\n  user: ['username_1', 'username_2'] // get me some info on these two users\n};\nvar userStatsResults = await emojme.userStats('mySubdomain', 'myToken', 'myCookie', userStatsOptions);\nconsole.log(userStatsResults);\n/*\n  {\n    mySubdomain: {\n      userStatsResults: [\n        {\n          user: 'username_1',\n          userEmoji: [{ all username_1's emoji }],\n          subdomain: mySubdomain,\n          originalCount: x,\n          aliasCount: y,\n          totalCount: x + y,\n          percentage: (x + y) / mySubdomain's total emoji count\n        },\n        {\n          user: 'username_2',\n          userEmoji: [{ all username_2's emoji }],\n          subdomain: mySubdomain,\n          originalCount: x,\n          aliasCount: y,\n          totalCount: x + y,\n          percentage: (x + y) / mySubdomain's total emoji count\n        }\n      ]\n    }\n  }\n*/\n\n//emojme-favorites\nvar favoritesResult = await emojme.favorites('mySubdomain', 'myToken', 'myCookie', {});\nconsole.log(favoritesResult);\n/*\n  {\n    mySubdomain: {\n      favoritesResult: {\n          user: '{myToken's user}',\n          favoriteEmoji: [\n             emojiName,\n             ...\n          ],\n          favoriteEmojiAdminList: [\n            {emojiName}: {adminList-style emoji object, with additional `usage` value}\n            ...\n          ],\n        }\n    }\n  }\n*/</code></pre><h2>Build directory output</h2><p>Okay you've run it, now what? Where are my dang emoji?</p>\n<ul>\n<li>Diagnostic info and intermediate results are written to the build directory. Some might come in handy!</li>\n<li><code>build/$SUBDOMAIN.emojiUploadErrors.json</code> will give you a json of emoji that failed to upload and why. Use it to reattempt an upload! Generated from <code>upload</code> and <code>sync</code> calls.</li>\n<li><code>build/$SUBDOMAIN.adminList.json</code> is the &quot;master list&quot; of a subdomain's emoji. Generated from <code>download</code> and <code>sync</code> calls.</li>\n<li><code>build/$USER.$SUBDOMAIN.adminList.json</code> is all the emoji created by a user. Generated from <code>user-stats</code> calls.</li>\n<li><code>build/diff.to-$SUBDOMAIN.from-$SUBDOMAINLIST.adminList.json</code> contains all emoji present in $SUBDOMAINLIST but not in $SUBDOMAIN. Generated from <code>sync</code> calls.</li>\n</ul>\n<h2>A closer look at options</h2><ul>\n<li><p>Universal options:</p>\n<ul>\n<li><strong>requires</strong> at least one <code>--subdomain</code>/<code>--token</code>/<code>--cookie</code> <strong>auth tuple</strong>. Can accept multiple auth tuples.<ul>\n<li>exception: sync can use a source/destination pattern, see below.</li>\n</ul>\n</li>\n<li><em>optional</em>: <code>--bust-cache</code> will force a redownload of emoji adminlist. If not supplied, a redownload is forced every  24 hours.</li>\n<li><em>optional</em>: <code>--no-output</code> will prevent writing of files in the ./build directory. It does not currently suppres stdout.</li>\n</ul>\n</li>\n<li><p><code>download</code></p>\n<ul>\n<li><strong>requires</strong> at least one <code>--subdomain</code>/<code>--token</code>/<code>--cookie</code> <strong>auth tuple</strong>. Can accept multiple auth tuples.</li>\n<li><em>optional</em>: <code>--save $user</code> will save actual emoji data for the specified user, rather than just adminList json. Find the emoji in ./build/subdomain/user/</li>\n<li><em>optional</em>: <code>--bust-cache</code> will force a redownload of emoji adminlist. If not supplied, a redownload is forced every  24 hours.</li>\n<li><em>optional</em>: <code>--no-output</code> will prevent writing of files in the ./build directory. It does not currently suppres stdout.</li>\n<li><em>optional</em>: <code>--since timestamp</code> will only download or save emoji created after the epoch time timestamp given, e.g. <code>1572064302751</code></li>\n</ul>\n</li>\n<li><code>upload</code><ul>\n<li><strong>requires</strong> at least one <code>--subdomain</code>/<code>--token</code>/<code>--cookie</code> <strong>auth tuple</strong>. Can accept multiple auth tuples.</li>\n<li><strong>requires</strong> at least one <code>--src</code> source json file.<ul>\n<li>Src json should contain a list of objects where each object contains a &quot;name&quot; and &quot;url&quot; for image source</li>\n<li>Src yaml should contain an <code>emojis</code> key whose value is a list of emoji objects. Each object should contain <code>name</code> and <code>src</code> if an original emoji, or <code>name</code>, <code>is_alias: 1</code>, and <code>alias_for</code> if an alias.</li>\n<li>If adding an alias, url will be ignored and &quot;is_alias&quot; should be set to &quot;1&quot;, and &quot;alias_for&quot; should be the name of the emoji to be aliased.</li>\n</ul>\n</li>\n<li><em>optional</em>: <code>--no-output</code> will prevent writing of files in the ./build directory. It does not currently suppres stdout.</li>\n</ul>\n</li>\n<li><code>add</code><ul>\n<li><strong>requires</strong> at least one <code>--subdomain</code>/<code>--token</code>/<code>--cookie</code> <strong>auth tuple</strong>. Can accept multiple auth tuples.</li>\n<li><strong>requires</strong> one of the following:<ol>\n<li><code>--src</code> path of local emoji file.<ul>\n<li><em>optional</em>: <code>--name</code> name of the emoji being uploaded. If not provided, the file name will be used.</li>\n</ul>\n</li>\n<li><code>--name</code> and <code>--alias-for</code> to create an alias called <code>$NAME</code> with the same image as <code>$ALIAS-FOR</code></li>\n</ol>\n</li>\n<li>Multiple <code>--src</code>'s or <code>--name</code>/<code>--alias-for</code> pairs may be provided, but don't mix the patterns. You'll confuse yourself.</li>\n<li><em>optional</em>: <code>--no-output</code> will prevent writing of files in the ./build directory. It does not currently suppres stdout.</li>\n</ul>\n</li>\n<li><code>user-stats</code><ul>\n<li><strong>requires</strong> at least one <code>--subdomain</code>/<code>--token</code>/<code>--cookie</code> <strong>auth tuple</strong>. Can accept multiple auth tuples.</li>\n<li>With no optional parameters given, this will print the top 10 emoji contributors</li>\n<li><em>optional</em>: one of the following:<ol>\n<li><code>--top</code> will show the top $TOP emoji contributors</li>\n<li><code>--user</code> will show statistics for $USER. Can accept multiple <code>--user</code> calls.</li>\n</ol>\n</li>\n<li><em>optional</em>: <code>--bust-cache</code> will force a redownload of emoji adminlist. If not supplied, a redownload is forced every  24 hours.</li>\n<li><em>optional</em>: <code>--no-output</code> will prevent writing of files in the ./build directory. It does not currently suppres stdout.</li>\n<li><em>optional</em>: <code>--since timestamp</code> will count the author statistics of only those emoji created after the epoch time timestamp given, e.g. <code>1572064302751</code></li>\n</ul>\n</li>\n<li><code>sync</code><ul>\n<li><strong>requires</strong> one of the following:<ol>\n<li>at least <strong>two</strong> <code>--subdomain</code>/<code>--token</code>/<code>--cookie</code> <strong>auth tuple</strong>. Can accept more than two auth tuples.</li>\n<li>at least <strong>one</strong> <code>--src-subdomain</code>/<code>--src-token</code> auth tuple and at least <strong>one</strong> <code>--dst-subdomain</code>/<code>--dst-token</code> auth tuples for &quot;one way&quot; syncing.</li>\n</ol>\n</li>\n<li><em>optional</em>: <code>--bust-cache</code> will force a redownload of emoji adminlist. If not supplied, a redownload is forced every  24 hours.</li>\n<li><em>optional</em>: <code>--no-output</code> will prevent writing of files in the ./build directory. It does not currently suppres stdout.</li>\n<li><em>optional</em>: <code>--since timestamp</code> will count the author statistics of only those emoji created after the epoch time timestamp given, e.g. <code>1572064302751</code></li>\n<li><em>optional</em>: <code>--dry-run</code> download adminLists for all requested subdomains and diff them, but don't upload any new emoji. Find the diffs in <code>./output/to-$DST_SUBDOMAIN.from-$SRC_SUBDOMAIN.adminList.json</code></li>\n</ul>\n</li>\n<li><code>favorites</code><ul>\n<li><strong>requires</strong> at least one <code>--subdomain</code>/<code>--token</code>/<code>--cookie</code> <strong>auth tuple</strong>. Can accept multiple auth tuples.</li>\n<li>With no optional parameters given, this will print the token's user's 10 most used emoji</li>\n<li><em>optional</em>: <code>--top</code> <em>verbose cli usage only</em> limits stdout to top N most used emoji</li>\n<li><em>optional</em>: <code>--usage</code> <em>verbose cli usage only</em> prints not only the user's favorite emoji, but also the usage numbers.</li>\n<li><em>optional</em>: <code>--bust-cache</code> will force a redownload of emoji adminlist and boot data. If not supplied, a redownload is forced every  24 hours.</li>\n<li><em>optional</em>: <code>--no-output</code> will prevent writing of files in the ./build directory. It does not currently suppres stdout.</li>\n</ul>\n</li>\n</ul>\n<h2>What's the difference between <code>Add</code> and <code>Upload</code>?</h2><p>Input type and use case! Technically (and behind the scenes) these commands do the same thing, which is post emoji to Slack.</p>\n<p>The difference is that <code>Upload</code> is designed to take an <code>adminList</code> (what Slack calls a list of emoji and their related metadata) in the form of a json file. You can create this json file yourself, or use the <code>download</code> command to get it from an existing slack instance. It should be a Json array of objects, where each object represents an emoji and has attributes:</p>\n<ul>\n<li><code>name</code> (the name of the emoji duh)</li>\n<li><code>url</code> (the source content of the emoji. either a url, a file path, or a raw <code>data:</code> string)</li>\n<li><code>is_alias</code> (either 0 for non-aliases or 1 for aliases)</li>\n<li><code>alias_for</code> (name of the emoji to alias if the emoji being uploaded is an alias)\nThere are other fields in an adminList, but no others are used at the current time.</li>\n</ul>\n<p><code>Add</code> is designed to allow users to upload a single or few emoji, directly from the command line, without having to craft a json file before hand. You can create either new emojis or new aliases (but not both, for now). Each new emoji needs a <code>--src</code>, and can take a <code>--name</code>, otherwise the file name will be used. Each new alias takes a <code>--name</code> and the name of the original emoji to alias as <code>--alias-for</code>.</p>\n<h2>CLI Examples</h2><p>It should be noted that there are many ways to run this project. <code>npx emojme add</code> will work when emojme is present in <code>node_modules</code> (such as when downloaded via <code>npm</code>). <code>node ./emojme add</code> and <code>node ./emojme-add</code> will work if you have cloned the repo. These examples will use the former construction, but feel free to do whatever.</p>\n<h3>emojme download</h3><ul>\n<li><p>Download all emoji from subdomain</p>\n<ul>\n<li><code>npx emojme download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE</code></li>\n<li>creates <code>./build/$SUBDOMAIN.adminList.json</code> containing url references to all emoji, but not the files themselves.</li>\n</ul>\n</li>\n<li><p>Download all emoji from subdomain using an authjson</p>\n<ul>\n<li><code>npx emojme download --auth-json '{&quot;token&quot;:&quot;$TOKEN&quot;,&quot;domain&quot;:&quot;$SUBDOMAIN&quot;,&quot;cookie&quot;:&quot;$COOKIE&quot;}'</code></li>\n<li>creates <code>./build/$SUBDOMAIN.adminList.json</code> containing url references to all emoji, but not the files themselves.</li>\n</ul>\n</li>\n<li><p>Download all emoji from multiple subdomains</p>\n<ul>\n<li><code>npx emojme download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2</code></li>\n<li>creates <code>./build/$SUBDOMAIN1.adminList.json</code> and <code>./build/$SUBDOMAIN2.adminList.json</code></li>\n</ul>\n</li>\n<li><p>download source content for emoji made by $USER1 and $USER2 in $SUBDOMAIN</p>\n<ul>\n<li><code>npx emojme download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --save $USER1 --save $USER2</code></li>\n<li>This will create directories <code>./build/$SUBDOMAIN/$USER1/</code> and <code>./build/$SUBDOMAIN/$USER2/</code>, each containing that user's raw emoji image files</li>\n</ul>\n</li>\n<li><p>download source content for all emoji in $SUBDOMAIN, grouping by user</p>\n<ul>\n<li><code>npx emojme download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --save-all</code></li>\n<li>This will create directories <code>./build/$SUBDOMAIN/$USER/</code> for each user in $SUBDOMAIN that has created an emoji</li>\n</ul>\n</li>\n</ul>\n<h3>emojme add</h3><ul>\n<li><p>add $FILE as :$NAME: and $URL as :$NAME2: to subdomain</p>\n<ul>\n<li><code>npx emojme add --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src $FILE --name $NAME --src $URL --name $NAME2</code></li>\n</ul>\n</li>\n<li><p>in $SUBDOMAIN1 and $SUBDOMAIN2, alias $ORIGINAL to $NAME</p>\n<ul>\n<li><code>npx emojme add --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 ---subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2 --alias-for '$ORIGINAL' --name '$NAME'</code></li>\n</ul>\n</li>\n<li><p>Alias :$ORIGINAL: as :$NAME:, and if :$NAME: exists, alias as :$NAME-1: instead</p>\n<ul>\n<li><code>npx emojme add --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --name $NAME --alias_for $ORIGINAL --avoid-collisions</code></li>\n<li>This has some amount of intelligence to it - if $ORIGINAL uses <code>_</code>'s, the alias will be <code>$ORIGINAL_1</code>, if the original has hyphens it will use hyphens, and if <code>-1</code> already exists it will use <code>-2</code>, etc.</li>\n</ul>\n</li>\n</ul>\n<h3>emojme upload</h3><ul>\n<li><p>upload emoji from source json to subdomain</p>\n<ul>\n<li><code>npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src './myfile.json'</code></li>\n</ul>\n</li>\n<li><p>upload emoji from source emojipacks yaml to subdomain</p>\n<ul>\n<li><code>npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src './emojipacks.yaml'</code></li>\n</ul>\n</li>\n<li><p>upload emoji from source json to multiple subdomains</p>\n<ul>\n<li><code>npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2 --src './myfile.json'</code></li>\n</ul>\n</li>\n<li><p>upload emoji from source json to subdomain, with each emoji being prefixed by $PREFIX</p>\n<ul>\n<li><code>npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src './myfile.json' --prefix '$PREFIX'</code></li>\n</ul>\n</li>\n<li><p>upload emoji from source json to subdomain, with each emoji being suffixed if it conficts with an existing emoji</p>\n<ul>\n<li><code>npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src './myfile.json' --avoid-collisions</code></li>\n</ul>\n</li>\n</ul>\n<h3>emojme-sync</h3><ul>\n<li><p>sync emoji so that $SUBDOMAIN1 and $SUBDOMAIN2 have the same emoji*</p>\n<ul>\n<li><sup>*the same emoji names, that is. If :hi: is different on the two subdomains they will remain different</sup></li>\n<li><code>npx emojme sync --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 --subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2</code></li>\n</ul>\n</li>\n<li><p>sync emoji so that $SUBDOMAIN1, $SUBDOMAIN2, and $SUBDOMAIN3 have the same emoji</p>\n<ul>\n<li><code>npx emojme sync --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 --subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2 --subdomain $SUBDOMAIN3 --token $TOKEN3 --cookie $COOKIE3</code></li>\n</ul>\n</li>\n<li><p>sync emoji from $SUBDOMAIN1 to $SUBDOMAIN2, so that $SUBDOMAIN1's emoji are a subset of $SUBDOMAIN2's emoji</p>\n<ul>\n<li><code>npx emojme sync --src-subdomain $SUBDOMAIN1 --src-token $TOKEN1 --dst-subdomain $SUBDOMAIN2 --dst-token $TOKEN2</code></li>\n</ul>\n</li>\n<li><p>sync emoji from $SUBDOMAIN1 to $SUBDOMAIN2 and $SUBDOMAIN3</p>\n<ul>\n<li><code>npx emojme sync --src-subdomain $SUBDOMAIN1 --src-token $TOKEN1 --dst-subdomain $SUBDOMAIN2 --dst-token $TOKEN2 --dst-subdomain $SUBDOMAIN3 --dst-token $TOKEN3</code></li>\n</ul>\n</li>\n<li><p>sync emoji from $SUBDOMAIN1 and $SUBDOMAIN2 to $SUBDOMAIN3</p>\n<ul>\n<li><code>npx emojme sync --src-subdomain $SUBDOMAIN1 --src-token $TOKEN1 --src-subdomain $SUBDOMAIN2 --src-token $TOKEN2 --dst-subdomain $SUBDOMAIN3 --dst-token $TOKEN3</code></li>\n</ul>\n</li>\n</ul>\n<h3>emojme user stats</h3><p>These commands all write files to the build directory, but become more immediately useful with the <code>--verbose</code> flag.</p>\n<ul>\n<li><p>get author statistics for user $USER (emoji upload count, etc)</p>\n<ul>\n<li><code>npx emojme user-stats --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --user $USER --verbose</code></li>\n<li>This will create json file <code>./build/$USER.$SUBDOMAIN.adminList.json</code></li>\n</ul>\n</li>\n<li><p>get user statistics for multiple users</p>\n<ul>\n<li><code>npx emojme user-stats --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --user $USER --user $USER2 --user $USER3</code></li>\n<li>This will create json files <code>./build/$USERX.$SUBDOMAIN.adminList.json</code> for each user passed</li>\n</ul>\n</li>\n<li><p>get user statistics for top $N contributors</p>\n<ul>\n<li><code>npx emojme user-stats --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --top $N</code></li>\n<li>Defaults to top 10 users.</li>\n</ul>\n</li>\n</ul>\n<h3>emojme-favorites</h3><ul>\n<li><p>Print the token's user's top 20 most used emoji</p>\n<ul>\n<li><code>npx emojme favorites --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 --top 20 --verbose</code></li>\n</ul>\n</li>\n<li><p>Print the usage numbers for the user's top 10 most used emoji</p>\n<ul>\n<li><code>npx emojme favorites --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 --usage --verbose</code></li>\n</ul>\n</li>\n</ul>\n<h2>Pro Moves :promoves:</h2><h3>Getting a list of single attributes from an adminList json:</h3><p>Hey try this with $ATTRIBUTE of &quot;url&quot;. You might need all those urls!</p>\n<pre class=\"prettyprint source\"><code>cat $ADMINLIST.json | jq '.[] | .[&quot;$ATTRIBUTE&quot;]'</code></pre><h3>Rate limiting and you</h3><p>Slack <a href=\"https://api.slack.com/changelog/2018-03-great-rate-limits\">threatened to release</a> then <a href=\"https://api.slack.com/docs/rate-limits\">released</a> rate limiting rules across its new api endpoints, and the rollout has included their undocumented endpoints now as well. As such, Emojme is going to slow down :capysad: Another nail in the coffin of making this a useful slackbot.</p>\n<p>Though it is unpublished, I have on good authority that <code>/emoji.adminList</code> is Tier 3 (when paginated) and <code>/emoji.add</code> is Tier 2, so emojme now has a &quot;fast part&quot; and a &quot;slow part&quot; respectively.</p>\n<p>I'm not one to judge how a person uses their own credentials, so there is a work around for those looking to get a bit more personal with the Slack networking infra team; Use the following environment variables to override my conservative defaults:</p>\n<pre class=\"prettyprint source lang-sh\"><code># How many requests to make at a time. Higher numbers are faster (as long as the other two params allow) and more prone to trip Slack's &quot;hey that's not a burst that's a malicous user&quot; alarm\nSLACK_REQUEST_CONCURRENCY\n# How many requests are to be sent per unit time. This is the real control of speed, the higher the more likely you are to be rate limited.\nSLACK_REQUEST_RATE\n# The unit of time, in ms. The lower the number the faster.\nSLACK_REQUEST_WINDOW\n\n# So, an example that has 10 in-flight requests at a time at a maximum rate of 200 requests per minute would be:\nSLACK_REQUEST_CONCURRENCY=10 \\\nSLACK_REQUEST_RATE=200 \\\nSLACK_REQUEST_WINDOW=60000 \\\nnode emojme-download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --save-all --bust-cache</code></pre><p>I have tried my darndest to make the slack client in this project 429 tolerant, but after a few ignored 429's Slack gets mean and says you can't try again, so have fun dealing with that.</p>\n<h3>FAQ</h3><ul>\n<li><p>I'm getting <code>invalid_auth</code> errors? huh???</p>\n<ul>\n<li>See #60. Essentially, Slack has gotten wise to our whole &quot;you can use a token for arbitrary lengths of time because Slack doesn't want to rotate them often and log us out of active sessions, or deal with zombie sessions that are authed with out of date tokens&quot;. They've switched from using User Tokens (xoxs-) to Cookie Tokens (xoxc-), in combination with a cookie that is shortlived. Very clever, but we are more cleverer. We'll just rip off that cookie and pass it through the same way we were doing the token. It'll be a pain, but only as insecure as it was before.</li>\n</ul>\n</li>\n<li><p>I don't see any progress when I run a cli command</p>\n<ul>\n<li>Do you have <code>--verbose</code> in your command? that's pretty useful.</li>\n</ul>\n</li>\n<li><p>My network requests are slow and jerky</p>\n<ul>\n<li>That's how we gotta live under <a href=\"#rate-limiting-and-you\">rate limiting</a>. To speed things up, try the env vars that are listed, but things might not go well. To make things less jerkey, knock down the concurrency so requests are more serial and there is no down time between bursts.</li>\n</ul>\n</li>\n<li><p>I just want to upload this thing fast, but I have to download 20k emoji to upload one?</p>\n<ul>\n<li>Nope! That is the normal behavior to not anger slack - we do more easy GET's to avoid some troublesome POSTs, but you can turn that off. Just add <code>--allow-collisions</code> (or <code>{collsions: true}</code>) to your upload request.</li>\n</ul>\n</li>\n</ul>\n<h2>Contributing</h2><p>Contribute! I'm garbo at js (and it's js's fault), so feel free to jump inand clean up, add features, and make the project live. I would recommend:</p>\n<ul>\n<li>Add tests</li>\n<li>Make your change</li>\n<li>Run tests <code>npm run test</code> or <code>npm run test:unit &amp;&amp; npm run test:integration</code><ul>\n<li>pro move: add a <code>debugger;</code> and use <code>it.only</code>, then <code>npm inspect node_modules/mocha/bin/_mocha spec/...</code> to debug a failing test.</li>\n</ul>\n</li>\n<li>Run end to end tests (requires a real slack instance) <code>npm run test:e2e -- --subdomain $YOUR_REAL_SUBDOMAIN --token $YOUR_REAL_TOKEN</code></li>\n<li>Lint</li>\n<li>Regenerate docs, if necessary</li>\n</ul>\n<h2>Inspirations</h2><ul>\n<li><a href=\"https://github.com/lambtron/emojipacks\">emojipacks</a> is my OG. It mostly worked but seems rather undermaintained.</li>\n<li><a href=\"https://github.com/Fauntleroy/neutral-face-emoji-tools\">neutral-face-emoji-tools</a> is a fantastic tool that has enabled me to make enough emoji that this tool became necessary.</li>\n</ul>\n<h2>Stupid ways to use this stupid library!</h2><ul>\n<li>https://github.com/jackellenberger/allmyemojichildren</li>\n<li>https://github.com/guyfedwards/emoji</li>\n<li>https://github.com/jackellenberger/emojme-hubot-plugin</li>\n<li>https://github.com/jackellenberger/emojme-emoji-anywhere</li>\n<li>https://github.com/jackellenberger/infinite-emoji-discord-bot</li>\n</ul>\n    </article>\n  </section>\n\n\n    \n\n\n  </div>\n\n  <br class=\"clear\">\n\n  <footer>\n    Documentation generated by <a href=\"https://github.com/jsdoc3/jsdoc\">JSDoc 3.5.5</a>\n  </footer>\n\n  <script src=\"scripts/linenumber.js\"></script>\n  <script src=\"scripts/pagelocation.js\"></script>\n\n  \n  \n</body>\n</html>"
  },
  {
    "path": "docs/module-add.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\n  <title>\n      add - Documentation\n  </title>\n\n  <link href=\"https://www.braintreepayments.com/images/favicon-ccda0b14.png\" rel=\"icon\" type=\"image/png\">\n\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/highlight.min.js\"></script>\n  <script>hljs.initHighlightingOnLoad();</script>\n\n  <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js\"></script>\n\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/prettify-tomorrow.css\">\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/jsdoc-default.css\">\n\n  \n\n  <!-- start Mixpanel -->\n  <script type=\"text/javascript\">(function(e,a){if(!a.__SV){var b=window;try{var c,l,i,j=b.location,g=j.hash;c=function(a,b){return(l=a.match(RegExp(b+\"=([^&]*)\")))?l[1]:null};g&&c(g,\"state\")&&(i=JSON.parse(decodeURIComponent(c(g,\"state\"))),\"mpeditor\"===i.action&&(b.sessionStorage.setItem(\"_mpcehash\",g),history.replaceState(i.desiredHash||\"\",e.title,j.pathname+j.search)))}catch(m){}var k,h;window.mixpanel=a;a._i=[];a.init=function(b,c,f){function e(b,a){var c=a.split(\".\");2==c.length&&(b=b[c[0]],a=c[1]);b[a]=function(){b.push([a].concat(Array.prototype.slice.call(arguments,\n  0)))}}var d=a;\"undefined\"!==typeof f?d=a[f]=[]:f=\"mixpanel\";d.people=d.people||[];d.toString=function(b){var a=\"mixpanel\";\"mixpanel\"!==f&&(a+=\".\"+f);b||(a+=\" (stub)\");return a};d.people.toString=function(){return d.toString(1)+\".people (stub)\"};k=\"disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user\".split(\" \");\n  for(h=0;h<k.length;h++)e(d,k[h]);a._i.push([b,c,f])};a.__SV=1.2;b=e.createElement(\"script\");b.type=\"text/javascript\";b.async=!0;b.src=\"undefined\"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:\"file:\"===e.location.protocol&&\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\".match(/^\\/\\//)?\"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\":\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\";c=e.getElementsByTagName(\"script\")[0];c.parentNode.insertBefore(b,c)}})(document,window.mixpanel||[]);\n  mixpanel.init(\"1919205b2da72e4da3b9b6639b444d59\");</script>\n  <!-- end Mixpanel -->\n</head>\n\n<body>\n  <svg style=\"display: none;\">\n    <defs>\n      <symbol id=\"linkIcon\" fill=\"#706d77\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n          <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n          <path d=\"M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z\"/>\n      </symbol>\n    </defs>\n  </svg>\n\n  <input type=\"checkbox\" id=\"nav-trigger\" class=\"nav-trigger\" />\n  <label for=\"nav-trigger\" class=\"navicon-button x\">\n    <div class=\"navicon\"></div>\n  </label>\n\n  <label for=\"nav-trigger\" class=\"overlay\"></label>\n\n  <div class=\"top-nav-wrapper\">\n    <ul>\n      <li >\n        <a href=\"index.html\">\n          \n            <svg fill=\"#6D6D6D\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n              <path d=\"M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z\"/>\n              <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n            </svg>\n          \n          \n        </a>\n      </li>\n\n      \n\n    </ul>\n  </div>\n\n  <nav>\n    <h3 class=\"reference-title\">\n      emojme\n    </h3>\n\n    <h3>Modules</h3><ul><li id=\"add-nav\"><a href=\"module-add.html\">add</a><ul class='methods'><li data-type=\"method\" id=\"add-add-nav\"><a href=\"module-add.html#~add\">add</a></li></ul></li><li id=\"download-nav\"><a href=\"module-download.html\">download</a><ul class='methods'><li data-type=\"method\" id=\"download-download-nav\"><a href=\"module-download.html#~download\">download</a></li></ul></li><li id=\"favorites-nav\"><a href=\"module-favorites.html\">favorites</a><ul class='methods'><li data-type=\"method\" id=\"favorites-favorites-nav\"><a href=\"module-favorites.html#~favorites\">favorites</a></li></ul></li><li id=\"sync-nav\"><a href=\"module-sync.html\">sync</a><ul class='methods'><li data-type=\"method\" id=\"sync-sync-nav\"><a href=\"module-sync.html#~sync\">sync</a></li></ul></li><li id=\"upload-nav\"><a href=\"module-upload.html\">upload</a><ul class='methods'><li data-type=\"method\" id=\"upload-upload-nav\"><a href=\"module-upload.html#~upload\">upload</a></li></ul></li><li id=\"userStats-nav\"><a href=\"module-userStats.html\">userStats</a><ul class='methods'><li data-type=\"method\" id=\"userStats-userStats-nav\"><a href=\"module-userStats.html#~userStats\">userStats</a></li></ul></li></ul>\n  </nav>\n\n  <div id=\"main\">\n    \n      <h1 class=\"page-title\">\n        add\n      </h1>\n    \n\n    \n\n<section>\n  <header>\n    \n      \n  </header>\n\n  <article>\n    <div class=\"container-overview\">\n      \n        \n\n        \n      \n    </div>\n\n    \n\n    \n\n    \n\n     \n\n    \n\n    \n\n    \n      <h3 class=\"subsection-title\">Methods</h3>\n\n      \n        \n\n\n  \n\n  <span class='name-container'>\n    <a class=\"link-icon\" href=\"#~add\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h4 class=\"name\" id=\"~add\">\n      <span class=\"type-signature\">(async, inner) </span>add<span class=\"signature\">(subdomains, tokens, cookies, options)</span><span class=\"type-signature\"> &rarr; {Promise.&lt;addResponseObject>}</span>\n    </h4>\n  </span>\n\n  \n\n\n\n  <div class=\"description\">\n    <p>Add emoji described by parameters within options to the specified subdomain(s).</p>\n<p>Note that options can accept both aliases and original emoji at the same time, but ordering can get complicated and honestly I'd skip it if I were you. For each emoji, make sure that every descriptor (src, name, aliasFor) has a value, using <code>null</code>s for fields that are not relevant to the current emoji.</p>\n  </div>\n\n\n\n\n\n\n\n  <h5>Parameters:</h5>\n  \n\n<table class=\"params\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n      <tr>\n        \n          <td class=\"name\"><code>subdomains</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>a single or list of subdomains to add emoji to. Must match respectively to <code>token</code>s and <code>cookie</code>s.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>tokens</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>a single or list of tokens to add emoji to. Must match respectively to <code>subdomain</code>s and <code>cookie</code>s.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>cookies</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to <code>subdomain</code>s and <code>token</code>s.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>options</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      object\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>contains singleton or arrays of emoji descriptors.</p>\n          \n            <h6>Properties</h6>\n            \n\n<table class=\"params\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n        <th>Attributes</th>\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n      <tr>\n        \n          <td class=\"name\"><code>src</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>source image files for the emoji to be added. If no corresponding <code>options.name</code> is given, the filename will be used</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>name</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>names of the emoji to be added, overriding filenames if given, and becoming the alias name if an <code>options.aliasFor</code> is given</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>aliasFor</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>names of emoji to be aliased to <code>options.name</code></p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>allowCollisions</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>true</code>, emoji being uploaded will not be checked against existing emoji. This will take less time up front but may cause more errors.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>avoidCollisions</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>true</code>, emoji being added will be renamed to not collide with existing emoji. See lib/util/helpers.avoidCollisions for logic and details // TODO: fix this link, maybe link to tests which has better examples</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>prefix</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>string to prefix all emoji being uploaded</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>bustCache</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>true</code>, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making <code>options.avoidCollisions</code> more accurate</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>output</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>false</code>, no files will be written during execution. Prevents saving of adminList for future use</p>\n          \n        </td>\n      </tr>\n\n    \n  </tbody>\n</table>\n\n          \n        </td>\n      </tr>\n\n    \n  </tbody>\n</table>\n\n\n\n\n\n<dl class=\"details\">\n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n    <dt class=\"tag-source\">Source:</dt>\n    <dd class=\"tag-source\">\n      <ul class=\"dummy\">\n        <li>\n          <a href=\"emojme-add.js.html\">emojme-add.js</a>, <a href=\"emojme-add.js.html#line71\">line 71</a>\n        </li>\n      </ul>\n    </dd>\n  \n\n  \n\n  \n\n  \n</dl>\n\n\n\n\n\n\n\n\n\n\n\n\n\n  <div class=\"example-container\">\n    <a class=\"link-icon\" href=\"#add-examples\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h5 id=\"add-examples\">Example</h5>\n    \n\n  <pre class=\"prettyprint\"><code>var addOptions = {\n  src: ['./emoji1.jpg', 'http://example.com/emoji2.png'], // upload these two images\n  name: ['myLocalEmoji', 'myOnlineEmoji'], // call them these two names\n  bustCache: false, // don't bother redownloading existing emoji\n  avoidCollisions: true, // if there are similarly named emoji, change my new emoji names\n  output: false // don't write any files\n};\nvar subdomains = ['mySubdomain1', 'mySubdomain2'] // can add one or multiple\nvar tokens = ['myToken1', 'myToken2'] // can add one or multiple\nvar cookies = ['myCookie1', 'myCookie2'] // can add one or multiple\nvar addResults = await emojme.add(subdomains, tokens, cookies, addOptions);\nconsole.log(userStatsResults);\n// {\n//   mySubomain1: {\n//     collisions: [], // only defined if avoidCollisons = false\n//     emojiList: [\n//       { name: 'myLocalEmoji', ... },\n//       { name: 'myOnlineEmoji', ... },\n//     ]\n//   },\n//   mySubomain2: {\n//     collisions: [], // only defined if avoidCollisons = false\n//     emojiList: [\n//       { name: 'myLocalEmoji', ... },\n//       { name: 'myOnlineEmoji', ... },\n//     ]\n//   }\n// }</code></pre>\n\n\n  </div>\n\n\n      \n    \n\n    \n      <h3 class=\"subsection-title\">Type Definitions</h3>\n\n      \n          \n\n\n  \n\n  <span class='name-container'>\n    <a class=\"link-icon\" href=\"#~addResponseObject\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h4 class=\"name\" id=\"~addResponseObject\">\n      <span class=\"type-signature\"></span>addResponseObject<span class=\"type-signature\"> :object</span>\n    </h4>\n  </span>\n\n  \n\n\n\n  <div class=\"description\">\n    <p>The add response object, like other response objects, is organized by input subdomain.</p>\n  </div>\n\n\n\n\n\n\n\n\n\n\n  <h5 class=\"subsection-title\">Properties:</h5>\n\n  \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>subdomain</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      object\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>each subdomain passed in to add will appear as a key in the response</p>\n          \n            <h6>Properties</h6>\n            \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>emojiList</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Array.&lt;emojiList>\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the list of emoji added to <code>subdomain</code>, with each element reflecting the parameters passed in to <code>add</code></p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>collisions</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Array.&lt;emojiList>\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>options.avoidCollisions</code> is <code>false</code>, emoji that cannot be uploaded due to existing conflicting emoji names will exist here</p>\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n\n\n\n<dl class=\"details\">\n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n    <dt class=\"tag-source\">Source:</dt>\n    <dd class=\"tag-source\">\n      <ul class=\"dummy\">\n        <li>\n          <a href=\"emojme-add.js.html\">emojme-add.js</a>, <a href=\"emojme-add.js.html#line12\">line 12</a>\n        </li>\n      </ul>\n    </dd>\n  \n\n  \n\n  \n\n  \n</dl>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n        \n\n    \n  </article>\n</section>\n\n\n\n\n  </div>\n\n  <br class=\"clear\">\n\n  <footer>\n    Documentation generated by <a href=\"https://github.com/jsdoc3/jsdoc\">JSDoc 3.5.5</a>\n  </footer>\n\n  <script src=\"scripts/linenumber.js\"></script>\n  <script src=\"scripts/pagelocation.js\"></script>\n\n  \n  \n</body>\n</html>"
  },
  {
    "path": "docs/module-download.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\n  <title>\n      download - Documentation\n  </title>\n\n  <link href=\"https://www.braintreepayments.com/images/favicon-ccda0b14.png\" rel=\"icon\" type=\"image/png\">\n\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/highlight.min.js\"></script>\n  <script>hljs.initHighlightingOnLoad();</script>\n\n  <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js\"></script>\n\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/prettify-tomorrow.css\">\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/jsdoc-default.css\">\n\n  \n\n  <!-- start Mixpanel -->\n  <script type=\"text/javascript\">(function(e,a){if(!a.__SV){var b=window;try{var c,l,i,j=b.location,g=j.hash;c=function(a,b){return(l=a.match(RegExp(b+\"=([^&]*)\")))?l[1]:null};g&&c(g,\"state\")&&(i=JSON.parse(decodeURIComponent(c(g,\"state\"))),\"mpeditor\"===i.action&&(b.sessionStorage.setItem(\"_mpcehash\",g),history.replaceState(i.desiredHash||\"\",e.title,j.pathname+j.search)))}catch(m){}var k,h;window.mixpanel=a;a._i=[];a.init=function(b,c,f){function e(b,a){var c=a.split(\".\");2==c.length&&(b=b[c[0]],a=c[1]);b[a]=function(){b.push([a].concat(Array.prototype.slice.call(arguments,\n  0)))}}var d=a;\"undefined\"!==typeof f?d=a[f]=[]:f=\"mixpanel\";d.people=d.people||[];d.toString=function(b){var a=\"mixpanel\";\"mixpanel\"!==f&&(a+=\".\"+f);b||(a+=\" (stub)\");return a};d.people.toString=function(){return d.toString(1)+\".people (stub)\"};k=\"disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user\".split(\" \");\n  for(h=0;h<k.length;h++)e(d,k[h]);a._i.push([b,c,f])};a.__SV=1.2;b=e.createElement(\"script\");b.type=\"text/javascript\";b.async=!0;b.src=\"undefined\"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:\"file:\"===e.location.protocol&&\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\".match(/^\\/\\//)?\"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\":\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\";c=e.getElementsByTagName(\"script\")[0];c.parentNode.insertBefore(b,c)}})(document,window.mixpanel||[]);\n  mixpanel.init(\"1919205b2da72e4da3b9b6639b444d59\");</script>\n  <!-- end Mixpanel -->\n</head>\n\n<body>\n  <svg style=\"display: none;\">\n    <defs>\n      <symbol id=\"linkIcon\" fill=\"#706d77\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n          <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n          <path d=\"M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z\"/>\n      </symbol>\n    </defs>\n  </svg>\n\n  <input type=\"checkbox\" id=\"nav-trigger\" class=\"nav-trigger\" />\n  <label for=\"nav-trigger\" class=\"navicon-button x\">\n    <div class=\"navicon\"></div>\n  </label>\n\n  <label for=\"nav-trigger\" class=\"overlay\"></label>\n\n  <div class=\"top-nav-wrapper\">\n    <ul>\n      <li >\n        <a href=\"index.html\">\n          \n            <svg fill=\"#6D6D6D\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n              <path d=\"M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z\"/>\n              <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n            </svg>\n          \n          \n        </a>\n      </li>\n\n      \n\n    </ul>\n  </div>\n\n  <nav>\n    <h3 class=\"reference-title\">\n      emojme\n    </h3>\n\n    <h3>Modules</h3><ul><li id=\"add-nav\"><a href=\"module-add.html\">add</a><ul class='methods'><li data-type=\"method\" id=\"add-add-nav\"><a href=\"module-add.html#~add\">add</a></li></ul></li><li id=\"download-nav\"><a href=\"module-download.html\">download</a><ul class='methods'><li data-type=\"method\" id=\"download-download-nav\"><a href=\"module-download.html#~download\">download</a></li></ul></li><li id=\"favorites-nav\"><a href=\"module-favorites.html\">favorites</a><ul class='methods'><li data-type=\"method\" id=\"favorites-favorites-nav\"><a href=\"module-favorites.html#~favorites\">favorites</a></li></ul></li><li id=\"sync-nav\"><a href=\"module-sync.html\">sync</a><ul class='methods'><li data-type=\"method\" id=\"sync-sync-nav\"><a href=\"module-sync.html#~sync\">sync</a></li></ul></li><li id=\"upload-nav\"><a href=\"module-upload.html\">upload</a><ul class='methods'><li data-type=\"method\" id=\"upload-upload-nav\"><a href=\"module-upload.html#~upload\">upload</a></li></ul></li><li id=\"userStats-nav\"><a href=\"module-userStats.html\">userStats</a><ul class='methods'><li data-type=\"method\" id=\"userStats-userStats-nav\"><a href=\"module-userStats.html#~userStats\">userStats</a></li></ul></li></ul>\n  </nav>\n\n  <div id=\"main\">\n    \n      <h1 class=\"page-title\">\n        download\n      </h1>\n    \n\n    \n\n<section>\n  <header>\n    \n      \n  </header>\n\n  <article>\n    <div class=\"container-overview\">\n      \n        \n\n        \n      \n    </div>\n\n    \n\n    \n\n    \n\n     \n\n    \n\n    \n\n    \n      <h3 class=\"subsection-title\">Methods</h3>\n\n      \n        \n\n\n  \n\n  <span class='name-container'>\n    <a class=\"link-icon\" href=\"#~download\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h4 class=\"name\" id=\"~download\">\n      <span class=\"type-signature\">(async, inner) </span>download<span class=\"signature\">(subdomains, tokens, cookies, options)</span><span class=\"type-signature\"> &rarr; {Promise.&lt;downloadResponseObject>}</span>\n    </h4>\n  </span>\n\n  \n\n\n\n  <div class=\"description\">\n    <p>Download the list of custom emoji that have been added to the given slack instances, by default saving a json of all available relevant data. Optionally save the source images for a given user.</p>\n  </div>\n\n\n\n\n\n\n\n  <h5>Parameters:</h5>\n  \n\n<table class=\"params\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n      <tr>\n        \n          <td class=\"name\"><code>subdomains</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>a single or list of subdomains from which to download emoji. Must match respectively to <code>token</code>s and <code>cookie</code>s.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>tokens</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>a single or list of tokens with which to authenticate. Must match respectively to <code>subdomain</code>s and <code>cookie</code>s.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>cookies</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to <code>subdomain</code>s and <code>token</code>s.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>options</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      object\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>contains singleton or arrays of emoji descriptors.</p>\n          \n            <h6>Properties</h6>\n            \n\n<table class=\"params\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n        <th>Attributes</th>\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n      <tr>\n        \n          <td class=\"name\"><code>save</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>A user name or array of user names whose emoji source images will be saved. All emoji source images are linked to in the default adminList, but passing a user name here will save that user's emoji to build/<subdomain>/<username></p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>saveAll</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>true</code>, download all emoji on slack instance from all users to disk in a single location.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>saveAllByUser</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>true</code>, download all emoji on slack instance from all users to disk, organized into directories by user.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>bustCache</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>true</code>, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making <code>options.avoidCollisions</code> more accurate</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>output</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>false</code>, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>verbose</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>true</code>, all messages will be written to stdout in addition to combined log file.</p>\n          \n        </td>\n      </tr>\n\n    \n  </tbody>\n</table>\n\n          \n        </td>\n      </tr>\n\n    \n  </tbody>\n</table>\n\n\n\n\n\n<dl class=\"details\">\n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n    <dt class=\"tag-source\">Source:</dt>\n    <dd class=\"tag-source\">\n      <ul class=\"dummy\">\n        <li>\n          <a href=\"emojme-download.js.html\">emojme-download.js</a>, <a href=\"emojme-download.js.html#line59\">line 59</a>\n        </li>\n      </ul>\n    </dd>\n  \n\n  \n\n  \n\n  \n</dl>\n\n\n\n\n\n\n\n\n\n\n\n\n\n  <div class=\"example-container\">\n    <a class=\"link-icon\" href=\"#download-examples\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h5 id=\"download-examples\">Example</h5>\n    \n\n  <pre class=\"prettyprint\"><code>var downloadOptions = {\n  save: ['username_1', 'username_2'], // Download the emoji source files for these two users\n  bustCache: true, // make sure this data is fresh\n  output: true // download the adminList to ./build\n};\nvar downloadResults = await emojme.download('mySubdomain', 'myToken', 'myCookie', downloadOptions);\nconsole.log(downloadResults);\n// {\n//   mySubdomain: {\n//     emojiList: [\n//       { name: 'emoji-from-mySubdomain', ... },\n//       ...\n//     ],\n//     saveResults: [\n//       './build/mySubdomain/username_1/an_emoji.jpg',\n//       './build/mySubdomain/username_1/another_emoji.gif',\n//       ... all of username_1's emoji\n//       './build/mySubdomain/username_2/some_emoji.jpg',\n//       './build/mySubdomain/username_2/some_other_emoji.gif',\n//       ... all of username_2's emoji\n//     ]\n//   }\n// }</code></pre>\n\n\n  </div>\n\n\n      \n    \n\n    \n      <h3 class=\"subsection-title\">Type Definitions</h3>\n\n      \n          \n\n\n  \n\n  <span class='name-container'>\n    <a class=\"link-icon\" href=\"#~downloadResponseObject\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h4 class=\"name\" id=\"~downloadResponseObject\">\n      <span class=\"type-signature\"></span>downloadResponseObject<span class=\"type-signature\"> :object</span>\n    </h4>\n  </span>\n\n  \n\n\n\n  <div class=\"description\">\n    <p>The download response object, like other response objects, is organized by input subdomain.</p>\n  </div>\n\n\n\n\n\n\n\n\n\n\n  <h5 class=\"subsection-title\">Properties:</h5>\n\n  \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>subdomain</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      object\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>each subdomain passed in to add will appear as a key in the response</p>\n          \n            <h6>Properties</h6>\n            \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>emojiList</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Array.&lt;emojiList>\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the list of emoji downloaded from <code>subdomain</code></p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>saveResults</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>an array of paths for emoji that have been downloaded. note that all users that have been passed with <code>options.save</code> will be grouped together here.</p>\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n\n\n\n<dl class=\"details\">\n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n    <dt class=\"tag-source\">Source:</dt>\n    <dd class=\"tag-source\">\n      <ul class=\"dummy\">\n        <li>\n          <a href=\"emojme-download.js.html\">emojme-download.js</a>, <a href=\"emojme-download.js.html#line9\">line 9</a>\n        </li>\n      </ul>\n    </dd>\n  \n\n  \n\n  \n\n  \n</dl>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n        \n\n    \n  </article>\n</section>\n\n\n\n\n  </div>\n\n  <br class=\"clear\">\n\n  <footer>\n    Documentation generated by <a href=\"https://github.com/jsdoc3/jsdoc\">JSDoc 3.5.5</a>\n  </footer>\n\n  <script src=\"scripts/linenumber.js\"></script>\n  <script src=\"scripts/pagelocation.js\"></script>\n\n  \n  \n</body>\n</html>"
  },
  {
    "path": "docs/module-favorites.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\n  <title>\n      favorites - Documentation\n  </title>\n\n  <link href=\"https://www.braintreepayments.com/images/favicon-ccda0b14.png\" rel=\"icon\" type=\"image/png\">\n\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/highlight.min.js\"></script>\n  <script>hljs.initHighlightingOnLoad();</script>\n\n  <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js\"></script>\n\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/prettify-tomorrow.css\">\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/jsdoc-default.css\">\n\n  \n\n  <!-- start Mixpanel -->\n  <script type=\"text/javascript\">(function(e,a){if(!a.__SV){var b=window;try{var c,l,i,j=b.location,g=j.hash;c=function(a,b){return(l=a.match(RegExp(b+\"=([^&]*)\")))?l[1]:null};g&&c(g,\"state\")&&(i=JSON.parse(decodeURIComponent(c(g,\"state\"))),\"mpeditor\"===i.action&&(b.sessionStorage.setItem(\"_mpcehash\",g),history.replaceState(i.desiredHash||\"\",e.title,j.pathname+j.search)))}catch(m){}var k,h;window.mixpanel=a;a._i=[];a.init=function(b,c,f){function e(b,a){var c=a.split(\".\");2==c.length&&(b=b[c[0]],a=c[1]);b[a]=function(){b.push([a].concat(Array.prototype.slice.call(arguments,\n  0)))}}var d=a;\"undefined\"!==typeof f?d=a[f]=[]:f=\"mixpanel\";d.people=d.people||[];d.toString=function(b){var a=\"mixpanel\";\"mixpanel\"!==f&&(a+=\".\"+f);b||(a+=\" (stub)\");return a};d.people.toString=function(){return d.toString(1)+\".people (stub)\"};k=\"disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user\".split(\" \");\n  for(h=0;h<k.length;h++)e(d,k[h]);a._i.push([b,c,f])};a.__SV=1.2;b=e.createElement(\"script\");b.type=\"text/javascript\";b.async=!0;b.src=\"undefined\"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:\"file:\"===e.location.protocol&&\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\".match(/^\\/\\//)?\"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\":\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\";c=e.getElementsByTagName(\"script\")[0];c.parentNode.insertBefore(b,c)}})(document,window.mixpanel||[]);\n  mixpanel.init(\"1919205b2da72e4da3b9b6639b444d59\");</script>\n  <!-- end Mixpanel -->\n</head>\n\n<body>\n  <svg style=\"display: none;\">\n    <defs>\n      <symbol id=\"linkIcon\" fill=\"#706d77\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n          <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n          <path d=\"M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z\"/>\n      </symbol>\n    </defs>\n  </svg>\n\n  <input type=\"checkbox\" id=\"nav-trigger\" class=\"nav-trigger\" />\n  <label for=\"nav-trigger\" class=\"navicon-button x\">\n    <div class=\"navicon\"></div>\n  </label>\n\n  <label for=\"nav-trigger\" class=\"overlay\"></label>\n\n  <div class=\"top-nav-wrapper\">\n    <ul>\n      <li >\n        <a href=\"index.html\">\n          \n            <svg fill=\"#6D6D6D\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n              <path d=\"M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z\"/>\n              <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n            </svg>\n          \n          \n        </a>\n      </li>\n\n      \n\n    </ul>\n  </div>\n\n  <nav>\n    <h3 class=\"reference-title\">\n      emojme\n    </h3>\n\n    <h3>Modules</h3><ul><li id=\"add-nav\"><a href=\"module-add.html\">add</a><ul class='methods'><li data-type=\"method\" id=\"add-add-nav\"><a href=\"module-add.html#~add\">add</a></li></ul></li><li id=\"download-nav\"><a href=\"module-download.html\">download</a><ul class='methods'><li data-type=\"method\" id=\"download-download-nav\"><a href=\"module-download.html#~download\">download</a></li></ul></li><li id=\"favorites-nav\"><a href=\"module-favorites.html\">favorites</a><ul class='methods'><li data-type=\"method\" id=\"favorites-favorites-nav\"><a href=\"module-favorites.html#~favorites\">favorites</a></li></ul></li><li id=\"sync-nav\"><a href=\"module-sync.html\">sync</a><ul class='methods'><li data-type=\"method\" id=\"sync-sync-nav\"><a href=\"module-sync.html#~sync\">sync</a></li></ul></li><li id=\"upload-nav\"><a href=\"module-upload.html\">upload</a><ul class='methods'><li data-type=\"method\" id=\"upload-upload-nav\"><a href=\"module-upload.html#~upload\">upload</a></li></ul></li><li id=\"userStats-nav\"><a href=\"module-userStats.html\">userStats</a><ul class='methods'><li data-type=\"method\" id=\"userStats-userStats-nav\"><a href=\"module-userStats.html#~userStats\">userStats</a></li></ul></li></ul>\n  </nav>\n\n  <div id=\"main\">\n    \n      <h1 class=\"page-title\">\n        favorites\n      </h1>\n    \n\n    \n\n<section>\n  <header>\n    \n      \n  </header>\n\n  <article>\n    <div class=\"container-overview\">\n      \n        \n\n        \n      \n    </div>\n\n    \n\n    \n\n    \n\n     \n\n    \n\n    \n\n    \n      <h3 class=\"subsection-title\">Methods</h3>\n\n      \n        \n\n\n  \n\n  <span class='name-container'>\n    <a class=\"link-icon\" href=\"#~favorites\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h4 class=\"name\" id=\"~favorites\">\n      <span class=\"type-signature\">(async, inner) </span>favorites<span class=\"signature\">(subdomains, tokens, cookies, options)</span><span class=\"type-signature\"> &rarr; {Promise.&lt;favoritesResponseObject>}</span>\n    </h4>\n  </span>\n\n  \n\n\n\n  <div class=\"description\">\n    <p>Get the contents of the &quot;Frequenly Used&quot; box for your specified user</p>\n  </div>\n\n\n\n\n\n\n\n  <h5>Parameters:</h5>\n  \n\n<table class=\"params\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n      <tr>\n        \n          <td class=\"name\"><code>subdomains</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>a single or list of subdomains from which to analyze emoji. Must match respectively to <code>token</code>s and <code>cookie</code>s.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>tokens</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>a single or list of tokens to add emoji to. Must match respectively to <code>subdomain</code>s and <code>cookie</code>s.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>cookies</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to <code>subdomain</code>s and <code>token</code>s.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>options</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      object\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>contains options on what to present</p>\n          \n            <h6>Properties</h6>\n            \n\n<table class=\"params\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n        <th>Attributes</th>\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n      <tr>\n        \n          <td class=\"name\"><code>lite</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      Number\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>do not attempt to marry favorites with complete adminlist content. Results will contain only emoji name and usage count.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>top</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      Number\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>(verbose cli only) count of top n emoji contriubtors you would like to retrieve user statistics on</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>usage</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      Number\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>(verbose cli only) print not just the list of favorite emoji, but their usage count</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>bustCache</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>true</code>, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making <code>options.avoidCollisions</code> more accurate</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>output</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>false</code>, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>verbose</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>true</code>, all messages will be written to stdout in addition to combined log file.</p>\n          \n        </td>\n      </tr>\n\n    \n  </tbody>\n</table>\n\n          \n        </td>\n      </tr>\n\n    \n  </tbody>\n</table>\n\n\n\n\n\n<dl class=\"details\">\n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n    <dt class=\"tag-source\">Source:</dt>\n    <dd class=\"tag-source\">\n      <ul class=\"dummy\">\n        <li>\n          <a href=\"emojme-favorites.js.html\">emojme-favorites.js</a>, <a href=\"emojme-favorites.js.html#line59\">line 59</a>\n        </li>\n      </ul>\n    </dd>\n  \n\n  \n\n  \n\n  \n</dl>\n\n\n\n\n\n\n\n\n\n\n\n\n\n  <div class=\"example-container\">\n    <a class=\"link-icon\" href=\"#favorites-examples\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h5 id=\"favorites-examples\">Example</h5>\n    \n\n  <pre class=\"prettyprint\"><code>var favoritesResult = await emojme.favorites('mySubdomain', 'myToken', 'myCookie', {});\nconsole.log(favoritesResult);\n// {\n//   mySubdomain: {\n//     favoritesResult: {\n//         user: '{myToken's user}',\n//         favoriteEmoji: [\n//            emojiName,\n//            ...\n//         ],\n//         favoriteEmojiAdminList: [\n//           {emojiName}: {adminList-style emoji object, with additional `usage` value}\n//           ...\n//         ],\n//       }\n//   }\n// }</code></pre>\n\n\n  </div>\n\n\n      \n    \n\n    \n      <h3 class=\"subsection-title\">Type Definitions</h3>\n\n      \n          \n\n\n  \n\n  <span class='name-container'>\n    <a class=\"link-icon\" href=\"#~favoritesResponseObject\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h4 class=\"name\" id=\"~favoritesResponseObject\">\n      <span class=\"type-signature\"></span>favoritesResponseObject<span class=\"type-signature\"> :object</span>\n    </h4>\n  </span>\n\n  \n\n\n\n  <div class=\"description\">\n    <p>The user-specific favorites response object, like other response objects, is organized by input subdomain.</p>\n  </div>\n\n\n\n\n\n\n\n\n\n\n  <h5 class=\"subsection-title\">Properties:</h5>\n\n  \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>subdomain</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      object\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>each subdomain passed in to add will appear as a key in the response</p>\n          \n            <h6>Properties</h6>\n            \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>favoritesResult.user</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      string\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the username associated with the given cookie token</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>favoritesResult.favoriteEmoji</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the list of 'favorite' emoji as deemed by slack, in desc sorted order</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>favoritesResult.favoriteEmojiAdminList</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Array.&lt;object>\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>an array of emoji objects, as organized by emojiAdminList</p>\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n\n\n\n<dl class=\"details\">\n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n    <dt class=\"tag-source\">Source:</dt>\n    <dd class=\"tag-source\">\n      <ul class=\"dummy\">\n        <li>\n          <a href=\"emojme-favorites.js.html\">emojme-favorites.js</a>, <a href=\"emojme-favorites.js.html#line14\">line 14</a>\n        </li>\n      </ul>\n    </dd>\n  \n\n  \n\n  \n\n  \n</dl>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n        \n\n    \n  </article>\n</section>\n\n\n\n\n  </div>\n\n  <br class=\"clear\">\n\n  <footer>\n    Documentation generated by <a href=\"https://github.com/jsdoc3/jsdoc\">JSDoc 3.5.5</a>\n  </footer>\n\n  <script src=\"scripts/linenumber.js\"></script>\n  <script src=\"scripts/pagelocation.js\"></script>\n\n  \n  \n</body>\n</html>"
  },
  {
    "path": "docs/module-sync.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\n  <title>\n      sync - Documentation\n  </title>\n\n  <link href=\"https://www.braintreepayments.com/images/favicon-ccda0b14.png\" rel=\"icon\" type=\"image/png\">\n\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/highlight.min.js\"></script>\n  <script>hljs.initHighlightingOnLoad();</script>\n\n  <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js\"></script>\n\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/prettify-tomorrow.css\">\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/jsdoc-default.css\">\n\n  \n\n  <!-- start Mixpanel -->\n  <script type=\"text/javascript\">(function(e,a){if(!a.__SV){var b=window;try{var c,l,i,j=b.location,g=j.hash;c=function(a,b){return(l=a.match(RegExp(b+\"=([^&]*)\")))?l[1]:null};g&&c(g,\"state\")&&(i=JSON.parse(decodeURIComponent(c(g,\"state\"))),\"mpeditor\"===i.action&&(b.sessionStorage.setItem(\"_mpcehash\",g),history.replaceState(i.desiredHash||\"\",e.title,j.pathname+j.search)))}catch(m){}var k,h;window.mixpanel=a;a._i=[];a.init=function(b,c,f){function e(b,a){var c=a.split(\".\");2==c.length&&(b=b[c[0]],a=c[1]);b[a]=function(){b.push([a].concat(Array.prototype.slice.call(arguments,\n  0)))}}var d=a;\"undefined\"!==typeof f?d=a[f]=[]:f=\"mixpanel\";d.people=d.people||[];d.toString=function(b){var a=\"mixpanel\";\"mixpanel\"!==f&&(a+=\".\"+f);b||(a+=\" (stub)\");return a};d.people.toString=function(){return d.toString(1)+\".people (stub)\"};k=\"disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user\".split(\" \");\n  for(h=0;h<k.length;h++)e(d,k[h]);a._i.push([b,c,f])};a.__SV=1.2;b=e.createElement(\"script\");b.type=\"text/javascript\";b.async=!0;b.src=\"undefined\"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:\"file:\"===e.location.protocol&&\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\".match(/^\\/\\//)?\"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\":\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\";c=e.getElementsByTagName(\"script\")[0];c.parentNode.insertBefore(b,c)}})(document,window.mixpanel||[]);\n  mixpanel.init(\"1919205b2da72e4da3b9b6639b444d59\");</script>\n  <!-- end Mixpanel -->\n</head>\n\n<body>\n  <svg style=\"display: none;\">\n    <defs>\n      <symbol id=\"linkIcon\" fill=\"#706d77\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n          <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n          <path d=\"M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z\"/>\n      </symbol>\n    </defs>\n  </svg>\n\n  <input type=\"checkbox\" id=\"nav-trigger\" class=\"nav-trigger\" />\n  <label for=\"nav-trigger\" class=\"navicon-button x\">\n    <div class=\"navicon\"></div>\n  </label>\n\n  <label for=\"nav-trigger\" class=\"overlay\"></label>\n\n  <div class=\"top-nav-wrapper\">\n    <ul>\n      <li >\n        <a href=\"index.html\">\n          \n            <svg fill=\"#6D6D6D\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n              <path d=\"M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z\"/>\n              <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n            </svg>\n          \n          \n        </a>\n      </li>\n\n      \n\n    </ul>\n  </div>\n\n  <nav>\n    <h3 class=\"reference-title\">\n      emojme\n    </h3>\n\n    <h3>Modules</h3><ul><li id=\"add-nav\"><a href=\"module-add.html\">add</a><ul class='methods'><li data-type=\"method\" id=\"add-add-nav\"><a href=\"module-add.html#~add\">add</a></li></ul></li><li id=\"download-nav\"><a href=\"module-download.html\">download</a><ul class='methods'><li data-type=\"method\" id=\"download-download-nav\"><a href=\"module-download.html#~download\">download</a></li></ul></li><li id=\"favorites-nav\"><a href=\"module-favorites.html\">favorites</a><ul class='methods'><li data-type=\"method\" id=\"favorites-favorites-nav\"><a href=\"module-favorites.html#~favorites\">favorites</a></li></ul></li><li id=\"sync-nav\"><a href=\"module-sync.html\">sync</a><ul class='methods'><li data-type=\"method\" id=\"sync-sync-nav\"><a href=\"module-sync.html#~sync\">sync</a></li></ul></li><li id=\"upload-nav\"><a href=\"module-upload.html\">upload</a><ul class='methods'><li data-type=\"method\" id=\"upload-upload-nav\"><a href=\"module-upload.html#~upload\">upload</a></li></ul></li><li id=\"userStats-nav\"><a href=\"module-userStats.html\">userStats</a><ul class='methods'><li data-type=\"method\" id=\"userStats-userStats-nav\"><a href=\"module-userStats.html#~userStats\">userStats</a></li></ul></li></ul>\n  </nav>\n\n  <div id=\"main\">\n    \n      <h1 class=\"page-title\">\n        sync\n      </h1>\n    \n\n    \n\n<section>\n  <header>\n    \n      \n  </header>\n\n  <article>\n    <div class=\"container-overview\">\n      \n        \n\n        \n      \n    </div>\n\n    \n\n    \n\n    \n\n     \n\n    \n\n    \n\n    \n      <h3 class=\"subsection-title\">Methods</h3>\n\n      \n        \n\n\n  \n\n  <span class='name-container'>\n    <a class=\"link-icon\" href=\"#~sync\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h4 class=\"name\" id=\"~sync\">\n      <span class=\"type-signature\">(async, inner) </span>sync<span class=\"signature\">(subdomains, tokens, cookies, options)</span><span class=\"type-signature\"> &rarr; {Promise.&lt;syncResponseObject>}</span>\n    </h4>\n  </span>\n\n  \n\n\n\n  <div class=\"description\">\n    <p>Sync emoji between slack subdomains</p>\n<p>Sync can be executed in either a &quot;one way&quot; or &quot;n way&quot; configuration, and both configurations can have a variable number of sources and destinations. In a &quot;one way&quot; configuration, all emoji from all source subdomains will be added to all destination subdomains&quot; and can be set by specifying <code>srcSubdomains</code> and <code>dstSubdomains</code>. In an &quot;n way&quot; configuration, every subdomain given is treated as the destination for every emoji in every other subdomain.</p>\n  </div>\n\n\n\n\n\n\n\n  <h5>Parameters:</h5>\n  \n\n<table class=\"params\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n      <tr>\n        \n          <td class=\"name\"><code>subdomains</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      null\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>Two ore more subdomains that you wish to have the same emoji pool</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>tokens</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      null\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>cookie tokens corresponding to the given subdomains</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>cookies</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      null\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>User cookies corresponding to the given subdomains</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>options</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      object\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>contains src<em> and dst</em> information for &quot;one way&quot; sync configuration. Either specify <code>subdomains</code> and <code>tokens</code>, or <code>srcSubdomains</code>, <code>srcTokens</code>, <code>dstSubdomains</code>, and <code>dstTokens</code>, not both.</p>\n          \n            <h6>Properties</h6>\n            \n\n<table class=\"params\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n        <th>Attributes</th>\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n      <tr>\n        \n          <td class=\"name\"><code>srcSubdomains</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>slack instances from which to draw emoji. No additions will be made to these subdomains</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>srcTokens</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>tokens for the slack instances from which to draw emoji</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>srcCookies</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>cookies auth cookies for the slack instances from which to draw emoji</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>dstSubdomains</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>slack instances in which all source emoji will be deposited. None of <code>dstSubdomain</code>'s emoji will end up in <code>srcSubdomain</code></p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>dstTokens</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>tokens for the slack instances where emoji will be deposited</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>dstCookies</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>cookies auth cookies for the slack instances from which to draw emoji</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>bustCache</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>true</code>, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making <code>options.avoidCollisions</code> more accurate</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>output</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>false</code>, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>verbose</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>true</code>, all messages will be written to stdout in addition to combined log file.</p>\n          \n        </td>\n      </tr>\n\n    \n  </tbody>\n</table>\n\n          \n        </td>\n      </tr>\n\n    \n  </tbody>\n</table>\n\n\n\n\n\n<dl class=\"details\">\n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n    <dt class=\"tag-source\">Source:</dt>\n    <dd class=\"tag-source\">\n      <ul class=\"dummy\">\n        <li>\n          <a href=\"emojme-sync.js.html\">emojme-sync.js</a>, <a href=\"emojme-sync.js.html#line68\">line 68</a>\n        </li>\n      </ul>\n    </dd>\n  \n\n  \n\n  \n\n  \n</dl>\n\n\n\n\n\n\n\n\n\n\n\n\n\n  <div class=\"example-container\">\n    <a class=\"link-icon\" href=\"#sync-examples\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h5 id=\"sync-examples\">Example</h5>\n    \n\n  <pre class=\"prettyprint\"><code>var syncOptions = {\n  srcSubdomains: ['srcSubdomain'], // copy all emoji from srcSubdomain...\n  srcTokens: ['srcToken'],\n  srcCookies: ['srcCookie'],\n  dstSubdomains: ['dstSubdomain1', 'dstSubdomain2'], // ...to dstSubdomain1 and dstSubdomain2\n  dstTokens: ['dstToken1', 'dstToken2'],\n  dstCookies: ['dstCookie1', 'dstCookie2'],\n  bustCache: true // get fresh lists to make sure we're not doing more lifting than we have to\n};\nvar syncResults = await emojme.sync(null, null, syncOptions);\nconsole.log(syncResults);\n// {\n//   dstSubdomain1: {\n//     emojiList: [\n//       { name: emoji-1-from-srcSubdomain ... },\n//       { name: emoji-2-from-srcSubdomain ... }\n//     ]\n//   },\n//   dstSubdomain2: {\n//     emojiList: [\n//       { name: emoji-1-from-srcSubdomain ... },\n//       { name: emoji-2-from-srcSubdomain ... }\n//     ]\n//   }\n// }</code></pre>\n\n\n  </div>\n\n\n      \n    \n\n    \n      <h3 class=\"subsection-title\">Type Definitions</h3>\n\n      \n          \n\n\n  \n\n  <span class='name-container'>\n    <a class=\"link-icon\" href=\"#~syncResponseObject\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h4 class=\"name\" id=\"~syncResponseObject\">\n      <span class=\"type-signature\"></span>syncResponseObject<span class=\"type-signature\"> :object</span>\n    </h4>\n  </span>\n\n  \n\n\n\n  <div class=\"description\">\n    <p>The sync response object, like other response objects, is organized by input subdomain.</p>\n  </div>\n\n\n\n\n\n\n\n\n\n\n  <h5 class=\"subsection-title\">Properties:</h5>\n\n  \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>subdomain</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      object\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>each subdomain passed in to add will appear as a key in the response</p>\n          \n            <h6>Properties</h6>\n            \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>emojiList</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Array.&lt;emojiList>\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the list of emoji added to <code>subdomain</code>, with each element an emoji pulled from either <code>srcSubdomain</code> or <code>subdomains</code> less the subdomain in question.</p>\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n\n\n\n<dl class=\"details\">\n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n    <dt class=\"tag-source\">Source:</dt>\n    <dd class=\"tag-source\">\n      <ul class=\"dummy\">\n        <li>\n          <a href=\"emojme-sync.js.html\">emojme-sync.js</a>, <a href=\"emojme-sync.js.html#line12\">line 12</a>\n        </li>\n      </ul>\n    </dd>\n  \n\n  \n\n  \n\n  \n</dl>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n        \n\n    \n  </article>\n</section>\n\n\n\n\n  </div>\n\n  <br class=\"clear\">\n\n  <footer>\n    Documentation generated by <a href=\"https://github.com/jsdoc3/jsdoc\">JSDoc 3.5.5</a>\n  </footer>\n\n  <script src=\"scripts/linenumber.js\"></script>\n  <script src=\"scripts/pagelocation.js\"></script>\n\n  \n  \n</body>\n</html>"
  },
  {
    "path": "docs/module-upload.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\n  <title>\n      upload - Documentation\n  </title>\n\n  <link href=\"https://www.braintreepayments.com/images/favicon-ccda0b14.png\" rel=\"icon\" type=\"image/png\">\n\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/highlight.min.js\"></script>\n  <script>hljs.initHighlightingOnLoad();</script>\n\n  <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js\"></script>\n\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/prettify-tomorrow.css\">\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/jsdoc-default.css\">\n\n  \n\n  <!-- start Mixpanel -->\n  <script type=\"text/javascript\">(function(e,a){if(!a.__SV){var b=window;try{var c,l,i,j=b.location,g=j.hash;c=function(a,b){return(l=a.match(RegExp(b+\"=([^&]*)\")))?l[1]:null};g&&c(g,\"state\")&&(i=JSON.parse(decodeURIComponent(c(g,\"state\"))),\"mpeditor\"===i.action&&(b.sessionStorage.setItem(\"_mpcehash\",g),history.replaceState(i.desiredHash||\"\",e.title,j.pathname+j.search)))}catch(m){}var k,h;window.mixpanel=a;a._i=[];a.init=function(b,c,f){function e(b,a){var c=a.split(\".\");2==c.length&&(b=b[c[0]],a=c[1]);b[a]=function(){b.push([a].concat(Array.prototype.slice.call(arguments,\n  0)))}}var d=a;\"undefined\"!==typeof f?d=a[f]=[]:f=\"mixpanel\";d.people=d.people||[];d.toString=function(b){var a=\"mixpanel\";\"mixpanel\"!==f&&(a+=\".\"+f);b||(a+=\" (stub)\");return a};d.people.toString=function(){return d.toString(1)+\".people (stub)\"};k=\"disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user\".split(\" \");\n  for(h=0;h<k.length;h++)e(d,k[h]);a._i.push([b,c,f])};a.__SV=1.2;b=e.createElement(\"script\");b.type=\"text/javascript\";b.async=!0;b.src=\"undefined\"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:\"file:\"===e.location.protocol&&\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\".match(/^\\/\\//)?\"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\":\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\";c=e.getElementsByTagName(\"script\")[0];c.parentNode.insertBefore(b,c)}})(document,window.mixpanel||[]);\n  mixpanel.init(\"1919205b2da72e4da3b9b6639b444d59\");</script>\n  <!-- end Mixpanel -->\n</head>\n\n<body>\n  <svg style=\"display: none;\">\n    <defs>\n      <symbol id=\"linkIcon\" fill=\"#706d77\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n          <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n          <path d=\"M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z\"/>\n      </symbol>\n    </defs>\n  </svg>\n\n  <input type=\"checkbox\" id=\"nav-trigger\" class=\"nav-trigger\" />\n  <label for=\"nav-trigger\" class=\"navicon-button x\">\n    <div class=\"navicon\"></div>\n  </label>\n\n  <label for=\"nav-trigger\" class=\"overlay\"></label>\n\n  <div class=\"top-nav-wrapper\">\n    <ul>\n      <li >\n        <a href=\"index.html\">\n          \n            <svg fill=\"#6D6D6D\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n              <path d=\"M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z\"/>\n              <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n            </svg>\n          \n          \n        </a>\n      </li>\n\n      \n\n    </ul>\n  </div>\n\n  <nav>\n    <h3 class=\"reference-title\">\n      emojme\n    </h3>\n\n    <h3>Modules</h3><ul><li id=\"add-nav\"><a href=\"module-add.html\">add</a><ul class='methods'><li data-type=\"method\" id=\"add-add-nav\"><a href=\"module-add.html#~add\">add</a></li></ul></li><li id=\"download-nav\"><a href=\"module-download.html\">download</a><ul class='methods'><li data-type=\"method\" id=\"download-download-nav\"><a href=\"module-download.html#~download\">download</a></li></ul></li><li id=\"favorites-nav\"><a href=\"module-favorites.html\">favorites</a><ul class='methods'><li data-type=\"method\" id=\"favorites-favorites-nav\"><a href=\"module-favorites.html#~favorites\">favorites</a></li></ul></li><li id=\"sync-nav\"><a href=\"module-sync.html\">sync</a><ul class='methods'><li data-type=\"method\" id=\"sync-sync-nav\"><a href=\"module-sync.html#~sync\">sync</a></li></ul></li><li id=\"upload-nav\"><a href=\"module-upload.html\">upload</a><ul class='methods'><li data-type=\"method\" id=\"upload-upload-nav\"><a href=\"module-upload.html#~upload\">upload</a></li></ul></li><li id=\"userStats-nav\"><a href=\"module-userStats.html\">userStats</a><ul class='methods'><li data-type=\"method\" id=\"userStats-userStats-nav\"><a href=\"module-userStats.html#~userStats\">userStats</a></li></ul></li></ul>\n  </nav>\n\n  <div id=\"main\">\n    \n      <h1 class=\"page-title\">\n        upload\n      </h1>\n    \n\n    \n\n<section>\n  <header>\n    \n      \n  </header>\n\n  <article>\n    <div class=\"container-overview\">\n      \n        \n\n        \n      \n    </div>\n\n    \n\n    \n\n    \n\n     \n\n    \n\n    \n\n    \n      <h3 class=\"subsection-title\">Methods</h3>\n\n      \n        \n\n\n  \n\n  <span class='name-container'>\n    <a class=\"link-icon\" href=\"#~upload\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h4 class=\"name\" id=\"~upload\">\n      <span class=\"type-signature\">(async, inner) </span>upload<span class=\"signature\">(subdomains, tokens, cookies, options)</span><span class=\"type-signature\"> &rarr; {Promise.&lt;uploadResponseObject>}</span>\n    </h4>\n  </span>\n\n  \n\n\n\n  <div class=\"description\">\n    <p>Upload multiple emoji described by an existing list on disk, either as a json emoji admin list or emojipacks-like yaml.</p>\n  </div>\n\n\n\n\n\n\n\n  <h5>Parameters:</h5>\n  \n\n<table class=\"params\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n      <tr>\n        \n          <td class=\"name\"><code>subdomains</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>a single or list of subdomains from which to download emoji. Must match respectively to <code>token</code>s and <code>cookie</code>s.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>tokens</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>a single or list of tokens with which to authenticate. Must match respectively to <code>subdomain</code>s and <code>cookie</code>s.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>cookies</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to <code>subdomain</code>s and <code>token</code>s.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>options</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      object\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>contains singleton or arrays of emoji descriptors.</p>\n          \n            <h6>Properties</h6>\n            \n\n<table class=\"params\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n        <th>Attributes</th>\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n      <tr>\n        \n          <td class=\"name\"><code>src</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>source emoji list files for the emoji to be added. Can either be in jsonEmojiListFormat or yamlEmojiListFormat</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>avoidCollisions</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>true</code>, emoji being added will be renamed to not collide with existing emoji. See lib/util/helpers.avoidCollisions for logic and details // TODO: fix this link, maybe link to tests which has better examples</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>prefix</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>string to prefix all emoji being uploaded</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>bustCache</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>true</code>, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making <code>options.avoidCollisions</code> more accurate</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>output</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>false</code>, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>verbose</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>true</code>, all messages will be written to stdout in addition to combined log file.</p>\n          \n        </td>\n      </tr>\n\n    \n  </tbody>\n</table>\n\n          \n        </td>\n      </tr>\n\n    \n  </tbody>\n</table>\n\n\n\n\n\n<dl class=\"details\">\n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n    <dt class=\"tag-source\">Source:</dt>\n    <dd class=\"tag-source\">\n      <ul class=\"dummy\">\n        <li>\n          <a href=\"emojme-upload.js.html\">emojme-upload.js</a>, <a href=\"emojme-upload.js.html#line111\">line 111</a>\n        </li>\n      </ul>\n    </dd>\n  \n\n  \n\n  \n\n  \n</dl>\n\n\n\n\n\n\n\n\n\n\n\n\n\n  <div class=\"example-container\">\n    <a class=\"link-icon\" href=\"#upload-examples\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h5 id=\"upload-examples\">Example</h5>\n    \n\n  <pre class=\"prettyprint\"><code>var uploadOptions = {\n  src: './emoji-list.json', // upload all the emoji in this json array of objects\n  avoidCollisions: true, // append '-1' or similar if we try to upload a dupe\n  prefix: 'new-' // prepend every emoji in src with \"new-\", e.g. \"emoji\" becomes \"new-emoji\"\n};\nvar uploadResults = await emojme.upload('mySubdomain', 'myToken', 'myCookie', uploadOptions);\nconsole.log(uploadResults);\n// {\n//   mySubdomain: {\n//     collisions: [\n//       { name: an-emoji-that-already-exists-in-mySubdomain ... }\n//     ],\n//     emojiList: [\n//       { name: emoji-from-emoji-list-json ... },\n//       { name: emoji-from-emoji-list-json ... },\n//       ...\n//     ]\n//   }\n// }</code></pre>\n\n\n  </div>\n\n\n      \n    \n\n    \n      <h3 class=\"subsection-title\">Type Definitions</h3>\n\n      \n          \n\n\n  \n\n  <span class='name-container'>\n    <a class=\"link-icon\" href=\"#~jsonEmojiListFormat\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h4 class=\"name\" id=\"~jsonEmojiListFormat\">\n      <span class=\"type-signature\"></span>jsonEmojiListFormat<span class=\"type-signature\"> :Array</span>\n    </h4>\n  </span>\n\n  \n\n\n\n  <div class=\"description\">\n    <p>The required format of a json file that can be used as the <code>options.src</code> for upload</p>\n<p>To see an example, use download, then look at <code>buidl/*.adminList.json</code></p>\n  </div>\n\n\n\n\n\n\n\n\n\n\n  <h5 class=\"subsection-title\">Properties:</h5>\n\n  \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>emojiList</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Array\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          \n          \n            <h6>Properties</h6>\n            \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>emojiObject</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      object\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          \n          \n            <h6>Properties</h6>\n            \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>name</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      string\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the name of the emoji</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>is_alias</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      1\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      0\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>whether or not the emoji is an alias. If <code>1</code>, <code>alias_for</code> is require and <code>url</code> is ignored. If <code>0</code> vice versa</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>alias_for</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      string\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the name of the emoji this emoji is apeing</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>url</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      string\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the remote url or local path of the emoji</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>user_display_name</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      string\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the name of the emoji creator</p>\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n\n\n\n<dl class=\"details\">\n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n    <dt class=\"tag-source\">Source:</dt>\n    <dd class=\"tag-source\">\n      <ul class=\"dummy\">\n        <li>\n          <a href=\"emojme-upload.js.html\">emojme-upload.js</a>, <a href=\"emojme-upload.js.html#line21\">line 21</a>\n        </li>\n      </ul>\n    </dd>\n  \n\n  \n\n  \n\n  \n</dl>\n\n\n\n\n\n\n\n\n\n\n\n\n\n  <div class=\"example-container\">\n    <a class=\"link-icon\" href=\"#jsonEmojiListFormat-examples\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h5 id=\"jsonEmojiListFormat-examples\">Example</h5>\n    \n\n  <pre class=\"prettyprint\"><code>[\n  {\n     \"name\": \"a_giving_lovely_generous_individual\",\n     \"is_alias\": 1,\n     \"alias_for\": \"caleb\"\n  },\n  {\n    \"name\": \"gooddoggy\",\n    \"is_alias\": 0,\n    \"alias_for\": null,\n    \"url\": \"https://emoji.slack-edge.com/T3T9KQULR/gooddoggy/849f53cf1de25f97.png\"\n  }\n]</code></pre>\n\n\n  </div>\n\n\n        \n          \n\n\n  \n\n  <span class='name-container'>\n    <a class=\"link-icon\" href=\"#~syncResponseObject\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h4 class=\"name\" id=\"~syncResponseObject\">\n      <span class=\"type-signature\"></span>syncResponseObject<span class=\"type-signature\"> :object</span>\n    </h4>\n  </span>\n\n  \n\n\n\n  <div class=\"description\">\n    <p>The upload response object, like other response objects, is organized by input subdomain.</p>\n  </div>\n\n\n\n\n\n\n\n\n\n\n  <h5 class=\"subsection-title\">Properties:</h5>\n\n  \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>subdomain</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      object\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>each subdomain passed in to add will appear as a key in the response</p>\n          \n            <h6>Properties</h6>\n            \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>emojiList</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Array.&lt;emojiList>\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the list of emoji added to <code>subdomain</code>, with each element an emoji pulled from either <code>srcSubdomain</code> or <code>subdomains</code> less the subdomain in question.</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>collisions</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Array.&lt;emojiList>\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>options.avoidCollisions</code> is <code>false</code>, emoji that cannot be uploaded due to existing conflicting emoji names will exist here</p>\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n\n\n\n<dl class=\"details\">\n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n    <dt class=\"tag-source\">Source:</dt>\n    <dd class=\"tag-source\">\n      <ul class=\"dummy\">\n        <li>\n          <a href=\"emojme-upload.js.html\">emojme-upload.js</a>, <a href=\"emojme-upload.js.html#line13\">line 13</a>\n        </li>\n      </ul>\n    </dd>\n  \n\n  \n\n  \n\n  \n</dl>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n        \n          \n\n\n  \n\n  <span class='name-container'>\n    <a class=\"link-icon\" href=\"#~yamlEmojiListFormat\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h4 class=\"name\" id=\"~yamlEmojiListFormat\">\n      <span class=\"type-signature\"></span>yamlEmojiListFormat<span class=\"type-signature\"> :object</span>\n    </h4>\n  </span>\n\n  \n\n\n\n  <div class=\"description\">\n    <p>The required format of a yaml file that can be used as the <code>options.src</code> for upload</p>\n  </div>\n\n\n\n\n\n\n\n\n\n\n  <h5 class=\"subsection-title\">Properties:</h5>\n\n  \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>topLevelYaml</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      object\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>all keys execpt for <code>emojis</code> are ignored</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>emojis</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Array\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the array of emoji objects</p>\n          \n            <h6>Properties</h6>\n            \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>emojiObject</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      object\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          \n          \n            <h6>Properties</h6>\n            \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>name</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      string\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the name of the emoji</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>src</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      string\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>alias for <code>name</code></p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>is_alias</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      1\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      0\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>whether or not the emoji is an alias. If <code>1</code>, <code>alias_for</code> is require and <code>url</code> is ignored. If <code>0</code> vice versa</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>alias_for</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      string\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the name of the emoji this emoji is apeing</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>url</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      string\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the remote url or local path of the emoji</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>user_display_name</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      string\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the name of the emoji creator</p>\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n\n\n\n<dl class=\"details\">\n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n    <dt class=\"tag-source\">Source:</dt>\n    <dd class=\"tag-source\">\n      <ul class=\"dummy\">\n        <li>\n          <a href=\"emojme-upload.js.html\">emojme-upload.js</a>, <a href=\"emojme-upload.js.html#line51\">line 51</a>\n        </li>\n      </ul>\n    </dd>\n  \n\n  \n\n  \n\n  \n</dl>\n\n\n\n\n\n\n\n\n\n\n\n\n\n  <div class=\"example-container\">\n    <a class=\"link-icon\" href=\"#yamlEmojiListFormat-examples\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h5 id=\"yamlEmojiListFormat-examples\">Example</h5>\n    \n\n  <pre class=\"prettyprint\"><code>title: animals\n emojis:\n   - name: llama\n     src: http://i.imgur.com/6bKXKUP.gif\n   - name: alpaca\n     src: http://i.imgur.com/c6QxTbM.gif</code></pre>\n\n\n  </div>\n\n\n        \n\n    \n  </article>\n</section>\n\n\n\n\n  </div>\n\n  <br class=\"clear\">\n\n  <footer>\n    Documentation generated by <a href=\"https://github.com/jsdoc3/jsdoc\">JSDoc 3.5.5</a>\n  </footer>\n\n  <script src=\"scripts/linenumber.js\"></script>\n  <script src=\"scripts/pagelocation.js\"></script>\n\n  \n  \n</body>\n</html>"
  },
  {
    "path": "docs/module-userStats.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\n  <title>\n      userStats - Documentation\n  </title>\n\n  <link href=\"https://www.braintreepayments.com/images/favicon-ccda0b14.png\" rel=\"icon\" type=\"image/png\">\n\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.7.0/highlight.min.js\"></script>\n  <script>hljs.initHighlightingOnLoad();</script>\n\n  <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js\"></script>\n\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/prettify-tomorrow.css\">\n  <link type=\"text/css\" rel=\"stylesheet\" href=\"styles/jsdoc-default.css\">\n\n  \n\n  <!-- start Mixpanel -->\n  <script type=\"text/javascript\">(function(e,a){if(!a.__SV){var b=window;try{var c,l,i,j=b.location,g=j.hash;c=function(a,b){return(l=a.match(RegExp(b+\"=([^&]*)\")))?l[1]:null};g&&c(g,\"state\")&&(i=JSON.parse(decodeURIComponent(c(g,\"state\"))),\"mpeditor\"===i.action&&(b.sessionStorage.setItem(\"_mpcehash\",g),history.replaceState(i.desiredHash||\"\",e.title,j.pathname+j.search)))}catch(m){}var k,h;window.mixpanel=a;a._i=[];a.init=function(b,c,f){function e(b,a){var c=a.split(\".\");2==c.length&&(b=b[c[0]],a=c[1]);b[a]=function(){b.push([a].concat(Array.prototype.slice.call(arguments,\n  0)))}}var d=a;\"undefined\"!==typeof f?d=a[f]=[]:f=\"mixpanel\";d.people=d.people||[];d.toString=function(b){var a=\"mixpanel\";\"mixpanel\"!==f&&(a+=\".\"+f);b||(a+=\" (stub)\");return a};d.people.toString=function(){return d.toString(1)+\".people (stub)\"};k=\"disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user\".split(\" \");\n  for(h=0;h<k.length;h++)e(d,k[h]);a._i.push([b,c,f])};a.__SV=1.2;b=e.createElement(\"script\");b.type=\"text/javascript\";b.async=!0;b.src=\"undefined\"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:\"file:\"===e.location.protocol&&\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\".match(/^\\/\\//)?\"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\":\"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js\";c=e.getElementsByTagName(\"script\")[0];c.parentNode.insertBefore(b,c)}})(document,window.mixpanel||[]);\n  mixpanel.init(\"1919205b2da72e4da3b9b6639b444d59\");</script>\n  <!-- end Mixpanel -->\n</head>\n\n<body>\n  <svg style=\"display: none;\">\n    <defs>\n      <symbol id=\"linkIcon\" fill=\"#706d77\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n          <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n          <path d=\"M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z\"/>\n      </symbol>\n    </defs>\n  </svg>\n\n  <input type=\"checkbox\" id=\"nav-trigger\" class=\"nav-trigger\" />\n  <label for=\"nav-trigger\" class=\"navicon-button x\">\n    <div class=\"navicon\"></div>\n  </label>\n\n  <label for=\"nav-trigger\" class=\"overlay\"></label>\n\n  <div class=\"top-nav-wrapper\">\n    <ul>\n      <li >\n        <a href=\"index.html\">\n          \n            <svg fill=\"#6D6D6D\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\">\n              <path d=\"M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z\"/>\n              <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\n            </svg>\n          \n          \n        </a>\n      </li>\n\n      \n\n    </ul>\n  </div>\n\n  <nav>\n    <h3 class=\"reference-title\">\n      emojme\n    </h3>\n\n    <h3>Modules</h3><ul><li id=\"add-nav\"><a href=\"module-add.html\">add</a><ul class='methods'><li data-type=\"method\" id=\"add-add-nav\"><a href=\"module-add.html#~add\">add</a></li></ul></li><li id=\"download-nav\"><a href=\"module-download.html\">download</a><ul class='methods'><li data-type=\"method\" id=\"download-download-nav\"><a href=\"module-download.html#~download\">download</a></li></ul></li><li id=\"favorites-nav\"><a href=\"module-favorites.html\">favorites</a><ul class='methods'><li data-type=\"method\" id=\"favorites-favorites-nav\"><a href=\"module-favorites.html#~favorites\">favorites</a></li></ul></li><li id=\"sync-nav\"><a href=\"module-sync.html\">sync</a><ul class='methods'><li data-type=\"method\" id=\"sync-sync-nav\"><a href=\"module-sync.html#~sync\">sync</a></li></ul></li><li id=\"upload-nav\"><a href=\"module-upload.html\">upload</a><ul class='methods'><li data-type=\"method\" id=\"upload-upload-nav\"><a href=\"module-upload.html#~upload\">upload</a></li></ul></li><li id=\"userStats-nav\"><a href=\"module-userStats.html\">userStats</a><ul class='methods'><li data-type=\"method\" id=\"userStats-userStats-nav\"><a href=\"module-userStats.html#~userStats\">userStats</a></li></ul></li></ul>\n  </nav>\n\n  <div id=\"main\">\n    \n      <h1 class=\"page-title\">\n        userStats\n      </h1>\n    \n\n    \n\n<section>\n  <header>\n    \n      \n  </header>\n\n  <article>\n    <div class=\"container-overview\">\n      \n        \n\n        \n      \n    </div>\n\n    \n\n    \n\n    \n\n     \n\n    \n\n    \n\n    \n      <h3 class=\"subsection-title\">Methods</h3>\n\n      \n        \n\n\n  \n\n  <span class='name-container'>\n    <a class=\"link-icon\" href=\"#~userStats\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h4 class=\"name\" id=\"~userStats\">\n      <span class=\"type-signature\">(async, inner) </span>userStats<span class=\"signature\">(subdomains, tokens, cookies, options)</span><span class=\"type-signature\"> &rarr; {Promise.&lt;userStatsResponseObject>}</span>\n    </h4>\n  </span>\n\n  \n\n\n\n  <div class=\"description\">\n    <p>Get a few useful-ish statistics for either specific users, or the top-n emoji creators</p>\n  </div>\n\n\n\n\n\n\n\n  <h5>Parameters:</h5>\n  \n\n<table class=\"params\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n      <tr>\n        \n          <td class=\"name\"><code>subdomains</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>a single or list of subdomains to analyze. Must match respectively to <code>token</code>s and <code>cookie</code>s.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>tokens</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>a single or list of tokens to add emoji to. Must match respectively to <code>subdomain</code>s and <code>cookie</code>s.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>cookies</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to <code>subdomain</code>s and <code>token</code>s.</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>options</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      object\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>contains options for what stats to present</p>\n          \n            <h6>Properties</h6>\n            \n\n<table class=\"params\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n        <th>Attributes</th>\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n      <tr>\n        \n          <td class=\"name\"><code>user</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      string\n    </span>\n\n    |\n\n    <span class=\"param-type\">\n      Array.&lt;string>\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>user name or array of user names you would like to retrieve user statistics on. If specified, ignores <code>top</code></p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>top</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      Number\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>count of top n emoji contriubtors you would like to retrieve user statistics on</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>bustCache</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>true</code>, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making <code>options.avoidCollisions</code> more accurate</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>output</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>false</code>, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files</p>\n          \n        </td>\n      </tr>\n\n    \n      <tr>\n        \n          <td class=\"name\"><code>verbose</code></td>\n        \n\n        <td class=\"type\">\n          \n            \n    <span class=\"param-type\">\n      boolean\n    </span>\n\n    \n\n\n          \n        </td>\n\n        \n          <td class=\"attributes\">\n            \n              &lt;optional><br>\n            \n\n            \n\n            \n          </td>\n        \n\n        \n\n        <td class=\"description last\">\n          <p>if <code>true</code>, all messages will be written to stdout in addition to combined log file.</p>\n          \n        </td>\n      </tr>\n\n    \n  </tbody>\n</table>\n\n          \n        </td>\n      </tr>\n\n    \n  </tbody>\n</table>\n\n\n\n\n\n<dl class=\"details\">\n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n    <dt class=\"tag-source\">Source:</dt>\n    <dd class=\"tag-source\">\n      <ul class=\"dummy\">\n        <li>\n          <a href=\"emojme-user-stats.js.html\">emojme-user-stats.js</a>, <a href=\"emojme-user-stats.js.html#line74\">line 74</a>\n        </li>\n      </ul>\n    </dd>\n  \n\n  \n\n  \n\n  \n</dl>\n\n\n\n\n\n\n\n\n\n\n\n\n\n  <div class=\"example-container\">\n    <a class=\"link-icon\" href=\"#userStats-examples\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h5 id=\"userStats-examples\">Example</h5>\n    \n\n  <pre class=\"prettyprint\"><code>var userStatsOptions = {\n  user: ['username_1', 'username_2'] // get me some info on these two users\n};\nvar userStatsResults = await emojme.userStats('mySubdomain', 'myToken', 'myCookie', userStatsOptions);\nconsole.log(userStatsResults);\n// {\n//   mySubdomain: {\n//     userStatsResults: [\n//       {\n//         user: 'username_1',\n//         userEmoji: [{ all username_1's emoji }],\n//         subdomain: mySubdomain,\n//         originalCount: x,\n//         aliasCount: y,\n//         totalCount: x + y,\n//         percentage: (x + y) / mySubdomain's total emoji count\n//       },\n//       {\n//         user: 'username_2',\n//         userEmoji: [{ all username_2's emoji }],\n//         subdomain: mySubdomain,\n//         originalCount: x,\n//         aliasCount: y,\n//         totalCount: x + y,\n//         percentage: (x + y) / mySubdomain's total emoji count\n//       }\n//     ]\n//   }\n// }</code></pre>\n\n\n  </div>\n\n\n      \n    \n\n    \n      <h3 class=\"subsection-title\">Type Definitions</h3>\n\n      \n          \n\n\n  \n\n  <span class='name-container'>\n    <a class=\"link-icon\" href=\"#~userStatsResponseObject\">\n      <svg height=\"20\" width=\"20\" style=\"fill: black;\">\n        <use xlink:href=\"#linkIcon\"></use>\n      </svg>\n    </a>\n    <h4 class=\"name\" id=\"~userStatsResponseObject\">\n      <span class=\"type-signature\"></span>userStatsResponseObject<span class=\"type-signature\"> :object</span>\n    </h4>\n  </span>\n\n  \n\n\n\n  <div class=\"description\">\n    <p>The user-specific userStats response object, like other response objects, is organized by input subdomain.</p>\n  </div>\n\n\n\n\n\n\n\n\n\n\n  <h5 class=\"subsection-title\">Properties:</h5>\n\n  \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>subdomain</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      object\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>each subdomain passed in to add will appear as a key in the response</p>\n          \n            <h6>Properties</h6>\n            \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>emojiList</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Array.&lt;emojiList>\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the list of emoji downloaded from <code>subdomain</code></p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>userStatsResults</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Array.&lt;object>\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>an array of user stats objects</p>\n          \n            <h6>Properties</h6>\n            \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>userStatsObject</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      object\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>An object containing several maybe-useful statistics, separated by user</p>\n          \n            <h6>Properties</h6>\n            \n\n<table class=\"props\">\n  <thead>\n    <tr>\n      \n        <th>Name</th>\n      \n\n      <th>Type</th>\n\n      \n\n      \n\n      <th class=\"last\">Description</th>\n    </tr>\n  </thead>\n\n  <tbody>\n    \n\n    <tr>\n      \n        <td class=\"name\"><code>user</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      string\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the name of the user in question</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>userEmoji</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Array.&lt;emojiList>\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the emojiList the user authored</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>subdomain</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      string\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>redundant :shrug:</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>originalCount</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Number\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the number of original emoji the user has created</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>aliasCount</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Number\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the number of emoji aliases the user has defined</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>totalCount</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Number\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the number of original and aliases the user has created</p>\n          </td>\n    </tr>\n\n  \n\n    <tr>\n      \n        <td class=\"name\"><code>percentage</code></td>\n      \n\n      <td class=\"type\">\n        \n          \n    <span class=\"param-type\">\n      Number\n    </span>\n\n    \n\n\n        \n      </td>\n\n        \n\n        \n\n        <td class=\"description last\">\n          <p>the percentage of emoji in the given subdomain that the user is responsible for</p>\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n          </td>\n    </tr>\n\n  \n  </tbody>\n</table>\n\n\n\n\n<dl class=\"details\">\n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n    <dt class=\"tag-source\">Source:</dt>\n    <dd class=\"tag-source\">\n      <ul class=\"dummy\">\n        <li>\n          <a href=\"emojme-user-stats.js.html\">emojme-user-stats.js</a>, <a href=\"emojme-user-stats.js.html#line11\">line 11</a>\n        </li>\n      </ul>\n    </dd>\n  \n\n  \n\n  \n\n  \n</dl>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n        \n\n    \n  </article>\n</section>\n\n\n\n\n  </div>\n\n  <br class=\"clear\">\n\n  <footer>\n    Documentation generated by <a href=\"https://github.com/jsdoc3/jsdoc\">JSDoc 3.5.5</a>\n  </footer>\n\n  <script src=\"scripts/linenumber.js\"></script>\n  <script src=\"scripts/pagelocation.js\"></script>\n\n  \n  \n</body>\n</html>"
  },
  {
    "path": "docs/scripts/linenumber.js",
    "content": "'use strict';\n\n/* global document */\n(function () {\n  var lineId, lines, totalLines, anchorHash;\n  var source = document.getElementsByClassName('prettyprint source linenums');\n  var i = 0;\n  var lineNumber = 0;\n\n  if (source && source[0]) {\n    anchorHash = document.location.hash.substring(1);\n    lines = source[0].getElementsByTagName('li');\n    totalLines = lines.length;\n\n    for (; i < totalLines; i++) {\n      lineNumber++;\n      lineId = 'line' + lineNumber;\n      lines[i].id = lineId;\n      if (lineId === anchorHash) {\n        lines[i].className += ' selected';\n      }\n    }\n  }\n})();\n"
  },
  {
    "path": "docs/scripts/pagelocation.js",
    "content": "'use strict';\n\n$(document).ready(function () {\n  var currentSectionNav, target;\n\n  // If an anchor hash is in the URL highlight the menu item\n  highlightActiveHash();\n  // If a specific page section is in the URL highlight the menu item\n  highlightActiveSection();\n\n  // If a specific page section is in the URL scroll that section up to the top\n  currentSectionNav = $('#' + getCurrentSectionName() + '-nav');\n\n  if (currentSectionNav.position()) {\n    $('nav').scrollTop(currentSectionNav.position().top);\n  }\n\n  // function to scroll to anchor when clicking an anchor linl\n  $('a[href*=\"#\"]:not([href=\"#\"])').click(function () {\n    /* eslint-disable no-invalid-this */\n    if (location.pathname.replace(/^\\//, '') === this.pathname.replace(/^\\//, '') && location.hostname === this.hostname) {\n      target = $(this.hash);\n      target = target.length ? target : $('[name=' + this.hash.slice(1) + ']');\n      if (target.length) {\n        $('html, body').animate({\n          scrollTop: target.offset().top\n        }, 1000);\n      }\n    }\n    /* eslint-enable no-invalid-this */\n  });\n});\n\n// If a new anchor section is selected, change the hightlighted menu item\n$(window).bind('hashchange', function (event) {\n  highlightActiveHash(event);\n});\n\nfunction highlightActiveHash(event) {\n  var oldUrl, oldSubSectionElement;\n\n  // check for and remove old hash active state\n  if (event && event.originalEvent.oldURL) {\n    oldUrl = event.originalEvent.oldURL;\n\n    if (oldUrl.indexOf('#') > -1) {\n      oldSubSectionElement = $('#' + getCurrentSectionName() + '-' + oldUrl.substring(oldUrl.indexOf('#') + 1) + '-nav');\n\n      if (oldSubSectionElement) {\n        oldSubSectionElement.removeClass('active');\n      }\n    }\n  }\n\n  if (getCurrentHashName()) {\n    $('#' + getCurrentSectionName() + '-' + getCurrentHashName() + '-nav').addClass('active');\n  }\n}\n\nfunction highlightActiveSection() {\n  var pageId = getCurrentSectionName();\n\n  $('#' + pageId + '-nav').addClass('active');\n}\n\nfunction getCurrentSectionName() {\n  var path = window.location.pathname;\n  var pageUrl = path.split('/').pop();\n\n  var sectionName = pageUrl.substring(0, pageUrl.indexOf('.'));\n\n  // remove the wodr module- if its in the url\n  sectionName = sectionName.replace('module-', '');\n\n  return sectionName;\n}\n\nfunction getCurrentHashName() {\n  var pageSubSectionId;\n  var pageSubSectionHash = window.location.hash;\n\n  if (pageSubSectionHash) {\n    pageSubSectionId = pageSubSectionHash.substring(1).replace('.', '');\n\n    return pageSubSectionId;\n  }\n\n  return false;\n}\n"
  },
  {
    "path": "docs/styles/jsdoc-default.css",
    "content": "@font-face {\n    font-family: \"Avenir Next W01\";\n    font-style: normal;\n    font-weight: 600;\n    src: url(\"https://fast.fonts.net/dv2/14/14c73713-e4df-4dba-933b-057feeac8dd1.woff2?d44f19a684109620e484167ba790e8180fd9e29df91d80ce3d096f014db863074e1ea706cf5ed4e1c042492e76df291ce1d24ec684d3d9da9684f55406b9f22bce02f0f30f556681593dafea074d7bd44e28a680d083ccfd44ed4f8a3087a20c56147c11f917ed1dbd85c4a18cf38da25e6ac78f008f472262304d50e7e0cb7541ef1642c676db6e4bde4924846f5daf486fbde9335e98f6a20f6664bc4525253d1d4fca42cf1c490483c8daf0237f6a0fd292563417ad80ca3e69321417747bdc6f0969f34b2a0401b5e2b9a4dfd5b06d9710850900c66b34870aef&projectId=f750d5c7-baa2-4767-afd7-45484f47fe17\") format('woff2');\n}\n\n@font-face {\n    font-family: \"Avenir Next W01\";\n    font-style: normal;\n    font-weight: 500;\n    src: url(\"https://fast.fonts.net/dv2/14/627fbb5a-3bae-4cd9-b617-2f923e29d55e.woff2?d44f19a684109620e484167ba790e8180fd9e29df91d80ce3d096f014db863074e1ea706cf5ed4e1c042492e76df291ce1d24ec684d3d9da9684f55406b9f22bce02f0f30f556681593dafea074d7bd44e28a680d083ccfd44ed4f8a3087a20c56147c11f917ed1dbd85c4a18cf38da25e6ac78f008f472262304d50e7e0cb7541ef1642c676db6e4bde4924846f5daf486fbde9335e98f6a20f6664bc4525253d1d4fca42cf1c490483c8daf0237f6a0fd292563417ad80ca3e69321417747bdc6f0969f34b2a0401b5e2b9a4dfd5b06d9710850900c66b34870aef&projectId=f750d5c7-baa2-4767-afd7-45484f47fe17\") format('woff2');\n}\n\n@font-face {\n    font-family: \"Avenir Next W01\";\n    font-style: normal;\n    font-weight: 400;\n    src: url(\"https://fast.fonts.net/dv2/14/2cd55546-ec00-4af9-aeca-4a3cd186da53.woff2?d44f19a684109620e484167ba790e8180fd9e29df91d80ce3d096f014db863074e1ea706cf5ed4e1c042492e76df291ce1d24ec684d3d9da9684f55406b9f22bce02f0f30f556681593dafea074d7bd44e28a680d083ccfd44ed4f8a3087a20c56147c11f917ed1dbd85c4a18cf38da25e6ac78f008f472262304d50e7e0cb7541ef1642c676db6e4bde4924846f5daf486fbde9335e98f6a20f6664bc4525253d1d4fca42cf1c490483c8daf0237f6a0fd292563417ad80ca3e69321417747bdc6f0969f34b2a0401b5e2b9a4dfd5b06d9710850900c66b34870aef&projectId=f750d5c7-baa2-4767-afd7-45484f47fe17\") format('woff2');\n}\n\n@font-face {\n    font-family: 'bt_mono';\n    font-style: normal;\n    font-weight: 400;\n    src: url('https://assets.braintreegateway.com/fonts/bt_mono_regular-webfont.eot');\n    src: url('https://assets.braintreegateway.com/fonts/bt_mono_regular-webfont.woff2') format('woff2'), url('https://assets.braintreegateway.com/fonts/bt_mono_regular-webfont.woff') format('woff'), url('https://assets.braintreegateway.com/fonts/bt_mono_regular-webfont.ttf') format('truetype'), url('https://assets.braintreegateway.com/fonts/bt_mono_regular-webfont.svg#bt_mono_reqular-webfont') format('svg');\n}\n\n@font-face {\n    font-family: 'bt_mono';\n    font-style: normal;\n    font-weight: 500;\n    src: url('https://assets.braintreegateway.com/fonts/bt_mono_medium-webfont.eot');\n    src: url('https://assets.braintreegateway.com/fonts/bt_mono_medium-webfont.woff2') format('woff2'), url('https://assets.braintreegateway.com/fonts/bt_mono_medium-webfont.woff') format('woff'), url('https://assets.braintreegateway.com/fonts/bt_mono_medium-webfont.ttf') format('truetype'), url('https://assets.braintreegateway.com/fonts/bt_mono_medium-webfont.svg#bt_mono_medium-webfont') format('svg');\n}\n\n@font-face {\n    font-family: 'bt_mono';\n    font-style: normal;\n    font-weight: 600;\n    src: url('https://assets.braintreegateway.com/fonts/bt_mono_bold-webfont.eot');\n    src: url('https://assets.braintreegateway.com/fonts/bt_mono_bold-webfont.woff2') format('woff2'), url('https://assets.braintreegateway.com/fonts/bt_mono_bold-webfont.woff') format('woff'), url('https://assets.braintreegateway.com/fonts/bt_mono_bold-webfont.ttf') format('truetype'), url('https://assets.braintreegateway.com/fonts/bt_mono_bold-webfont.svg#bt_mono_bold-webfont') format('svg');\n}\n\n@font-face {\n    font-family: 'bt_mono';\n    font-style: normal;\n    font-weight: 900;\n    src: url('https://assets.braintreegateway.com/fonts/bt_mono_heavy-webfont.eot');\n    src: url('https://assets.braintreegateway.com/fonts/bt_mono_heavy-webfont.woff2') format('woff2'), url('https://assets.braintreegateway.com/fonts/bt_mono_heavy-webfont.woff') format('woff'), url('https://assets.braintreegateway.com/fonts/bt_mono_heavy-webfont.ttf') format('truetype'), url('https://assets.braintreegateway.com/fonts/bt_mono_heavy-webfont.svg#bt_mono_heavy-webfont') format('svg');\n}\n\n* {\n    box-sizing: border-box\n}\n\nhtml, body {\n    height: 100%;\n    width: 100%;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n    color: #3e3c42;\n    text-rendering: optimizeLegibility;\n    margin: 0;\n}\n\nbody {\n    color: #3e3c42;\n    background-color: #f3f3f3;\n    width: 100%;\n    font: 16px/1.875 \"Avenir Next W01\", \"Avenir Next\", \"Helvetica Neue\", Helvetica, sans-serif;\n    font-size: 16px;\n    line-height: 160%;\n}\n\na, a:active {\n    color: #0095dd;\n    text-decoration: none;\n}\n\na:hover {\n    text-decoration: underline\n}\n\np, ul, ol, blockquote {\n    margin-bottom: 1em;\n}\n\np {\n    max-width: 800px;\n}\n\nh1, h2, h3, h4, h5, h6 {\n    color: #706d77;\n    font-weight: 500;\n    margin: 0;\n    line-height: 1;\n}\n\nh1 {\n    color: #4b484f;\n    font-weight: 500;\n    font-size: 40px;\n    display: block;\n}\n\nh1 span {\n    color: #999;\n    font-size: 32px;\n    display: block;\n    line-height: 1.5;\n}\n\nh1.page-title {\n    border-bottom: 1px dashed #ccc;\n    margin-bottom: 20px;\n    padding-bottom: 30px;\n}\n\nh2 {\n    font-size: 30px;\n    margin: 1.5em 0 0;\n}\n\nh3 {\n    font-size: 20px;\n    margin: 1.5em 0 0;\n    text-transform: uppercase;\n}\n\nh3.reference-title {\n    display: block;\n    font-weight: 400;\n    margin-top: 2em;\n    max-width: 200px;\n}\n\nh3.reference-title small {\n    display: inline-block;\n    color: #0095dd;\n    margin-left: 5px;\n    font-weight: 500;\n}\n\nh3.subsection-title {\n    border-bottom: 1px solid #ececec;\n    padding-bottom: 20px;\n    margin-top: 3em;\n    margin-bottom: 1em;\n}\n\nh4 {\n    font-size: 16px;\n    margin: 1em 0 0;\n    font-weight: bold;\n}\n\nh4.name {\n    font-size: 20px;\n    margin-top: 0;\n    font-weight: 500;\n}\n\nh5 {\n    margin: 2em 0 0.5em 0;\n    font-size: 14px;\n    font-weight: 500;\n    text-transform: uppercase;\n}\n\n.container-overview .subsection-title {\n    font-size: 14px;\n    text-transform: uppercase;\n    margin: 8px 0 15px 0;\n    font-weight: bold;\n    color: #4D4E53;\n    padding-top: 10px;\n}\n\nh6 {\n    font-size: 100%;\n    letter-spacing: -0.01em;\n    margin: 6px 0 3px 0;\n    font-style: italic;\n    text-transform: uppercase;\n    font-weight: 500;\n}\n\ntt, code, kbd, samp {\n    font-family: \"Source Code Pro\", monospace;\n    background: #f4f4f4;\n    padding: 1px 5px;\n    border-radius: 5px;\n}\n\n.class-description {\n    margin-bottom: 1em;\n    margin-top: 1em;\n    padding: 10px 20px;\n    background-color: rgba(26, 159, 224, 0.1);\n}\n\n.class-description:empty {\n    margin: 0\n}\n\n#main {\n    background-color: white;\n    float: right;\n    min-width: 360px;\n    width: calc(100% - 300px);\n    padding: 30px;\n    z-index: 100;\n}\n\nheader {\n    display: block;\n    max-width: 1400px;\n}\n\nsection {\n    display: block;\n    max-width: 1400px;\n    background-color: #fff;\n}\n\n.variation {\n    display: none\n}\n\n.signature-attributes {\n    font-size: 60%;\n    color: #aaa;\n    font-style: italic;\n    font-weight: lighter;\n}\n\n.rule {\n    width: 100%;\n    margin-top: 20px;\n    display: block;\n    border-top: 1px solid #ccc;\n}\n\nul {\n    list-style-type: none;\n    padding-left: 0;\n}\n\nul li a {\n    font-weight: 500;\n}\n\nul ul {\n    padding-top: 5px;\n}\n\nul li ul {\n    padding-left: 20px;\n}\n\nul li ul li a {\n    font-weight: normal;\n}\n\nnav {\n    float: left;\n    display: block;\n    width: 300px;\n    background: #f7f7f7;\n    overflow-x: visible;\n    overflow-y: auto;\n    height: 100%;\n    padding: 0px 30px 100px 30px;\n    height: 100%;\n    position: fixed;\n    transition: left 0.2s;\n    z-index: 998;\n    margin-top: 0px;\n    top: 43px;\n}\n\n.navicon-button {\n    display: inline-block;\n    position: fixed;\n    bottom: 1.5em;\n    right: 1.5em;\n    z-index: 2;\n}\n\nnav h3 {\n    font-size: 13px;\n    text-transform: uppercase;\n    letter-spacing: 1px;\n    font-weight: bold;\n    line-height: 24px;\n    margin: 40px 0 10px 0;\n    padding: 0;\n}\n\nnav ul {\n    font-size: 100%;\n    line-height: 17px;\n    padding: 0;\n    margin: 0;\n    list-style-type: none;\n    border: none;\n    padding-left: 0;\n}\n\nnav ul a {\n    font-size: 16px;\n}\n\nnav ul a, nav ul a:active {\n    display: block;\n}\n\nnav ul a:hover, nav ul a:active {\n    color: hsl(200, 100%, 43%);\n    text-decoration: none;\n}\n\nnav>ul {\n    padding: 0 10px;\n}\n\nnav>ul li:first-child {\n    padding-top: 0;\n}\n\nnav ul li ul {\n    padding-left: 0;\n}\n\nnav>ul>li {\n    border-bottom: 1px solid #e2e2e2;\n    padding: 10px 0 20px 0;\n}\n\nnav>ul>li.active ul {\n    border-left: 3px solid #0095dd;\n    padding-left: 15px;\n}\n\nnav>ul>li.active ul li.active a {\n    font-weight: bold;\n}\n\nnav>ul>li.active a {\n    color: #0095dd;\n}\n\nnav>ul>li>a {\n    color: #706d77;\n    padding: 20px 0;\n    font-size: 18px;\n}\n\nnav ul ul {\n    margin-bottom: 10px padding-left: 0;\n}\n\nnav ul ul a {\n    color: #5f5c63;\n}\n\nnav ul ul a, nav ul ul a:active {\n    font-family: 'bt_mono', monospace;\n    font-size: 14px;\n    padding-left: 20px;\n    padding-top: 3px;\n    padding-bottom: 9px;\n}\n\nnav h2 {\n    font-size: 12px;\n    margin: 0;\n    padding: 0;\n}\n\nnav>h2>a {\n    color: hsl(202, 71%, 50%);\n    border-bottom: 1px solid hsl(202, 71%, 50%);\n    padding-bottom: 5px;\n}\n\nnav>h2>a:hover {\n    font-weight: 500;\n    text-decoration: none;\n}\n\nfooter {\n    background-color: #fff;\n    color: hsl(0, 0%, 28%);\n    margin-left: 300px;\n    display: block;\n    font-style: italic;\n    font-size: 12px;\n    padding: 30px;\n    text-align: center;\n}\n\n.ancestors {\n    color: #999;\n}\n\n.ancestors a {\n    color: #999 !important;\n    text-decoration: none;\n}\n\n.clear {\n    clear: both;\n}\n\n.important {\n    font-weight: bold;\n    color: #950B02;\n}\n\n.yes-def {\n    text-indent: -1000px;\n}\n\n.type-signature {\n    color: #aaa;\n}\n\n.name, .signature {\n    font-family: 'bt_mono', monospace;\n    word-wrap: break-word;\n}\n\n.details {\n    margin-top: 14px;\n    font-size: 13px;\n    text-align: right;\n    background: #ffffff;\n    /* Old browsers */\n    background: -moz-linear-gradient(left, #ffffff 0%, #fafafa 100%);\n    /* FF3.6-15 */\n    background: -webkit-linear-gradient(left, #ffffff 0%, #fafafa 100%);\n    /* Chrome10-25,Safari5.1-6 */\n    background: linear-gradient(to right, #ffffff 0%, #fafafa 100%);\n    /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */\n    filter: progid: DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#fafafa', GradientType=1);\n    padding-right: 5px;\n}\n\n.details dt {\n    display: inline-block;\n}\n\n.details dd {\n    display: inline-block;\n    margin: 0;\n}\n\n.details dd a {\n    font-style: italic;\n    font-weight: normal;\n    line-height: 1;\n}\n\n.details ul {\n    margin: 0\n}\n\n.details ul {\n    list-style-type: none\n}\n\n.details li {}\n\n.details pre.prettyprint {\n    margin: 0\n}\n\n.details .object-value {\n    padding-top: 0\n}\n\n.description {\n    margin-bottom: 1em;\n    margin-top: 1em;\n}\n\n.code-caption {\n    font-style: italic;\n    margin: 0;\n    font-size: 16px;\n    color: #545454;\n}\n\n.prettyprint {\n    font-size: 13px;\n    border: 1px solid #ddd;\n    border-radius: 3px;\n    overflow: auto;\n    background-color: #fbfbfb;\n}\n\n.prettyprint.source {\n    width: inherit;\n}\n\n.prettyprint code {\n    font-size: 100%;\n    line-height: 18px;\n    display: block;\n    margin: 0 30px;\n    background-color: #fbfbfb;\n    color: #4D4E53;\n}\n\n.prettyprint>code {\n    padding: 30px 15px;\n}\n\n.prettyprint .linenums code {\n    padding: 0 15px;\n}\n\n.prettyprint .linenums li:first-of-type code {\n    padding-top: 15px;\n}\n\n.prettyprint code span.line {\n    display: inline-block;\n}\n\n.prettyprint.linenums {\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n}\n\n.prettyprint.linenums ol {\n    padding-left: 0\n}\n\n.prettyprint.linenums li {\n    border-left: 3px #ddd solid\n}\n\n.prettyprint.linenums li.selected, .prettyprint.linenums li.selected * {\n    background-color: lightyellow\n}\n\n.prettyprint.linenums li * {\n    -webkit-user-select: text;\n    -moz-user-select: text;\n    -ms-user-select: text;\n    user-select: text;\n}\n\n.readme .prettyprint {\n   max-width: 800px;\n}\n\n.params, .props {\n    border-spacing: 0;\n    border: 1px solid #ddd;\n    border-radius: 3px;\n    width: 100%;\n    font-size: 14px;\n}\n\n.params .name, .props .name, .name code {\n    color: #4D4E53;\n    font-family: 'bt_mono', monospace;\n    font-size: 100%;\n}\n\n.params td, .params th, .props td, .props th {\n    margin: 0px;\n    text-align: left;\n    vertical-align: top;\n    padding: 10px;\n    display: table-cell;\n}\n\n.params td {\n    border-top: 1px solid #eee;\n}\n\n.params thead tr, .props thead tr {\n    background-color: #fff;\n    font-weight: bold;\n}\n\n.params .params thead tr, .props .props thead tr {\n    background-color: #fff;\n    font-weight: bold;\n}\n\n.params td.description>p:first-child, .props td.description>p:first-child {\n    margin-top: 0;\n    padding-top: 0;\n}\n\n.params td.description>p:last-child, .props td.description>p:last-child {\n    margin-bottom: 0;\n    padding-bottom: 0;\n}\n\ndl.param-type {\n    margin-top: 5px;\n}\n\n.param-type dt, .param-type dd {\n    display: inline-block\n}\n\n.param-type dd {\n    font-family: Consolas, Monaco, 'Andale Mono', monospace\n}\n\n.disabled {\n    color: #454545\n}\n\n\n/* tag source style */\n\n.tag-deprecated {\n  padding-right: 5px;\n}\n\n.tag-source {\n    border-bottom: 1px solid rgba(28, 160, 224, 0.35);\n}\n\n.tag-source:first-child {\n    border-bottom: 1px solid rgba(28, 160, 224, 1);\n}\n\n\n/* navicon button */\n\n.navicon-button {\n    position: relative;\n    transition: 0.25s;\n    cursor: pointer;\n    user-select: none;\n    opacity: .8;\n    background-color: white;\n    border-radius: 100%;\n    width: 50px;\n    height: 50px;\n    -webkit-box-shadow: 0px 2px 9px 0px rgba(0, 0, 0, 0.31);\n    -moz-box-shadow: 0px 2px 9px 0px rgba(0, 0, 0, 0.31);\n    box-shadow: 0px 2px 9px 0px rgba(0, 0, 0, 0.31);\n}\n\n.navicon-button .navicon:before, .navicon-button .navicon:after {\n    transition: 0.25s;\n}\n\n.navicon-button:hover {\n    transition: 0.5s;\n    opacity: 1;\n}\n\n.navicon-button:hover .navicon:before, .navicon-button:hover .navicon:after {\n    transition: 0.25s;\n}\n\n.navicon-button:hover .navicon:before {\n    top: .425rem;\n}\n\n.navicon-button:hover .navicon:after {\n    top: -.425rem;\n}\n\n\n/* navicon */\n\n.navicon {\n    position: relative;\n    width: 1.5em;\n    height: .195rem;\n    background: #000;\n    top: calc(50% - .09rem);\n    left: calc(50% - .75rem);\n    transition: 0.3s;\n    border-radius: 5px;\n}\n\n.navicon:before, .navicon:after {\n    display: block;\n    content: \"\";\n    height: .195rem;\n    width: 1.5rem;\n    background: #000;\n    position: absolute;\n    z-index: -1;\n    transition: 0.3s 0.25s;\n}\n\n.navicon:before {\n    top: 0.425rem;\n    height: .195rem;\n    border-radius: 5px;\n}\n\n.navicon:after {\n    top: -0.425rem;\n    border-radius: 5px;\n}\n\n\n/* open */\n\n.nav-trigger:checked+label:not(.steps) .navicon:before, .nav-trigger:checked+label:not(.steps) .navicon:after {\n    top: 0 !important;\n}\n\n.nav-trigger:checked+label .navicon:before, .nav-trigger:checked+label .navicon:after {\n    transition: 0.5s;\n}\n\n\n/* Minus */\n\n.nav-trigger:checked+label {\n    transform: scale(0.75);\n}\n\n\n/* × and + */\n\n.nav-trigger:checked+label.plus .navicon, .nav-trigger:checked+label.x .navicon {\n    background: transparent;\n}\n\n.nav-trigger:checked+label.plus .navicon:before, .nav-trigger:checked+label.x .navicon:before {\n    transform: rotate(-45deg);\n    background: #000;\n}\n\n.nav-trigger:checked+label.plus .navicon:after, .nav-trigger:checked+label.x .navicon:after {\n    transform: rotate(45deg);\n    background: #000;\n}\n\n.nav-trigger:checked+label.plus {\n    transform: scale(0.75) rotate(45deg);\n}\n\n.nav-trigger:checked~nav {\n    left: 0 !important;\n}\n\n.nav-trigger:checked~.overlay {\n    display: block;\n}\n\n.nav-trigger {\n    position: fixed;\n    top: 0;\n    clip: rect(0, 0, 0, 0);\n}\n\n.overlay {\n    display: none;\n    position: fixed;\n    top: 0;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    width: 100%;\n    height: 100%;\n    background: hsla(0, 0%, 0%, 0.5);\n    z-index: 1;\n}\n\ntable {\n    border-collapse: separate;\n    ;\n    display: block;\n    overflow-x: auto;\n    /*table-layout:fixed;*/\n}\n\ntable tbody td {\n    border-top: 1px solid hsl(207, 10%, 86%);\n    border-right: 1px solid #eee;\n    padding: 5px;\n    /*word-wrap: break-word;*/\n}\n\ntd table.params, td table.props {\n    border: 0;\n}\n\n@media only screen and (min-width: 320px) and (max-width: 680px) {\n    body {\n        overflow-x: hidden;\n    }\n    #main {\n        padding: 30px 30px;\n        width: 100%;\n        min-width: 360px;\n    }\n    nav {\n        background: #FFF;\n        width: 300px;\n        height: 100%;\n        position: fixed;\n        top: 0;\n        right: 0;\n        bottom: 0;\n        left: -300px;\n        z-index: 3;\n        padding: 0 10px;\n        transition: left 0.2s;\n        margin-top: 0;\n    }\n    .navicon-button {\n        display: inline-block;\n        position: fixed;\n        bottom: 1.5em;\n        right: 20px;\n        z-index: 1000;\n    }\n    .top-nav-wrapper {\n        display: none;\n    }\n    #main h1.page-title {\n        margin: 0.5em 0;\n    }\n    footer {\n        margin-left: 0;\n        margin-bottom: 30px;\n    }\n}\n\n.top-nav-wrapper {\n    background-color: #ececec;\n    position: fixed;\n    top: 0px;\n    left: 0px;\n    padding: 10px 10px 0 10px;\n    z-index: 999;\n    width: 300px;\n}\n\n.top-nav-wrapper ul {\n    margin: 0;\n}\n\n.top-nav-wrapper ul li {\n    display: inline-block;\n    padding: 0 10px;\n    vertical-align: top;\n}\n\n.top-nav-wrapper ul li.active {\n    border-bottom: 2px solid rgba(28, 160, 224, 1);\n}\n\n.search-wrapper {\n    display: inline-block;\n    position: relative;\n}\n\n.search-wrapper svg {\n    position: absolute;\n    left: 0px;\n}\n\ninput.search-input {\n    background: transparent;\n    box-shadow: 0;\n    border: 0;\n    border-bottom: 1px solid #c7c7c7;\n    padding: 7px 15px 12px 35px;\n    margin: 0 auto;\n}\n\n\n/* Smooth outline with box-shadow: */\n\ninput.search-input:focus {\n    border-bottom: 2px solid rgba(28, 160, 224, 1);\n    outline: none;\n}\n\n\n/* Hightlight JS Paradiso Light Theme */\n\n.hljs-comment, .hljs-quote {\n    color: #776e71\n}\n\n.hljs-variable, .hljs-template-variable, .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-regexp, .hljs-link, .hljs-meta {\n    color: #ef6155\n}\n\n.hljs-number, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params, .hljs-deletion {\n    color: #f99b15\n}\n\n.hljs-title, .hljs-section, .hljs-attribute {\n    color: #fec418\n}\n\n.hljs-string, .hljs-symbol, .hljs-bullet, .hljs-addition {\n    color: #48b685\n}\n\n.hljs-keyword, .hljs-selector-tag {\n    color: #815ba4\n}\n\n.hljs {\n    display: block;\n    overflow-x: auto;\n    background: #e7e9db;\n    color: #4f424c;\n    padding: 0.5em\n}\n\n.hljs-emphasis {\n    font-style: italic\n}\n\n.hljs-strong {\n    font-weight: bold\n}\n\n.link-icon {\n  opacity: 0;\n  position: absolute;\n  margin-left: -25px;\n  padding-right: 5px;\n  padding-top: 2px;\n}\n\n.example-container .link-icon {\n  margin-top: -6px;\n}\n\n.example-container:hover .link-icon,\n.name-container:hover .link-icon {\n  opacity: .5;\n}\n\n.name-container {\n  display: flex;\n  padding-top: 1em;\n}\n"
  },
  {
    "path": "docs/styles/prettify-jsdoc.css",
    "content": "/* JSDoc prettify.js theme */\n\n/* plain text */\n.pln {\n  color: #000000;\n  font-weight: normal;\n  font-style: normal;\n}\n\n/* string content */\n.str {\n  color: hsl(104, 100%, 24%);\n  font-weight: normal;\n  font-style: normal;\n}\n\n/* a keyword */\n.kwd {\n  color: #000000;\n  font-weight: bold;\n  font-style: normal;\n}\n\n/* a comment */\n.com {\n  font-weight: normal;\n  font-style: italic;\n}\n\n/* a type name */\n.typ {\n  color: #000000;\n  font-weight: normal;\n  font-style: normal;\n}\n\n/* a literal value */\n.lit {\n  color: #006400;\n  font-weight: normal;\n  font-style: normal;\n}\n\n/* punctuation */\n.pun {\n  color: #000000;\n  font-weight: bold;\n  font-style: normal;\n}\n\n/* lisp open bracket */\n.opn {\n  color: #000000;\n  font-weight: bold;\n  font-style: normal;\n}\n\n/* lisp close bracket */\n.clo {\n  color: #000000;\n  font-weight: bold;\n  font-style: normal;\n}\n\n/* a markup tag name */\n.tag {\n  color: #006400;\n  font-weight: normal;\n  font-style: normal;\n}\n\n/* a markup attribute name */\n.atn {\n  color: #006400;\n  font-weight: normal;\n  font-style: normal;\n}\n\n/* a markup attribute value */\n.atv {\n  color: #006400;\n  font-weight: normal;\n  font-style: normal;\n}\n\n/* a declaration */\n.dec {\n  color: #000000;\n  font-weight: bold;\n  font-style: normal;\n}\n\n/* a variable name */\n.var {\n  color: #000000;\n  font-weight: normal;\n  font-style: normal;\n}\n\n/* a function name */\n.fun {\n  color: #000000;\n  font-weight: bold;\n  font-style: normal;\n}\n\n/* Specify class=linenums on a pre to get line numbering */\nol.linenums {\n  margin-top: 0;\n  margin-bottom: 0;\n}\n"
  },
  {
    "path": "docs/styles/prettify-tomorrow.css",
    "content": "/* Tomorrow Theme */\n/* Original theme - https://github.com/chriskempson/tomorrow-theme */\n/* Pretty printing styles. Used with prettify.js. */\n/* SPAN elements with the classes below are added by prettyprint. */\n/* plain text */\n.pln {\n  color: #4d4d4c; }\n\n@media screen {\n  /* string content */\n  .str {\n    color: hsl(104, 100%, 24%); }\n\n  /* a keyword */\n  .kwd {\n    color: hsl(240, 100%, 50%); }\n\n  /* a comment */\n  .com {\n    color: hsl(0, 0%, 60%); }\n\n  /* a type name */\n  .typ {\n    color: hsl(240, 100%, 32%); }\n\n  /* a literal value */\n  .lit {\n    color: hsl(240, 100%, 40%); }\n\n  /* punctuation */\n  .pun {\n    color: #000000; }\n\n  /* lisp open bracket */\n  .opn {\n    color: #000000; }\n\n  /* lisp close bracket */\n  .clo {\n    color: #000000; }\n\n  /* a markup tag name */\n  .tag {\n    color: #c82829; }\n\n  /* a markup attribute name */\n  .atn {\n    color: #f5871f; }\n\n  /* a markup attribute value */\n  .atv {\n    color: #3e999f; }\n\n  /* a declaration */\n  .dec {\n    color: #f5871f; }\n\n  /* a variable name */\n  .var {\n    color: #c82829; }\n\n  /* a function name */\n  .fun {\n    color: #4271ae; } }\n/* Use higher contrast and text-weight for printable form. */\n@media print, projection {\n  .str {\n    color: #060; }\n\n  .kwd {\n    color: #006;\n    font-weight: bold; }\n\n  .com {\n    color: #600;\n    font-style: italic; }\n\n  .typ {\n    color: #404;\n    font-weight: bold; }\n\n  .lit {\n    color: #044; }\n\n  .pun, .opn, .clo {\n    color: #440; }\n\n  .tag {\n    color: #006;\n    font-weight: bold; }\n\n  .atn {\n    color: #404; }\n\n  .atv {\n    color: #060; } }\n/* Style */\n/*\npre.prettyprint {\n  background: white;\n  font-family: Consolas, Monaco, 'Andale Mono', monospace;\n  font-size: 12px;\n  line-height: 1.5;\n  border: 1px solid #ccc;\n  padding: 10px; }\n*/\n\n/* Get LI elements to show when they are in the main article */\narticle ul li {\n  list-style-type: circle;\n  margin-left: 25px;\n}\n\n/* Specify class=linenums on a pre to get line numbering */\nol.linenums {\n  margin-top: 0;\n  margin-bottom: 0; }\n\n/* IE indents via margin-left */\nli.L0,\nli.L1,\nli.L2,\nli.L3,\nli.L4,\nli.L5,\nli.L6,\nli.L7,\nli.L8,\nli.L9 {\n  /* */ }\n\n/* Alternate shading for lines */\nli.L1,\nli.L3,\nli.L5,\nli.L7,\nli.L9 {\n  /* */ }\n"
  },
  {
    "path": "emojme-add.js",
    "content": "const _ = require('lodash');\nconst commander = require('commander');\n\nconst EmojiAdminList = require('./lib/emoji-admin-list');\nconst EmojiAdd = require('./lib/emoji-add');\n\nconst FileUtils = require('./lib/util/file-utils');\nconst Helpers = require('./lib/util/helpers');\nconst Cli = require('./lib/util/cli');\n/** @module add */\n\n/**\n * The add response object, like other response objects, is organized by input subdomain.\n * @typedef {object} addResponseObject\n * @property {object} subdomain each subdomain passed in to add will appear as a key in the response\n * @property {emojiList[]} subdomain.emojiList the list of emoji added to `subdomain`, with each element reflecting the parameters passed in to `add`\n * @property {emojiList[]} subdomain.collisions if `options.avoidCollisions` is `false`, emoji that cannot be uploaded due to existing conflicting emoji names will exist here\n */\n\n/**\n * Add emoji described by parameters within options to the specified subdomain(s).\n *\n * Note that options can accept both aliases and original emoji at the same time, but ordering can get complicated and honestly I'd skip it if I were you. For each emoji, make sure that every descriptor (src, name, aliasFor) has a value, using `null`s for fields that are not relevant to the current emoji.\n *\n * @async\n * @param {string|string[]} subdomains a single or list of subdomains to add emoji to. Must match respectively to `token`s and `cookie`s.\n * @param {string|string[]} tokens a single or list of tokens to add emoji to. Must match respectively to `subdomain`s and `cookie`s.\n * @param {string|string[]} cookies a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to `subdomain`s and `token`s.\n * @param {object} options contains singleton or arrays of emoji descriptors.\n * @param {string|string[]} [options.src] source image files for the emoji to be added. If no corresponding `options.name` is given, the filename will be used\n * @param {string|string[]} [options.name] names of the emoji to be added, overriding filenames if given, and becoming the alias name if an `options.aliasFor` is given\n * @param {string|string[]} [options.aliasFor] names of emoji to be aliased to `options.name`\n * @param {boolean} [options.allowCollisions] if `true`, emoji being uploaded will not be checked against existing emoji. This will take less time up front but may cause more errors.\n * @param {boolean} [options.avoidCollisions] if `true`, emoji being added will be renamed to not collide with existing emoji. See {@link lib/util/helpers.avoidCollisions} for logic and details // TODO: fix this link, maybe link to tests which has better examples\n * @param {string} [options.prefix] string to prefix all emoji being uploaded\n * @param {boolean} [options.bustCache] if `true`, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making `options.avoidCollisions` more accurate\n * @param {boolean} [options.output] if `false`, no files will be written during execution. Prevents saving of adminList for future use\n *\n * @returns {Promise<addResponseObject>} addResponseObject result object\n *\n * @example\nvar addOptions = {\n  src: ['./emoji1.jpg', 'http://example.com/emoji2.png'], // upload these two images\n  name: ['myLocalEmoji', 'myOnlineEmoji'], // call them these two names\n  bustCache: false, // don't bother redownloading existing emoji\n  avoidCollisions: true, // if there are similarly named emoji, change my new emoji names\n  output: false // don't write any files\n};\nvar subdomains = ['mySubdomain1', 'mySubdomain2'] // can add one or multiple\nvar tokens = ['myToken1', 'myToken2'] // can add one or multiple\nvar cookies = ['myCookie1', 'myCookie2'] // can add one or multiple\nvar addResults = await emojme.add(subdomains, tokens, cookies, addOptions);\nconsole.log(userStatsResults);\n// {\n//   mySubomain1: {\n//     collisions: [], // only defined if avoidCollisons = false\n//     emojiList: [\n//       { name: 'myLocalEmoji', ... },\n//       { name: 'myOnlineEmoji', ... },\n//     ]\n//   },\n//   mySubomain2: {\n//     collisions: [], // only defined if avoidCollisons = false\n//     emojiList: [\n//       { name: 'myLocalEmoji', ... },\n//       { name: 'myOnlineEmoji', ... },\n//     ]\n//   }\n// }\n */\nasync function add(subdomains, tokens, cookies, options) {\n  subdomains = Helpers.arrayify(subdomains);\n  tokens = Helpers.arrayify(tokens);\n  cookies = Helpers.arrayify(cookies);\n  options = options || {};\n  const aliases = Helpers.arrayify(options.aliasFor);\n  const names = Helpers.arrayify(options.name);\n  const sources = Helpers.arrayify(options.src);\n  let inputEmoji = []; let name; let alias; let\n    source;\n\n  while (aliases.length || sources.length) {\n    name = names.shift();\n    if (source = sources.shift()) {\n      inputEmoji.push({\n        is_alias: 0,\n        url: source,\n        name: name || source.match(/(?:.*\\/)?(.*).(jpg|jpeg|png|gif)/)[1],\n      });\n    } else {\n      alias = aliases.shift();\n      inputEmoji.push({\n        is_alias: 1,\n        alias_for: alias,\n        name,\n      });\n    }\n  }\n\n  if (names.length || _.find(inputEmoji, ['name', undefined])) {\n    return Promise.reject(new Error('Invalid input. Either not all inputs have been consumed, or not all emoji are well formed. Consider simplifying input, or padding input with `null` values.'));\n  }\n\n  const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies);\n\n  const addPromises = authTuples.map(async (authTuple) => {\n    let emojiToUpload = []; let\n      collisions = [];\n\n    if (options.prefix) {\n      inputEmoji = Helpers.applyPrefix(inputEmoji, options.prefix);\n    }\n\n    if (options.allowCollisions) {\n      emojiToUpload = inputEmoji;\n    } else {\n      const existingEmojiList = await new EmojiAdminList(...authTuple, options.output)\n        .get(options.bustCache);\n      const existingNameList = existingEmojiList.map(e => e.name);\n\n      if (options.avoidCollisions) {\n        inputEmoji = Helpers.avoidCollisions(inputEmoji, existingEmojiList);\n      }\n\n      [collisions, emojiToUpload] = _.partition(inputEmoji,\n        emoji => existingNameList.includes(emoji.name));\n    }\n\n    const emojiAdd = new EmojiAdd(...authTuple);\n    return emojiAdd.upload(emojiToUpload).then((uploadResult) => {\n      if (uploadResult.errorList && uploadResult.errorList.length > 1 && options.output) {\n        FileUtils.writeJson(`./build/${this.subdomain}.emojiUploadErrors.json`, uploadResult.errorList);\n      }\n      return Object.assign({}, uploadResult, { collisions });\n    });\n  });\n\n  return Helpers.formatResultsHash(await Promise.all(addPromises));\n}\n\nfunction addCli() {\n  const program = new commander.Command();\n\n  Cli.requireAuth(program);\n  Cli.allowIoControl(program);\n  Cli.allowEmojiAlterations(program)\n    .option('--src <value>', 'source image/gif/#content for emoji you\\'d like to upload', Cli.list, null)\n    .option('--name <value>', 'name of the emoji from --src that you\\'d like to upload', Cli.list, null)\n    .option('--alias-for <value>', 'name of the emoji you\\'d like --name to be an alias of. Specifying this will negate --src', Cli.list, null)\n    .parse(process.argv);\n\n  Cli.unpackAuthJson(program);\n\n  return add(program.subdomain, program.token, program.cookie, {\n    src: program.src,\n    name: program.name,\n    aliasFor: program.aliasFor,\n    bustCache: program.bustCache,\n    allowCollisions: program.allowCollisions,\n    avoidCollisions: program.avoidCollisions,\n    prefix: program.prefix,\n    output: program.output,\n  });\n}\n\n\nif (require.main === module) {\n  addCli();\n}\n\nmodule.exports = {\n  add,\n  addCli,\n};\n"
  },
  {
    "path": "emojme-download.js",
    "content": "const commander = require('commander');\n\nconst EmojiAdminList = require('./lib/emoji-admin-list');\n\nconst Cli = require('./lib/util/cli');\nconst Helpers = require('./lib/util/helpers');\n/** @module download */\n\n/**\n * The download response object, like other response objects, is organized by input subdomain.\n * @typedef {object} downloadResponseObject\n * @property {object} subdomain each subdomain passed in to add will appear as a key in the response\n * @property {emojiList[]} subdomain.emojiList the list of emoji downloaded from `subdomain`\n * @property {string[]} subdomain.saveResults an array of paths for emoji that have been downloaded. note that all users that have been passed with `options.save` will be grouped together here.\n */\n\n/**\n * Download the list of custom emoji that have been added to the given slack instances, by default saving a json of all available relevant data. Optionally save the source images for a given user.\n *\n * @async\n * @param {string|string[]} subdomains a single or list of subdomains from which to download emoji. Must match respectively to `token`s and `cookie`s.\n * @param {string|string[]} tokens a single or list of tokens with which to authenticate. Must match respectively to `subdomain`s and `cookie`s.\n * @param {string|string[]} cookies a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to `subdomain`s and `token`s.\n * @param {object} options contains singleton or arrays of emoji descriptors.\n * @param {string|string[]} [options.save] A user name or array of user names whose emoji source images will be saved. All emoji source images are linked to in the default adminList, but passing a user name here will save that user's emoji to build/<subdomain>/<username>\n * @param {boolean} [options.saveAll] if `true`, download all emoji on slack instance from all users to disk in a single location.\n * @param {boolean} [options.saveAllByUser] if `true`, download all emoji on slack instance from all users to disk, organized into directories by user.\n * @param {boolean} [options.bustCache] if `true`, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making `options.avoidCollisions` more accurate\n * @param {boolean} [options.output] if `false`, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files\n * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file.\n *\n * @returns {Promise<downloadResponseObject>} downloadResponseObject result object\n *\n * @example\nvar downloadOptions = {\n  save: ['username_1', 'username_2'], // Download the emoji source files for these two users\n  bustCache: true, // make sure this data is fresh\n  output: true // download the adminList to ./build\n};\nvar downloadResults = await emojme.download('mySubdomain', 'myToken', 'myCookie', downloadOptions);\nconsole.log(downloadResults);\n// {\n//   mySubdomain: {\n//     emojiList: [\n//       { name: 'emoji-from-mySubdomain', ... },\n//       ...\n//     ],\n//     saveResults: [\n//       './build/mySubdomain/username_1/an_emoji.jpg',\n//       './build/mySubdomain/username_1/another_emoji.gif',\n//       ... all of username_1's emoji\n//       './build/mySubdomain/username_2/some_emoji.jpg',\n//       './build/mySubdomain/username_2/some_other_emoji.gif',\n//       ... all of username_2's emoji\n//     ]\n//   }\n// }\n */\nasync function download(subdomains, tokens, cookies, options) {\n  subdomains = Helpers.arrayify(subdomains);\n  tokens = Helpers.arrayify(tokens);\n  cookies = Helpers.arrayify(cookies);\n  options = options || {};\n\n  const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies);\n\n  const downloadPromises = authTuples.map(async (authTuple) => {\n    const subdomain = authTuple[0];\n    let saveResults = [];\n\n    const adminList = new EmojiAdminList(...authTuple, options.output);\n    const emojiList = await adminList.get(options.bustCache, options.since);\n    if ((options.save && options.save.length) || options.saveAll || options.saveAllByUser) {\n      saveResults = saveResults.concat(await EmojiAdminList.save(emojiList, subdomain, {\n        save: options.save, saveAll: options.saveAll, saveAllByUser: options.saveAllByUser,\n      }));\n    }\n\n    return { emojiList, subdomain, saveResults };\n  });\n\n  return Helpers.formatResultsHash(await Promise.all(downloadPromises));\n}\nfunction downloadCli() {\n  const program = new commander.Command();\n\n  Cli.requireAuth(program);\n  Cli.allowIoControl(program)\n    .option('--save <user>', 'save all of <user>\\'s emoji to disk at build/$subdomain/$user', Cli.list, [])\n    .option('--save-all', 'save all emoji from all users to disk at build/$subdomain')\n    .option('--save-all-by-user', 'save all emoji from all users to disk at build/$subdomain/$user')\n    .parse(process.argv);\n  Cli.unpackAuthJson(program);\n\n  return download(program.subdomain, program.token, program.cookie, {\n    save: program.save,\n    saveAll: program.saveAll,\n    saveAllByUser: program.saveAllByUser,\n    bustCache: program.bustCache,\n    output: program.output,\n    since: program.since,\n  });\n}\n\nif (require.main === module) {\n  downloadCli();\n}\n\nmodule.exports = {\n  download,\n  downloadCli,\n};\n"
  },
  {
    "path": "emojme-favorites.js",
    "content": "const _ = require('lodash');\nconst commander = require('commander');\nconst util = require('util');\n\nconst ClientBoot = require('./lib/client-boot');\nconst EmojiAdminList = require('./lib/emoji-admin-list');\n\nconst logger = require('./lib/logger');\nconst Cli = require('./lib/util/cli');\nconst FileUtils = require('./lib/util/file-utils');\nconst Helpers = require('./lib/util/helpers');\n/** @module favorites */\n\n/**\n * The user-specific favorites response object, like other response objects, is organized by input subdomain.\n * @typedef {object} favoritesResponseObject\n * @property {object} subdomain each subdomain passed in to add will appear as a key in the response\n * @property {string} subdomain.favoritesResult.user the username associated with the given cookie token\n * @property {string[]} subdomain.favoritesResult.favoriteEmoji the list of 'favorite' emoji as deemed by slack, in desc sorted order\n * @property {object[]} subdomain.favoritesResult.favoriteEmojiAdminList an array of emoji objects, as organized by emojiAdminList\n */\n\n/**\n * Get the contents of the \"Frequenly Used\" box for your specified user\n *\n * @async\n * @param {string|string[]} subdomains a single or list of subdomains from which to analyze emoji. Must match respectively to `token`s and `cookie`s.\n * @param {string|string[]} tokens a single or list of tokens to add emoji to. Must match respectively to `subdomain`s and `cookie`s.\n * @param {string|string[]} cookies a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to `subdomain`s and `token`s.\n * @param {object} options contains options on what to present\n * @param {Number} [options.lite] do not attempt to marry favorites with complete adminlist content. Results will contain only emoji name and usage count.\n * @param {Number} [options.top] (verbose cli only) count of top n emoji contriubtors you would like to retrieve user statistics on\n * @param {Number} [options.usage] (verbose cli only) print not just the list of favorite emoji, but their usage count\n * @param {boolean} [options.bustCache] if `true`, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making `options.avoidCollisions` more accurate\n * @param {boolean} [options.output] if `false`, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files\n * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file.\n *\n * @returns {Promise<favoritesResponseObject>} fovoritesResponseObject result object\n *\n * @example\nvar favoritesResult = await emojme.favorites('mySubdomain', 'myToken', 'myCookie', {});\nconsole.log(favoritesResult);\n// {\n//   mySubdomain: {\n//     favoritesResult: {\n//         user: '{myToken's user}',\n//         favoriteEmoji: [\n//            emojiName,\n//            ...\n//         ],\n//         favoriteEmojiAdminList: [\n//           {emojiName}: {adminList-style emoji object, with additional `usage` value}\n//           ...\n//         ],\n//       }\n//   }\n// }\n */\nasync function favorites(subdomains, tokens, cookies, options) {\n  subdomains = Helpers.arrayify(subdomains);\n  tokens = Helpers.arrayify(tokens);\n  cookies = Helpers.arrayify(cookies);\n  options = options || {};\n\n  const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies);\n\n  const favoritesPromises = authTuples.map(async (authTuple) => {\n    let emojiList = [];\n    if (!options.lite) {\n      const emojiAdminList = new EmojiAdminList(...authTuple, options.output);\n      emojiList = await emojiAdminList.get(options.bustCache);\n    }\n\n    const bootClient = new ClientBoot(...authTuple, options.output);\n    const bootData = await bootClient.get(options.bustCache);\n    const user = ClientBoot.extractName(bootData);\n    const favoriteEmojiUsage = ClientBoot.extractEmojiUse(bootData);\n    const favoriteEmojiList = favoriteEmojiUsage.map(e => e.name);\n    const favoriteEmojiAdminList = _.reduce(favoriteEmojiUsage, (acc, usageObj) => {\n      acc.push({\n        [usageObj.name]: {\n          ...EmojiAdminList.find(emojiList, usageObj.name),\n          usage: usageObj.usage,\n        },\n      });\n      return acc;\n    }, []);\n\n    const result = {\n      user,\n      subdomain: bootClient.subdomain,\n      favoriteEmoji: favoriteEmojiList,\n      favoriteEmojiAdminList,\n    };\n\n    const safeUserName = FileUtils.sanitize(result.user);\n    if (options.output) FileUtils.writeJson(`./build/${safeUserName}.${bootClient.subdomain}.favorites.json`, result.favoriteEmojiAdminList, null, 3);\n\n    const topNFavorites = util.inspect(\n      (options.usage ? favoriteEmojiList : favoriteEmojiUsage)\n        .slice(0, options.top),\n    );\n    logger.info(`[${bootClient.subdomain}] Favorite emoji for ${result.user}: ${topNFavorites}`);\n\n    return { subdomain: bootClient.subdomain, favoritesResult: result };\n  });\n\n  return Helpers.formatResultsHash(_.flatten(await Promise.all(favoritesPromises)));\n}\n\nfunction favoritesCli() {\n  const program = new commander.Command();\n\n  Cli.requireAuth(program);\n  Cli.allowIoControl(program);\n  program\n    .option('--top <value>', '(verbose cli only) the top n favorites you\\'d like to see', 10)\n    .option('--usage', '(verbose cli only) print emoji usage of favorites in addition to their names', false)\n    .option('--lite', 'do not attempt to marry favorites with complete adminlist content. Results will contain only emoji name and usage count.', false)\n    .parse(process.argv);\n  Cli.unpackAuthJson(program);\n\n  return favorites(program.subdomain, program.token, program.cookie, {\n    top: program.top,\n    usage: program.usage,\n    lite: program.lite,\n    bustCache: program.bustCache,\n    output: program.output,\n  }).catch((err) => {\n    console.error('An error occurred: ', err);\n  });\n}\n\nif (require.main === module) {\n  favoritesCli();\n}\n\nmodule.exports = {\n  favorites,\n  favoritesCli,\n};\n"
  },
  {
    "path": "emojme-sync.js",
    "content": "const _ = require('lodash');\nconst commander = require('commander');\n\nconst EmojiAdminList = require('./lib/emoji-admin-list');\nconst EmojiAdd = require('./lib/emoji-add');\n\nconst Cli = require('./lib/util/cli');\nconst FileUtils = require('./lib/util/file-utils');\nconst Helpers = require('./lib/util/helpers');\n/** @module sync */\n\n/**\n * The sync response object, like other response objects, is organized by input subdomain.\n * @typedef {object} syncResponseObject\n * @property {object} subdomain each subdomain passed in to add will appear as a key in the response\n * @property {emojiList[]} subdomain.emojiList the list of emoji added to `subdomain`, with each element an emoji pulled from either `srcSubdomain` or `subdomains` less the subdomain in question.\n */\n\n/**\n * Sync emoji between slack subdomains\n *\n * Sync can be executed in either a \"one way\" or \"n way\" configuration, and both configurations can have a variable number of sources and destinations. In a \"one way\" configuration, all emoji from all source subdomains will be added to all destination subdomains\" and can be set by specifying `srcSubdomains` and `dstSubdomains`. In an \"n way\" configuration, every subdomain given is treated as the destination for every emoji in every other subdomain.\n *\n * @async\n * @param {string|string[]|null} subdomains Two ore more subdomains that you wish to have the same emoji pool\n * @param {string|string[]|null} tokens cookie tokens corresponding to the given subdomains\n * @param {string|string[]|null} cookies User cookies corresponding to the given subdomains\n * @param {object} options contains src* and dst* information for \"one way\" sync configuration. Either specify `subdomains` and `tokens`, or `srcSubdomains`, `srcTokens`, `dstSubdomains`, and `dstTokens`, not both.\n * @param {string|string[]} [options.srcSubdomains] slack instances from which to draw emoji. No additions will be made to these subdomains\n * @param {string|string[]} [options.srcTokens] tokens for the slack instances from which to draw emoji\n * @param {string|string[]} [options.srcCookies] cookies auth cookies for the slack instances from which to draw emoji\n * @param {string|string[]} [options.dstSubdomains] slack instances in which all source emoji will be deposited. None of `dstSubdomain`'s emoji will end up in `srcSubdomain`\n * @param {string|string[]} [options.dstTokens] tokens for the slack instances where emoji will be deposited\n * @param {string|string[]} [options.dstCookies] cookies auth cookies for the slack instances from which to draw emoji\n * @param {boolean} [options.bustCache] if `true`, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making `options.avoidCollisions` more accurate\n * @param {boolean} [options.output] if `false`, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files\n * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file.\n *\n * @returns {Promise<syncResponseObject>} syncResponseObject result object\n *\n * @example\nvar syncOptions = {\n  srcSubdomains: ['srcSubdomain'], // copy all emoji from srcSubdomain...\n  srcTokens: ['srcToken'],\n  srcCookies: ['srcCookie'],\n  dstSubdomains: ['dstSubdomain1', 'dstSubdomain2'], // ...to dstSubdomain1 and dstSubdomain2\n  dstTokens: ['dstToken1', 'dstToken2'],\n  dstCookies: ['dstCookie1', 'dstCookie2'],\n  bustCache: true // get fresh lists to make sure we're not doing more lifting than we have to\n};\nvar syncResults = await emojme.sync(null, null, syncOptions);\nconsole.log(syncResults);\n// {\n//   dstSubdomain1: {\n//     emojiList: [\n//       { name: emoji-1-from-srcSubdomain ... },\n//       { name: emoji-2-from-srcSubdomain ... }\n//     ]\n//   },\n//   dstSubdomain2: {\n//     emojiList: [\n//       { name: emoji-1-from-srcSubdomain ... },\n//       { name: emoji-2-from-srcSubdomain ... }\n//     ]\n//   }\n// }\n */\nasync function sync(subdomains, tokens, cookies, options) {\n  let diffs;\n  subdomains = Helpers.arrayify(subdomains);\n  tokens = Helpers.arrayify(tokens);\n  cookies = Helpers.arrayify(cookies);\n  options = options || {};\n\n  const [authTuples, srcPairs, dstPairs] = Helpers.zipAuthTuples(\n    subdomains,\n    tokens,\n    cookies,\n    options,\n  );\n\n  if (subdomains.length > 0) {\n    const emojiLists = await Promise.all(\n      authTuples.map(async authTuple => new EmojiAdminList(...authTuple, options.output)\n        .get(options.bustCache, options.since)),\n    );\n\n    diffs = EmojiAdminList.diff(emojiLists, subdomains);\n  } else if (srcPairs && dstPairs) {\n    const srcDstPromises = [srcPairs, dstPairs].map(pairs => Promise.all(\n      pairs.map(async pair => new EmojiAdminList(...pair, options.output)\n        .get(options.bustCache, options.since)),\n    ));\n\n    const [srcEmojiLists, dstEmojiLists] = await Promise.all(srcDstPromises);\n    diffs = EmojiAdminList.diff(\n      srcEmojiLists, options.srcSubdomains, dstEmojiLists, options.dstSubdomains,\n    );\n  } else {\n    throw new Error('Invalid Input');\n  }\n\n  const uploadedDiffPromises = diffs.map((diffObj) => {\n    const pathSlug = `to-${diffObj.dstSubdomain}.from-${diffObj.srcSubdomains.join('-')}`;\n    if (options.output) FileUtils.writeJson(`./build/${pathSlug}.emojiAdminList.json`, diffObj.emojiList);\n    if (options.dryRun) return { subdomain: diffObj.dstSubdomain, emojiList: diffObj.emojiList };\n\n    const emojiAdd = new EmojiAdd(diffObj.dstSubdomain, _.find(\n      authTuples,\n      [0, diffObj.dstSubdomain],\n    )[1], options.output);\n    return emojiAdd.upload(diffObj.emojiList).then((results) => {\n      if (results.errorList && results.errorList.length > 0 && options.output) FileUtils.writeJson(`./build/${pathSlug}.emojiUploadErrors.json`, results.errorList);\n      return results;\n    });\n  });\n\n  return Helpers.formatResultsHash(await Promise.all(uploadedDiffPromises));\n}\n\nfunction syncCli() {\n  const program = new commander.Command();\n\n  Cli.requireAuth(program);\n  Cli.allowIoControl(program)\n    .option('--src-subdomain [value]', 'subdomain from which to draw emoji for one way sync', Cli.list, null)\n    .option('--src-token [value]', 'token with which to draw emoji for one way sync', Cli.list, null)\n    .option('--src-cookie [value]', 'cookie with which to draw emoji for one way sync', Cli.list, null)\n    .option('--dst-subdomain [value]', 'subdomain to which to emoji will be added is one way sync', Cli.list, null)\n    .option('--dst-token [value]', 'token with which emoji will be added for one way sync', Cli.list, null)\n    .option('--dst-cookie [value]', 'cookie with which emoji will be added for one way sync', Cli.list, null)\n    // Notice that this is missing --force and --prefix. These have been\n    // deemed TOO POWERFUL for mortal usage. If you _really_ want that\n    // power, you can download then upload the adminlist you retrieve.\n    .option('--dry-run', 'if set to true, nothing will be uploaded or synced', false)\n    .parse(process.argv);\n\n  Cli.unpackAuthJson(program);\n\n  return sync(program.subdomain, program.token, program.cookie, {\n    srcSubdomains: program.srcSubdomain,\n    srcTokens: program.srcToken,\n    srcCookies: program.srcCookie,\n    dstSubdomains: program.dstSubdomain,\n    dstTokens: program.dstToken,\n    dstCookies: program.dstCookie,\n    bustCache: program.bustCache,\n    output: program.output,\n    since: program.since,\n    dryRun: program.dryRun,\n  });\n}\n\nif (require.main === module) {\n  syncCli();\n}\n\nmodule.exports = {\n  sync,\n  syncCli,\n};\n"
  },
  {
    "path": "emojme-upload.js",
    "content": "const _ = require('lodash');\nconst fs = require('graceful-fs');\nconst commander = require('commander');\n\nconst EmojiAdminList = require('./lib/emoji-admin-list');\nconst EmojiAdd = require('./lib/emoji-add');\n\nconst Cli = require('./lib/util/cli');\nconst FileUtils = require('./lib/util/file-utils');\nconst Helpers = require('./lib/util/helpers');\n/** @module upload */\n\n/**\n * The upload response object, like other response objects, is organized by input subdomain.\n * @typedef {object} syncResponseObject\n * @property {object} subdomain each subdomain passed in to add will appear as a key in the response\n * @property {emojiList[]} subdomain.emojiList the list of emoji added to `subdomain`, with each element an emoji pulled from either `srcSubdomain` or `subdomains` less the subdomain in question.\n * @property {emojiList[]} subdomain.collisions if `options.avoidCollisions` is `false`, emoji that cannot be uploaded due to existing conflicting emoji names will exist here\n */\n\n/**\n * The required format of a json file that can be used as the `options.src` for {@link upload}\n *\n * To see an example, use {@link download}, then look at `buidl/*.adminList.json`\n *\n * @typedef {Array} jsonEmojiListFormat\n * @property {Array} emojiList\n * @property {object} emojiList.emojiObject\n * @property {string} emojiList.emojiObject.name the name of the emoji\n * @property {1|0} emojiList.emojiObject.is_alias whether or not the emoji is an alias. If `1`, `alias_for` is require and `url` is ignored. If `0` vice versa\n * @property {string} emojiList.emojiObject.alias_for the name of the emoji this emoji is apeing\n * @property {string} emojiList.emojiObject.url the remote url or local path of the emoji\n * @property {string} emojiList.emojiObject.user_display_name the name of the emoji creator\n *\n * @example\n * [\n *   {\n *      \"name\": \"a_giving_lovely_generous_individual\",\n *      \"is_alias\": 1,\n *      \"alias_for\": \"caleb\"\n *   },\n *   {\n *     \"name\": \"gooddoggy\",\n *     \"is_alias\": 0,\n *     \"alias_for\": null,\n *     \"url\": \"https://emoji.slack-edge.com/T3T9KQULR/gooddoggy/849f53cf1de25f97.png\"\n *   }\n * ]\n */\n\n/**\n * The required format of a yaml file that can be used as the `options.src` for {@link upload}\n * @typedef {object} yamlEmojiListFormat\n * @property {object} topLevelYaml all keys execpt for `emojis` are ignored\n * @property {Array} emojis the array of emoji objects\n * @property {object} emojis.emojiObject\n * @property {string} emojis.emojiObject.name the name of the emoji\n * @property {string} emojis.emojiObject.src alias for `name`\n * @property {1|0} emojis.emojiObject.is_alias whether or not the emoji is an alias. If `1`, `alias_for` is require and `url` is ignored. If `0` vice versa\n * @property {string} emojis.emojiObject.alias_for the name of the emoji this emoji is apeing\n * @property {string} emojis.emojiObject.url the remote url or local path of the emoji\n * @property {string} emojis.emojiObject.user_display_name the name of the emoji creator\n *\n * @example\n *  title: animals\n *  emojis:\n *    - name: llama\n *      src: http://i.imgur.com/6bKXKUP.gif\n *    - name: alpaca\n *      src: http://i.imgur.com/c6QxTbM.gif\n */\n\n/**\n * Upload multiple emoji described by an existing list on disk, either as a json emoji admin list or emojipacks-like yaml.\n *\n * @async\n * @param {string|string[]} subdomains a single or list of subdomains from which to download emoji. Must match respectively to `token`s and `cookie`s.\n * @param {string|string[]} tokens a single or list of tokens with which to authenticate. Must match respectively to `subdomain`s and `cookie`s.\n * @param {string|string[]} cookies a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to `subdomain`s and `token`s.\n * @param {object} options contains singleton or arrays of emoji descriptors.\n * @param {string|string[]} options.src source emoji list files for the emoji to be added. Can either be in {@link jsonEmojiListFormat} or {@link yamlEmojiListFormat}\n * @param {boolean} [options.avoidCollisions] if `true`, emoji being added will be renamed to not collide with existing emoji. See {@link lib/util/helpers.avoidCollisions} for logic and details // TODO: fix this link, maybe link to tests which has better examples\n * @param {string} [options.prefix] string to prefix all emoji being uploaded\n * @param {boolean} [options.bustCache] if `true`, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making `options.avoidCollisions` more accurate\n * @param {boolean} [options.output] if `false`, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files\n * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file.\n *\n * @returns {Promise<uploadResponseObject>} uploadResponseObject result object\n *\n * @example\nvar uploadOptions = {\n  src: './emoji-list.json', // upload all the emoji in this json array of objects\n  avoidCollisions: true, // append '-1' or similar if we try to upload a dupe\n  prefix: 'new-' // prepend every emoji in src with \"new-\", e.g. \"emoji\" becomes \"new-emoji\"\n};\nvar uploadResults = await emojme.upload('mySubdomain', 'myToken', 'myCookie', uploadOptions);\nconsole.log(uploadResults);\n// {\n//   mySubdomain: {\n//     collisions: [\n//       { name: an-emoji-that-already-exists-in-mySubdomain ... }\n//     ],\n//     emojiList: [\n//       { name: emoji-from-emoji-list-json ... },\n//       { name: emoji-from-emoji-list-json ... },\n//       ...\n//     ]\n//   }\n// }\n */\nasync function upload(subdomains, tokens, cookies, options) {\n  subdomains = Helpers.arrayify(subdomains);\n  tokens = Helpers.arrayify(tokens);\n  cookies = Helpers.arrayify(cookies);\n  options = options || {};\n  let inputEmoji;\n\n  // TODO this isn't handling --src file --src file correctly\n  if (Array.isArray(options.src)) {\n    inputEmoji = options.src;\n  } else if (!fs.existsSync(options.src)) {\n    throw new Error(`Emoji source file ${options.src} does not exist`);\n  } else {\n    const fileType = options.src.split('.').slice(-1)[0];\n    if (fileType.match(/yaml|yml/)) {\n      inputEmoji = FileUtils.readYaml(options.src);\n    } else if (fileType.match(/json/)) {\n      inputEmoji = FileUtils.readJson(options.src);\n    } else {\n      throw new Error(`Filetype ${fileType} is not supported`);\n    }\n  }\n\n  const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies);\n\n  const uploadPromises = authTuples.map(async (authTuple) => {\n    let emojiToUpload = []; let\n      collisions = [];\n\n    if (options.prefix) {\n      inputEmoji = Helpers.applyPrefix(inputEmoji, options.prefix);\n    }\n\n    if (options.allowCollisions) {\n      emojiToUpload = inputEmoji;\n    } else {\n      const existingEmojiList = await new EmojiAdminList(...authTuple, options.output)\n        .get(options.bustCache);\n      const existingNameList = existingEmojiList.map(e => e.name);\n\n      if (options.avoidCollisions) {\n        inputEmoji = Helpers.avoidCollisions(inputEmoji, existingEmojiList);\n      }\n\n      [collisions, emojiToUpload] = _.partition(inputEmoji,\n        emoji => existingNameList.includes(emoji.name));\n    }\n\n    const emojiAdd = new EmojiAdd(...authTuple);\n    const uploadResult = await emojiAdd.upload(emojiToUpload);\n    return Object.assign({}, uploadResult, { collisions });\n  });\n\n  return Helpers.formatResultsHash(await Promise.all(uploadPromises));\n}\n\nfunction uploadCli() {\n  const program = new commander.Command();\n\n  Cli.requireAuth(program);\n  Cli.allowIoControl(program);\n  Cli.allowEmojiAlterations(program)\n    .option('--src <value>', 'source file(s) for emoji json or yaml you\\'d like to upload')\n    .parse(process.argv);\n  Cli.unpackAuthJson(program);\n\n  return upload(program.subdomain, program.token, program.cookie, {\n    src: program.src,\n    bustCache: program.bustCache,\n    allowCollisions: program.allowCollisions,\n    avoidCollisions: program.avoidCollisions,\n    prefix: program.prefix,\n    output: program.output,\n  });\n}\n\nif (require.main === module) {\n  uploadCli();\n}\n\nmodule.exports = {\n  upload,\n  uploadCli,\n};\n"
  },
  {
    "path": "emojme-user-stats.js",
    "content": "const _ = require('lodash');\nconst commander = require('commander');\n\nconst EmojiAdminList = require('./lib/emoji-admin-list');\n\nconst Cli = require('./lib/util/cli');\nconst FileUtils = require('./lib/util/file-utils');\nconst Helpers = require('./lib/util/helpers');\n/** @module userStats */\n\n/**\n * The user-specific userStats response object, like other response objects, is organized by input subdomain.\n * @typedef {object} userStatsResponseObject\n * @property {object} subdomain each subdomain passed in to add will appear as a key in the response\n * @property {emojiList[]} subdomain.emojiList the list of emoji downloaded from `subdomain`\n * @property {object[]} subdomain.userStatsResults an array of user stats objects\n * @property {object} subdomain.userStatsResults.userStatsObject An object containing several maybe-useful statistics, separated by user\n * @property {string} subdomain.userStatsResults.userStatsObject.user the name of the user in question\n * @property {emojiList[]} subdomain.userStatsResults.userStatsObject.userEmoji the emojiList the user authored\n * @property {string} subdomain.userStatsResults.userStatsObject.subdomain redundant :shrug:\n * @property {Number} subdomain.userStatsResults.userStatsObject.originalCount the number of original emoji the user has created\n * @property {Number} subdomain.userStatsResults.userStatsObject.aliasCount the number of emoji aliases the user has defined\n * @property {Number} subdomain.userStatsResults.userStatsObject.totalCount the number of original and aliases the user has created\n * @property {Number} subdomain.userStatsResults.userStatsObject.percentage the percentage of emoji in the given subdomain that the user is responsible for\n */\n\n/**\n * Get a few useful-ish statistics for either specific users, or the top-n emoji creators\n *\n * @async\n * @param {string|string[]} subdomains a single or list of subdomains to analyze. Must match respectively to `token`s and `cookie`s.\n * @param {string|string[]} tokens a single or list of tokens to add emoji to. Must match respectively to `subdomain`s and `cookie`s.\n * @param {string|string[]} cookies a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to `subdomain`s and `token`s.\n * @param {object} options contains options for what stats to present\n * @param {string|string[]} [options.user] user name or array of user names you would like to retrieve user statistics on. If specified, ignores `top`\n * @param {Number} [options.top] count of top n emoji contriubtors you would like to retrieve user statistics on\n * @param {boolean} [options.bustCache] if `true`, ignore any adminList younger than 24 hours and repull slack instance's existing emoji. Can be useful for making `options.avoidCollisions` more accurate\n * @param {boolean} [options.output] if `false`, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files\n * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file.\n *\n * @returns {Promise<userStatsResponseObject>} userStatsResponseObject result object\n *\n * @example\nvar userStatsOptions = {\n  user: ['username_1', 'username_2'] // get me some info on these two users\n};\nvar userStatsResults = await emojme.userStats('mySubdomain', 'myToken', 'myCookie', userStatsOptions);\nconsole.log(userStatsResults);\n// {\n//   mySubdomain: {\n//     userStatsResults: [\n//       {\n//         user: 'username_1',\n//         userEmoji: [{ all username_1's emoji }],\n//         subdomain: mySubdomain,\n//         originalCount: x,\n//         aliasCount: y,\n//         totalCount: x + y,\n//         percentage: (x + y) / mySubdomain's total emoji count\n//       },\n//       {\n//         user: 'username_2',\n//         userEmoji: [{ all username_2's emoji }],\n//         subdomain: mySubdomain,\n//         originalCount: x,\n//         aliasCount: y,\n//         totalCount: x + y,\n//         percentage: (x + y) / mySubdomain's total emoji count\n//       }\n//     ]\n//   }\n// }\n */\nasync function userStats(subdomains, tokens, cookies, options) {\n  subdomains = Helpers.arrayify(subdomains);\n  tokens = Helpers.arrayify(tokens);\n  cookies = Helpers.arrayify(cookies);\n  const users = Helpers.arrayify(options.user);\n  options = options || {};\n\n  const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies);\n\n  const userStatsPromises = authTuples.map(async (authTuple) => {\n    const emojiAdminList = new EmojiAdminList(...authTuple, options.output);\n    const emojiList = await emojiAdminList.get(options.bustCache, options.since);\n    if (users && users.length > 0) {\n      const results = EmojiAdminList.summarizeUser(emojiList, authTuple[0], users);\n      return results.map((result) => {\n        const safeUserName = FileUtils.sanitize(result.user);\n        FileUtils.writeJson(`./build/${safeUserName}.${result.subdomain}.adminList.json`, result.userEmoji, null, 3);\n        return { subdomain: authTuple[0], userStatsResults: results, emojiList };\n      });\n    }\n    const results = EmojiAdminList.summarizeSubdomain(emojiList, authTuple[0], options.top);\n    results.forEach((result) => {\n      const safeUserName = FileUtils.sanitize(result.user);\n      FileUtils.writeJson(`./build/${safeUserName}.${result.subdomain}.adminList.json`, result.userEmoji, null, 3);\n    });\n\n    return { subdomain: authTuple[0], userStatsResults: results, emojiList };\n  });\n\n  return Helpers.formatResultsHash(_.flatten(await Promise.all(userStatsPromises)));\n}\n\nfunction userStatsCli() {\n  const program = new commander.Command();\n\n  Cli.requireAuth(program);\n  Cli.allowIoControl(program)\n    .option('--user <value>', 'slack user you\\'d like to get stats on. Can be specified multiple times for multiple users.', Cli.list, null)\n    .option('--top <value>', 'the top n users you\\'d like user emoji statistics on', 10)\n    .parse(process.argv);\n  Cli.unpackAuthJson(program);\n\n  return userStats(program.subdomain, program.token, program.cookie, {\n    user: program.user,\n    top: program.top,\n    bustCache: program.bustCache,\n    output: program.output,\n    since: program.since,\n  });\n}\n\nif (require.main === module) {\n  userStatsCli();\n}\n\nmodule.exports = {\n  userStats,\n  userStatsCli,\n};\n"
  },
  {
    "path": "emojme.js",
    "content": "#!/usr/bin/env node\n/* eslint-disable global-require */\n\nconst program = require('commander');\n\nif (require.main === module) {\n  program\n    .version(require('./package').version)\n    .command('download', 'download all emoji from given subdomain')\n    .command('upload', 'upload source emoji to given subdomain')\n    .command('add', 'upload source emoji to given subdomain')\n    .command('user-stats', 'get emoji statistics for given user on given subdomain')\n    .command('sync', 'get emoji statistics for given user on given subdomain')\n    .command('favorites', 'get favorite emoji and personal emoji usage statistics')\n    .parse(process.argv);\n} else {\n  module.exports = {\n    add: require('./emojme-add').add,\n    download: require('./emojme-download').download,\n    upload: require('./emojme-upload').upload,\n    sync: require('./emojme-sync').sync,\n    userStats: require('./emojme-user-stats').userStats,\n    favorites: require('./emojme-favorites').favorites,\n  };\n}\n"
  },
  {
    "path": "lib/client-boot.js",
    "content": "const _ = require('lodash');\n\nconst SlackClient = require('./slack-client');\nconst FileUtils = require('./util/file-utils');\n\nconst ENDPOINT = '/client.boot';\nconst FLANNEL_API_VER = '4';\n\n// Methods to upload emoji to slack.\n// Instance methods require slack client.\n// Static methods do not.\nclass ClientBoot {\n  constructor(subdomain, token, cookie, output) {\n    this.subdomain = subdomain;\n    this.token = token;\n    this.cookie = cookie;\n    this.output = output || false;\n    this.slack = new SlackClient(this.subdomain, this.cookie, SlackClient.rateLimitTier(2));\n    this.endpoint = ENDPOINT;\n    this.flannel_api_ver = FLANNEL_API_VER;\n  }\n\n  async get(bustCache) {\n    const path = `./build/${this.subdomain}.clientBoot.json`;\n\n    if (bustCache || FileUtils.isExpired(path)) {\n      const bootData = await this.slack.request(this.endpoint, this.createMultipart());\n      if (this.output) FileUtils.writeJson(path, bootData);\n      return bootData;\n    }\n    return FileUtils.readJson(path);\n  }\n\n  createMultipart() {\n    return {\n      token: this.token,\n      flannel_api_ver: this.flannel_api_ver,\n    };\n  }\n\n  static extractEmojiUse(data) {\n    const emojiToUsageMap = JSON.parse(data.prefs.emoji_use);\n    const emojiToUsageArray = _.reduce(emojiToUsageMap, (acc, usage, name) => {\n      acc.push({ name, usage });\n      return acc;\n    }, []);\n\n    return _.orderBy(emojiToUsageArray, 'usage', 'desc');\n  }\n\n  static extractName(data) {\n    return data.self.name;\n  }\n}\n\nmodule.exports = ClientBoot;\n"
  },
  {
    "path": "lib/emoji-add.js",
    "content": "const fs = require('graceful-fs');\nconst _ = require('lodash');\n\nconst logger = require('./logger');\nconst SlackClient = require('./slack-client');\nconst FileUtils = require('./util/file-utils');\n\nconst ENDPOINT = '/emoji.add';\n\n// Methods to upload emoji to slack.\n// Instance methods require slack client.\n// Static methods do not.\nclass EmojiAdd {\n  constructor(subdomain, token, cookie, output) {\n    this.subdomain = subdomain;\n    this.token = token;\n    this.cookie = cookie;\n    this.output = output || false;\n    this.slack = new SlackClient(this.subdomain, this.cookie, SlackClient.rateLimitTier(2));\n    this.endpoint = ENDPOINT;\n  }\n\n  async uploadSingle(emoji) {\n    const parts = await this.constructor.createMultipart(emoji, this.token);\n    try {\n      const body = await this.slack.request(this.endpoint, parts);\n      if (!body.ok) {\n        throw new Error(body.error);\n      }\n    } catch (err) {\n      logger.warning(`[${this.subdomain}] error on ${emoji.name}: ${err.message || err}`);\n      return Object.assign({}, emoji, { error: err.message || err });\n    }\n    logger.debug(`[${this.subdomain}] ${emoji.name} uploaded successfully`);\n\n    return false; // no error\n  }\n\n  async upload(src) {\n    let masterList;\n\n    if (Array.isArray(src)) {\n      masterList = src;\n    } else if (!fs.existsSync(src)) {\n      throw new Error(`Emoji source file ${masterList} does not exist`);\n    } else {\n      masterList = FileUtils.readJson(src);\n    }\n\n    logger.info(`[${this.subdomain}] Attempting to upload ${masterList.length} emoji to ${this.subdomain}`);\n\n    const [aliasList, emojiList] = _.partition(masterList, 'is_alias');\n    const emojiResultArray = await Promise.all(\n      emojiList.map(emoji => this.uploadSingle(emoji)),\n    );\n    const aliasResultArray = await Promise.all(\n      aliasList.map(emoji => this.uploadSingle(emoji)),\n    );\n\n    const resultArray = emojiResultArray.concat(aliasResultArray);\n    const errors = _.filter(resultArray);\n    const totalCount = resultArray.length;\n    const errorsCount = errors.length;\n    const successCount = totalCount - errorsCount;\n    logger.info(`\\n[${this.subdomain}] Batch upload complete.\\n  total requests: ${totalCount} \\n  successes: ${successCount} \\n  errors: ${errorsCount}`);\n    return { subdomain: this.subdomain, emojiList: masterList, errorList: errors };\n  }\n\n  static async createMultipart(emoji, token) {\n    if (emoji.is_alias === 1) {\n      return {\n        token,\n        name: emoji.name,\n        mode: 'alias',\n        alias_for: emoji.alias_for,\n      };\n    }\n    const emojiData = await FileUtils.getData(emoji.url || emoji.src);\n    return {\n      token,\n      name: emoji.name,\n      mode: 'data',\n      image: emojiData,\n    };\n  }\n}\n\nmodule.exports = EmojiAdd;\n"
  },
  {
    "path": "lib/emoji-admin-list.js",
    "content": "const _ = require('lodash');\n\nconst Throttle = require('superagent-throttle');\nconst logger = require('./logger');\nconst SlackClient = require('./slack-client');\nconst FileUtils = require('./util/file-utils');\nconst Helpers = require('./util/helpers');\n\nconst ENDPOINT = '/emoji.adminList';\nconst PAGE_SIZE = 500;\n\n// Methods and datastructures to retrieve\n// parse, compare, and summarize results from\n// the \"emoji admin list\" endpoint.\n// Instace methods require slack client.\n// Static methods do not.\nclass EmojiAdminList {\n  constructor(subdomain, token, cookie, output) {\n    this.subdomain = subdomain;\n    this.token = token;\n    this.cookie = cookie;\n    this.output = output || false;\n    this.slack = new SlackClient(this.subdomain, this.cookie, SlackClient.rateLimitTier(3));\n    this.endpoint = ENDPOINT;\n    this.pageSize = PAGE_SIZE;\n  }\n\n  setPageSize(pageSize) {\n    this.pageSize = pageSize || PAGE_SIZE;\n  }\n\n  createMultipart(page) {\n    return {\n      query: '',\n      page,\n      count: this.pageSize,\n      token: this.token,\n    };\n  }\n\n  async get(bustCache, sinceTimestamp) {\n    let emojiList;\n    const sinceString = sinceTimestamp ? `${sinceTimestamp}.` : '';\n    const path = `./build/${sinceString}${this.subdomain}.adminList.json`;\n\n    if (bustCache || FileUtils.isExpired(path)) {\n      const emojiLists = await this.getAdminListPages();\n      emojiList = this.constructor.since([].concat(...emojiLists), sinceTimestamp);\n      if (this.output) FileUtils.writeJson(path, emojiList);\n    } else {\n      emojiList = FileUtils.readJson(path);\n    }\n    logger.info(`[${this.subdomain}] Found ${emojiList.length} emoji`);\n    return emojiList;\n  }\n\n  async getAdminListPages() {\n    const firstPageBody = await this.slack.request(this.endpoint, this.createMultipart(1));\n    if (!firstPageBody.ok) {\n      throw new Error(`Slack request failed with error ${firstPageBody.error}`);\n    }\n    if (!firstPageBody.emoji) {\n      throw new Error('Unable to retrieve first page of emoji');\n    }\n\n    const { pages } = firstPageBody.paging;\n    const promiseArray = [Promise.resolve(firstPageBody.emoji)];\n    logger.info(`[${this.subdomain}] Found ${firstPageBody.custom_emoji_total_count} total emoji on ${firstPageBody.paging.pages} pages`);\n\n    for (let i = 2; i <= pages; i++) {\n      promiseArray.push(\n        this.slack.request(\n          this.endpoint,\n          this.createMultipart(i),\n        ).then((body) => {\n          if (!body.ok || !body.emoji || body.emoji.length === 0) {\n            throw new Error(`Failed to fetch page ${i - 1}`);\n          }\n          logger.debug(`[${this.subdomain}] retrieved page ${i - 1}`);\n          return body.emoji;\n        }).catch((err) => { logger.error(`[${this.subdomain}] AdminList page request failed with ${err}`); }),\n      );\n    }\n\n    return Promise.all(promiseArray).then(emojiLists => emojiLists.filter(Boolean));\n  }\n\n  static find(emojiList, emojiName) {\n    return _.find(emojiList, { name: emojiName });\n  }\n\n  // give a count and percentage of all emoji created by [user]\n  static summarizeUser(emojiList, subdomain, users) {\n    users = Helpers.arrayify(users);\n    const groupedEmoji = _.groupBy(emojiList, 'user_display_name');\n\n    return users.map((user) => {\n      if (!(user in groupedEmoji)) {\n        logger.warning(`[${subdomain}] Could not find ${user} in emoji contributors`);\n        return false;\n      }\n\n      const userEmoji = groupedEmoji[user];\n      const userTotal = userEmoji.length;\n      const originals = _.filter(userEmoji, { is_alias: 0 }).length;\n      const aliases = userTotal - originals;\n      const percentage = (((userTotal * 1.0) / emojiList.length) * 100.0).toPrecision(4);\n\n      logger.info(`[${subdomain}] ${user}'s emoji: \\n\\ttotal: ${userTotal} \\n\\toriginals: ${originals} \\n\\taliases: ${aliases} \\n\\tpercentage:${percentage}%`);\n\n      return {\n        user,\n        userEmoji,\n        subdomain,\n        originalCount: originals,\n        aliasCount: aliases,\n        totalCount: userTotal,\n        percentage,\n      };\n    }).filter(Boolean);\n  }\n\n  static summarizeSubdomain(emojiList, subdomain, n) {\n    const groupedEmoji = _.countBy(emojiList, 'user_display_name');\n\n    const sortedUsers = _.chain(groupedEmoji)\n      .map((count, user) => ({ user, count }))\n      .orderBy('count', 'desc')\n      .value();\n    logger.info(`[${subdomain}] The top ${n} contributors for ${subdomain}'s ${emojiList.length} emoji are:`);\n    const topUsers = sortedUsers.slice(0, n);\n    return this.summarizeUser(emojiList, subdomain, topUsers.map(e => e.user));\n  }\n\n  static diff(srcLists, srcSubdomains, dstLists, dstSubdomains) {\n    dstLists = dstLists || srcLists;\n    dstSubdomains = dstSubdomains || srcSubdomains;\n\n    const diffs = [];\n    const uniqEmojiList = _.unionBy(...srcLists, 'name');\n\n    _.zipWith(dstSubdomains, dstLists, (dstSubdomain, dstEmojiList) => {\n      const missingEmojiList = _.differenceBy(uniqEmojiList, dstEmojiList, 'name');\n      diffs.push({\n        dstSubdomain,\n        srcSubdomains: _.without(srcSubdomains, dstSubdomains),\n        emojiList: missingEmojiList,\n      });\n    });\n\n    return diffs;\n  }\n\n  static since(emojiList, sinceTimestamp) {\n    if (!sinceTimestamp) {\n      return emojiList;\n    }\n\n    return _.filter(emojiList, ({ created }) => created > sinceTimestamp);\n  }\n\n  static async save(emojiList, subdomain, options) {\n    const groupedEmoji = _.groupBy(emojiList, 'user_display_name');\n    const promiseArray = [];\n    const specialTier = SlackClient.rateLimitTier('special');\n    const throttle = new Throttle({\n      concurrent: process.env.SLACK_REQUEST_CONCURRENCY\n        || specialTier.slackRequestConcurrency,\n      rate: process.env.SLACK_REQUEST_RATE\n        || specialTier.slackRequestRate,\n      ratePer: process.env.SLACK_REQUEST_WINDOW\n        || specialTier.slackRequestWindow,\n    });\n    let subdomainDirCreated = !options.saveAll;\n    const users = (options.saveAll || options.saveAllByUser)\n      ? Object.keys(groupedEmoji)\n      : Helpers.arrayify(options.save);\n\n    users.forEach((user) => {\n      let dirPath = `build/${subdomain}`;\n\n      if (!(user in groupedEmoji)) {\n        logger.warning(`[${subdomain}] Could not find ${user} in emoji contributors`);\n        return Promise.resolve();\n      }\n\n      logger.info(`[${subdomain}] Found ${groupedEmoji[user].length} emoji to save by ${user}.`);\n\n      if (!options.saveAll) {\n        dirPath = `${dirPath}/${user}`;\n        FileUtils.mkdirp(dirPath);\n      } else if (!subdomainDirCreated) {\n        FileUtils.mkdirp(dirPath);\n        subdomainDirCreated = true; // prevent wasting time trying to make and remake this\n      }\n\n      return groupedEmoji[user].forEach((emoji) => {\n        let path = `${dirPath}/${emoji.name}`;\n        try {\n          let fileType;\n          if (emoji.url.match(/^.*\\.(gif|png|jpg|jpg)$/)) {\n            fileType = emoji.url.split('.').slice(-1);\n          } else if (emoji.url.match(/^data:.*/)) {\n            fileType = emoji.url.match(/^data:image\\/(gif|png|jpg|jpeg).*/)[1];\n          } else {\n            throw new Error('unable to retrieve emoji source url');\n          }\n          path += `.${fileType}`;\n\n          promiseArray.push(FileUtils.getData(emoji.url, { throttle })\n            .then(emojiData => FileUtils.saveData(emojiData, path))\n            .then(() => { logger.debug(`[${subdomain}] saved ${emoji.name} to ${path}`); return path; }));\n        } catch (err) {\n          // There is a bizarre case where you can make an alias for a default emoji\n          // and all of a suddent it disappears? the url becomes `null` and `alias_for` = '1'???\n          logger.warning(`[${subdomain}] unable to save ${emoji.name} to ${path}`);\n        }\n      });\n    });\n\n    return Promise.all(promiseArray);\n  }\n}\n\nmodule.exports = EmojiAdminList;\n"
  },
  {
    "path": "lib/logger.js",
    "content": "const winston = require('winston');\n\nconst logger = winston.createLogger({\n  levels: winston.config.syslog.levels,\n  transports: [\n    // console log anything warning or worse\n    new winston.transports.Console({\n      format: winston.format.combine(\n        winston.format.colorize(),\n        winston.format.simple(),\n      ),\n      colorize: true,\n      level: 'warning',\n    }),\n    // log everything to a file\n    new winston.transports.File({\n      filename: 'log/combined.log',\n      format: winston.format.combine(\n        winston.format.timestamp({\n          format: 'YYY-MM-DD hh:mm:ss A ZZ',\n        }),\n        winston.format.json(),\n      ),\n      prettyPrint: true,\n      timestamp: true,\n      level: 'debug',\n    }),\n  ],\n});\n\nmodule.exports = logger;\n"
  },
  {
    "path": "lib/slack-client.js",
    "content": "const _ = require('lodash');\nconst superagent = require('superagent');\nconst Throttle = require('superagent-throttle');\nconst sleep = require('util').promisify(setTimeout);\nconst logger = require('./logger');\n\nconst RATE_LIMIT_TIER = {\n  1: {\n    slackRequestConcurrency: 1,\n    slackRequestRate: 1,\n    slackRequestWindow: 60000,\n  },\n  2: {\n    slackRequestConcurrency: 5,\n    slackRequestRate: 20,\n    slackRequestWindow: 60000,\n  },\n  3: {\n    slackRequestConcurrency: 11,\n    slackRequestRate: 49,\n    slackRequestWindow: 60000,\n  },\n  4: {\n    slackRequestConcurrency: 25,\n    slackRequestRate: 100,\n    slackRequestWindow: 60000,\n  },\n  special: {\n    slackRequestConcurrency: 200,\n    slackRequestRate: 3000,\n    slackRequestWindow: 60000,\n  },\n};\n\n// slack why\nconst cookieName = 'd';\n\nclass SlackClient {\n  constructor(subdomain, cookie, options) {\n    this.subdomain = subdomain;\n    this.cookie = cookie;\n    this.backoffTime = 0;\n    this.backoffs = [];\n    // If not otherwise specified, use tier 2 rate limiting\n    this.options = Object.assign({}, this.constructor.rateLimitTier(2), options);\n    // Throttle requests to Slacks \"Tier 2\" limit,\n    // 20 requests per minute, or one request per 3000ms.\n    // Run a few requests simultaneously and hope Slack treats\n    // it as an acceptable \"burst\".\n    this.throttle = new Throttle({\n      concurrent: process.env.SLACK_REQUEST_CONCURRENCY\n        || this.options.slackRequestConcurrency,\n      rate: process.env.SLACK_REQUEST_RATE\n        || this.options.slackRequestRate,\n      ratePer: process.env.SLACK_REQUEST_WINDOW\n        || this.options.slackRequestWindow,\n    });\n  }\n\n  url(endpoint) {\n    return `https://${this.subdomain}.slack.com/api${endpoint}`;\n  }\n\n  attachParts(req, hash, action) {\n    _.each(hash, (val, key) => {\n      if (key === '_attach') {\n        this.attachParts(req, val, 'attach');\n        return;\n      }\n      if (typeof val.then === 'function') {\n        val.then(() => {\n          req.attach(key, './test.jpg');\n        });\n      } else {\n        req[action](key, val);\n      }\n    });\n  }\n\n  async request(endpoint, parts) {\n    if (this.backoffTime > 0) {\n      logger.debug(`[${this.subdomain}] Waiting for ${this.backoffTime} to begin next request to ${endpoint}.`);\n      await sleep(this.backoffTime);\n    }\n\n    const req = superagent.post(this.url(endpoint));\n    req.use(this.throttle.plugin());\n    req.set('Cookie', `${cookieName}=${this.cookie}`);\n    if (parts) {\n      _.each(parts, (val, key) => {\n        if (key === 'image') {\n          req.attach(key, val, { filename: parts.name });\n        } else {\n          req.field(key, val);\n        }\n      });\n    }\n\n    try {\n      const res = await req;\n\n      if (this.backoffs.length) {\n        this.backoffTime += (this.backoffs.pop() * this.options.slackRequestConcurrency);\n        logger.debug(`[${this.subdomain}] ${this.backoffs.length} backoffs in the queue.`);\n      } else {\n        if (this.throttle.concurrent !== this.options.slackRequestConcurrency) {\n          logger.debug(`[${this.subdomain}] Returning to default concurrency: ${this.options.slackRequestConcurrency} from previous ${this.throttle.concurrent}`);\n        }\n\n        // Once all backoffs have been satisfied, no longer apply any backoff or throttling.\n        this.throttle.options({ concurrent: this.options.slackRequestConcurrency });\n        this.backoffTime = 0;\n      }\n\n      if (!res.body) {\n        throw new Error('No body received from slack');\n      }\n      if (res.statusCode >= 400) {\n        throw new Error(`Error response: ${res.statusCode} for req ${req.body}`);\n      }\n\n      return res.body;\n    } catch (err) {\n      let retryAfter;\n\n      // Sometimes throttling our requests isn't enough and we still get rate limited.\n      // In those cases, retry the request after however long Slack asks us to wait.\n      if ((err.status === 429) && (retryAfter = err.response.headers['retry-after'] * 1000)) {\n        logger.info(`[${this.subdomain}] Rate limiting detected.`);\n        logger.debug(`[${this.subdomain}] Endpoint ${endpoint} rate limited. Will retry request after ${retryAfter} ms backoff`);\n\n        // Prevent sending multiple requests later and re-angering the rate limiter\n        this.throttle.options({ concurrent: 1 });\n        this.backoffTime += retryAfter * this.options.slackRequestConcurrency;\n        this.backoffs.push(retryAfter);\n\n        return this.request(endpoint, parts);\n      }\n      throw new Error(`Caught error from Superagent \"${err.message}\" for req to ${endpoint}, ${parts.name}`);\n    }\n  }\n\n  static rateLimitTier(tier) {\n    return RATE_LIMIT_TIER[tier];\n  }\n}\n\nmodule.exports = SlackClient;\n"
  },
  {
    "path": "lib/util/cli.js",
    "content": "const _ = require('lodash');\n\nconst logger = require('../logger');\n\nfunction list(val, memo) {\n  memo = memo || [];\n  memo.push(val === '' ? null : val);\n  return memo;\n}\n\nfunction verbosity() {\n  logger.transports[0].level = 'debug';\n}\n\nfunction requireAuth(program) {\n  return program\n    .option('-s, --subdomain <value>', 'slack subdomain. Can be specified multiple times, paired with respective token.', list, [])\n    .option('-d, --domain <value>', 'alias for --subdomain', list, [])\n    .option('-t, --token <value>', 'slack cookie token. ususaly starts xoxc-... Can be specified multiple times, paired with respective subdomains. User tokens (xoxs-...) are no longer supported. :(', list, [])\n    .option('-c, --cookie <value>', 'slack cookie. paired with respective subdomains and tokens.', list, [])\n    .option('-a --auth-json <value>', 'A json-string containing keys \"domain\", \"token\", and \"cookie\", as generated by the Emojme: Emoji Anywhere Chrome Extension. Can be used as a replacement for or in leu of subdomain, token, and cookie options.', list, []);\n}\n\nfunction allowEmojiAlterations(program) {\n  return program\n    .option('--allow-collisions', 'do not cull collisions ever, upload everything just as it is and accept the collisions. This will be faster for known-good uploads, more rate-limiting prone for untrusted uploads.', false)\n    .option('--avoid-collisions', 'instead of culling collisions, rename the emoji to be uploaded \"intelligently\"', false)\n    .option('--prefix <value>', 'prefix all emoji to be uploaded with <value>');\n}\n\nfunction allowIoControl(program) {\n  return program\n    .option('--bust-cache', 'force a redownload of all cached info.', false)\n    .option('--no-output', 'prevent writing of files in build/ and log/')\n    .option('--since <value>', 'only consider emoji since the given epoch timestamp')\n    .option('--verbose', 'log debug messages to console', verbosity);\n}\n\nfunction unpackAuthJson(program) {\n  // Take a commander program, and if an authJson is included, split it into\n  // constituent subdomain, token, and cookies\n  if (!program.authJson || _.isEmpty(program.authJson)) {\n    return;\n  }\n\n  _.each(program.authJson, (authJson) => {\n    const auth = JSON.parse(authJson);\n\n    if ((auth.domain || auth.subdomain) && auth.token && auth.cookie) {\n      program.subdomain.push(auth.domain || auth.subdomain);\n      program.token.push(auth.token);\n      program.cookie.push(auth.cookie);\n    }\n  });\n}\n\nmodule.exports = {\n  list,\n  requireAuth,\n  allowIoControl,\n  allowEmojiAlterations,\n  unpackAuthJson,\n};\n"
  },
  {
    "path": "lib/util/file-utils.js",
    "content": "const yaml = require('js-yaml');\nconst superagent = require('superagent');\nconst fs = require('graceful-fs');\n\nconst logger = require('../logger');\n\nconst MAX_AGE = (1000 * 60 * 60 * 24); // one day\nconst VALID_FILENAME_CHARS = /[\\w\\-. ]+/;\n\nfunction mkdirp(path) {\n  const dirs = path.split('/');\n  for (let i = 1; i <= dirs.length; i++) {\n    const subPath = dirs.slice(0, i).join('/');\n    if (!fs.existsSync(subPath)) fs.mkdirSync(subPath);\n  }\n}\n\nfunction writeJson(path, data) {\n  this.mkdirp('build');\n  fs.writeFileSync(\n    path,\n    JSON.stringify(data, null, 2),\n  );\n}\n\nfunction readJson(path) {\n  if (!fs.existsSync(path)) {\n    return {};\n  }\n\n  return JSON.parse(fs.readFileSync(path, 'utf8'));\n}\n\nfunction readYaml(path) {\n  if (fs.existsSync(path)) {\n    const contents = yaml.safeLoad(fs.readFileSync(path, 'utf8'));\n    if (contents.emojis) {\n      return contents.emojis;\n    }\n  }\n  return {};\n}\n\nfunction isExpired(path, expirationTimestamp) {\n  const currentTime = Date.now();\n  let maxAge = currentTime - MAX_AGE;\n\n  if (expirationTimestamp) {\n    maxAge = Math.max(maxAge, expirationTimestamp);\n  }\n\n  if (!fs.existsSync(path) || fs.statSync(path).ctimeMs < maxAge) {\n    return true;\n  }\n  logger.debug(`Cached request is still fresh. To force a new request, delete or move ${path}`);\n  return false;\n}\n\nfunction saveData(data, path) {\n  this.mkdirp('build');\n  return new Promise((resolve) => {\n    resolve(fs.writeFileSync(path, data, { encoding: 'base64' }));\n  });\n}\n\nasync function getData(path, options) {\n  path = path || '';\n  options = options || {};\n\n  try {\n    if (path.match(/^http.*/)) {\n      const req = superagent.get(path);\n      req.retry(3);\n      req.buffer(true);\n      if (options.throttle) {\n        req.use(options.throttle.plugin());\n      }\n      const res = await req.parse(superagent.parse.image);\n\n      return res.body;\n    } if (path.match(/^data:/)) {\n      return path.replace(/^.*base64,/, '');\n    } if (path.match(/\\.(gif|jpg|jpeg|png)/) && fs.existsSync(path)) {\n      return fs.readFileSync(path);\n    }\n    throw new Error('Emoji Url does not contain acceptable data');\n  } catch (e) {\n    throw e;\n  }\n}\n\nfunction sanitize(str) {\n  return str\n    .split('')\n    .filter(s => VALID_FILENAME_CHARS.test(s)) // remove illegal characters\n    .join('')\n    .replace(/ +(?= )/g, '') // remove repeated spaces\n    .trim();\n}\n\nmodule.exports = {\n  mkdirp,\n  writeJson,\n  readJson,\n  readYaml,\n  isExpired,\n  saveData,\n  getData,\n  sanitize,\n};\n"
  },
  {
    "path": "lib/util/helpers.js",
    "content": "const _ = require('lodash');\n\nfunction validAuthTuples(subdomains, tokens, cookies) {\n  return subdomains.length === tokens.length\n    && tokens.length === cookies.length\n    && _.every(subdomains, _.isString)\n    && _.every(tokens, _.isString)\n    && _.every(cookies, _.isString);\n}\n\nfunction validSrcDstPairs(options) {\n  return options.srcSubdomains\n    && options.srcTokens\n    && options.srcCookies\n    && (options.srcSubdomains.length === options.srcTokens.length)\n    && (options.srcTokens.length === options.srcCookies.length)\n    && options.dstSubdomains\n    && options.dstTokens\n    && options.dstCookies\n    && (options.dstSubdomains.length === options.dstTokens.length)\n    && (options.dstTokens.length === options.dstCookies.length);\n}\n\nfunction atLeastOneValidInputType(subdomains, tokens, cookies, options) {\n  return (validAuthTuples(subdomains, tokens, cookies)\n      && subdomains.length > 0)\n    || (\n      validSrcDstPairs(options)\n      && options.srcSubdomains.length > 0\n      && options.dstSubdomains.length > 0\n    );\n}\n\nfunction zipAuthTuples(subdomains, tokens, cookies, options) {\n  let srcPairs = [];\n  let dstPairs = [];\n  options = options || {};\n\n  if (options && !_.isEmpty(options)\n    && options.srcSubdomains && options.srcTokens && options.srcCookies) {\n    srcPairs = _.zip(options.srcSubdomains, options.srcTokens, options.srcCookies);\n    dstPairs = _.zip(options.dstSubdomains, options.dstTokens, options.dstCookies);\n  }\n\n  const authTuples = _.uniq(_.zip(subdomains, tokens, cookies).concat(srcPairs, dstPairs));\n\n  if (!atLeastOneValidInputType(subdomains, tokens, cookies, options)) {\n    throw new Error('Invalid input. Ensure that every given \"subdomain\" has a matching \"token\" and \"cookie\"');\n  }\n\n  return [authTuples, srcPairs, dstPairs];\n}\n\nfunction applyPrefix(emojiList, prefix) {\n  return emojiList.map(e => ({ ...e, name: prefix + e.name }));\n}\n\n// Given an emoji, return its slug-name, i.e. the name without delimiter or id\nfunction slugName(emoji, returnIdDelim) {\n  let id; let idMatch; let delimiter; let\n    delimiterMatch;\n  let name = emoji.name;\n\n  if (idMatch = name.match(/^[A-z-_]*([0-9])$/)) {\n    id = parseInt(idMatch[1], 10);\n    name = name.slice(0, -1);\n  } else {\n    id = 0;\n  }\n\n  if (delimiterMatch = name.match(/[-_]/g)) {\n    delimiter = delimiterMatch.slice(-1);\n    if (name.lastIndexOf(delimiter) === name.length - 1) {\n      name = name.slice(0, -1);\n    }\n  } else {\n    // If emoji1 collides, the next emoji is emoji2\n    // If emoji collides, the next emoji is emoji-1\n    delimiter = idMatch ? '' : '-';\n  }\n\n  if (returnIdDelim) return [name, id, delimiter];\n  return name;\n}\n\n// Group array of emoji objects into hash of emojiSlug : emojiList\n// where emojiSlug is the emoji name without delimiter or id\n// e.g. emoji-1 and emoji_1 both have slug emoji\nfunction groupEmoji(emojiList) {\n  let maxId = -1;\n\n  return _(emojiList).sortBy('name').groupBy((e) => {\n    const [nameSlug, id] = slugName(e, true);\n    maxId = (id > maxId) ? id : maxId;\n    return nameSlug;\n  }).reduce((acc, val, key) => {\n    acc[key] = { list: val, maxId: 0 };\n    return acc;\n  }, {});\n}\n\n// Given array of existing emoji and an array of emoji to add\n// rename new emoji to avoid colliding with existing emoji and themsevles.\nfunction avoidCollisions(newEmoji, existingEmoji) {\n  const usedNameList = existingEmoji.map(e => e.name);\n  const completeNameList = usedNameList.concat(newEmoji.map(e => e.name));\n  const groupedEmoji = groupEmoji(existingEmoji.concat(newEmoji));\n\n  return newEmoji.map((emoji) => {\n    if (!usedNameList.includes(emoji.name)) {\n      usedNameList.push(emoji.name);\n      return emoji;\n    }\n\n    const [nameSlug, id, delimiter] = slugName(emoji, true); // eslint-disable-line no-unused-vars\n    let maxId = groupedEmoji[nameSlug].maxId += 1;\n    let newName = `${nameSlug}${delimiter}${maxId}`;\n\n    while (completeNameList.includes(newName)) {\n      maxId = groupedEmoji[nameSlug].maxId += 1;\n      newName = `${nameSlug}${delimiter}${maxId}`;\n      usedNameList.push(emoji.name);\n      completeNameList.push(emoji.name);\n    }\n\n    return { ...emoji, name: newName, collision: emoji.name };\n  });\n}\n\nfunction formatResultsHash(resultsArray) {\n  return _.reduce(resultsArray, (acc, elem) => {\n    if (!elem.subdomain) {\n      throw new Error('Found results unattached from subdomain');\n    }\n\n    acc[elem.subdomain] = _.omit(elem, 'subdomain');\n    return acc;\n  }, {});\n}\n\nfunction arrayify(elem) {\n  return elem ? _.castArray(elem) : [];\n}\n\nmodule.exports = {\n  arrayify,\n  applyPrefix,\n  avoidCollisions,\n  groupEmoji,\n  slugName,\n  zipAuthTuples,\n  formatResultsHash,\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"emojme\",\n  \"description\": \"The Emojartist's toolbox for spreading their work across the slackosphere\",\n  \"version\": \"2.0.1\",\n  \"keywords\": [\n    \"emoji\",\n    \"slack\",\n    \"sync\",\n    \"download\",\n    \"upload\"\n  ],\n  \"author\": \"Jack Ellenberger <jellenberger@uchicago.edu>\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/jackellenberger/emojme\"\n  },\n  \"main\": \"emojme.js\",\n  \"files\": [\n    \"emojme*\",\n    \"lib\",\n    \"README.md\"\n  ],\n  \"bin\": {\n    \"emojme\": \"./emojme.js\"\n  },\n  \"engines\": {\n    \"node\": \">=10.0.0\"\n  },\n  \"scripts\": {\n    \"lint\": \"eslint . || true\",\n    \"test\": \"mocha spec/unit/**/* && mocha spec/integration/**/*\",\n    \"test:unit\": \"mocha spec/unit/**/*\",\n    \"test:integration\": \"mocha spec/integration/**/*\",\n    \"test:debug\": \"node inspect node_modules/mocha/bin/_mocha\",\n    \"test:e2e\": \"mocha spec/e2e/**\",\n    \"generate-docs\": \"rm -rf docs/* && node_modules/.bin/jsdoc --configure .jsdoc.json --verbose && cp -r docs/emojme/$(./emojme.js --version)/* docs/ && rm -rf docs/emojme\",\n    \"generate-usage\": \"./scripts/usage.sh\"\n  },\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"commander\": \"^2.17.1\",\n    \"graceful-fs\": \"^4.1.11\",\n    \"js-yaml\": \"^3.13.1\",\n    \"lodash\": \"^4.17.14\",\n    \"superagent\": \"^3.8.3\",\n    \"superagent-throttle\": \"^1.0.0\",\n    \"winston\": \"^3.1.0\"\n  },\n  \"devDependencies\": {\n    \"chai\": \"^4.1.2\",\n    \"chai-shallow-deep-equal\": \"^1.4.6\",\n    \"eslint\": \"^5.13.0\",\n    \"eslint-config-airbnb-base\": \"^13.1.0\",\n    \"eslint-plugin-import\": \"^2.14.0\",\n    \"jsdoc\": \"^3.6.3\",\n    \"jsdoc-template\": \"braintree/jsdoc-template#3.2.0\",\n    \"mocha\": \"^5.2.0\",\n    \"sinon\": \"^7.2.3\"\n  }\n}\n"
  },
  {
    "path": "scripts/usage.sh",
    "content": "#!/bin/bash\n\n# Don't lose all our progress if we can avoid it\nmv USAGE.md USAGE.md.old\n\n# Get top level commands into USAGE.md\necho \"# Commands\" > USAGE.md\necho \"\\`\\`\\`\" >> USAGE.md\n./emojme.js >> USAGE.md\necho \"\\`\\`\\`\" >> USAGE.md\n\n# Parse back out the commands so we can run them\n# NOTE: thie head'ing depends on the above, be careful not to heck it up!\nfor command in $(cat USAGE.md | sed -n '/^Commands:$/,$p' | head -n-2 | tail -n+2 | awk '{print $1}'); do\n  echo >> USAGE.md\n  echo \"## emojme $command\" >> USAGE.md\n  echo \"\\`\\`\\`\" >> USAGE.md\n  node emojme-${command}.js --help >> USAGE.md\n  echo \"\\`\\`\\`\" >> USAGE.md\ndone\n\n"
  },
  {
    "path": "spec/e2e/emojme-download.js",
    "content": "const chai = require('chai');\nchai.use(require('chai-shallow-deep-equal'));\n\nconst assert = chai.assert;\n\nconst commander = require('commander');\n\nconst downloadCli = require('../../emojme-download').downloadCli;\n\nlet subdomain; let token; let\n  cookie;\n\nbefore(() => {\n  const program = new commander.Command();\n\n  console.log('THIS WILL MAKE REAL REQUESTS AGAINST REAL SLACK. CONSIDER YOURSELF CHASTISED.');\n\n  program\n    .option('-s --subdomain <value>', 'slack subdomain for testing.')\n    .option('-t --token <value>', 'slack token for testing.')\n    .option('-c --cookie <value>', 'slack cookie for testing.')\n    .parse(process.argv);\n\n  subdomain = program.subdomain;\n  token = program.token;\n  cookie = program.cookie;\n});\n\ndescribe('emojme download', () => {\n  it('fails when no authentication is specified', () => {\n    process.argv = [\n      'node',\n      'emojme',\n      'download',\n    ];\n\n    return downloadCli().then(() => {\n      throw Error('FAIL you should not be able to use emojme without a subdomain and token.');\n    }).catch((err) => {\n      assert.equal(err.message, 'Invalid input. Ensure that every given \"subdomain\" has a matching \"token\" and \"cookie\"');\n    });\n  });\n\n  it('downloads an emoji.adminList when authenticated', (done) => {\n    // Note that for this to work you may need to increase timeout\n    process.argv = [\n      'node',\n      'emojme',\n      'download',\n      '--subdomain', subdomain,\n      '--token', token,\n      '--cookie', cookie,\n      '--verbose',\n    ];\n\n    return downloadCli().then(() => {\n      // If we don't get an auth error, we're happy.\n      done();\n    });\n  });\n});\n"
  },
  {
    "path": "spec/fixtures/clientBoot.json",
    "content": "{\n  \"ok\": true,\n  \"self\": {\n    \"id\": \"UUSERID\",\n    \"name\": \"username\",\n    \"prefs\": {\n      \"user_colors\": \"\",\n      \"color_names_in_list\": true,\n      \"keyboard\": null,\n      \"email_alerts\": \"none\",\n      \"email_alerts_sleep_until\": 0,\n      \"email_tips\": true,\n      \"email_weekly\": true,\n      \"email_offers\": true,\n      \"email_research\": true,\n      \"email_developer\": true,\n      \"welcome_message_hidden\": false,\n      \"search_sort\": \"not_set\",\n      \"search_file_sort\": \"score\",\n      \"search_channel_sort\": \"relevant\",\n      \"search_people_sort\": \"relevant\",\n      \"expand_inline_imgs\": true,\n      \"expand_internal_inline_imgs\": true,\n      \"expand_snippets\": false,\n      \"posts_formatting_guide\": true,\n      \"seen_welcome_2\": true,\n      \"seen_ssb_prompt\": false,\n      \"spaces_new_xp_banner_dismissed\": false,\n      \"search_only_my_channels\": false,\n      \"search_only_current_team\": false,\n      \"search_hide_my_channels\": false,\n      \"search_only_show_online\": false,\n      \"search_hide_deactivated_users\": false,\n      \"emoji_mode\": \"default\",\n      \"emoji_use\": \"{\\\"emoji-0\\\":10,\\\"emoji-1\\\":9,\\\"emoji-2\\\":8,\\\"emoji-3\\\":7,\\\"emoji-4\\\":6,\\\"emoji-5\\\":5,\\\"emoji-6\\\":4,\\\"emoji-7\\\":3,\\\"emoji-8\\\":2,\\\"emoji-9\\\":1}\",\n      \"has_invited\": true,\n      \"has_uploaded\": true,\n      \"has_created_channel\": true,\n      \"has_searched\": true,\n      \"search_exclude_channels\": \"\",\n      \"messages_theme\": \"default\",\n      \"webapp_spellcheck\": true,\n      \"no_joined_overlays\": true,\n      \"no_created_overlays\": false,\n      \"dropbox_enabled\": false,\n      \"seen_domain_invite_reminder\": false,\n      \"seen_member_invite_reminder\": false,\n      \"mute_sounds\": false,\n      \"arrow_history\": false,\n      \"tab_ui_return_selects\": true,\n      \"obey_inline_img_limit\": true,\n      \"require_at\": false,\n      \"ssb_space_window\": \"\",\n      \"mac_ssb_bounce\": \"\",\n      \"mac_ssb_bullet\": true,\n      \"expand_non_media_attachments\": true,\n      \"show_typing\": true,\n      \"pagekeys_handled\": true,\n      \"last_snippet_type\": \"auto\",\n      \"display_real_names_override\": 0,\n      \"display_display_names\": true,\n      \"time24\": false,\n      \"enter_is_special_in_tbt\": false,\n      \"msg_input_send_btn\": false,\n      \"msg_input_send_btn_auto_set\": false,\n      \"graphic_emoticons\": false,\n      \"convert_emoticons\": true,\n      \"ss_emojis\": true,\n      \"seen_onboarding_start\": false,\n      \"onboarding_cancelled\": true,\n      \"seen_onboarding_slackbot_conversation\": false,\n      \"seen_onboarding_channels\": false,\n      \"seen_onboarding_direct_messages\": false,\n      \"seen_onboarding_invites\": false,\n      \"seen_onboarding_search\": false,\n      \"seen_onboarding_recent_mentions\": false,\n      \"seen_onboarding_starred_items\": false,\n      \"seen_onboarding_private_groups\": false,\n      \"seen_onboarding_banner\": false,\n      \"onboarding_slackbot_conversation_step\": 0,\n      \"set_tz_automatically\": false,\n      \"dnd_enabled\": true,\n      \"dnd_start_hour\": \"22:00\",\n      \"dnd_end_hour\": \"8:00\",\n      \"dnd_before_monday\": \"8:00\",\n      \"dnd_after_monday\": \"22:00\",\n      \"dnd_enabled_monday\": \"partial\",\n      \"dnd_before_tuesday\": \"8:00\",\n      \"dnd_after_tuesday\": \"22:00\",\n      \"dnd_enabled_tuesday\": \"partial\",\n      \"dnd_before_wednesday\": \"8:00\",\n      \"dnd_after_wednesday\": \"22:00\",\n      \"dnd_enabled_wednesday\": \"partial\",\n      \"dnd_before_thursday\": \"8:00\",\n      \"dnd_after_thursday\": \"22:00\",\n      \"dnd_enabled_thursday\": \"partial\",\n      \"dnd_before_friday\": \"8:00\",\n      \"dnd_after_friday\": \"22:00\",\n      \"dnd_enabled_friday\": \"partial\",\n      \"dnd_before_saturday\": \"8:00\",\n      \"dnd_after_saturday\": \"22:00\",\n      \"dnd_enabled_saturday\": \"partial\",\n      \"dnd_before_sunday\": \"8:00\",\n      \"dnd_after_sunday\": \"22:00\",\n      \"dnd_enabled_sunday\": \"partial\",\n      \"dnd_days\": \"every_day\",\n      \"dnd_custom_new_badge_seen\": false,\n      \"dnd_notification_schedule_new_badge_seen\": false,\n      \"unread_collapsed_channels\": null,\n      \"sidebar_behavior\": \"\",\n      \"channel_sort\": \"default\",\n      \"separate_private_channels\": false,\n      \"separate_shared_channels\": true,\n      \"sidebar_theme\": \"default\",\n      \"sidebar_theme_custom_values\": \"\",\n      \"no_invites_widget_in_sidebar\": false,\n      \"no_omnibox_in_channels\": false,\n      \"k_key_omnibox_auto_hide_count\": 5,\n      \"show_sidebar_quickswitcher_button\": false,\n      \"ent_org_wide_channels_sidebar\": true,\n      \"mark_msgs_read_immediately\": true,\n      \"start_scroll_at_oldest\": true,\n      \"snippet_editor_wrap_long_lines\": false,\n      \"ls_disabled\": false,\n      \"f_key_search\": false,\n      \"k_key_omnibox\": true,\n      \"prompted_for_email_disabling\": false,\n      \"no_macelectron_banner\": false,\n      \"no_macssb1_banner\": true,\n      \"no_macssb2_banner\": false,\n      \"no_winssb1_banner\": true,\n      \"hide_user_group_info_pane\": false,\n      \"mentions_exclude_at_user_groups\": false,\n      \"mentions_exclude_reactions\": false,\n      \"privacy_policy_seen\": true,\n      \"enterprise_migration_seen\": true,\n      \"last_tos_acknowledged\": \"tos_mar2018\",\n      \"search_exclude_bots\": false,\n      \"load_lato_2\": false,\n      \"fuller_timestamps\": false,\n      \"last_seen_at_channel_warning\": 1520548435691,\n      \"emoji_autocomplete_big\": false,\n      \"two_factor_auth_enabled\": false,\n      \"two_factor_type\": null,\n      \"two_factor_backup_type\": null,\n      \"hide_hex_swatch\": false,\n      \"show_jumper_scores\": false,\n      \"enterprise_mdm_custom_msg\": \"\",\n      \"enterprise_excluded_app_teams\": null,\n      \"client_logs_pri\": \"\",\n      \"flannel_server_pool\": \"random\",\n      \"mentions_exclude_at_channels\": true,\n      \"confirm_clear_all_unreads\": true,\n      \"confirm_user_marked_away\": true,\n      \"box_enabled\": false,\n      \"seen_single_emoji_msg\": true,\n      \"confirm_sh_call_start\": true,\n      \"preferred_skin_tone\": \"\",\n      \"show_all_skin_tones\": false,\n      \"whats_new_read\": 1560276001,\n      \"frecency_jumper\": \"{\\\"aww\\\":{\\\"count\\\":1,\\\"id\\\":\\\"Eaww\\\",\\\"visits\\\":[1563475762736]},\\\"dancing\\\":[{\\\"count\\\":1,\\\"id\\\":\\\"Edancing-rgb-letter-g\\\",\\\"visits\\\":[1564070534361]},{\\\"count\\\":1,\\\"id\\\":\\\"Edancing-rgb-letter-d\\\",\\\"visits\\\":[1564070538944]}],\\\"chibic\\\":{\\\"count\\\":1,\\\"id\\\":\\\"Echibi_cool_buster\\\",\\\"visits\\\":[1563905521230]},\\\"siren\\\":{\\\"count\\\":2,\\\"id\\\":\\\"Esiren-1448\\\",\\\"visits\\\":[1562852554115,1562852572414]},\\\"moonma\\\":{\\\"count\\\":1,\\\"id\\\":\\\"Emoonman\\\",\\\"visits\\\":[1561479804481]}}\",\n      \"frecency_ent_jumper\": \"\",\n      \"frecency_ent_jumper_backup\": \"\",\n      \"jumbomoji\": true,\n      \"newxp_seen_last_message\": 0,\n      \"show_memory_instrument\": false,\n      \"enable_unread_view\": false,\n      \"seen_unread_view_coachmark\": false,\n      \"enable_react_emoji_picker\": true,\n      \"seen_custom_status_badge\": false,\n      \"seen_custom_status_callout\": false,\n      \"seen_custom_status_expiration_badge\": false,\n      \"used_custom_status_kb_shortcut\": false,\n      \"seen_guest_admin_slackbot_announcement\": false,\n      \"seen_threads_notification_banner\": false,\n      \"seen_name_tagging_coachmark\": true,\n      \"all_unreads_sort_order\": \"\",\n      \"locale\": \"en-US\",\n      \"seen_intl_channel_names_coachmark\": false,\n      \"seen_p2_locale_change_message\": 1556807549196,\n      \"seen_locale_change_message\": 3,\n      \"seen_japanese_locale_change_message\": false,\n      \"seen_shared_channels_coachmark\": false,\n      \"seen_shared_channels_opt_in_change_message\": false,\n      \"has_recently_shared_a_channel\": false,\n      \"seen_channel_browser_admin_coachmark\": false,\n      \"seen_administration_menu\": false,\n      \"seen_drafts_section_coachmark\": true,\n      \"seen_emoji_update_overlay_coachmark\": false,\n      \"seen_sonic_deluxe_toast\": 2,\n      \"allow_calls_to_set_current_status\": true,\n      \"in_interactive_mas_migration_flow\": false,\n      \"sunset_interactive_message_views\": 0,\n      \"shdep_promo_code_submitted\": false,\n      \"seen_shdep_slackbot_message\": false,\n      \"seen_calls_interactive_coachmark\": false,\n      \"allow_cmd_tab_iss\": false,\n      \"workflow_builder_coachmarks\": \"{}\",\n      \"seen_gdrive_coachmark\": false,\n      \"overloaded_message_enabled\": true,\n      \"seen_highlights_coachmark\": false,\n      \"seen_highlights_arrows_coachmark\": false,\n      \"seen_highlights_warm_welcome\": false,\n      \"seen_new_search_ui\": true,\n      \"seen_channel_search\": false,\n      \"seen_people_search\": false,\n      \"a11y_animations\": true,\n      \"seen_keyboard_shortcuts_coachmark\": false,\n      \"needs_initial_password_set\": false,\n      \"lessons_enabled\": false,\n      \"tractor_enabled\": false,\n      \"tractor_experiment_group\": \"\",\n      \"opened_slackbot_dm\": false,\n      \"newxp_suggested_channels\": \"\",\n      \"onboarding_complete\": true,\n      \"welcome_place_state\": \"none\",\n      \"whocanseethis_dm_mpdm_badge\": false,\n      \"highlight_words\": \"\",\n      \"threads_everything\": false,\n      \"no_text_in_notifications\": false,\n      \"push_show_preview\": true,\n      \"growls_enabled\": true,\n      \"all_channels_loud\": false,\n      \"push_dm_alert\": true,\n      \"push_mention_alert\": true,\n      \"push_everything\": false,\n      \"push_idle_wait\": 2,\n      \"push_sound\": \"b2.mp3\",\n      \"new_msg_snd\": \"knock_brush.mp3\",\n      \"push_loud_channels\": \"\",\n      \"push_mention_channels\": \"CCHANNELID\",\n      \"push_loud_channels_set\": \"CCHANNELID\",\n      \"loud_channels\": \"\",\n      \"never_channels\": \"\",\n      \"loud_channels_set\": \"\",\n      \"at_channel_suppressed_channels\": \"\",\n      \"push_at_channel_suppressed_channels\": \"\",\n      \"muted_channels\": \"\",\n      \"all_notifications_prefs\": \"{\\\"global\\\":{\\\"global_desktop\\\":\\\"mentions_dms\\\",\\\"global_mpdm_desktop\\\":\\\"everything\\\",\\\"global_mobile\\\":\\\"mentions_dms\\\",\\\"global_mpdm_mobile\\\":\\\"everything\\\",\\\"mobile_sound\\\":\\\"b2.mp3\\\",\\\"desktop_sound\\\":\\\"knock_brush.mp3\\\",\\\"global_keywords\\\":\\\"\\\",\\\"push_idle_wait\\\":2,\\\"no_text_in_notifications\\\":false,\\\"push_show_preview\\\":true,\\\"threads_everything\\\":false},\\\"channels\\\":{\\\"CCHANNELID\\\":{\\\"desktop\\\":\\\"mentions_dms\\\",\\\"mobile\\\":\\\"mentions_dms\\\",\\\"muted\\\":false,\\\"suppress_at_channel\\\":false}}}\",\n      \"growth_msg_limit_approaching_cta_count\": 0,\n      \"growth_msg_limit_approaching_cta_ts\": 0,\n      \"growth_msg_limit_reached_cta_count\": 0,\n      \"growth_msg_limit_reached_cta_last_ts\": 0,\n      \"growth_msg_limit_long_reached_cta_count\": 0,\n      \"growth_msg_limit_long_reached_cta_last_ts\": 0,\n      \"growth_msg_limit_sixty_day_banner_cta_count\": 0,\n      \"growth_msg_limit_sixty_day_banner_cta_last_ts\": 0,\n      \"growth_all_banners_prefs\": \"{\\\"activation_msg_limit_banner_cta_count\\\":1,\\\"activation_msg_limit_banner_cta_last_ts\\\":1514906560,\\\"i18n_japan_beta_send_btn_banner_cta_count\\\":0,\\\"i18n_japan_beta_send_btn_banner_cta_last_ts\\\":0,\\\"approaching_msg_limit_banner_cta_count\\\":null,\\\"approaching_msg_limit_banner_cta_last_ts\\\":null,\\\"just_reached_msg_limit_banner_cta_count\\\":null,\\\"just_reached_msg_limit_banner_cta_last_ts\\\":null,\\\"thirty_days_after_msg_limit_banner_cta_count\\\":null,\\\"thirty_days_after_msg_limit_banner_cta_last_ts\\\":null,\\\"sixty_days_after_msg_limit_banner_cta_count\\\":null,\\\"sixty_days_after_msg_limit_banner_cta_last_ts\\\":null}\",\n      \"analytics_upsell_coachmark_seen\": false,\n      \"seen_app_space_coachmark\": false,\n      \"seen_app_space_tutorial\": false,\n      \"purchaser\": false,\n      \"app_action_picker\": \"{ \\\"enabled\\\": false }\",\n      \"show_ent_onboarding\": true,\n      \"folders_enabled\": false,\n      \"folder_data\": \"[]\",\n      \"seen_corporate_export_alert\": false,\n      \"show_autocomplete_help\": 1,\n      \"deprecation_toast_last_seen\": 0,\n      \"deprecation_modal_last_seen\": 0,\n      \"failover_proxy_check_completed\": 0,\n      \"edge_upload_proxy_check_completed\": 3,\n      \"app_subdomain_check_completed\": 8,\n      \"add_apps_prompt_dismissed\": false,\n      \"add_channel_prompt_dismissed\": false,\n      \"channel_sidebar_hide_invite\": false,\n      \"in_prod_surveys_enabled\": true,\n      \"dismissed_installed_app_dm_suggestions\": \"\",\n      \"tz\": \"America/Chicago\",\n      \"locales_enabled\": {\n        \"de-DE\": \"Deutsch (Deutschland)\",\n        \"en-GB\": \"English (UK)\",\n        \"en-US\": \"English (US)\",\n        \"es-ES\": \"Español (España)\",\n        \"es-LA\": \"Español (Latinoamérica)\",\n        \"fr-FR\": \"Français (France)\",\n        \"pt-BR\": \"Português (Brasil)\",\n        \"ja-JP\": \"日本語\"\n      }\n    },\n    \"created\": 1510585552,\n    \"manual_presence\": \"active\"\n  },\n  \"team\": {\n    \"id\": \"TTEAMID\",\n    \"name\": \"teamname\",\n    \"email_domain\": \"\",\n    \"domain\": \"domain\",\n    \"msg_edit_window_mins\": -1,\n    \"prefs\": {\n      \"locale\": \"en-US\",\n      \"invites_only_admins\": true,\n      \"show_join_leave\": true,\n      \"default_channels\": [\n        \"CCHANNEL\"\n      ],\n      \"display_email_addresses\": false,\n      \"who_can_create_channels\": \"regular\",\n      \"who_can_archive_channels\": \"regular\",\n      \"who_can_create_groups\": \"regular\",\n      \"who_can_kick_channels\": \"owner\",\n      \"who_can_kick_groups\": \"regular\",\n      \"gdrive_enabled_team\": true,\n      \"slackbot_responses_disabled\": false,\n      \"hide_referers\": true,\n      \"msg_edit_window_mins\": -1,\n      \"allow_message_deletion\": true,\n      \"calling_app_name\": \"Slack\",\n      \"allow_calls\": true,\n      \"allow_calls_interactive_screen_sharing\": true,\n      \"display_real_names\": false,\n      \"who_can_at_everyone\": \"regular\",\n      \"who_can_at_channel\": \"ra\",\n      \"who_can_manage_channel_posting_prefs\": \"ra\",\n      \"who_can_post_general\": \"ra\",\n      \"retention_type\": 0,\n      \"retention_duration\": 0,\n      \"group_retention_type\": 0,\n      \"group_retention_duration\": 0,\n      \"dm_retention_type\": 0,\n      \"dm_retention_duration\": 0,\n      \"file_retention_type\": 0,\n      \"file_retention_duration\": 0,\n      \"allow_retention_override\": false,\n      \"default_rxns\": [\n        \"simple_smile\",\n        \"thumbsup\",\n        \"white_check_mark\",\n        \"heart\",\n        \"eyes\"\n      ],\n      \"compliance_export_start\": 0,\n      \"warn_before_at_channel\": \"always\",\n      \"disallow_public_file_urls\": false,\n      \"who_can_create_delete_user_groups\": \"admin\",\n      \"who_can_edit_user_groups\": \"admin\",\n      \"who_can_change_team_profile\": \"admin\",\n      \"subteams_auto_create_owner\": false,\n      \"subteams_auto_create_admin\": false,\n      \"discoverable\": \"unlisted\",\n      \"dnd_days\": \"every_day\",\n      \"invite_requests_enabled\": true,\n      \"disable_file_uploads\": \"allow_all\",\n      \"disable_file_editing\": false,\n      \"disable_file_deleting\": false,\n      \"file_limit_whitelisted\": false,\n      \"uses_customized_custom_status_presets\": false,\n      \"disable_email_ingestion\": false,\n      \"who_can_manage_guests\": {\n        \"type\": [\n          \"admin\"\n        ]\n      },\n      \"who_can_create_shared_channels\": \"admin\",\n      \"who_can_manage_shared_channels\": {\n        \"type\": [\n          \"admin\"\n        ]\n      },\n      \"who_can_post_in_shared_channels\": {\n        \"type\": [\n          \"ra\"\n        ]\n      },\n      \"allow_shared_channel_perms_override\": false,\n      \"who_can_manage_ext_shared_channels\": {\n        \"type\": [\n          \"ORG_ADMINS_AND_OWNERS\"\n        ]\n      },\n      \"dropbox_legacy_picker\": false,\n      \"onedrive_enabled_team\": false,\n      \"can_receive_shared_channels_invites\": true,\n      \"enterprise_default_channels\": [],\n      \"enterprise_mandatory_channels\": [],\n      \"enterprise_mdm_disable_file_download\": false,\n      \"mobile_passcode_timeout_in_seconds\": -1,\n      \"has_hipaa_compliance\": false,\n      \"all_users_can_purchase\": true,\n      \"self_serve_select\": false,\n      \"loud_channel_mentions_limit\": 10000,\n      \"enable_shared_channels\": 2,\n      \"enterprise_mobile_device_check\": false,\n      \"disable_sidebar_connect_prompts\": [],\n      \"disable_sidebar_install_prompts\": [],\n      \"block_file_download\": false,\n      \"custom_contact_email\": null,\n      \"dnd_enabled\": true,\n      \"dnd_start_hour\": \"22:00\",\n      \"dnd_end_hour\": \"08:00\",\n      \"dnd_before_monday\": \"08:00\",\n      \"dnd_after_monday\": \"22:00\",\n      \"dnd_before_tuesday\": \"08:00\",\n      \"dnd_after_tuesday\": \"22:00\",\n      \"dnd_before_wednesday\": \"08:00\",\n      \"dnd_after_wednesday\": \"22:00\",\n      \"dnd_before_thursday\": \"08:00\",\n      \"dnd_after_thursday\": \"22:00\",\n      \"dnd_before_friday\": \"08:00\",\n      \"dnd_after_friday\": \"22:00\",\n      \"dnd_before_saturday\": \"08:00\",\n      \"dnd_after_saturday\": \"22:00\",\n      \"dnd_before_sunday\": \"08:00\",\n      \"dnd_after_sunday\": \"22:00\",\n      \"dnd_enabled_monday\": \"partial\",\n      \"dnd_enabled_tuesday\": \"partial\",\n      \"dnd_enabled_wednesday\": \"partial\",\n      \"dnd_enabled_thursday\": \"partial\",\n      \"dnd_enabled_friday\": \"partial\",\n      \"dnd_enabled_saturday\": \"partial\",\n      \"dnd_enabled_sunday\": \"partial\",\n      \"custom_status_presets\": [\n        [\n          \":spiral_calendar_pad:\",\n          \"In a meeting\",\n          \"In a meeting\",\n          \"1_hour\"\n        ],\n        [\n          \":bus:\",\n          \"Commuting\",\n          \"Commuting\",\n          \"30_minutes\"\n        ],\n        [\n          \":face_with_thermometer:\",\n          \"Out sick\",\n          \"Out sick\",\n          \"all_day\"\n        ],\n        [\n          \":palm_tree:\",\n          \"Vacationing\",\n          \"Vacationing\",\n          \"no_expiration\"\n        ],\n        [\n          \":house_with_garden:\",\n          \"Working remotely\",\n          \"Working remotely\",\n          \"all_day\"\n        ]\n      ],\n      \"custom_status_default_emoji\": \":speech_balloon:\",\n      \"auth_mode\": \"normal\",\n      \"who_can_manage_integrations\": {\n        \"type\": [\n          \"regular\"\n        ]\n      },\n      \"app_whitelist_enabled\": false,\n      \"invites_limit\": true\n    },\n    \"icon\": {\n      \"image_34\": \"https://avatars.slack-edge.com/2017-11-13/270775423888_61d633e513ed13fdbcd0_34.png\",\n      \"image_44\": \"https://avatars.slack-edge.com/2017-11-13/270775423888_61d633e513ed13fdbcd0_44.png\",\n      \"image_68\": \"https://avatars.slack-edge.com/2017-11-13/270775423888_61d633e513ed13fdbcd0_68.png\",\n      \"image_88\": \"https://avatars.slack-edge.com/2017-11-13/270775423888_61d633e513ed13fdbcd0_88.png\",\n      \"image_102\": \"https://avatars.slack-edge.com/2017-11-13/270775423888_61d633e513ed13fdbcd0_102.png\",\n      \"image_132\": \"https://avatars.slack-edge.com/2017-11-13/270775423888_61d633e513ed13fdbcd0_132.png\",\n      \"image_230\": \"https://avatars.slack-edge.com/2017-11-13/270775423888_61d633e513ed13fdbcd0_230.png\",\n      \"image_original\": \"https://avatars.slack-edge.com/2017-11-13/270775423888_61d633e513ed13fdbcd0_original.png\"\n    },\n    \"over_storage_limit\": false,\n    \"messages_count\": 51294,\n    \"plan\": \"\",\n    \"onboarding_channel_id\": \"\",\n    \"date_create\": 1510365798,\n    \"limit_ts\": 1553817600000000,\n    \"avatar_base_url\": \"https://ca.slack-edge.com/\"\n  },\n  \"accept_tos_url\": null,\n  \"latest_event_ts\": \"1565672021.000000\",\n  \"channels\": [\n    {\n      \"id\": \"CCHANELID\",\n      \"name\": \"channel-name\",\n      \"is_channel\": true,\n      \"is_group\": false,\n      \"is_im\": false,\n      \"created\": 1510539800,\n      \"is_archived\": false,\n      \"is_general\": false,\n      \"unlinked\": 0,\n      \"name_normalized\": \"channel-name\",\n      \"is_shared\": false,\n      \"is_frozen\": false,\n      \"parent_conversation\": null,\n      \"creator\": \"UUSERID\",\n      \"is_ext_shared\": false,\n      \"is_org_shared\": false,\n      \"shared_team_ids\": [\n        \"TTEAMID\"\n      ],\n      \"pending_shared\": [],\n      \"pending_connected_team_ids\": [],\n      \"is_pending_ext_shared\": false,\n      \"is_member\": false,\n      \"is_private\": false,\n      \"is_mpim\": false,\n      \"previous_names\": [],\n      \"priority\": 0\n    }\n  ],\n  \"groups\": [\n    {\n      \"id\": \"GGROUPID\",\n      \"name\": \"group-name\",\n      \"is_channel\": false,\n      \"is_group\": true,\n      \"is_im\": false,\n      \"created\": 1558839826,\n      \"is_archived\": false,\n      \"is_general\": false,\n      \"unlinked\": 0,\n      \"name_normalized\": \"group-name\",\n      \"is_shared\": false,\n      \"is_frozen\": false,\n      \"parent_conversation\": null,\n      \"creator\": \"UUSERID\",\n      \"is_ext_shared\": false,\n      \"is_org_shared\": false,\n      \"shared_team_ids\": [\n        \"TTEAMID\"\n      ],\n      \"pending_shared\": [],\n      \"pending_connected_team_ids\": [],\n      \"is_pending_ext_shared\": false,\n      \"is_member\": true,\n      \"is_private\": true,\n      \"is_mpim\": false,\n      \"last_read\": \"1565620503.000300\",\n      \"latest\": \"1565620503.000300\",\n      \"is_open\": true,\n      \"members\": [\n        \"UUSERID1\",\n        \"UUSERID2\"\n      ],\n      \"topic\": {\n        \"value\": \"\",\n        \"creator\": \"\",\n        \"last_set\": 0\n      },\n      \"purpose\": {\n        \"value\": \"\",\n        \"creator\": \"\",\n        \"last_set\": 0\n      },\n      \"priority\": 0\n    }\n  ],\n  \"ims\": [\n    {\n      \"id\": \"DDIRECTMESSAGEID\",\n      \"created\": 1510585552,\n      \"is_frozen\": false,\n      \"is_archived\": false,\n      \"is_im\": true,\n      \"is_org_shared\": false,\n      \"user\": \"USLACKBOT\",\n      \"last_read\": \"1564105295.000100\",\n      \"latest\": \"1564105295.000100\",\n      \"is_open\": true,\n      \"priority\": 0\n    }\n  ],\n  \"cache_ts\": 1565672621,\n  \"read_only_channels\": [],\n  \"non_threadable_channels\": [],\n  \"thread_only_channels\": [],\n  \"can_manage_shared_channels\": false,\n  \"mpims\": [\n    {\n      \"id\": \"GGROUPID\",\n      \"name\": \"mpdm-username1--username2--username3-1\",\n      \"is_channel\": false,\n      \"is_group\": true,\n      \"is_im\": false,\n      \"created\": 1527272038,\n      \"is_archived\": false,\n      \"is_general\": false,\n      \"unlinked\": 0,\n      \"name_normalized\": \"mpdm-username1--username2--username3-1\",\n      \"is_shared\": false,\n      \"is_frozen\": false,\n      \"parent_conversation\": null,\n      \"creator\": \"UUSERID\",\n      \"is_ext_shared\": false,\n      \"is_org_shared\": false,\n      \"shared_team_ids\": [\n        \"TTEAMID\"\n      ],\n      \"pending_shared\": [],\n      \"pending_connected_team_ids\": [],\n      \"is_pending_ext_shared\": false,\n      \"is_member\": true,\n      \"is_private\": true,\n      \"is_mpim\": true,\n      \"last_read\": \"1527458159.000075\",\n      \"latest\": \"1527458159.000075\",\n      \"is_open\": false,\n      \"members\": [\n        \"UUSERID1\",\n        \"UUSERID2\",\n        \"UUSERID3\"\n      ],\n      \"topic\": {\n        \"value\": \"Group messaging\",\n        \"creator\": \"UUSERID\",\n        \"last_set\": 1527272038\n      },\n      \"purpose\": {\n        \"value\": \"Group messaging with: @username1 @username2 @username3\",\n        \"creator\": \"UUSERID\",\n        \"last_set\": 1527272038\n      },\n      \"priority\": 0\n    }\n  ],\n  \"subteams\": {\n    \"self\": [],\n    \"all\": []\n  },\n  \"dnd\": {\n    \"dnd_enabled\": true,\n    \"next_dnd_start_ts\": 1565665200,\n    \"next_dnd_end_ts\": 1565701200,\n    \"snooze_enabled\": false\n  },\n  \"cache_version\": \"v18-kudu\",\n  \"cache_ts_version\": \"v2-bunny\",\n  \"url\": \"wss://cerberus-xxxx.lb.slack-msgs.com/websocket/sha-of-socket-presumably=\"\n}\n"
  },
  {
    "path": "spec/fixtures/emojiList.json",
    "content": "[\n  {\n    \"name\": \"emoji-1\",\n    \"url\": \"./spec/fixtures/Example.jpg\",\n    \"user_display_name\": \"test-user-0\"\n  },\n  {\n    \"name\": \"emoji-2\",\n    \"is_alias\": 1,\n    \"alias_for\": \"emoji-1\",\n    \"user_display_name\": \"test-user-0\"\n  },\n  {\n    \"name\": \"emoji-3\",\n    \"url\": \"./spec/fixtures/Example.jpg\",\n    \"user_display_name\": \"test-user-1\"\n  },\n  {\n    \"name\": \"emoji-4\",\n    \"is_alias\": 1,\n    \"alias_for\": \"emoji-3\",\n    \"user_display_name\": \"test-user-0\"\n  }\n]\n"
  },
  {
    "path": "spec/fixtures/emojiList.yaml",
    "content": "---\ntitle: 'some title it doesnt matter'\nanIgnoredKey: 'all keys outside \"emojis\" are ignored'\nemojis:\n  - name: 'emoji-1'\n    src: './spec/fixtures/Example.jpg'\n    fullname: 'this key will be ignored'\n  - name: 'emoji-2'\n    is_alias: 1\n    alias_for: 'emoji-1'\n  - name: 'emoji-3'\n    src: './spec/fixtures/Example.jpg'\n  - name: 'emoji-4'\n    is_alias: 1\n    alias_for: 'emoji-3'\n"
  },
  {
    "path": "spec/integration/emojme-add-spec.js",
    "content": "const chai = require('chai');\nchai.use(require('chai-shallow-deep-equal'));\n\nconst assert = chai.assert;\nconst sinon = require('sinon');\n\nconst EmojiAdd = require('../../lib/emoji-add');\nconst EmojiAdminList = require('../../lib/emoji-admin-list');\nconst SlackClient = require('../../lib/slack-client');\nconst FileUtils = require('../../lib/util/file-utils');\nconst add = require('../../emojme-add').add;\nconst addCli = require('../../emojme-add').addCli;\n\nconst logger = require('../../lib/logger');\n\nlet sandbox;\nlet warningSpy; let infoSpy; let\n  debugSpy;\n\nbeforeEach(() => {\n  sandbox = sinon.createSandbox();\n  warningSpy = sandbox.spy(logger, 'warning');\n  infoSpy = sandbox.spy(logger, 'info');\n  debugSpy = sandbox.spy(logger, 'debug');\n});\n\nafterEach(() => {\n  sandbox.restore();\n  logger.transports[0].level = 'warning';\n});\n\ndescribe('add', () => {\n  context('pre upload configuration', () => {\n    beforeEach(() => {\n      const uploadStub = sandbox.stub(EmojiAdd.prototype, 'upload');\n      uploadStub.callsFake(arg1 => Promise.resolve({ subdomain: 'subdomain', emojiList: arg1 }));\n\n      sandbox.stub(EmojiAdminList.prototype, 'get').withArgs(sinon.match.any).resolves(\n        [{ name: 'emoji-1' }],\n      );\n    });\n\n    describe('renames emoji to avoid collisions when avoidCollisions is set', () => {\n      const validateResults = ((results) => {\n        assert.shallowDeepEqual(results, {\n          subdomain:\n          {\n            collisions: [],\n            emojiList: [\n              {\n                name: 'emoji-5',\n                collision: 'emoji-1',\n              },\n              { name: 'emoji-2' },\n              { name: 'emoji-3' },\n              { name: 'emoji-4' },\n            ],\n          },\n        });\n      });\n\n      it('using the cli', () => {\n        process.argv = [\n          'node',\n          'emojme',\n          'add',\n          '--subdomain', 'subdomain',\n          '--token', 'token',\n          '--cookie', 'cookie',\n          '--name', 'emoji-1', '--alias-for', 'emoji',\n          '--name', 'emoji-2', '--alias-for', 'emoji',\n          '--name', 'emoji-3', '--alias-for', 'emoji',\n          '--name', 'emoji-4', '--alias-for', 'emoji',\n          '--avoid-collisions',\n        ];\n\n        return addCli().then(validateResults);\n      });\n\n      it('using the module', () => {\n        const options = {\n          name: ['emoji-1', 'emoji-2', 'emoji-3', 'emoji-4'],\n          aliasFor: ['emoji', 'emoji', 'emoji', 'emoji'],\n          avoidCollisions: true,\n        };\n\n        return add('subdomain', 'token', 'cookie', options).then(validateResults);\n      });\n    });\n\n    describe('allows slack to return exceptions when allowCollisions is set', () => {\n      beforeEach(() => {\n        sandbox.restore();\n        sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves(\n          {\n            error: 'error_name_taken',\n            alias_for: 'emoji',\n            is_alias: 1,\n            name: 'emoji-1',\n          },\n        );\n      });\n\n      const validateResults = ((results) => {\n        assert.shallowDeepEqual(results, {\n          subdomain:\n          {\n            collisions: [],\n            emojiList: [{\n              alias_for: 'emoji',\n              is_alias: 1,\n              name: 'emoji-1',\n            }, {\n              alias_for: 'emoji',\n              is_alias: 1,\n              name: 'emoji-2',\n            }, {\n              alias_for: 'emoji',\n              is_alias: 1,\n              name: 'emoji-3',\n            }, {\n              alias_for: 'emoji',\n              is_alias: 1,\n              name: 'emoji-4',\n            },\n            ],\n            errorList: [\n              {\n                error: 'error_name_taken',\n                alias_for: 'emoji',\n                is_alias: 1,\n                name: 'emoji-1',\n              }, {\n                error: 'error_name_taken',\n                alias_for: 'emoji',\n                is_alias: 1,\n                name: 'emoji-2',\n              }, {\n                error: 'error_name_taken',\n                alias_for: 'emoji',\n                is_alias: 1,\n                name: 'emoji-3',\n              }, {\n                error: 'error_name_taken',\n                alias_for: 'emoji',\n                is_alias: 1,\n                name: 'emoji-4',\n              },\n            ],\n          },\n        });\n      });\n\n      it('using the cli', () => {\n        process.argv = [\n          'node',\n          'emojme',\n          'add',\n          '--subdomain', 'subdomain',\n          '--token', 'token',\n          '--cookie', 'cookie',\n          '--name', 'emoji-1', '--alias-for', 'emoji',\n          '--name', 'emoji-2', '--alias-for', 'emoji',\n          '--name', 'emoji-3', '--alias-for', 'emoji',\n          '--name', 'emoji-4', '--alias-for', 'emoji',\n          '--allow-collisions',\n        ];\n\n        return addCli().then(validateResults);\n      });\n\n      it('using the module', () => {\n        const options = {\n          name: ['emoji-1', 'emoji-2', 'emoji-3', 'emoji-4'],\n          aliasFor: ['emoji', 'emoji', 'emoji', 'emoji'],\n          allowCollisions: true,\n        };\n\n        return add('subdomain', 'token', 'cookie', options).then(validateResults);\n      });\n    });\n\n    describe('collects and does not attempt to upload collisions when avoidCollisions is false', () => {\n      const validateResults = ((results) => {\n        assert.shallowDeepEqual(results, {\n          subdomain: {\n            collisions: [\n              { name: 'emoji-1' },\n            ],\n            emojiList: [\n              { name: 'emoji-2' },\n              { name: 'emoji-3' },\n              { name: 'emoji-4' },\n            ],\n          },\n        });\n      });\n\n      it('using the cli', () => {\n        process.argv = [\n          'node',\n          'emojme',\n          'add',\n          '--subdomain', 'subdomain',\n          '--token', 'token',\n          '--cookie', 'cookie',\n          '--name', 'emoji-1', '--alias-for', 'emoji',\n          '--name', 'emoji-2', '--alias-for', 'emoji',\n          '--name', 'emoji-3', '--alias-for', 'emoji',\n          '--name', 'emoji-4', '--alias-for', 'emoji',\n        ];\n\n        return addCli().then(validateResults);\n      });\n\n      it('using the module', () => {\n        const options = {\n          name: ['emoji-1', 'emoji-2', 'emoji-3', 'emoji-4'],\n          aliasFor: ['emoji', 'emoji', 'emoji', 'emoji'],\n          avoidCollisions: false,\n        };\n\n        return add('subdomain', 'token', 'cookie', options).then(validateResults);\n      });\n    });\n  });\n\n  context('upload behavior', () => {\n    beforeEach(() => {\n      sandbox.stub(EmojiAdminList.prototype, 'get').withArgs(sinon.match.any).resolves(\n        [{ name: 'emoji-1' }],\n      );\n    });\n\n    describe('returns array of subdomain specific results when uploading aliases', () => {\n      beforeEach(() => {\n        const requestStub = sandbox.stub(SlackClient.prototype, 'request');\n        requestStub.withArgs(sinon.match.any).resolves(\n          { ok: true },\n        );\n        requestStub.withArgs(sinon.match.any).onFirstCall().resolves(\n          { ok: false, error: 'an error message' },\n        );\n      });\n\n      const validateResults = ((results) => {\n        assert.equal(warningSpy.callCount, 1);\n        assert.equal(infoSpy.callCount, 4);\n        assert.equal(debugSpy.callCount, 5);\n\n        assert.equal(results.subdomain1.emojiList.length, 3); // 4 minus 1 collision\n        assert.deepEqual(results.subdomain1.errorList, [{\n          name: 'emoji-2',\n          is_alias: 1,\n          alias_for: 'emoji',\n          error: 'an error message',\n        }]); // error on first call\n\n        assert.equal(results.subdomain1.collisions.length, 1); // collision with emoji-1\n\n        assert.equal(results.subdomain2.emojiList.length, 3); // 4 minus 1 collision\n        assert.equal(results.subdomain2.errorList.length, 0); // no errors\n        assert.equal(results.subdomain2.collisions.length, 1); // collision with emoji-1\n      });\n\n      it('using the cli', () => {\n        process.argv = [\n          'node',\n          'emojme',\n          'add',\n          '--subdomain', 'subdomain1',\n          '--subdomain', 'subdomain2',\n          '--token', 'token1',\n          '--token', 'token2',\n          '--cookie', 'cookie1',\n          '--cookie', 'cookie2',\n          '--name', 'emoji-1', '--alias-for', 'emoji',\n          '--name', 'emoji-2', '--alias-for', 'emoji',\n          '--name', 'emoji-3', '--alias-for', 'emoji',\n          '--name', 'emoji-4', '--alias-for', 'emoji',\n          '--verbose',\n        ];\n\n        return addCli().then(validateResults);\n      });\n\n      it('using the module', () => {\n        const subdomains = ['subdomain1', 'subdomain2'];\n        const tokens = ['token1', 'token2'];\n        const cookies = ['cookie1', 'cookie2'];\n        const options = {\n          name: ['emoji-1', 'emoji-2', 'emoji-3', 'emoji-4'],\n          aliasFor: ['emoji', 'emoji', 'emoji', 'emoji'],\n          avoidCollisions: false,\n        };\n\n        return add(subdomains, tokens, cookies, options).then(validateResults);\n      });\n    });\n\n    describe('returns array of subdomain specific results when uploading new emoji', () => {\n      beforeEach(() => {\n        sandbox.stub(FileUtils, 'getData').withArgs(sinon.match.any).resolves('emoji data');\n\n        const requestStub = sandbox.stub(SlackClient.prototype, 'request');\n        requestStub.withArgs(sinon.match.any).resolves(\n          { ok: true },\n        );\n        requestStub.withArgs(sinon.match.any).onFirstCall().resolves(\n          { ok: false, error: 'an error message' },\n        );\n      });\n\n      const validateResults = ((results) => {\n        assert.equal(results.subdomain1.emojiList.length, 3); // 4 minus 1 collision\n        assert.deepEqual(results.subdomain1.errorList, [{\n          name: 'emoji-2',\n          url: 'emoji-2.jpg',\n          is_alias: 0,\n          error: 'an error message',\n        }]); // error on first call\n\n        assert.equal(results.subdomain1.collisions.length, 1); // collision with emoji-1\n\n        assert.equal(results.subdomain2.emojiList.length, 3); // 4 minus 1 collision\n        assert.equal(results.subdomain2.errorList.length, 0); // no errors\n        assert.equal(results.subdomain2.collisions.length, 1); // collision with emoji-1\n      });\n\n      it('using the cli', () => {\n        process.argv = [\n          'node',\n          'emojme',\n          'add',\n          '--subdomain', 'subdomain1',\n          '--subdomain', 'subdomain2',\n          '--token', 'token1',\n          '--token', 'token2',\n          '--cookie', 'cookie1',\n          '--cookie', 'cookie2',\n          '--src', 'emoji-1.jpg',\n          '--src', 'emoji-2.jpg',\n          '--src', 'emoji-3.jpg',\n          '--src', 'emoji-4.jpg',\n        ];\n\n        return addCli().then(validateResults);\n      });\n\n      it('using the module', () => {\n        const subdomains = ['subdomain1', 'subdomain2'];\n        const tokens = ['token1', 'token2'];\n        const cookies = ['cookie1', 'cookie2'];\n        const options = {\n          src: ['emoji-1.jpg', 'emoji-2.jpg', 'emoji-3.jpg', 'emoji-4.jpg'],\n          avoidCollisions: false,\n        };\n\n        return add(subdomains, tokens, cookies, options).then(validateResults);\n      });\n    });\n\n    describe('allows mixed new / alias inputs when correctly formatted', () => {\n      beforeEach(() => {\n        sandbox.stub(EmojiAdd.prototype, 'upload').callsFake(arg1 => Promise.resolve({ subdomain: 'subdomain', emojiList: arg1 }));\n      });\n\n      const validateResults = ((results) => {\n        assert.deepEqual(results, {\n          subdomain: {\n            collisions: [],\n            emojiList: [\n              {\n                name: 'new-emoji-1',\n                url: 'new-emoji-1.jpg',\n                is_alias: 0,\n              }, {\n                name: 'alias-name-2',\n                alias_for: 'alias-src-2',\n                is_alias: 1,\n              }, {\n                name: 'alias-name-3',\n                alias_for: 'alias-src-3',\n                is_alias: 1,\n              }, {\n                name: 'emoji-name-4',\n                url: 'new-emoji-4.gif',\n                is_alias: 0,\n              },\n            ],\n          },\n        });\n      });\n\n      it('using the cli', () => {\n        process.argv = [\n          'node',\n          'emojme',\n          'add',\n          '--subdomain', 'subdomain',\n          '--token', 'token',\n          '--cookie', 'cookie',\n          '--src', 'new-emoji-1.jpg', '--name', '',\n          '--src', '', '--name', 'alias-name-2', '--alias-for', 'alias-src-2',\n          '--src', '', '--name', 'alias-name-3', '--alias-for', 'alias-src-3',\n          '--src', 'new-emoji-4.gif', '--name', 'emoji-name-4',\n        ];\n\n        return addCli().then(validateResults);\n      });\n\n      it('using the module', () => {\n        const options = {\n          src: ['new-emoji-1.jpg', null, null, 'new-emoji-4.gif'],\n          name: [null, 'alias-name-2', 'alias-name-3', 'emoji-name-4'],\n          aliasFor: ['alias-src-2', 'alias-src-3'],\n        };\n\n        return add('subdomain', 'tokens', 'cookies', options).then(validateResults);\n      });\n    });\n\n    describe('rejects poorly formatted inputs', () => {\n      const validateError = ((err) => {\n        assert.equal(err.message, 'Invalid input. Either not all inputs have been consumed, or not all emoji are well formed. Consider simplifying input, or padding input with `null` values.');\n      });\n\n      it('using the cli', () => {\n        process.argv = [\n          'node',\n          'emojme',\n          'add',\n          '--subdomain', 'subdomain',\n          '--token', 'token',\n          '--cookie', 'cookie',\n          '--src', 'emoji-1.jpg', '--name', 'emoji-1',\n          '--name', 'emoji-2', '--alias-for', 'emoji-2-original',\n          '--alias-for', 'unattached-alias',\n        ];\n\n        return addCli().then(() => assert.fail()).catch(validateError); // eslint-disable-line no-undef\n      });\n\n      it('using the module', () => {\n        const options = {\n          src: ['emoji-1.jpg'],\n          name: ['emoji-1', 'emoji-2'],\n          aliasFor: ['emoji-2-original', 'unattached-alias'],\n        };\n\n        return add('subdomain', 'tokens', 'cookies', options)\n          .then(() => assert.fail()).catch(validateError); // eslint-disable-line no-undef\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "spec/integration/emojme-download-spec.js",
    "content": "const chai = require('chai');\nchai.use(require('chai-shallow-deep-equal'));\n\nconst assert = chai.assert;\nconst sinon = require('sinon');\n\nconst specHelper = require('../spec-helper');\nconst EmojiAdminList = require('../../lib/emoji-admin-list');\nconst FileUtils = require('../../lib/util/file-utils');\nconst download = require('../../emojme-download').download;\nconst downloadCli = require('../../emojme-download').downloadCli;\n\nlet sandbox;\n\nbeforeEach(() => {\n  sandbox = sinon.createSandbox();\n});\n\nafterEach(() => {\n  sandbox.restore();\n});\n\ndescribe('download', () => {\n  const subdomains = ['subdomain1', 'subdomain2'];\n  const tokens = ['token1', 'token2'];\n  const cookies = ['cookie1', 'cookie2'];\n\n  beforeEach(() => {\n    const getStub = sandbox.stub(EmojiAdminList.prototype, 'getAdminListPages');\n    getStub.callsFake(() => Promise.resolve(specHelper.testEmojiList(10)));\n\n    // prevent writing during tests\n    sandbox.stub(FileUtils, 'saveData').callsFake((arg1, arg2) => Promise.resolve(arg2));\n    sandbox.stub(FileUtils, 'mkdirp');\n  });\n\n  describe('downloads emojiList when save is not set', () => {\n    const validateResults = ((results) => {\n      assert.deepEqual(results.subdomain1.emojiList, specHelper.testEmojiList(10));\n      assert.deepEqual(results.subdomain2.emojiList, specHelper.testEmojiList(10));\n\n      assert.deepEqual(results.subdomain1.saveResults, []);\n      assert.deepEqual(results.subdomain2.saveResults, []);\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'download',\n        '--subdomain', 'subdomain1',\n        '--subdomain', 'subdomain2',\n        '--token', 'token1',\n        '--token', 'token2',\n        '--cookie', 'cookie1',\n        '--cookie', 'cookie2',\n      ];\n      return downloadCli().then(validateResults);\n    });\n\n    it('using the module', () => download(subdomains, tokens, cookies).then(validateResults));\n  });\n\n  describe('downloads emojiList containing only the emoji created since since_ts', () => {\n    const validateResults = ((results) => {\n      assert.equal(results.subdomain1.emojiList.length, 4);\n      results.subdomain1.emojiList.forEach((emoji) => {\n        assert.equal(emoji.created > 86400 * 5, true);\n      });\n      assert.equal(results.subdomain2.emojiList.length, 4);\n      results.subdomain2.emojiList.forEach((emoji) => {\n        assert.equal(emoji.created > 86400 * 5, true);\n      });\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'download',\n        '--subdomain', 'subdomain1',\n        '--subdomain', 'subdomain2',\n        '--token', 'token1',\n        '--token', 'token2',\n        '--cookie', 'cookie1',\n        '--cookie', 'cookie2',\n        '--since', 86400 * 5, // 5 minutes from epoch\n      ];\n      return downloadCli().then(validateResults);\n    });\n\n    it('using the module', () => download(subdomains, tokens, cookies, { since: 86400 * 5 }).then(validateResults));\n  });\n\n  describe('downloads emoji for specified users when save is set', () => {\n    const validateResults = ((results) => {\n      assert.deepEqual(results.subdomain1.emojiList, specHelper.testEmojiList(10));\n      assert.deepEqual(results.subdomain2.emojiList, specHelper.testEmojiList(10));\n\n      assert.equal(results.subdomain1.saveResults.length, 10);\n      assert.equal(results.subdomain2.saveResults.length, 10);\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'download',\n        '--subdomain', 'subdomain1',\n        '--subdomain', 'subdomain2',\n        '--token', 'token1',\n        '--token', 'token2',\n        '--cookie', 'cookie1',\n        '--cookie', 'cookie2',\n        '--save', 'test-user-1',\n        '--save', 'test-user-0',\n      ];\n      return downloadCli().then(validateResults);\n    });\n\n    it('using the module', () => download(subdomains, tokens, cookies, { save: ['test-user-1', 'test-user-0'] }).then(validateResults));\n  });\n\n  describe('downloads emoji for all users to a single location when saveAll is set', () => {\n    const validateResults = ((results) => {\n      assert.deepEqual(results.subdomain.emojiList, specHelper.testEmojiList(10));\n\n      assert.equal(results.subdomain.saveResults.length, 10);\n      results.subdomain.saveResults.map(path => assert.match(path, /build\\/subdomain\\/emoji-[0-9]*.jpg/));\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'download',\n        '--subdomain', 'subdomain',\n        '--token', 'token',\n        '--cookie', 'cookie',\n        '--save-all',\n      ];\n      return downloadCli().then(validateResults);\n    });\n\n    it('using the module', () => {\n      download('subdomain', 'token', 'cookie', { saveAll: true }).then(validateResults);\n    });\n  });\n\n  describe('downloads emoji for all users to a user directories when saveAllByUser is set', () => {\n    const validateResults = ((results) => {\n      assert.deepEqual(results.subdomain.emojiList, specHelper.testEmojiList(10));\n\n      assert.equal(results.subdomain.saveResults.length, 10);\n\n      results.subdomain.saveResults.map(path => assert.match(path, /build\\/subdomain\\/test-user-[0-9]\\/emoji-[0-9]*.jpg/));\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'download',\n        '--subdomain', 'subdomain',\n        '--token', 'token',\n        '--cookie', 'cookie',\n        '--save-all-by-user',\n      ];\n      return downloadCli().then(validateResults);\n    });\n\n    it('using the module', () => {\n      download('subdomain', 'token', 'cookie', { saveAllByUser: true }).then(validateResults);\n    });\n  });\n});\n"
  },
  {
    "path": "spec/integration/emojme-favorites-spec.js",
    "content": "const chai = require('chai');\nchai.use(require('chai-shallow-deep-equal'));\n\nconst assert = chai.assert;\nconst sinon = require('sinon');\nconst _ = require('lodash');\n\nconst specHelper = require('../spec-helper');\nconst ClientBoot = require('../../lib/client-boot');\nconst EmojiAdminList = require('../../lib/emoji-admin-list');\nconst FileUtils = require('../../lib/util/file-utils');\n\nconst favorites = require('../../emojme-favorites').favorites;\nconst favoritesCli = require('../../emojme-favorites').favoritesCli;\n\nlet sandbox;\nbeforeEach(() => {\n  sandbox = sinon.createSandbox();\n});\n\nafterEach(() => {\n  sandbox.restore();\n});\n\ndescribe('favorites', () => {\n  beforeEach(() => {\n    const clientBootGetStub = sandbox.stub(ClientBoot.prototype, 'get');\n    clientBootGetStub.resolves(\n      specHelper.mockedBootData(),\n    );\n\n    const emojiAdminListGetStub = sandbox.stub(EmojiAdminList.prototype, 'get');\n    emojiAdminListGetStub.resolves(\n      specHelper.testEmojiList(10),\n    );\n\n    // prevent writing during tests\n    sandbox.stub(FileUtils, 'saveData').callsFake((arg1, arg2) => Promise.resolve(arg2));\n    sandbox.stub(FileUtils, 'writeJson');\n  });\n\n  describe('returns favoritesResultObject', () => {\n    const validateResults = ((result) => {\n      let usage1 = 10;\n      let usage2 = 10;\n      assert.shallowDeepEqual(result, {\n        subdomain1: {\n          favoritesResult: {\n            user: 'username',\n            subdomain: 'subdomain1',\n            favoriteEmoji: specHelper.testEmojiList(10).map(e => e.name),\n            favoriteEmojiAdminList: _.reduce(specHelper.testEmojiList(10), (acc, e) => {\n              acc.push({ [e.name]: { ...e, usage: usage1-- } });\n              return acc;\n            }, []),\n          },\n        },\n        subdomain2: {\n          favoritesResult: {\n            user: 'username',\n            subdomain: 'subdomain2',\n            favoriteEmoji: specHelper.testEmojiList(10).map(e => e.name),\n            favoriteEmojiAdminList: _.reduce(specHelper.testEmojiList(10), (acc, e) => {\n              acc.push({ [e.name]: { ...e, usage: usage2-- } });\n              return acc;\n            }, []),\n          },\n        },\n      });\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'favorites',\n        '--subdomain', 'subdomain1',\n        '--subdomain', 'subdomain2',\n        '--token', 'token1',\n        '--token', 'token2',\n        '--cookie', 'cookie1',\n        '--cookie', 'cookie2',\n      ];\n      return favoritesCli().then(validateResults);\n    });\n\n    it('using the module', () => favorites(['subdomain1', 'subdomain1', 'subdomain2'],\n      ['token1', 'token2', 'token3'],\n      ['cookie1', 'cookie2', 'cookie3'],\n      {}).then(validateResults));\n  });\n});\n"
  },
  {
    "path": "spec/integration/emojme-sync-spec.js",
    "content": "const chai = require('chai');\nchai.use(require('chai-shallow-deep-equal'));\n\nconst assert = chai.assert;\nconst sinon = require('sinon');\n\nconst EmojiAdd = require('../../lib/emoji-add');\nconst EmojiAdminList = require('../../lib/emoji-admin-list');\nconst FileUtils = require('../../lib/util/file-utils');\nconst Helpers = require('../../lib/util/helpers');\n\nconst specHelper = require('../spec-helper');\nconst sync = require('../../emojme-sync').sync;\nconst syncCli = require('../../emojme-sync').syncCli;\n\nlet sandbox;\nbeforeEach(() => {\n  sandbox = sinon.createSandbox();\n});\n\nafterEach(() => {\n  sandbox.restore();\n});\n\ndescribe('sync', () => {\n  beforeEach(() => {\n    const uploadStub = sandbox.stub(EmojiAdd.prototype, 'upload');\n    uploadStub.callsFake((arg1) => {\n      const subdomain = uploadStub.thisValues[uploadStub.callCount - 1].subdomain;\n      return Promise.resolve({ subdomain, emojiList: arg1 });\n    });\n\n    // Each subdomain will have 2 unique emoji and 2 emoji shared\n    // between them.\n    const getStub = sandbox.stub(EmojiAdminList.prototype, 'getAdminListPages');\n    getStub.callsFake(() => {\n      const subdomain = (getStub.thisValues[getStub.callCount - 1].subdomain);\n      const uniqEmoji = Helpers.applyPrefix(specHelper.testEmojiList(2), `${subdomain}-`);\n      const sharedEmoji = specHelper.testEmojiList(2);\n\n      return uniqEmoji.concat(sharedEmoji);\n    });\n\n    // prevent writing during tests\n    sandbox.stub(FileUtils, 'saveData').callsFake((arg1, arg2) => Promise.resolve(arg2));\n    sandbox.stub(FileUtils, 'writeJson');\n  });\n\n  describe('syncs one directionally when src and dst auth pairs are specified', () => {\n    const validateResults = ((results) => {\n      assert.shallowDeepEqual(results, {\n        dstSubdomain: {\n          emojiList: [\n            { name: 'srcSubdomain-emoji-0' },\n            { name: 'srcSubdomain-emoji-1' },\n          ],\n        },\n      });\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'sync',\n        '--src-subdomain', 'srcSubdomain',\n        '--src-token', 'srcToken',\n        '--src-cookie', 'srcCookie',\n        '--dst-subdomain', 'dstSubdomain',\n        '--dst-token', 'dstToken',\n        '--dst-cookie', 'dstcookie',\n      ];\n      return syncCli().then(validateResults);\n    });\n\n    it('using the module', () => sync([], [], [], {\n      srcSubdomains: ['srcSubdomain'],\n      srcTokens: ['srcToken'],\n      srcCookies: ['srcCookie'],\n      dstSubdomains: ['dstSubdomain'],\n      dstTokens: ['dstToken'],\n      dstCookies: ['dstCookie'],\n    }).then(validateResults));\n  });\n\n  describe('syncs one directionally the emoji created since a certain time, if specified', () => {\n    const validateResults = ((results) => {\n      assert.shallowDeepEqual(results, {\n        dstSubdomain: {\n          emojiList: [\n            { name: 'srcSubdomain-emoji-1' },\n          ],\n        },\n      });\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'sync',\n        '--src-subdomain', 'srcSubdomain',\n        '--src-token', 'srcToken',\n        '--src-cookie', 'srcCookie',\n        '--dst-subdomain', 'dstSubdomain',\n        '--dst-token', 'dstToken',\n        '--dst-cookie', 'dstcookie',\n        '--since', '1',\n      ];\n      return syncCli().then(validateResults);\n    });\n\n    it('using the module', () => sync([], [], [], {\n      srcSubdomains: ['srcSubdomain'],\n      srcTokens: ['srcToken'],\n      srcCookies: ['srcCookie'],\n      dstSubdomains: ['dstSubdomain'],\n      dstTokens: ['dstToken'],\n      dstCookies: ['dstCookie'],\n      since: '1',\n    }).then(validateResults));\n  });\n\n  describe('syncs one directionally from multiple sources to a single destionation when specified', () => {\n    const validateResults = ((results) => {\n      assert.shallowDeepEqual(results, {\n        dstSubdomain: {\n          emojiList: [\n            { name: 'srcSubdomain-1-emoji-0' },\n            { name: 'srcSubdomain-1-emoji-1' },\n            { name: 'srcSubdomain-2-emoji-0' },\n            { name: 'srcSubdomain-2-emoji-1' },\n          ],\n        },\n      });\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'sync',\n        '--src-subdomain', 'srcSubdomain-1',\n        '--src-subdomain', 'srcSubdomain-2',\n        '--src-token', 'srcToken-1',\n        '--src-token', 'srcToken-2',\n        '--src-cookie', 'srcCookie-1',\n        '--src-cookie', 'srcCookie-2',\n        '--dst-subdomain', 'dstSubdomain',\n        '--dst-token', 'dstToken',\n        '--dst-cookie', 'dstcookie',\n      ];\n      return syncCli().then(validateResults);\n    });\n\n    it('using the module', () => sync([], [], [], {\n      srcSubdomains: ['srcSubdomain-1', 'srcSubdomain-2'],\n      srcTokens: ['srcToken-1', 'srcToken-2'],\n      srcCookies: ['srcCookie-1', 'srcCookie-2'],\n      dstSubdomains: ['dstSubdomain'],\n      dstTokens: ['dstToken'],\n      dstCookies: ['dstCookie'],\n    }).then(validateResults));\n  });\n\n  describe('syncs one directionally from multiple sources to a multiple destionations when specified', () => {\n    const validateResults = ((results) => {\n      assert.shallowDeepEqual(results, {\n        'dstSubdomain-1': {\n          emojiList: [\n            { name: 'srcSubdomain-1-emoji-0' },\n            { name: 'srcSubdomain-1-emoji-1' },\n            { name: 'srcSubdomain-2-emoji-0' },\n            { name: 'srcSubdomain-2-emoji-1' },\n          ],\n        },\n        'dstSubdomain-2': {\n          emojiList: [\n            { name: 'srcSubdomain-1-emoji-0' },\n            { name: 'srcSubdomain-1-emoji-1' },\n            { name: 'srcSubdomain-2-emoji-0' },\n            { name: 'srcSubdomain-2-emoji-1' },\n          ],\n        },\n      });\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'sync',\n        '--src-subdomain', 'srcSubdomain-1',\n        '--src-subdomain', 'srcSubdomain-2',\n        '--src-token', 'srcToken-1',\n        '--src-token', 'srcToken-2',\n        '--src-cookie', 'srcCookie-1',\n        '--src-cookie', 'srcCookie-2',\n        '--dst-subdomain', 'dstSubdomain-1',\n        '--dst-subdomain', 'dstSubdomain-2',\n        '--dst-token', 'dstToken-1',\n        '--dst-token', 'dstToken-2',\n        '--dst-cookie', 'dstcookie-1',\n        '--dst-cookie', 'dstcookie-2',\n      ];\n      return syncCli().then(validateResults);\n    });\n\n    it('using the module', () => sync([], [], [], {\n      srcSubdomains: ['srcSubdomain-1', 'srcSubdomain-2'],\n      srcTokens: ['srcToken-1', 'srcToken-2'],\n      srcCookies: ['srcCookie-1', 'srcCookie-2'],\n      dstSubdomains: ['dstSubdomain-1', 'dstSubdomain-2'],\n      dstTokens: ['dstToken-1', 'dstToken-2'],\n      dstCookies: ['dstCookie-1', 'dstCookie-2'],\n    }).then(validateResults));\n  });\n\n  describe('syncs all emoji across all auth pairs when mutliple subdomains and tokens are specified', () => {\n    const validateResults = ((results) => {\n      assert.shallowDeepEqual(results, {\n        'subdomain-1': {\n          emojiList: [\n            { name: 'subdomain-2-emoji-0' },\n            { name: 'subdomain-2-emoji-1' },\n            { name: 'subdomain-3-emoji-0' },\n            { name: 'subdomain-3-emoji-1' },\n          ],\n        },\n        'subdomain-2': {\n          emojiList: [\n            { name: 'subdomain-1-emoji-0' },\n            { name: 'subdomain-1-emoji-1' },\n            { name: 'subdomain-3-emoji-0' },\n            { name: 'subdomain-3-emoji-1' },\n          ],\n        },\n        'subdomain-3': {\n          emojiList: [\n            { name: 'subdomain-1-emoji-0' },\n            { name: 'subdomain-1-emoji-1' },\n            { name: 'subdomain-2-emoji-0' },\n            { name: 'subdomain-2-emoji-1' },\n          ],\n        },\n      });\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'sync',\n        '--subdomain', 'subdomain-1',\n        '--subdomain', 'subdomain-2',\n        '--subdomain', 'subdomain-3',\n        '--token', 'token-1',\n        '--token', 'token-2',\n        '--token', 'token-3',\n        '--cookie', 'cookie-1',\n        '--cookie', 'cookie-2',\n        '--cookie', 'cookie-3',\n      ];\n      return syncCli().then(validateResults);\n    });\n\n    it('using the module', () => sync(\n      ['subdomain-1', 'subdomain-2', 'subdomain-3'],\n      ['token-1', 'token-2', 'token-3'],\n      ['cookie-1', 'cookie-2', 'cookie-3'],\n      {},\n    ).then(validateResults));\n  });\n});\n"
  },
  {
    "path": "spec/integration/emojme-upload-spec.js",
    "content": "const chai = require('chai');\n\nconst assert = chai.assert;\n\nconst sinon = require('sinon');\nconst fs = require('graceful-fs');\n\nconst EmojiAdd = require('../../lib/emoji-add');\nconst EmojiAdminList = require('../../lib/emoji-admin-list');\nconst SlackClient = require('../../lib/slack-client');\nconst FileUtils = require('../../lib/util/file-utils');\nconst upload = require('../../emojme-upload').upload;\nconst uploadCli = require('../../emojme-upload').uploadCli;\n\nlet sandbox;\nlet uploadStub;\n\nbeforeEach(() => {\n  sandbox = sinon.createSandbox();\n});\n\nafterEach(() => {\n  sandbox.restore();\n});\n\ndescribe('upload', () => {\n  beforeEach(() => {\n    uploadStub = sandbox.stub(EmojiAdd.prototype, 'upload');\n    uploadStub.callsFake(arg1 => Promise.resolve({ subdomain: 'subdomain', emojiList: arg1 }));\n\n    sandbox.stub(EmojiAdminList.prototype, 'get').withArgs(sinon.match.any).resolves(\n      [{ name: 'emoji-1' }],\n    );\n  });\n\n  describe('uploads emoji from specified json', () => {\n    const validateResults = ((results) => {\n      const fixture = JSON.parse(fs.readFileSync('./spec/fixtures/emojiList.json', 'utf-8'));\n\n      assert.deepEqual(results, {\n        subdomain:\n        {\n          collisions: [\n            fixture[0],\n          ],\n          emojiList: [\n            fixture[1],\n            fixture[2],\n            fixture[3],\n          ],\n        },\n      });\n\n      assert.deepEqual(uploadStub.getCall(0).args, [\n        [\n          fixture[1],\n          fixture[2],\n          fixture[3],\n        ],\n      ]);\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'upload',\n        '--subdomain', 'subdomain',\n        '--token', 'token',\n        '--cookie', 'cookie',\n        '--src', './spec/fixtures/emojiList.json',\n      ];\n      return uploadCli().then(validateResults);\n    });\n\n    it('using the module', () => {\n      const options = { src: './spec/fixtures/emojiList.json' };\n\n      return upload('subdomain', 'token', 'cookie', options).then(validateResults);\n    });\n  });\n\n  describe('uploads emoji from specified yaml', () => {\n    const validateResults = ((results) => {\n      const fixture = FileUtils.readYaml('./spec/fixtures/emojiList.yaml', 'utf-8');\n      assert.deepEqual(results, {\n        subdomain:\n        {\n          collisions: [\n            fixture[0],\n          ],\n          emojiList: [\n            fixture[1],\n            fixture[2],\n            fixture[3],\n          ],\n        },\n      });\n\n      assert.deepEqual(uploadStub.getCall(0).args, [\n        [\n          fixture[1],\n          fixture[2],\n          fixture[3],\n        ],\n      ]);\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'upload',\n        '--subdomain', 'subdomain',\n        '--token', 'token',\n        '--cookie', 'cookie',\n        '--src', './spec/fixtures/emojiList.yaml',\n      ];\n      return uploadCli().then(validateResults);\n    });\n\n    it('using the module', () => {\n      const options = { src: './spec/fixtures/emojiList.yaml' };\n\n      return upload('subdomain', 'token', 'cookie', options).then(validateResults);\n    });\n  });\n\n  describe('renames emoji to avoid collisions when avoidCollisions is set', () => {\n    const validateResults = ((results) => {\n      const fixture = JSON.parse(fs.readFileSync('./spec/fixtures/emojiList.json', 'utf-8'));\n      assert.deepEqual(results, {\n        subdomain:\n        {\n          collisions: [],\n          emojiList: [\n            { ...fixture[0], name: 'emoji-5', collision: 'emoji-1' },\n            fixture[1],\n            fixture[2],\n            fixture[3],\n          ],\n        },\n      });\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'upload',\n        '--subdomain', 'subdomain',\n        '--token', 'token',\n        '--cookie', 'cookie',\n        '--src', './spec/fixtures/emojiList.json',\n        '--avoid-collisions',\n      ];\n      return uploadCli().then(validateResults);\n    });\n\n    it('using the module', () => {\n      const options = {\n        src: './spec/fixtures/emojiList.json',\n        avoidCollisions: true,\n      };\n\n      return upload('subdomain', 'token', 'cookie', options).then(validateResults);\n    });\n  });\n\n  describe('collects and does not attempt to upload collisions when avoidCollisions is false', () => {\n    const validateResults = ((results) => {\n      const fixture = JSON.parse(fs.readFileSync('./spec/fixtures/emojiList.json', 'utf-8'));\n      assert.deepEqual(results, {\n        subdomain:\n        {\n          collisions: [\n            fixture[0],\n          ],\n          emojiList: [\n            fixture[1],\n            fixture[2],\n            fixture[3],\n          ],\n        },\n      });\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'upload',\n        '--subdomain', 'subdomain',\n        '--token', 'token',\n        '--cookie', 'cookie',\n        '--src', './spec/fixtures/emojiList.json',\n      ];\n      return uploadCli().then(validateResults);\n    });\n\n    it('using the module', () => {\n      const options = {\n        src: './spec/fixtures/emojiList.json',\n      };\n\n      return upload('subdomain', 'token', 'cookie', options).then(validateResults);\n    });\n  });\n\n  describe('allows collision errors on the slack side when allowCollisions is set', () => {\n    beforeEach(() => {\n      sandbox.restore();\n      sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves(\n        {\n          error: 'error_name_taken',\n          alias_for: 'emoji',\n          is_alias: 1,\n          name: 'emoji-1',\n        },\n      );\n    });\n\n    const validateResults = ((results) => {\n      const fixture = JSON.parse(fs.readFileSync('./spec/fixtures/emojiList.json', 'utf-8'));\n      assert.deepEqual(results, {\n        subdomain:\n        {\n          collisions: [],\n          emojiList: [\n            fixture[0],\n            fixture[1],\n            fixture[2],\n            fixture[3],\n          ],\n          errorList: [\n            { ...fixture[0], error: 'error_name_taken' },\n            { ...fixture[2], error: 'error_name_taken' },\n            { ...fixture[1], error: 'error_name_taken' },\n            { ...fixture[3], error: 'error_name_taken' },\n          ],\n        },\n      });\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'upload',\n        '--subdomain', 'subdomain',\n        '--token', 'token',\n        '--cookie', 'cookie',\n        '--src', './spec/fixtures/emojiList.json',\n        '--allow-collisions',\n      ];\n      return uploadCli().then(validateResults);\n    });\n\n    it('using the module', () => {\n      const options = {\n        src: './spec/fixtures/emojiList.json',\n        allowCollisions: true,\n      };\n\n      return upload('subdomain', 'token', 'cookie', options).then(validateResults);\n    });\n  });\n});\n"
  },
  {
    "path": "spec/integration/emojme-user-stats-spec.js",
    "content": "const chai = require('chai');\nchai.use(require('chai-shallow-deep-equal'));\n\nconst assert = chai.assert;\nconst sinon = require('sinon');\n\nconst specHelper = require('../spec-helper');\nconst EmojiAdminList = require('../../lib/emoji-admin-list');\nconst FileUtils = require('../../lib/util/file-utils');\n\nconst userStats = require('../../emojme-user-stats').userStats;\nconst userStatsCli = require('../../emojme-user-stats').userStatsCli;\n\nlet sandbox;\nbeforeEach(() => {\n  sandbox = sinon.createSandbox();\n});\n\nafterEach(() => {\n  sandbox.restore();\n});\n\ndescribe('user-stats', () => {\n  beforeEach(() => {\n    const getStub = sandbox.stub(EmojiAdminList.prototype, 'getAdminListPages');\n    getStub.resolves(\n      specHelper.testEmojiList(10),\n    );\n\n    // prevent writing during tests\n    sandbox.stub(FileUtils, 'saveData').callsFake((arg1, arg2) => Promise.resolve(arg2));\n    sandbox.stub(FileUtils, 'writeJson');\n  });\n\n  describe('when one user is given it returns their user stats', () => {\n    const validateResults = ((result) => {\n      assert.shallowDeepEqual(result, {\n        subdomain1: {\n          userStatsResults: [{\n            user: 'test-user-0',\n            // userEmoji: sinon.match.array,\n            subdomain: 'subdomain1',\n            originalCount: 5,\n            aliasCount: 0,\n            totalCount: 5,\n            percentage: '50.00',\n          }],\n        },\n        subdomain2: {\n          userStatsResults: [{\n            user: 'test-user-0',\n            // userEmoji: sinon.match.array,\n            subdomain: 'subdomain2',\n            originalCount: 5,\n            aliasCount: 0,\n            totalCount: 5,\n            percentage: '50.00',\n          }],\n        },\n      });\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'user-stats',\n        '--subdomain', 'subdomain1',\n        '--subdomain', 'subdomain2',\n        '--token', 'token1',\n        '--token', 'token2',\n        '--cookie', 'cookie1',\n        '--cookie', 'cookie2',\n        '--user', 'test-user-0',\n      ];\n      return userStatsCli().then(validateResults);\n    });\n\n    it('using the module', () => userStats(['subdomain1', 'subdomain2'],\n      ['token1', 'token2'],\n      ['cookie1', 'cookie2'],\n      { user: ['test-user-0'] }).then(validateResults));\n  });\n\n  describe('when multiple users are given it returns all their user stats', () => {\n    const validateResults = ((result) => {\n      assert.shallowDeepEqual(result, {\n        subdomain: {\n          userStatsResults: [\n            {\n              user: 'test-user-0',\n              // userEmoji: sinon.match.array,\n              subdomain: 'subdomain',\n              originalCount: 5,\n              aliasCount: 0,\n              totalCount: 5,\n              percentage: '50.00',\n            }, {\n              user: 'test-user-1',\n              // userEmoji: sinon.match.array,\n              subdomain: 'subdomain',\n              originalCount: 0,\n              aliasCount: 5,\n              totalCount: 5,\n              percentage: '50.00',\n            },\n          ],\n        },\n      });\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'user-stats',\n        '--subdomain', 'subdomain',\n        '--token', 'token',\n        '--cookie', 'cookie',\n        '--user', 'test-user-0',\n        '--user', 'test-user-1',\n      ];\n      return userStatsCli().then(validateResults);\n    });\n\n    it('using the module', () => userStats('subdomain', 'token', 'cookie',\n      { user: ['test-user-0', 'test-user-1', 'non-existant-user'] }).then(validateResults));\n  });\n\n  describe('when no users are given, give the top n users', () => {\n    const validateResults = ((result) => {\n      assert.shallowDeepEqual(result, {\n        subdomain: {\n          userStatsResults: [\n            {\n              user: 'test-user-0',\n              // userEmoji: sinon.match.array,\n              subdomain: 'subdomain',\n              originalCount: 5,\n              aliasCount: 0,\n              totalCount: 5,\n              percentage: '50.00',\n            }, {\n              user: 'test-user-1',\n              // userEmoji: sinon.match.array,\n              subdomain: 'subdomain',\n              originalCount: 0,\n              aliasCount: 5,\n              totalCount: 5,\n              percentage: '50.00',\n            },\n          ],\n        },\n      });\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'user-stats',\n        '--subdomain', 'subdomain',\n        '--token', 'token',\n        '--cookie', 'cookie',\n        '--top', '2',\n      ];\n      return userStatsCli().then(validateResults);\n    });\n\n    it('using the module', () => userStats('subdomain', 'token', 'cookie', { top: 2 }).then(validateResults));\n  });\n\n  describe('gives user stats about emoji created after --since', () => {\n    const validateResults = ((result) => {\n      assert.equal(result.subdomain.emojiList.length, 4);\n      assert.shallowDeepEqual(result, {\n        subdomain: {\n          userStatsResults: [\n            {\n              user: 'test-user-0',\n              // userEmoji: sinon.match.array,\n              subdomain: 'subdomain',\n              originalCount: 2,\n              aliasCount: 0,\n              totalCount: 2,\n              percentage: '50.00',\n            }, {\n              user: 'test-user-1',\n              // userEmoji: sinon.match.array,\n              subdomain: 'subdomain',\n              originalCount: 0,\n              aliasCount: 2,\n              totalCount: 2,\n              percentage: '50.00',\n            },\n          ],\n        },\n      });\n    });\n\n    it('using the cli', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'user-stats',\n        '--subdomain', 'subdomain',\n        '--token', 'token',\n        '--cookie', 'cookie',\n        '--since', 86400 * 5,\n      ];\n      return userStatsCli().then(validateResults);\n    });\n\n    it('using the module', () => userStats('subdomain', 'token', 'cookie', { since: 86400 * 5 }).then(validateResults));\n  });\n});\n"
  },
  {
    "path": "spec/spec-helper.js",
    "content": "const fs = require('fs');\n\nmodule.exports = {\n  authTuple: ['subdomain1', 'token1'],\n  authTuples(n) {\n    return Array(n).map((x, i) => [`subdomain${i}`, `token${i}`]);\n  },\n  emojiName(i) {\n    return `emoji-${i}`;\n  },\n  userName(i) {\n    return `test-user-${i % 2}`;\n  },\n  testEmoji(i) {\n    return {\n      name: this.emojiName(i),\n      is_alias: i % 2,\n      alias_for: this.emojiName(1),\n      url: './spec/fixtures/Example.jpg',\n      user_display_name: this.userName(i),\n      created: i * 86400,\n    };\n  },\n  testEmojiList(n) {\n    return Array(n).fill(0).map((x, i) => this.testEmoji(i));\n  },\n  mockedSlackResponse(emojiCount, pageSize, page, ok) {\n    return {\n      ok: ok === undefined ? true : ok,\n      emoji: this.testEmojiList(pageSize),\n      custom_emoji_total_count: emojiCount,\n      paging: {\n        count: pageSize,\n        total: emojiCount,\n        page,\n        pages: Math.ceil(emojiCount / pageSize),\n      },\n    };\n  },\n  mockedBootData() {\n    return JSON.parse(fs.readFileSync('spec/fixtures/clientBoot.json'));\n  },\n};\n"
  },
  {
    "path": "spec/unit/lib/emoji-add-spec.js",
    "content": "const assert = require('chai').assert;\nconst sinon = require('sinon');\nconst fs = require('graceful-fs');\n\nconst EmojiAdd = require('../../../lib/emoji-add');\nconst SlackClient = require('../../../lib/slack-client');\nconst logger = require('../../../lib/logger');\n\nconst specHelper = require('../../spec-helper');\n\nlet sandbox; let emojiAdd; let\n  infoSpy;\n\nbeforeEach(() => {\n  sandbox = sinon.createSandbox();\n  emojiAdd = new EmojiAdd('subdomain', 'token');\n\n  infoSpy = sandbox.spy(logger, 'info');\n});\n\nafterEach(() => {\n  sandbox.restore();\n});\n\ndescribe('EmojiAdd', () => {\n  describe('createMultipart', () => {\n    it('creates an alias multipart request', () => {\n      const emoji = {\n        name: 'name',\n        is_alias: 1,\n        alias_for: 'some other emoji',\n      };\n\n      return EmojiAdd.createMultipart(emoji, 'token').then((result) => {\n        assert.deepEqual(result, {\n          token: 'token',\n          name: emoji.name,\n          mode: 'alias',\n          alias_for: emoji.alias_for,\n        });\n      });\n    });\n\n    it('creates a multipart emoji request', () => {\n      const emoji = {\n        name: 'name',\n        url: './spec/fixtures/Example.jpg',\n      };\n\n      return EmojiAdd.createMultipart(emoji, 'token').then((result) => {\n        assert.deepEqual(result, {\n          token: 'token',\n          name: emoji.name,\n          mode: 'data',\n          image: fs.readFileSync(emoji.url),\n        });\n      });\n    });\n  });\n\n  describe('uploadSingle', () => {\n    const emoji = {\n      name: 'name',\n      url: './spec/fixtures/Example.jpg',\n    };\n\n    it('adds error responses to result', () => {\n      sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves(\n        { ok: false, error: 'sample error' },\n      );\n\n      return emojiAdd.uploadSingle(emoji).then((result) => {\n        assert.deepEqual(result,\n          Object.assign({}, emoji, { error: 'sample error' }));\n      });\n    });\n\n    it('does not return anything for successful responses', () => {\n      sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves(\n        { ok: true },\n      );\n\n      return emojiAdd.uploadSingle(emoji).then((result) => {\n        assert.equal(result, false);\n      });\n    });\n  });\n\n  describe('upload', () => {\n    it('handles source file inputs', () => {\n      sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves(\n        specHelper.mockedSlackResponse(),\n      );\n\n      return emojiAdd.upload('./spec/fixtures/emojiList.json').then((results) => {\n        assert.deepEqual(results.errorList, []);\n      });\n    });\n\n    it('handles array inputs', () => {\n      sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves(\n        specHelper.mockedSlackResponse(),\n      );\n\n      return emojiAdd.upload(specHelper.testEmojiList(2)).then((results) => {\n        assert.deepEqual(results.errorList, []);\n      });\n    });\n\n    it('uploads new emoji first, then aliases', () => {\n      sandbox.spy(emojiAdd, 'uploadSingle');\n\n      sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves(\n        specHelper.mockedSlackResponse(),\n      );\n\n      return emojiAdd.upload('./spec/fixtures/emojiList.json').then((results) => {\n        assert.deepEqual(results.errorList, []);\n\n        const calls = emojiAdd.uploadSingle.getCalls();\n        assert.equal(calls[0].args[0].name, 'emoji-1');\n        assert.equal(calls[1].args[0].name, 'emoji-3');\n        assert.equal(calls[2].args[0].name, 'emoji-2');\n        assert.equal(calls[3].args[0].name, 'emoji-4');\n      });\n    });\n\n    it('gathers unsuccessful results', () => {\n      sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves(\n        { ok: false, error: 'sample error' },\n      );\n\n      return emojiAdd.upload(specHelper.testEmojiList(2)).then((results) => {\n        for (const result in results.errors) {\n          assert.equal(result.error, 'sample error');\n        }\n      });\n    });\n\n    it('gathers successful results', () => {\n      sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves(\n        specHelper.mockedSlackResponse(),\n      );\n\n      return emojiAdd.upload('./spec/fixtures/emojiList.json').then((results) => {\n        const infoCalls = infoSpy.getCalls();\n\n        assert.deepEqual(results.errorList, []);\n        assert.equal(infoSpy.callCount, 2);\n        assert.match(infoCalls[1].lastArg, /.*total requests: 4[\\s\\S]*successes: 4[\\s\\S]*errors: 0.*/);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "spec/unit/lib/emoji-admin-list-spec.js",
    "content": "const assert = require('chai').assert;\nconst sinon = require('sinon');\nconst _ = require('lodash');\nconst fs = require('graceful-fs');\n\nconst EmojiAdminList = require('../../../lib/emoji-admin-list');\nconst SlackClient = require('../../../lib/slack-client');\nconst FileUtils = require('../../../lib/util/file-utils');\n\nconst specHelper = require('../../spec-helper');\n\nlet sandbox;\nlet adminList;\nbeforeEach(() => {\n  sandbox = sinon.createSandbox();\n  adminList = new EmojiAdminList(...specHelper.authTuple);\n});\n\nafterEach(() => {\n  sandbox.restore();\n});\n\ndescribe('EmojiAdminList', () => {\n  describe('createMultipart', () => {\n    it('creates multipart request for specified page', () => {\n      let pageNum;\n\n      for (pageNum in [0, 1, 10]) {\n        const part = adminList.createMultipart(pageNum);\n\n        assert.deepEqual(part, {\n          query: '',\n          page: pageNum,\n          count: adminList.pageSize,\n          token: specHelper.authTuple[1],\n        });\n      }\n    });\n  });\n\n  describe('get', () => {\n    const testEmojiList = specHelper.testEmojiList(3);\n\n    it('uses cached json file if it is not expired', (done) => {\n      sandbox.stub(fs, 'existsSync').withArgs(sinon.match.any).returns(true);\n      sandbox.stub(fs, 'statSync').withArgs(sinon.match.any).returns({ ctimeMs: Date.now() });\n      sandbox.stub(FileUtils, 'readJson').withArgs(sinon.match.any).returns(testEmojiList);\n\n      sandbox.stub(EmojiAdminList.prototype, 'getAdminListPages').resolves(testEmojiList);\n\n      adminList.get().then((emojiList) => {\n        assert.deepEqual(emojiList, testEmojiList);\n        done();\n      });\n    });\n\n    it('ignores cached json file if it is expired', (done) => {\n      sandbox.stub(fs, 'existsSync').withArgs(sinon.match.any).returns(true);\n      sandbox.stub(fs, 'statSync').withArgs(sinon.match.any).returns({ ctimeMs: 0 });\n      sandbox.stub(FileUtils, 'writeJson').withArgs(sinon.match.any);\n\n      sandbox.stub(EmojiAdminList.prototype, 'getAdminListPages').resolves(testEmojiList);\n\n      adminList.get().then((emojiList) => {\n        assert.deepEqual(emojiList, testEmojiList);\n        done();\n      });\n    });\n\n    it('generates new emojilist if no cache file exists', (done) => {\n      sandbox.stub(FileUtils, 'isExpired').withArgs(sinon.match.any).returns(true);\n      sandbox.stub(FileUtils, 'writeJson').withArgs(sinon.match.any);\n\n      sandbox.stub(EmojiAdminList.prototype, 'getAdminListPages').resolves(testEmojiList);\n\n      adminList.get().then((emojiList) => {\n        assert.deepEqual(emojiList, testEmojiList);\n        done();\n      });\n    });\n  });\n\n  describe('getAdminListPages', () => {\n    it('pulls initial page with total number of pages', (done) => {\n      sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves(\n        specHelper.mockedSlackResponse(1, 1, 1, true),\n      );\n\n      adminList.getAdminListPages().then((emojiLists) => {\n        assert.deepEqual(emojiLists[0], specHelper.testEmojiList(1));\n        assert.equal(emojiLists.length, 1);\n        done();\n      });\n    });\n\n    it('generates as many requests as pages', (done) => {\n      const req = sandbox.stub(SlackClient.prototype, 'request');\n      for (let i = 0; i <= 10; i++) {\n        req.onCall(i).resolves(\n          specHelper.mockedSlackResponse(10, 1, i + 1, true),\n        );\n      }\n\n      adminList.setPageSize(1);\n      adminList.getAdminListPages().then((emojiLists) => {\n        assert.equal(emojiLists.length, 10);\n        done();\n      });\n    });\n\n    it('rejects when requests return errors in body', (done) => {\n      const req = sandbox.stub(SlackClient.prototype, 'request');\n      req.onCall(0).resolves(specHelper.mockedSlackResponse(2, 1, 1, true));\n      req.onCall(1).resolves(specHelper.mockedSlackResponse(2, 1, 2, false));\n\n      adminList.setPageSize(1);\n      adminList.getAdminListPages().then((emojiLists) => {\n        assert.equal(emojiLists.length, 1);\n        done();\n      });\n    });\n  });\n\n  describe('summarizeUser', () => {\n    const emojiList = specHelper.testEmojiList(10);\n\n    it('returns null if user is not a contributor', () => {\n      const result = EmojiAdminList.summarizeUser(emojiList, 'subdomain', 'a non existent user');\n\n      assert.deepEqual(result, []);\n    });\n\n    it('returns a user\\'s emoji contributions', () => {\n      const result = EmojiAdminList.summarizeUser(emojiList, 'subdomain', 'test-user-0');\n\n      assert.equal(result.length, 1);\n      assert.equal(result[0].user, 'test-user-0');\n    });\n\n    it('returns multiple users\\' contributions if provided', () => {\n      const result = EmojiAdminList.summarizeUser(emojiList, 'subdomain', ['test-user-0', 'test-user-1']);\n\n      assert.equal(result.length, 2);\n      assert.equal(result[0].user, 'test-user-0');\n      assert.equal(result[1].user, 'test-user-1');\n    });\n\n    it('returns existent users and filters out non existent users', () => {\n      const result = EmojiAdminList.summarizeUser(emojiList, 'subdomain', ['test-user-0', 'non existent user', 'test-user-1']);\n\n      assert.equal(result.length, 2);\n      assert.equal(result[0].user, 'test-user-0');\n      assert.equal(result[1].user, 'test-user-1');\n    });\n  });\n\n  describe('summarizeSubdomain', () => {\n    const emojiList = specHelper.testEmojiList(11);\n\n    it('returns sorted list of contributors', () => {\n      const result = EmojiAdminList.summarizeSubdomain(emojiList, 'subdomain', 10);\n\n      assert.isAbove(result[0].totalCount, result[1].totalCount);\n    });\n\n    it('returns all contributors if count > number of contributors', () => {\n      const result = EmojiAdminList.summarizeSubdomain(emojiList, 'subdomain', 10);\n\n      assert.equal(result.length, _.uniqBy(emojiList, 'user_display_name').length);\n    });\n\n    it('returns n contributors when n is provided', () => {\n      const n = 1;\n      const result = EmojiAdminList.summarizeSubdomain(emojiList, 'subdomain', n);\n\n      assert.equal(result.length, n);\n    });\n  });\n\n  describe('since', () => {\n    const emojiList = specHelper.testEmojiList(10);\n\n    context('when given a time in the future', () => {\n      it('returns an empty array', () => {\n        const result = EmojiAdminList.since(emojiList, Date.now() + 86400);\n\n        assert.deepEqual(result, []);\n      });\n    });\n\n    context('when given a time older than any emoji', () => {\n      it('returns the passed in emojiList', () => {\n        const result = EmojiAdminList.since(emojiList, -1);\n\n        assert.deepEqual(result, emojiList);\n      });\n    });\n\n    context('when given a time bisecting emoji creation dates', () => {\n      it('returns the part of the emojiList that was created after the given time', () => {\n        const result = EmojiAdminList.since(emojiList, 86400 * 5);\n\n        assert.equal(result.length, 4);\n        result.forEach((emoji) => {\n          assert.equal(emoji.created > 86400 * 5, true);\n        });\n      });\n    });\n  });\n\n  describe('diff', () => {\n    context('when explicit source and destination are given', () => {\n      it('creates upload diffs for every subdomain given', () => {\n        const srcLists = [specHelper.testEmojiList(10)];\n        const srcSubdomains = ['src 1'];\n        const dstLists = [specHelper.testEmojiList(5), specHelper.testEmojiList(10)];\n        const dstSubdomains = ['dst 1', 'dst 2'];\n\n        const [diffTo1, diffTo2] = EmojiAdminList.diff(\n          srcLists, srcSubdomains, dstLists, dstSubdomains,\n        );\n        assert.equal(diffTo1.dstSubdomain, 'dst 1');\n        assert.equal(diffTo1.emojiList.length, 5);\n\n        assert.equal(diffTo2.dstSubdomain, 'dst 2');\n        assert.equal(diffTo2.emojiList.length, 0);\n      });\n\n      it('diffs contain emoji from all other subdomains', () => {\n        const srcLists = [specHelper.testEmojiList(10), specHelper.testEmojiList(20)];\n        const srcSubdomains = ['src 1', 'src 2'];\n        const dstLists = [specHelper.testEmojiList(1)];\n        const dstSubdomains = ['dst 1'];\n\n        const [diffTo1] = EmojiAdminList.diff(srcLists, srcSubdomains, dstLists, dstSubdomains);\n\n        assert.equal(diffTo1.dstSubdomain, 'dst 1');\n        assert.equal(diffTo1.emojiList.length, 19);\n      });\n    });\n\n    context('when destination is not given', () => {\n      it('makes the given subdomains and emoji both the src and dst', () => {\n        const lists = [specHelper.testEmojiList(5), specHelper.testEmojiList(10)];\n        const subdomains = ['sub 1', 'sub 2'];\n\n        const [diffTo1, diffTo2] = EmojiAdminList.diff(lists, subdomains);\n        assert.equal(diffTo1.dstSubdomain, 'sub 1');\n        assert.equal(diffTo1.emojiList.length, 5);\n\n        assert.equal(diffTo2.dstSubdomain, 'sub 2');\n        assert.equal(diffTo2.emojiList.length, 0);\n      });\n\n      it('creates upload diffs for every given subdomain', () => {\n        const lists = [\n          specHelper.testEmojiList(5),\n          specHelper.testEmojiList(10),\n          specHelper.testEmojiList(20),\n        ];\n        const subdomains = ['sub 1', 'sub 2', 'sub 3'];\n\n        const [diffTo1, diffTo2, diffTo3] = EmojiAdminList.diff(lists, subdomains);\n\n        assert.equal(diffTo1.dstSubdomain, 'sub 1');\n        assert.equal(diffTo1.emojiList.length, 15);\n\n        assert.equal(diffTo2.dstSubdomain, 'sub 2');\n        assert.equal(diffTo2.emojiList.length, 10);\n\n        assert.equal(diffTo3.dstSubdomain, 'sub 3');\n        assert.equal(diffTo3.emojiList.length, 0);\n      });\n    });\n\n    it('creates accurate diffs', () => {\n      const subdomains = ['sub 1', 'sub 2', 'sub 3'];\n      const lists = [\n        [\n          { name: 'present-in-all' },\n          { name: 'present-in-1-and-2' },\n        ],\n        [\n          { name: 'present-in-all' },\n          { name: 'present-in-1-and-2' },\n          { name: 'present-in-2-and-3' },\n        ],\n        [\n          { name: 'present-in-all' },\n          { name: 'present-in-2-and-3' },\n          { name: 'present-in-3' },\n        ],\n      ];\n\n      const [diffTo1, diffTo2, diffTo3] = EmojiAdminList.diff(lists, subdomains);\n\n      assert.equal(diffTo1.dstSubdomain, 'sub 1');\n      assert.deepEqual(diffTo1.emojiList, [\n        { name: 'present-in-2-and-3' },\n        { name: 'present-in-3' },\n      ]);\n\n      assert.equal(diffTo2.dstSubdomain, 'sub 2');\n      assert.deepEqual(diffTo2.emojiList, [\n        { name: 'present-in-3' },\n      ]);\n\n      assert.equal(diffTo3.dstSubdomain, 'sub 3');\n      assert.deepEqual(diffTo3.emojiList, [\n        { name: 'present-in-1-and-2' },\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "spec/unit/lib/file-utils-spec.js",
    "content": "const assert = require('chai').assert;\nconst fs = require('graceful-fs');\n\nconst FileUtils = require('../../../lib/util/file-utils');\n\ndescribe('FileUtils', () => {\n  describe('getData', () => {\n    const fileData = fs.readFileSync('./spec/fixtures/Example.jpg');\n\n\n    it('downloads links', (done) => {\n      const path = 'https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg';\n      FileUtils.getData(path).then((urlData) => {\n        assert.deepEqual(fileData, urlData);\n        done();\n      });\n    }).timeout(10000);\n\n    it('passes through existing data', (done) => {\n      const path = `data:image/jpeg;${Buffer.from(fileData).toString('base64')}`;\n      FileUtils.getData(path).then((urlData) => {\n        assert.deepEqual(path, urlData);\n        done();\n      });\n    });\n\n    it('reads in file paths', (done) => {\n      const path = './spec/fixtures/Example.jpg';\n      FileUtils.getData(path).then((urlData) => {\n        assert.deepEqual(fileData, urlData);\n        done();\n      });\n    });\n\n    it('rejects with error if no data is gettable', (done) => {\n      const path = 'malformed';\n      FileUtils.getData(path).then(() => {\n        throw new Error('Should not get here');\n      }).catch((err) => {\n        assert.isDefined(err);\n        done();\n      });\n    });\n  });\n\n  describe('sanitize', () => {\n    it('removes emoji', (done) => {\n      const input = 'frog emoji 🐸is best';\n      const expectedOutput = 'frog emoji is best';\n      assert.equal(FileUtils.sanitize(input), expectedOutput);\n      done();\n    });\n\n    it('replaces non alphanumeric chars', (done) => {\n      const input = 'abc 123 ,/^ =+% \\\\|?';\n      const expectedOutput = 'abc 123';\n      assert.equal(FileUtils.sanitize(input), expectedOutput);\n      done();\n    });\n\n    it('retains dashes and underscores', (done) => {\n      const input = 'Jack_Skellenberger (2016-2020)';\n      const expectedOutput = 'Jack_Skellenberger 2016-2020';\n      assert.equal(FileUtils.sanitize(input), expectedOutput);\n      done();\n    });\n  });\n});\n"
  },
  {
    "path": "spec/unit/lib/slack-client-spec.js",
    "content": "const assert = require('chai').assert;\nconst SlackClient = require('../../../lib/slack-client');\n\ndescribe('SlackClient', () => {\n  describe('constructor', () => {\n    const tierTwoLimits = SlackClient.rateLimitTier(2);\n    const tierThreeLimits = SlackClient.rateLimitTier(3);\n\n    it('defaults to using tier 2 rate limiting when no limits are specified', () => {\n      const slackClient = new SlackClient('subdomain', 'cookie');\n\n      assert.deepEqual(slackClient.options, tierTwoLimits);\n    });\n\n    it('allows rate limit tier overrides to be set', () => {\n      const slackClient = new SlackClient('subdomain', 'cookie', tierThreeLimits);\n\n      assert.deepEqual(slackClient.options, tierThreeLimits);\n    });\n\n    it('overrides passed variables with environment variables when present', () => {\n      process.env.SLACK_REQUEST_CONCURRENCY = 1;\n      process.env.SLACK_REQUEST_RATE = 2;\n      process.env.SLACK_REQUEST_WINDOW = 3;\n\n      const slackClient = new SlackClient('subdomain', 'cookie', tierThreeLimits);\n\n      assert.include(slackClient.throttle, {\n        concurrent: '1',\n        rate: '2',\n        ratePer: '3',\n      });\n\n      delete process.env.SLACK_REQUEST_CONCURRENCY;\n      delete process.env.SLACK_REQUEST_RATE;\n      delete process.env.SLACK_REQUEST_WINDOW;\n    });\n  });\n});\n"
  },
  {
    "path": "spec/unit/lib/util/cli-spec.js",
    "content": "const chai = require('chai');\n\nconst assert = chai.assert;\nconst commander = require('commander');\nconst Cli = require('../../../../lib/util/cli');\n\ndescribe('Cli', () => {\n  describe('unpackAuthJson', () => {\n    let program;\n\n    beforeEach(() => {\n      program = new commander.Command();\n      Cli.requireAuth(program);\n    });\n\n    it('is ignored if no auth json is specified', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'download',\n        '--subdomain', 'subdomain',\n        '--token', 'token',\n        '--cookie', 'cookie',\n        '--auth-json', '{}',\n      ];\n\n      program.parse(process.argv);\n\n      Cli.unpackAuthJson(program);\n      assert.deepEqual(program.subdomain, ['subdomain']);\n      assert.deepEqual(program.token, ['token']);\n      assert.deepEqual(program.cookie, ['cookie']);\n    });\n\n    it('is can be used once, alone', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'download', // arbitrary\n        '--auth-json', '{\"subdomain\":\"subdomain\", \"token\":\"token\", \"cookie\":\"cookie\"}',\n      ];\n\n      program.parse(process.argv);\n\n      Cli.unpackAuthJson(program);\n      assert.deepEqual(program.subdomain, ['subdomain']);\n      assert.deepEqual(program.token, ['token']);\n      assert.deepEqual(program.cookie, ['cookie']);\n    });\n\n    it('can be used repeatedly', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'download',\n        '--auth-json', '{\"subdomain\":\"subdomain1\", \"token\":\"token1\", \"cookie\":\"cookie1\"}',\n        '--auth-json', '{\"subdomain\":\"subdomain2\", \"token\":\"token2\", \"cookie\":\"cookie2\"}',\n      ];\n\n      program.parse(process.argv);\n\n      Cli.unpackAuthJson(program);\n      assert.deepEqual(program.subdomain, ['subdomain1', 'subdomain2']);\n      assert.deepEqual(program.token, ['token1', 'token2']);\n      assert.deepEqual(program.cookie, ['cookie1', 'cookie2']);\n    });\n\n    it('can be used in conjunction with --subdomain, --token, and --cookie flags', () => {\n      process.argv = [\n        'node',\n        'emojme',\n        'download',\n        '--subdomain', 'subdomain1',\n        '--token', 'token1',\n        '--cookie', 'cookie1',\n        '--auth-json', '{\"subdomain\":\"subdomain2\", \"token\":\"token2\", \"cookie\":\"cookie2\"}',\n        '--subdomain', 'subdomain3',\n        '--token', 'token3',\n        '--cookie', 'cookie3',\n      ];\n\n      program.parse(process.argv);\n\n      Cli.unpackAuthJson(program);\n      assert.deepEqual(program.subdomain, ['subdomain1', 'subdomain3', 'subdomain2']);\n      assert.deepEqual(program.token, ['token1', 'token3', 'token2']);\n      assert.deepEqual(program.cookie, ['cookie1', 'cookie3', 'cookie2']);\n    });\n  });\n});\n"
  },
  {
    "path": "spec/unit/lib/util/helpers-spec.js",
    "content": "const chai = require('chai');\n\nconst assert = chai.assert;\nconst commander = require('commander');\nconst Helpers = require('../../../../lib/util/helpers');\nconst Cli = require('../../../../lib/util/cli');\n\ndescribe('Helpers', () => {\n  describe('zipAuthTuples', () => {\n    it('zips together equal length subdomain and token lists', () => {\n      const subdomains = ['subdomain 1'];\n      const tokens = ['token 1'];\n      const cookies = ['cookie 1'];\n      const options = {};\n\n      const [authTuples, srcPairs, dstPairs] = Helpers.zipAuthTuples(\n        subdomains,\n        tokens,\n        cookies,\n        options,\n      );\n\n      assert.deepEqual(authTuples, [['subdomain 1', 'token 1', 'cookie 1']]);\n      assert.deepEqual(srcPairs, []);\n      assert.deepEqual(dstPairs, []);\n    });\n\n    it('zips src and dst auth pairs when given', () => {\n      const subdomains = ['subdomain 1'];\n      const tokens = ['token 1'];\n      const cookies = ['cookie 1'];\n      const options = {\n        srcSubdomains: ['src subdomain 1', 'src subdomain 2'],\n        srcTokens: ['src token 1', 'src token 2'],\n        srcCookies: ['src cookie 1', 'src cookie 2'],\n        dstSubdomains: ['dst subdomain 1'],\n        dstTokens: ['dst token 1'],\n        dstCookies: ['dst cookie 1'],\n      };\n\n      const [authTuples, srcPairs, dstPairs] = Helpers.zipAuthTuples(\n        subdomains,\n        tokens,\n        cookies,\n        options,\n      );\n\n      const expectedSrcPairs = [['src subdomain 1', 'src token 1', 'src cookie 1'], ['src subdomain 2', 'src token 2', 'src cookie 2']];\n      const expectedDstPairs = [['dst subdomain 1', 'dst token 1', 'dst cookie 1']];\n\n      assert.deepEqual(authTuples, [['subdomain 1', 'token 1', 'cookie 1']].concat(expectedSrcPairs, expectedDstPairs));\n      assert.deepEqual(srcPairs, expectedSrcPairs);\n      assert.deepEqual(dstPairs, expectedDstPairs);\n    });\n\n    it('throws an error when auth pairs are mismatched', () => {\n      const subdomains = ['subdomain 1'];\n      const tokens = [];\n      const options = {};\n\n      assert.throws(\n        (() => { Helpers.zipAuthTuples(subdomains, tokens, options); }),\n        Error, /Invalid input/,\n      );\n    });\n\n    it('throws an error when src/dst auth pairs are mismatched', () => {\n      const subdomains = ['subdomain 1'];\n      const tokens = [];\n      const options = {\n        srcSubdomains: ['src subdomain 1', 'src subdomain 2'],\n        srcTokens: [],\n        dstSubdomains: ['dst subdomain 1'],\n        dstTokens: ['dst token 1'],\n      };\n\n      assert.throws(\n        (() => { Helpers.zipAuthTuples(subdomains, tokens, options); }),\n        Error, /Invalid input/,\n      );\n    });\n  });\n\n  describe('avoidCollisions', () => {\n    it('does not add id when adding unique emoji, even when emoji name slug space overlaps', () => {\n      const existingEmojiList = [\n        { name: 'emoji1' },\n      ];\n\n      const newEmojiList = [\n        { name: 'emoji' },\n      ];\n\n      const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList);\n      assert.deepEqual(result,\n        [{ name: 'emoji' }]);\n    });\n\n    it('adds id when a direct emoji collision is detected', () => {\n      const existingEmojiList = [\n        { name: 'emoji' },\n      ];\n\n      const newEmojiList = [\n        { name: 'emoji' },\n      ];\n\n      const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList);\n      assert.deepEqual(result,\n        [{ name: 'emoji-1', collision: 'emoji' }]);\n    });\n\n    it('adapts to new emoji name delimiter when one is present', () => {\n      const existingEmojiList = [];\n\n      const newEmojiList = [\n        { name: 'e_m_o_j_i' }, { name: 'e_m_o_j_i' },\n      ];\n\n      const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList);\n      assert.deepEqual(result, [\n        { name: 'e_m_o_j_i' },\n        { name: 'e_m_o_j_i_1', collision: 'e_m_o_j_i' },\n      ]);\n    });\n\n    it('adapts to uploaded emoji name delimiter when one is present', () => {\n      const existingEmojiList = [\n        { name: 'emoji1' },\n      ];\n\n      const newEmojiList = [\n        { name: 'emoji1' },\n      ];\n\n      const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList);\n      assert.deepEqual(result,\n        [{ name: 'emoji2', collision: 'emoji1' }]);\n    });\n\n    it('adds id to all but first emoji when multiple identical emoji names are added', () => {\n      const existingEmojiList = [];\n\n      const newEmojiList = [\n        { name: 'emoji' },\n        { name: 'emoji' },\n        { name: 'emoji' },\n        { name: 'emoji' },\n      ];\n\n      const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList);\n      assert.deepEqual(result, [\n        { name: 'emoji' },\n        { name: 'emoji-1', collision: 'emoji' },\n        { name: 'emoji-2', collision: 'emoji' },\n        { name: 'emoji-3', collision: 'emoji' },\n      ]);\n    });\n\n    it('gracefully folds in existing id\\'d emoji', () => {\n      const existingEmojiList = [{ name: 'emoji-2' }];\n\n      const newEmojiList = [\n        { name: 'emoji' },\n        { name: 'emoji' },\n        { name: 'emoji' },\n      ];\n\n      const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList);\n      assert.deepEqual(result, [\n        { name: 'emoji' },\n        { name: 'emoji-1', collision: 'emoji' },\n        { name: 'emoji-3', collision: 'emoji' },\n      ]);\n    });\n\n    it('does not clobber id\\'d new emoji names', () => {\n      const existingEmojiList = [{ name: 'emoji-1' }];\n\n      const newEmojiList = [\n        { name: 'emoji-1' },\n        { name: 'emoji-2' },\n        { name: 'emoji-3' },\n      ];\n\n      const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList);\n      assert.deepEqual(result, [\n        {\n          collision: 'emoji-1',\n          name: 'emoji-4',\n        },\n        { name: 'emoji-2' },\n        { name: 'emoji-3' },\n      ]);\n    });\n\n\n    it('gracefully folds in id\\'d new emoji', () => {\n      const existingEmojiList = [];\n\n      const newEmojiList = [\n        { name: 'emoji' },\n        { name: 'emoji-2' },\n        { name: 'emoji' },\n        { name: 'emoji' },\n      ];\n\n      const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList);\n      assert.deepEqual(result, [\n        { name: 'emoji' },\n        { name: 'emoji-2' },\n        { name: 'emoji-1', collision: 'emoji' },\n        { name: 'emoji-3', collision: 'emoji' },\n      ]);\n    });\n\n\n    it('does not increment numberal emoji names', () => {\n      const existingEmojiList = [\n        { name: '1984' },\n      ];\n\n      const newEmojiList = [\n        { name: '1984' }, { name: '1984' },\n      ];\n\n      const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList);\n      assert.deepEqual(result, [\n        { name: '1984-1', collision: '1984' },\n        { name: '1984-2', collision: '1984' },\n      ]);\n    });\n  });\n\n  describe('formatResultHash', () => {\n    it('organizes promise array output into more easily indexable hash', () => {\n      const promiseArrayResult = [\n        {\n          subdomain: 'subdomain1',\n          result1: 'first part of results',\n          result2: ['second', 'part', 'of', 'results'],\n          result3: {\n            third: 'part',\n            of: 'results',\n          },\n        },\n        {\n          subdomain: 'subdomain2',\n          result4: 'first part of results',\n          result5: ['second', 'part', 'of', 'results'],\n          result6: {\n            third: 'part',\n            of: 'results',\n          },\n        },\n      ];\n      assert.deepEqual(Helpers.formatResultsHash(promiseArrayResult), {\n        subdomain1: {\n          result1: 'first part of results',\n          result2: ['second', 'part', 'of', 'results'],\n          result3: {\n            third: 'part',\n            of: 'results',\n          },\n        },\n        subdomain2: {\n          result4: 'first part of results',\n          result5: ['second', 'part', 'of', 'results'],\n          result6: {\n            third: 'part',\n            of: 'results',\n          },\n        },\n      });\n    });\n  });\n});\n"
  }
]