Repository: jackellenberger/emojme Branch: master Commit: 03f337b4f9cc Files: 61 Total size: 467.1 KB Directory structure: gitextract_k03dkd1k/ ├── .circleci/ │ └── config.yml ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .jsdoc.json ├── CHANGELOG.md ├── README.md ├── USAGE.md ├── create-json.sh ├── docs/ │ ├── emojme-add.js.html │ ├── emojme-download.js.html │ ├── emojme-favorites.js.html │ ├── emojme-sync.js.html │ ├── emojme-upload.js.html │ ├── emojme-user-stats.js.html │ ├── index.html │ ├── module-add.html │ ├── module-download.html │ ├── module-favorites.html │ ├── module-sync.html │ ├── module-upload.html │ ├── module-userStats.html │ ├── scripts/ │ │ ├── linenumber.js │ │ └── pagelocation.js │ └── styles/ │ ├── jsdoc-default.css │ ├── prettify-jsdoc.css │ └── prettify-tomorrow.css ├── emojme-add.js ├── emojme-download.js ├── emojme-favorites.js ├── emojme-sync.js ├── emojme-upload.js ├── emojme-user-stats.js ├── emojme.js ├── lib/ │ ├── client-boot.js │ ├── emoji-add.js │ ├── emoji-admin-list.js │ ├── logger.js │ ├── slack-client.js │ └── util/ │ ├── cli.js │ ├── file-utils.js │ └── helpers.js ├── package.json ├── scripts/ │ └── usage.sh └── spec/ ├── e2e/ │ └── emojme-download.js ├── fixtures/ │ ├── clientBoot.json │ ├── emojiList.json │ └── emojiList.yaml ├── integration/ │ ├── emojme-add-spec.js │ ├── emojme-download-spec.js │ ├── emojme-favorites-spec.js │ ├── emojme-sync-spec.js │ ├── emojme-upload-spec.js │ └── emojme-user-stats-spec.js ├── spec-helper.js └── unit/ └── lib/ ├── emoji-add-spec.js ├── emoji-admin-list-spec.js ├── file-utils-spec.js ├── slack-client-spec.js └── util/ ├── cli-spec.js └── helpers-spec.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ # Javascript Node CircleCI 2.0 configuration file # # Check https://circleci.com/docs/2.0/language-javascript/ for more details # version: 2 jobs: build: docker: # specify the version you desire here - image: circleci/node:10.13.0-jessie # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images # documented at https://circleci.com/docs/2.0/circleci-images/ # - image: circleci/mongo:3.4.4 working_directory: ~/emojme steps: - checkout # Download and cache dependencies - restore_cache: keys: - v1-dependencies-{{ checksum "package.json" }} # fallback to using the latest cache if no exact match is found - v1-dependencies- - run: npm install - save_cache: paths: - node_modules key: v1-dependencies-{{ checksum "package.json" }} # run tests! - run: npm test ================================================ FILE: .eslintignore ================================================ docs/ ================================================ FILE: .eslintrc.json ================================================ { "extends": "airbnb-base", "rules": { "no-console": "off", "no-plusplus": "off", "no-param-reassign": "off", "linebreak-style": "off", "prefer-destructuring": "off", "guard-for-in": "off", "no-restricted-syntax": "off", "no-cond-assign": "off", "no-multi-assign": "off", "max-len": [ "error", 100, 2, { "ignoreUrls": true, "ignoreComments": true, "ignoreRegExpLiterals": true, "ignoreStrings": true, "ignoreTemplateLiterals": true } ] }, "env": { "commonjs": true, "node": true, "mocha": true } } ================================================ FILE: .gitignore ================================================ # Script results build/ log/* # Secret inputs .env .config # node projects amirite node_modules/ package-lock.json emojme@* # Moving files around *.old # OSX Garb .DS_Store ================================================ FILE: .jsdoc.json ================================================ { "tags": { "allowUnknownTags": true, "dictionaries": ["jsdoc"] }, "source": { "include": [".", "lib", "package.json", "README.md"], "includePattern": ".js$", "excludePattern": "(node_modules/|docs|spec)" }, "plugins": [ "plugins/markdown" ], "templates": { "referenceTitle": "emojme", "disableSort": false, "collapse": true }, "opts": { "destination": "./docs/", "encoding": "utf8", "private": true, "recurse": true, "template": "./node_modules/jsdoc-template" } } ================================================ FILE: CHANGELOG.md ================================================ # 2.0.0 * Require cookie tokens and cookies >:[ * All operations that previously required a (subdomain, token) tuple now require a (subdomain, token, cookie) tuple. * This means the addition of a `--cookie` command line argument. * cookie is also now the third ordered argument in emojme module methods. * Check the readme for how to collect a cookie. * alias --subdomain to --domain for kicks * Reduce adminList request rate slightly to dodge rate limiting. * AuthPairs are now AuthTuples as they represent subdomain, token, and cookie. # 1.9.1 * Add Emojme chrome extension to README * Resolve (#59), sanitizing user names for disk interaction # 1.9.0 * Clean up README readablility * Add `--since` option to download, user-stats, and sync * Add `--dry-run` option to emojme sync # 1.8.1 * Add `--lite` option to emojme favorites. * Does not download complete adminList * returns only emoji name and usage count in `favoriteEmojiAdminList` * adds a little more documentation around `allowCollisions` # 1.8.0 * Add confusingly named `allowCollisions` to `add` and `upload` endpoints alongside existing `avoidCollisions` param * 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. * 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. # 1.7.0 * Add /client.boot endpoint accessor * Add emojme favorites function to find a user's favorite emoji * This comprises the content of the `Frequently Used` emoji box * Also includes personal emoji usage counts (!?) # 1.6.3 * Update README to reflect slack's new api_token location * Fix rate limiting for good this time * Resolve npm audit vulnerability # 1.6.0 * Implement rate limiting * rate varies depending on endpoint * Can be overridden but new environment variables * SLACK_REQUEST_CONCURRENCY * SLACK_REQUEST_RATE * SLACK_REQUEST_WINDOW * Add naive backoff logic * Add timestamps to logs # 1.5.1 * Resolve npm audit vulnerability # 1.5.0 * Rework logging to be less noisy and more organized. * Use Winston * log warning and worse to the console * log everything to log/combined.log * Add verbosity control * Fix bug related to incorrect upload summary output # 1.4.0 * Revamp download * `--save` can no longer be called with 'all' (but that never worked) * `--save-all-by-user` added to save all emoji by all users into build/$subdomain/$user * `--save-all` added to save all emoji to build/$subdomain * Add jsdoc documentation, available at https://jackellenberger.github.io/emojme * Configure circle ci * Clarify what a user token should look like # 1.3.3 * Fix bug preventing correct package contents from being uploaded to npm * Fix bug preventing empty slack instances from adding and syncing emoji # 1.3.2 * Add keywords, bin, etc to package.json * Add module usage instructions to readme # 1.3.1 * Create CHANGELOG.md * Allow certain required `Add` params to be nulled out by providing an empty string * For example, `add --src 'source.jpg' --name ''` will act identically to `add --src 'source.jpg'` * This resolves an issue where adding multiple emoji of different shapes (i.e. new vs alias vs default named new) could become misaligned * Add emojiList to output of `user-stats` for consistency and ease of use * Add `Cli` methods to ease testing. * Resolve issue where repeated invocation of cli from a single process could pollute commander args ================================================ FILE: README.md ================================================ # [emojme](https://github.com/jackellenberger/emojme) - [Documentation](https://jackellenberger.github.io/emojme) ## Table of Contents * [Project Overview](#what-it-is) * [Breaking Changes](#breaking-changes) * [2.0.0](#2-0-0) * [Requirements](#requirements) * [Installation](#installation) * [Getting a slack token](#finding-a-slack-token) * [Getting a slack cookie](#finding-a-slack-cookie) * [Usage](#usage) * [Command Line](#usage) * [Module](#module) * [Build directory output](#build-directory-output) * [A closer look at options](#a-closer-look-at-options) * [Add vs Upload](#whats-the-difference-between-add-and-upload) * [CLI Examples](#cli-examples) * [Download](#emojme-download) * [Add](#emojme-add) * [Upload](#emojme-upload) * [Sync](#emojme-sync) * [User Stats](#emojme-user-stats) * [Favorites](#emojme-favorites) * [Pro Moves](#pro-moves-promoves) * [Rate Limiting](#rate-limiting-and-you) * [FAQ](#faq) * [Other Projects of Note](#inspirations) ## What it is Emojme is a set of tools to manage your Slack emoji, either directly from the command line or from within your own Javascript project. Primary features are: * Uploading new emoji * Individually, by passing a file or url * In bulk, by passing a json "adminList" or a yaml "emojipack" file * To one or many slack instances at once * Download existing emoji * From one or many slack instances * Download all emoji * Download some emoji * Sync emoji between mulitple slack instance * One to one, one to many, many to one, or many to many * Analyze emoji authorship * Who makes the most emoji in your slack instance? * Analyze emoji usage * Which emoji do you use most? jsdocs are available at [https://jackellenberger.github.io/emojme](https://jackellenberger.github.io/emojme). Read em. ## Breaking Changes ### 2.0.0 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? - 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)`. - 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`. - Read on for examples and instructions on how to collect your cookie from the jar. ## Requirements To 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: * Cookie tokens can be grabbed from any logged in slack webpage by following [these instructions](#finding-a-slack-token). * Auth Cookies are grabbed with even more difficulty, again from logged in slack pages, following [these instructions](#finding-a-slack-cookie). * 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. * 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. * 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. ## Installation ### Command Line Via npm ```bash $ (nvm use 10 || nvm install 10) && npm install emojme $ npx emojme [command] [options] ``` Via github ```bash $ git clone https://github.com/jackellenberger/emojme.git $ cd emojme $ node ./emojme [command] [options] ``` 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 [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. ### Finding a slack token Update 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}`. As 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. Update 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 💨. #### Cookie Token One-liner To extract the Slack cookie token, run the following script in your devtools console while being logged into your Slack team: ```js JSON.parse(localStorage.localConfig_v2).teams[document.location.pathname.match(/^\/client\/([TE][A-Z0-9]+)/)[1]].token ``` Thanks again to @mootari for finding this (and all of `localStorage.localConfig_v2`!) #### Finding a slack cookie As 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. 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. [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. ![So easy! So Fun! With just one chrome extension!](/images/emojme-chrome-extension.jpg) You 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. ![I have an MFA in drawing with a mouse](/images/how-to-get-a-cookie.jpg) ## Usage Emojme can be used either as a command line tool or as a node module to be mixed in with your existing projects. Complete CLI flags can be found in [USAGe.md](USAGE.md), but each command takes the `--help` option. ### Module In your project's directory ```bash npm install --save emojme ``` In your project ```node var emojme = require('emojme'); // emojme-download var downloadOptions = { save: ['username_1', 'username_2'], // Download the emoji source files for these two users bustCache: true, // make sure this data is fresh output: true // download the adminList to ./build }; var downloadResults = await emojme.download('mySubdomain', 'myToken', 'myCookie', downloadOptions); console.log(downloadResults); /* { mySubdomain: { emojiList: [ { name: 'emoji-from-mySubdomain', ... }, ... ], saveResults: [ './build/mySubdomain/username_1/an_emoji.jpg', './build/mySubdomain/username_1/another_emoji.gif', ... all of username_1's emoji './build/mySubdomain/username_2/some_emoji.jpg', './build/mySubdomain/username_2/some_other_emoji.gif', ... all of username_2's emoji ] } } */ // emojme-upload var uploadOptions = { src: './emoji-list.json', // upload all the emoji in this json array of objects avoidCollisions: true, // append '-1' or similar if we try to upload a dupe prefix: 'new-' // prepend every emoji in src with "new-", e.g. "emoji" becomes "new-emoji" }; var uploadResults = await emojme.upload('mySubdomain', 'myToken', 'myCookie', uploadOptions); console.log(uploadResults); /* { mySubdomain: { collisions: [ { name: an-emoji-that-already-exists-in-mySubdomain ... } ], emojiList: [ { name: emoji-from-emoji-list-json ... }, { name: emoji-from-emoji-list-json ... }, ... ] } } */ // emojme-add var addOptions = { src: ['./emoji1.jpg', 'http://example.com/emoji2.png'], // upload these two images name: ['myLocalEmoji', 'myOnlineEmoji'], // call them these two names bustCache: false, // don't bother redownloading existing emoji avoidCollisions: true, // if there are similarly named emoji, change my new emoji names output: false // don't write any files }; var subdomains = ['mySubdomain1', 'mySubdomain2'] // can add one or multiple var tokens = ['myToken1', 'myToken2'] // can add one or multiple var addResults = await emojme.add(subdomains, tokens, addOptions); console.log(addResults); /* { mySubomain1: { collisions: [], // only defined if avoidCollisons = false emojiList: [ { name: 'myLocalEmoji', ... }, { name: 'myOnlineEmoji', ... }, ] }, mySubomain2: { collisions: [], // only defined if avoidCollisons = false emojiList: [ { name: 'myLocalEmoji', ... }, { name: 'myOnlineEmoji', ... }, ] } } */ // emojme-sync var syncOptions = { srcSubdomains: ['srcSubdomain'], // copy all emoji from srcSubdomain... srcTokens: ['srcToken'], dstSubdomains: ['dstSubdomain1', 'dstSubdomain2'], // ...to dstSubdomain1 and dstSubdomain2 dstTokens: ['dstToken1', 'dstToken2'], bustCache: true // get fresh lists to make sure we're not doing more lifting than we have to }; var syncResults = await emojme.sync(null, null, syncOptions); console.log(syncResults); /* { dstSubdomain1: { emojiList: [ { name: emoji-1-from-srcSubdomain ... }, { name: emoji-2-from-srcSubdomain ... } ] }, dstSubdomain2: { emojiList: [ { name: emoji-1-from-srcSubdomain ... }, { name: emoji-2-from-srcSubdomain ... } ] } } */ //emojme-user-stats var userStatsOptions = { user: ['username_1', 'username_2'] // get me some info on these two users }; var userStatsResults = await emojme.userStats('mySubdomain', 'myToken', 'myCookie', userStatsOptions); console.log(userStatsResults); /* { mySubdomain: { userStatsResults: [ { user: 'username_1', userEmoji: [{ all username_1's emoji }], subdomain: mySubdomain, originalCount: x, aliasCount: y, totalCount: x + y, percentage: (x + y) / mySubdomain's total emoji count }, { user: 'username_2', userEmoji: [{ all username_2's emoji }], subdomain: mySubdomain, originalCount: x, aliasCount: y, totalCount: x + y, percentage: (x + y) / mySubdomain's total emoji count } ] } } */ //emojme-favorites var favoritesResult = await emojme.favorites('mySubdomain', 'myToken', 'myCookie', {}); console.log(favoritesResult); /* { mySubdomain: { favoritesResult: { user: '{myToken's user}', favoriteEmoji: [ emojiName, ... ], favoriteEmojiAdminList: [ {emojiName}: {adminList-style emoji object, with additional `usage` value} ... ], } } } */ ``` ## Build directory output Okay you've run it, now what? Where are my dang emoji? * Diagnostic info and intermediate results are written to the build directory. Some might come in handy! * `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. * `build/$SUBDOMAIN.adminList.json` is the "master list" of a subdomain's emoji. Generated from `download` and `sync` calls. * `build/$USER.$SUBDOMAIN.adminList.json` is all the emoji created by a user. Generated from `user-stats` calls. * `build/diff.to-$SUBDOMAIN.from-$SUBDOMAINLIST.adminList.json` contains all emoji present in $SUBDOMAINLIST but not in $SUBDOMAIN. Generated from `sync` calls. ## A closer look at options * Universal options: * **requires** at least one `--subdomain`/`--token`/`--cookie` **auth tuple**. Can accept multiple auth tuples. * exception: sync can use a source/destination pattern, see below. * _optional_: `--bust-cache` will force a redownload of emoji adminlist. If not supplied, a redownload is forced every 24 hours. * _optional_: `--no-output` will prevent writing of files in the ./build directory. It does not currently suppres stdout. * `download` * **requires** at least one `--subdomain`/`--token`/`--cookie` **auth tuple**. Can accept multiple auth tuples. * _optional_: `--save $user` will save actual emoji data for the specified user, rather than just adminList json. Find the emoji in ./build/subdomain/user/ * _optional_: `--bust-cache` will force a redownload of emoji adminlist. If not supplied, a redownload is forced every 24 hours. * _optional_: `--no-output` will prevent writing of files in the ./build directory. It does not currently suppres stdout. * _optional_: `--since timestamp` will only download or save emoji created after the epoch time timestamp given, e.g. `1572064302751` * `upload` * **requires** at least one `--subdomain`/`--token`/`--cookie` **auth tuple**. Can accept multiple auth tuples. * **requires** at least one `--src` source json file. * Src json should contain a list of objects where each object contains a "name" and "url" for image source * 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. * 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. * _optional_: `--no-output` will prevent writing of files in the ./build directory. It does not currently suppres stdout. * `add` * **requires** at least one `--subdomain`/`--token`/`--cookie` **auth tuple**. Can accept multiple auth tuples. * **requires** one of the following: 1. `--src` path of local emoji file. * _optional_: `--name` name of the emoji being uploaded. If not provided, the file name will be used. 1. `--name` and `--alias-for` to create an alias called `$NAME` with the same image as `$ALIAS-FOR` * Multiple `--src`'s or `--name`/`--alias-for` pairs may be provided, but don't mix the patterns. You'll confuse yourself. * _optional_: `--no-output` will prevent writing of files in the ./build directory. It does not currently suppres stdout. * `user-stats` * **requires** at least one `--subdomain`/`--token`/`--cookie` **auth tuple**. Can accept multiple auth tuples. * With no optional parameters given, this will print the top 10 emoji contributors * _optional_: one of the following: 1. `--top` will show the top $TOP emoji contributors 1. `--user` will show statistics for $USER. Can accept multiple `--user` calls. * _optional_: `--bust-cache` will force a redownload of emoji adminlist. If not supplied, a redownload is forced every 24 hours. * _optional_: `--no-output` will prevent writing of files in the ./build directory. It does not currently suppres stdout. * _optional_: `--since timestamp` will count the author statistics of only those emoji created after the epoch time timestamp given, e.g. `1572064302751` * `sync` * **requires** one of the following: 1. at least **two** `--subdomain`/`--token`/`--cookie` **auth tuple**. Can accept more than two auth tuples. 1. at least **one** `--src-subdomain`/`--src-token` auth tuple and at least **one** `--dst-subdomain`/`--dst-token` auth tuples for "one way" syncing. * _optional_: `--bust-cache` will force a redownload of emoji adminlist. If not supplied, a redownload is forced every 24 hours. * _optional_: `--no-output` will prevent writing of files in the ./build directory. It does not currently suppres stdout. * _optional_: `--since timestamp` will count the author statistics of only those emoji created after the epoch time timestamp given, e.g. `1572064302751` * _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` * `favorites` * **requires** at least one `--subdomain`/`--token`/`--cookie` **auth tuple**. Can accept multiple auth tuples. * With no optional parameters given, this will print the token's user's 10 most used emoji * _optional_: `--top` _verbose cli usage only_ limits stdout to top N most used emoji * _optional_: `--usage` _verbose cli usage only_ prints not only the user's favorite emoji, but also the usage numbers. * _optional_: `--bust-cache` will force a redownload of emoji adminlist and boot data. If not supplied, a redownload is forced every 24 hours. * _optional_: `--no-output` will prevent writing of files in the ./build directory. It does not currently suppres stdout. ## What's the difference between `Add` and `Upload`? Input type and use case! Technically (and behind the scenes) these commands do the same thing, which is post emoji to Slack. The 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: * `name` (the name of the emoji duh) * `url` (the source content of the emoji. either a url, a file path, or a raw `data:` string) * `is_alias` (either 0 for non-aliases or 1 for aliases) * `alias_for` (name of the emoji to alias if the emoji being uploaded is an alias) There are other fields in an adminList, but no others are used at the current time. `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`. ## CLI Examples It 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. ### emojme download * Download all emoji from subdomain * `npx emojme download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE` * creates `./build/$SUBDOMAIN.adminList.json` containing url references to all emoji, but not the files themselves. * Download all emoji from subdomain using an authjson * `npx emojme download --auth-json '{"token":"$TOKEN","domain":"$SUBDOMAIN","cookie":"$COOKIE"}'` * creates `./build/$SUBDOMAIN.adminList.json` containing url references to all emoji, but not the files themselves. * Download all emoji from multiple subdomains * `npx emojme download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2` * creates `./build/$SUBDOMAIN1.adminList.json` and `./build/$SUBDOMAIN2.adminList.json` * download source content for emoji made by $USER1 and $USER2 in $SUBDOMAIN * `npx emojme download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --save $USER1 --save $USER2` * This will create directories `./build/$SUBDOMAIN/$USER1/` and `./build/$SUBDOMAIN/$USER2/`, each containing that user's raw emoji image files * download source content for all emoji in $SUBDOMAIN, grouping by user * `npx emojme download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --save-all` * This will create directories `./build/$SUBDOMAIN/$USER/` for each user in $SUBDOMAIN that has created an emoji ### emojme add * add $FILE as :$NAME: and $URL as :$NAME2: to subdomain * `npx emojme add --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src $FILE --name $NAME --src $URL --name $NAME2` * in $SUBDOMAIN1 and $SUBDOMAIN2, alias $ORIGINAL to $NAME * `npx emojme add --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 ---subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2 --alias-for '$ORIGINAL' --name '$NAME'` * Alias :$ORIGINAL: as :$NAME:, and if :$NAME: exists, alias as :$NAME-1: instead * `npx emojme add --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --name $NAME --alias_for $ORIGINAL --avoid-collisions` * 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. ### emojme upload * upload emoji from source json to subdomain * `npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src './myfile.json'` * upload emoji from source emojipacks yaml to subdomain * `npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src './emojipacks.yaml'` * upload emoji from source json to multiple subdomains * `npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2 --src './myfile.json'` * upload emoji from source json to subdomain, with each emoji being prefixed by $PREFIX * `npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src './myfile.json' --prefix '$PREFIX'` * upload emoji from source json to subdomain, with each emoji being suffixed if it conficts with an existing emoji * `npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src './myfile.json' --avoid-collisions` ### emojme-sync * sync emoji so that $SUBDOMAIN1 and $SUBDOMAIN2 have the same emoji* * *the same emoji names, that is. If :hi: is different on the two subdomains they will remain different * `npx emojme sync --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 --subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2` * sync emoji so that $SUBDOMAIN1, $SUBDOMAIN2, and $SUBDOMAIN3 have the same emoji * `npx emojme sync --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 --subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2 --subdomain $SUBDOMAIN3 --token $TOKEN3 --cookie $COOKIE3` * sync emoji from $SUBDOMAIN1 to $SUBDOMAIN2, so that $SUBDOMAIN1's emoji are a subset of $SUBDOMAIN2's emoji * `npx emojme sync --src-subdomain $SUBDOMAIN1 --src-token $TOKEN1 --dst-subdomain $SUBDOMAIN2 --dst-token $TOKEN2` * sync emoji from $SUBDOMAIN1 to $SUBDOMAIN2 and $SUBDOMAIN3 * `npx emojme sync --src-subdomain $SUBDOMAIN1 --src-token $TOKEN1 --dst-subdomain $SUBDOMAIN2 --dst-token $TOKEN2 --dst-subdomain $SUBDOMAIN3 --dst-token $TOKEN3` * sync emoji from $SUBDOMAIN1 and $SUBDOMAIN2 to $SUBDOMAIN3 * `npx emojme sync --src-subdomain $SUBDOMAIN1 --src-token $TOKEN1 --src-subdomain $SUBDOMAIN2 --src-token $TOKEN2 --dst-subdomain $SUBDOMAIN3 --dst-token $TOKEN3` ### emojme user stats These commands all write files to the build directory, but become more immediately useful with the `--verbose` flag. * get author statistics for user $USER (emoji upload count, etc) * `npx emojme user-stats --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --user $USER --verbose` * This will create json file `./build/$USER.$SUBDOMAIN.adminList.json` * get user statistics for multiple users * `npx emojme user-stats --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --user $USER --user $USER2 --user $USER3` * This will create json files `./build/$USERX.$SUBDOMAIN.adminList.json` for each user passed * get user statistics for top $N contributors * `npx emojme user-stats --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --top $N` * Defaults to top 10 users. ### emojme-favorites * Print the token's user's top 20 most used emoji * `npx emojme favorites --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 --top 20 --verbose` * Print the usage numbers for the user's top 10 most used emoji * `npx emojme favorites --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 --usage --verbose` ## Pro Moves :promoves: ### Creating a json file from a directory of images You 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`. ``` brew install jq ./create-json.sh $PATH ``` ### Getting a list of single attributes from an adminList json: Hey try this with $ATTRIBUTE of "url". You might need all those urls! ``` cat $ADMINLIST.json | jq '.[] | .["$ATTRIBUTE"]' ``` ### Rate limiting and you Slack [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. Though 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. 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: ```sh # 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 SLACK_REQUEST_CONCURRENCY # 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. SLACK_REQUEST_RATE # The unit of time, in ms. The lower the number the faster. SLACK_REQUEST_WINDOW # So, an example that has 10 in-flight requests at a time at a maximum rate of 200 requests per minute would be: SLACK_REQUEST_CONCURRENCY=10 \ SLACK_REQUEST_RATE=200 \ SLACK_REQUEST_WINDOW=60000 \ node emojme-download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --save-all --bust-cache ``` 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. ### FAQ * I'm getting `invalid_auth` errors? huh??? * 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. * I don't see any progress when I run a cli command * Do you have `--verbose` in your command? that's pretty useful. * My network requests are slow and jerky * 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. * I just want to upload this thing fast, but I have to download 20k emoji to upload one? * 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. ## Contributing 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: * Add tests * Make your change * Run tests `npm run test` or `npm run test:unit && npm run test:integration` * pro move: add a `debugger;` and use `it.only`, then `npm inspect node_modules/mocha/bin/_mocha spec/...` to debug a failing test. * Run end to end tests (requires a real slack instance) `npm run test:e2e -- --subdomain $YOUR_REAL_SUBDOMAIN --token $YOUR_REAL_TOKEN` * Lint * Regenerate docs, if necessary ## Inspirations * [emojipacks](https://github.com/lambtron/emojipacks) is my OG. It mostly worked but seems rather undermaintained. * [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. ## Stupid ways to use this stupid library! * https://github.com/jackellenberger/allmyemojichildren * https://github.com/guyfedwards/emoji * https://github.com/jackellenberger/emojme-hubot-plugin * https://github.com/jackellenberger/emojme-emoji-anywhere * https://github.com/jackellenberger/infinite-emoji-discord-bot ================================================ FILE: USAGE.md ================================================ # Commands ``` Usage: emojme [options] [command] Options: -V, --version output the version number -h, --help output usage information Commands: download download all emoji from given subdomain upload upload source emoji to given subdomain add upload source emoji to given subdomain user-stats get emoji statistics for given user on given subdomain sync get emoji statistics for given user on given subdomain favorites get favorite emoji and personal emoji usage statistics help [cmd] display help for [cmd] ``` ## emojme download ``` Usage: emojme-download [options] Options: -s, --subdomain slack subdomain. Can be specified multiple times, paired with respective token. (default: []) -d, --domain alias for --subdomain (default: []) -t, --token slack cookie token. ususaly starts xoxc-... Can be specified multiple times, paired with respective subdomains. User tokens (xoxs-...) are no longer supported. :( (default: []) -c, --cookie slack cookie. paired with respective subdomains and tokens. (default: []) -a --auth-json 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: []) --bust-cache force a redownload of all cached info. --no-output prevent writing of files in build/ and log/ --since only consider emoji since the given epoch timestamp --verbose log debug messages to console --save save all of 's emoji to disk at build/$subdomain/$user (default: []) --save-all save all emoji from all users to disk at build/$subdomain --save-all-by-user save all emoji from all users to disk at build/$subdomain/$user -h, --help output usage information ``` ## emojme upload ``` Usage: emojme-upload [options] Options: -s, --subdomain slack subdomain. Can be specified multiple times, paired with respective token. (default: []) -d, --domain alias for --subdomain (default: []) -t, --token slack cookie token. ususaly starts xoxc-... Can be specified multiple times, paired with respective subdomains. User tokens (xoxs-...) are no longer supported. :( (default: []) -c, --cookie slack cookie. paired with respective subdomains and tokens. (default: []) -a --auth-json 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: []) --bust-cache force a redownload of all cached info. --no-output prevent writing of files in build/ and log/ --since only consider emoji since the given epoch timestamp --verbose log debug messages to console --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. --avoid-collisions instead of culling collisions, rename the emoji to be uploaded "intelligently" --prefix prefix all emoji to be uploaded with --src source file(s) for emoji json or yaml you'd like to upload -h, --help output usage information ``` ## emojme add ``` Usage: emojme-add [options] Options: -s, --subdomain slack subdomain. Can be specified multiple times, paired with respective token. (default: []) -d, --domain alias for --subdomain (default: []) -t, --token slack cookie token. ususaly starts xoxc-... Can be specified multiple times, paired with respective subdomains. User tokens (xoxs-...) are no longer supported. :( (default: []) -c, --cookie slack cookie. paired with respective subdomains and tokens. (default: []) -a --auth-json 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: []) --bust-cache force a redownload of all cached info. --no-output prevent writing of files in build/ and log/ --since only consider emoji since the given epoch timestamp --verbose log debug messages to console --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. --avoid-collisions instead of culling collisions, rename the emoji to be uploaded "intelligently" --prefix prefix all emoji to be uploaded with --src source image/gif/#content for emoji you'd like to upload (default: null) --name name of the emoji from --src that you'd like to upload (default: null) --alias-for name of the emoji you'd like --name to be an alias of. Specifying this will negate --src (default: null) -h, --help output usage information ``` ## emojme user-stats ``` Usage: emojme-user-stats [options] Options: -s, --subdomain slack subdomain. Can be specified multiple times, paired with respective token. (default: []) -d, --domain alias for --subdomain (default: []) -t, --token slack cookie token. ususaly starts xoxc-... Can be specified multiple times, paired with respective subdomains. User tokens (xoxs-...) are no longer supported. :( (default: []) -c, --cookie slack cookie. paired with respective subdomains and tokens. (default: []) -a --auth-json 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: []) --bust-cache force a redownload of all cached info. --no-output prevent writing of files in build/ and log/ --since only consider emoji since the given epoch timestamp --verbose log debug messages to console --user slack user you'd like to get stats on. Can be specified multiple times for multiple users. (default: null) --top the top n users you'd like user emoji statistics on (default: 10) -h, --help output usage information ``` ## emojme sync ``` Usage: emojme-sync [options] Options: -s, --subdomain slack subdomain. Can be specified multiple times, paired with respective token. (default: []) -d, --domain alias for --subdomain (default: []) -t, --token slack cookie token. ususaly starts xoxc-... Can be specified multiple times, paired with respective subdomains. User tokens (xoxs-...) are no longer supported. :( (default: []) -c, --cookie slack cookie. paired with respective subdomains and tokens. (default: []) -a --auth-json 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: []) --bust-cache force a redownload of all cached info. --no-output prevent writing of files in build/ and log/ --since only consider emoji since the given epoch timestamp --verbose log debug messages to console --src-subdomain [value] subdomain from which to draw emoji for one way sync (default: null) --src-token [value] token with which to draw emoji for one way sync (default: null) --src-cookie [value] cookie with which to draw emoji for one way sync (default: null) --dst-subdomain [value] subdomain to which to emoji will be added is one way sync (default: null) --dst-token [value] token with which emoji will be added for one way sync (default: null) --dst-cookie [value] cookie with which emoji will be added for one way sync (default: null) --dry-run if set to true, nothing will be uploaded or synced -h, --help output usage information ``` ## emojme favorites ``` Usage: emojme-favorites [options] Options: -s, --subdomain slack subdomain. Can be specified multiple times, paired with respective token. (default: []) -d, --domain alias for --subdomain (default: []) -t, --token slack cookie token. ususaly starts xoxc-... Can be specified multiple times, paired with respective subdomains. User tokens (xoxs-...) are no longer supported. :( (default: []) -c, --cookie slack cookie. paired with respective subdomains and tokens. (default: []) -a --auth-json 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: []) --bust-cache force a redownload of all cached info. --no-output prevent writing of files in build/ and log/ --since only consider emoji since the given epoch timestamp --verbose log debug messages to console --top (verbose cli only) the top n favorites you'd like to see (default: 10) --usage (verbose cli only) print emoji usage of favorites in addition to their names --lite do not attempt to marry favorites with complete adminlist content. Results will contain only emoji name and usage count. -h, --help output usage information ``` ================================================ FILE: create-json.sh ================================================ ls $1 | jq -R "reduce . as \$i ({}; {\"src\": (\"$1/\" + \$i), \"name\": (\$i | sub(\".png\"; \"\") | sub(\".gif\"; \"\") | sub(\".jpg\"; \"\") | sub(\".jpeg\"; \"\"))})" | jq -s '.' > emoji.json ================================================ FILE: docs/emojme-add.js.html ================================================ emojme-add.js - Documentation

emojme-add.js

const _ = require('lodash');
const commander = require('commander');

const EmojiAdminList = require('./lib/emoji-admin-list');
const EmojiAdd = require('./lib/emoji-add');

const FileUtils = require('./lib/util/file-utils');
const Helpers = require('./lib/util/helpers');
const Cli = require('./lib/util/cli');
/** @module add */

/**
 * The add response object, like other response objects, is organized by input subdomain.
 * @typedef {object} addResponseObject
 * @property {object} subdomain each subdomain passed in to add will appear as a key in the response
 * @property {emojiList[]} subdomain.emojiList the list of emoji added to `subdomain`, with each element reflecting the parameters passed in to `add`
 * @property {emojiList[]} subdomain.collisions if `options.avoidCollisions` is `false`, emoji that cannot be uploaded due to existing conflicting emoji names will exist here
 */

/**
 * Add emoji described by parameters within options to the specified subdomain(s).
 *
 * 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.
 *
 * @async
 * @param {string|string[]} subdomains a single or list of subdomains to add emoji to. Must match respectively to `token`s and `cookie`s.
 * @param {string|string[]} tokens a single or list of tokens to add emoji to. Must match respectively to `subdomain`s and `cookie`s.
 * @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.
 * @param {object} options contains singleton or arrays of emoji descriptors.
 * @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
 * @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
 * @param {string|string[]} [options.aliasFor] names of emoji to be aliased to `options.name`
 * @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.
 * @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
 * @param {string} [options.prefix] string to prefix all emoji being uploaded
 * @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
 * @param {boolean} [options.output] if `false`, no files will be written during execution. Prevents saving of adminList for future use
 *
 * @returns {Promise<addResponseObject>} addResponseObject result object
 *
 * @example
var addOptions = {
  src: ['./emoji1.jpg', 'http://example.com/emoji2.png'], // upload these two images
  name: ['myLocalEmoji', 'myOnlineEmoji'], // call them these two names
  bustCache: false, // don't bother redownloading existing emoji
  avoidCollisions: true, // if there are similarly named emoji, change my new emoji names
  output: false // don't write any files
};
var subdomains = ['mySubdomain1', 'mySubdomain2'] // can add one or multiple
var tokens = ['myToken1', 'myToken2'] // can add one or multiple
var cookies = ['myCookie1', 'myCookie2'] // can add one or multiple
var addResults = await emojme.add(subdomains, tokens, cookies, addOptions);
console.log(userStatsResults);
// {
//   mySubomain1: {
//     collisions: [], // only defined if avoidCollisons = false
//     emojiList: [
//       { name: 'myLocalEmoji', ... },
//       { name: 'myOnlineEmoji', ... },
//     ]
//   },
//   mySubomain2: {
//     collisions: [], // only defined if avoidCollisons = false
//     emojiList: [
//       { name: 'myLocalEmoji', ... },
//       { name: 'myOnlineEmoji', ... },
//     ]
//   }
// }
 */
async function add(subdomains, tokens, cookies, options) {
  subdomains = Helpers.arrayify(subdomains);
  tokens = Helpers.arrayify(tokens);
  cookies = Helpers.arrayify(cookies);
  options = options || {};
  const aliases = Helpers.arrayify(options.aliasFor);
  const names = Helpers.arrayify(options.name);
  const sources = Helpers.arrayify(options.src);
  let inputEmoji = []; let name; let alias; let
    source;

  while (aliases.length || sources.length) {
    name = names.shift();
    if (source = sources.shift()) {
      inputEmoji.push({
        is_alias: 0,
        url: source,
        name: name || source.match(/(?:.*\/)?(.*).(jpg|jpeg|png|gif)/)[1],
      });
    } else {
      alias = aliases.shift();
      inputEmoji.push({
        is_alias: 1,
        alias_for: alias,
        name,
      });
    }
  }

  if (names.length || _.find(inputEmoji, ['name', undefined])) {
    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.'));
  }

  const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies);

  const addPromises = authTuples.map(async (authTuple) => {
    let emojiToUpload = []; let
      collisions = [];

    if (options.prefix) {
      inputEmoji = Helpers.applyPrefix(inputEmoji, options.prefix);
    }

    if (options.allowCollisions) {
      emojiToUpload = inputEmoji;
    } else {
      const existingEmojiList = await new EmojiAdminList(...authTuple, options.output)
        .get(options.bustCache);
      const existingNameList = existingEmojiList.map(e => e.name);

      if (options.avoidCollisions) {
        inputEmoji = Helpers.avoidCollisions(inputEmoji, existingEmojiList);
      }

      [collisions, emojiToUpload] = _.partition(inputEmoji,
        emoji => existingNameList.includes(emoji.name));
    }

    const emojiAdd = new EmojiAdd(...authTuple);
    return emojiAdd.upload(emojiToUpload).then((uploadResult) => {
      if (uploadResult.errorList && uploadResult.errorList.length > 1 && options.output) {
        FileUtils.writeJson(`./build/${this.subdomain}.emojiUploadErrors.json`, uploadResult.errorList);
      }
      return Object.assign({}, uploadResult, { collisions });
    });
  });

  return Helpers.formatResultsHash(await Promise.all(addPromises));
}

function addCli() {
  const program = new commander.Command();

  Cli.requireAuth(program);
  Cli.allowIoControl(program);
  Cli.allowEmojiAlterations(program)
    .option('--src <value>', 'source image/gif/#content for emoji you\'d like to upload', Cli.list, null)
    .option('--name <value>', 'name of the emoji from --src that you\'d like to upload', Cli.list, null)
    .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)
    .parse(process.argv);

  Cli.unpackAuthJson(program);

  return add(program.subdomain, program.token, program.cookie, {
    src: program.src,
    name: program.name,
    aliasFor: program.aliasFor,
    bustCache: program.bustCache,
    allowCollisions: program.allowCollisions,
    avoidCollisions: program.avoidCollisions,
    prefix: program.prefix,
    output: program.output,
  });
}


if (require.main === module) {
  addCli();
}

module.exports = {
  add,
  addCli,
};

================================================ FILE: docs/emojme-download.js.html ================================================ emojme-download.js - Documentation

emojme-download.js

const commander = require('commander');

const EmojiAdminList = require('./lib/emoji-admin-list');

const Cli = require('./lib/util/cli');
const Helpers = require('./lib/util/helpers');
/** @module download */

/**
 * The download response object, like other response objects, is organized by input subdomain.
 * @typedef {object} downloadResponseObject
 * @property {object} subdomain each subdomain passed in to add will appear as a key in the response
 * @property {emojiList[]} subdomain.emojiList the list of emoji downloaded from `subdomain`
 * @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.
 */

/**
 * 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.
 *
 * @async
 * @param {string|string[]} subdomains a single or list of subdomains from which to download emoji. Must match respectively to `token`s and `cookie`s.
 * @param {string|string[]} tokens a single or list of tokens with which to authenticate. Must match respectively to `subdomain`s and `cookie`s.
 * @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.
 * @param {object} options contains singleton or arrays of emoji descriptors.
 * @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>
 * @param {boolean} [options.saveAll] if `true`, download all emoji on slack instance from all users to disk in a single location.
 * @param {boolean} [options.saveAllByUser] if `true`, download all emoji on slack instance from all users to disk, organized into directories by user.
 * @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
 * @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
 * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file.
 *
 * @returns {Promise<downloadResponseObject>} downloadResponseObject result object
 *
 * @example
var downloadOptions = {
  save: ['username_1', 'username_2'], // Download the emoji source files for these two users
  bustCache: true, // make sure this data is fresh
  output: true // download the adminList to ./build
};
var downloadResults = await emojme.download('mySubdomain', 'myToken', 'myCookie', downloadOptions);
console.log(downloadResults);
// {
//   mySubdomain: {
//     emojiList: [
//       { name: 'emoji-from-mySubdomain', ... },
//       ...
//     ],
//     saveResults: [
//       './build/mySubdomain/username_1/an_emoji.jpg',
//       './build/mySubdomain/username_1/another_emoji.gif',
//       ... all of username_1's emoji
//       './build/mySubdomain/username_2/some_emoji.jpg',
//       './build/mySubdomain/username_2/some_other_emoji.gif',
//       ... all of username_2's emoji
//     ]
//   }
// }
 */
async function download(subdomains, tokens, cookies, options) {
  subdomains = Helpers.arrayify(subdomains);
  tokens = Helpers.arrayify(tokens);
  cookies = Helpers.arrayify(cookies);
  options = options || {};

  const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies);

  const downloadPromises = authTuples.map(async (authTuple) => {
    const subdomain = authTuple[0];
    let saveResults = [];

    const adminList = new EmojiAdminList(...authTuple, options.output);
    const emojiList = await adminList.get(options.bustCache, options.since);
    if ((options.save && options.save.length) || options.saveAll || options.saveAllByUser) {
      saveResults = saveResults.concat(await EmojiAdminList.save(emojiList, subdomain, {
        save: options.save, saveAll: options.saveAll, saveAllByUser: options.saveAllByUser,
      }));
    }

    return { emojiList, subdomain, saveResults };
  });

  return Helpers.formatResultsHash(await Promise.all(downloadPromises));
}
function downloadCli() {
  const program = new commander.Command();

  Cli.requireAuth(program);
  Cli.allowIoControl(program)
    .option('--save <user>', 'save all of <user>\'s emoji to disk at build/$subdomain/$user', Cli.list, [])
    .option('--save-all', 'save all emoji from all users to disk at build/$subdomain')
    .option('--save-all-by-user', 'save all emoji from all users to disk at build/$subdomain/$user')
    .parse(process.argv);
  Cli.unpackAuthJson(program);

  return download(program.subdomain, program.token, program.cookie, {
    save: program.save,
    saveAll: program.saveAll,
    saveAllByUser: program.saveAllByUser,
    bustCache: program.bustCache,
    output: program.output,
    since: program.since,
  });
}

if (require.main === module) {
  downloadCli();
}

module.exports = {
  download,
  downloadCli,
};

================================================ FILE: docs/emojme-favorites.js.html ================================================ emojme-favorites.js - Documentation

emojme-favorites.js

const _ = require('lodash');
const commander = require('commander');
const util = require('util');

const ClientBoot = require('./lib/client-boot');
const EmojiAdminList = require('./lib/emoji-admin-list');

const logger = require('./lib/logger');
const Cli = require('./lib/util/cli');
const FileUtils = require('./lib/util/file-utils');
const Helpers = require('./lib/util/helpers');
/** @module favorites */

/**
 * The user-specific favorites response object, like other response objects, is organized by input subdomain.
 * @typedef {object} favoritesResponseObject
 * @property {object} subdomain each subdomain passed in to add will appear as a key in the response
 * @property {string} subdomain.favoritesResult.user the username associated with the given cookie token
 * @property {string[]} subdomain.favoritesResult.favoriteEmoji the list of 'favorite' emoji as deemed by slack, in desc sorted order
 * @property {object[]} subdomain.favoritesResult.favoriteEmojiAdminList an array of emoji objects, as organized by emojiAdminList
 */

/**
 * Get the contents of the "Frequenly Used" box for your specified user
 *
 * @async
 * @param {string|string[]} subdomains a single or list of subdomains from which to analyze emoji. Must match respectively to `token`s and `cookie`s.
 * @param {string|string[]} tokens a single or list of tokens to add emoji to. Must match respectively to `subdomain`s and `cookie`s.
 * @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.
 * @param {object} options contains options on what to present
 * @param {Number} [options.lite] do not attempt to marry favorites with complete adminlist content. Results will contain only emoji name and usage count.
 * @param {Number} [options.top] (verbose cli only) count of top n emoji contriubtors you would like to retrieve user statistics on
 * @param {Number} [options.usage] (verbose cli only) print not just the list of favorite emoji, but their usage count
 * @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
 * @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
 * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file.
 *
 * @returns {Promise<favoritesResponseObject>} fovoritesResponseObject result object
 *
 * @example
var favoritesResult = await emojme.favorites('mySubdomain', 'myToken', 'myCookie', {});
console.log(favoritesResult);
// {
//   mySubdomain: {
//     favoritesResult: {
//         user: '{myToken's user}',
//         favoriteEmoji: [
//            emojiName,
//            ...
//         ],
//         favoriteEmojiAdminList: [
//           {emojiName}: {adminList-style emoji object, with additional `usage` value}
//           ...
//         ],
//       }
//   }
// }
 */
async function favorites(subdomains, tokens, cookies, options) {
  subdomains = Helpers.arrayify(subdomains);
  tokens = Helpers.arrayify(tokens);
  cookies = Helpers.arrayify(cookies);
  options = options || {};

  const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies);

  const favoritesPromises = authTuples.map(async (authTuple) => {
    let emojiList = [];
    if (!options.lite) {
      const emojiAdminList = new EmojiAdminList(...authTuple, options.output);
      emojiList = await emojiAdminList.get(options.bustCache);
    }

    const bootClient = new ClientBoot(...authTuple, options.output);
    const bootData = await bootClient.get(options.bustCache);
    const user = ClientBoot.extractName(bootData);
    const favoriteEmojiUsage = ClientBoot.extractEmojiUse(bootData);
    const favoriteEmojiList = favoriteEmojiUsage.map(e => e.name);
    const favoriteEmojiAdminList = _.reduce(favoriteEmojiUsage, (acc, usageObj) => {
      acc.push({
        [usageObj.name]: {
          ...EmojiAdminList.find(emojiList, usageObj.name),
          usage: usageObj.usage,
        },
      });
      return acc;
    }, []);

    const result = {
      user,
      subdomain: bootClient.subdomain,
      favoriteEmoji: favoriteEmojiList,
      favoriteEmojiAdminList,
    };

    const safeUserName = FileUtils.sanitize(result.user);
    if (options.output) FileUtils.writeJson(`./build/${safeUserName}.${bootClient.subdomain}.favorites.json`, result.favoriteEmojiAdminList, null, 3);

    const topNFavorites = util.inspect(
      (options.usage ? favoriteEmojiList : favoriteEmojiUsage)
        .slice(0, options.top),
    );
    logger.info(`[${bootClient.subdomain}] Favorite emoji for ${result.user}: ${topNFavorites}`);

    return { subdomain: bootClient.subdomain, favoritesResult: result };
  });

  return Helpers.formatResultsHash(_.flatten(await Promise.all(favoritesPromises)));
}

function favoritesCli() {
  const program = new commander.Command();

  Cli.requireAuth(program);
  Cli.allowIoControl(program);
  program
    .option('--top <value>', '(verbose cli only) the top n favorites you\'d like to see', 10)
    .option('--usage', '(verbose cli only) print emoji usage of favorites in addition to their names', false)
    .option('--lite', 'do not attempt to marry favorites with complete adminlist content. Results will contain only emoji name and usage count.', false)
    .parse(process.argv);
  Cli.unpackAuthJson(program);

  return favorites(program.subdomain, program.token, program.cookie, {
    top: program.top,
    usage: program.usage,
    lite: program.lite,
    bustCache: program.bustCache,
    output: program.output,
  }).catch((err) => {
    console.error('An error occurred: ', err);
  });
}

if (require.main === module) {
  favoritesCli();
}

module.exports = {
  favorites,
  favoritesCli,
};

================================================ FILE: docs/emojme-sync.js.html ================================================ emojme-sync.js - Documentation

emojme-sync.js

const _ = require('lodash');
const commander = require('commander');

const EmojiAdminList = require('./lib/emoji-admin-list');
const EmojiAdd = require('./lib/emoji-add');

const Cli = require('./lib/util/cli');
const FileUtils = require('./lib/util/file-utils');
const Helpers = require('./lib/util/helpers');
/** @module sync */

/**
 * The sync response object, like other response objects, is organized by input subdomain.
 * @typedef {object} syncResponseObject
 * @property {object} subdomain each subdomain passed in to add will appear as a key in the response
 * @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.
 */

/**
 * Sync emoji between slack subdomains
 *
 * 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.
 *
 * @async
 * @param {string|string[]|null} subdomains Two ore more subdomains that you wish to have the same emoji pool
 * @param {string|string[]|null} tokens cookie tokens corresponding to the given subdomains
 * @param {string|string[]|null} cookies User cookies corresponding to the given subdomains
 * @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.
 * @param {string|string[]} [options.srcSubdomains] slack instances from which to draw emoji. No additions will be made to these subdomains
 * @param {string|string[]} [options.srcTokens] tokens for the slack instances from which to draw emoji
 * @param {string|string[]} [options.srcCookies] cookies auth cookies for the slack instances from which to draw emoji
 * @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`
 * @param {string|string[]} [options.dstTokens] tokens for the slack instances where emoji will be deposited
 * @param {string|string[]} [options.dstCookies] cookies auth cookies for the slack instances from which to draw emoji
 * @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
 * @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
 * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file.
 *
 * @returns {Promise<syncResponseObject>} syncResponseObject result object
 *
 * @example
var syncOptions = {
  srcSubdomains: ['srcSubdomain'], // copy all emoji from srcSubdomain...
  srcTokens: ['srcToken'],
  srcCookies: ['srcCookie'],
  dstSubdomains: ['dstSubdomain1', 'dstSubdomain2'], // ...to dstSubdomain1 and dstSubdomain2
  dstTokens: ['dstToken1', 'dstToken2'],
  dstCookies: ['dstCookie1', 'dstCookie2'],
  bustCache: true // get fresh lists to make sure we're not doing more lifting than we have to
};
var syncResults = await emojme.sync(null, null, syncOptions);
console.log(syncResults);
// {
//   dstSubdomain1: {
//     emojiList: [
//       { name: emoji-1-from-srcSubdomain ... },
//       { name: emoji-2-from-srcSubdomain ... }
//     ]
//   },
//   dstSubdomain2: {
//     emojiList: [
//       { name: emoji-1-from-srcSubdomain ... },
//       { name: emoji-2-from-srcSubdomain ... }
//     ]
//   }
// }
 */
async function sync(subdomains, tokens, cookies, options) {
  let diffs;
  subdomains = Helpers.arrayify(subdomains);
  tokens = Helpers.arrayify(tokens);
  cookies = Helpers.arrayify(cookies);
  options = options || {};

  const [authTuples, srcPairs, dstPairs] = Helpers.zipAuthTuples(
    subdomains,
    tokens,
    cookies,
    options,
  );

  if (subdomains.length > 0) {
    const emojiLists = await Promise.all(
      authTuples.map(async authTuple => new EmojiAdminList(...authTuple, options.output)
        .get(options.bustCache, options.since)),
    );

    diffs = EmojiAdminList.diff(emojiLists, subdomains);
  } else if (srcPairs && dstPairs) {
    const srcDstPromises = [srcPairs, dstPairs].map(pairs => Promise.all(
      pairs.map(async pair => new EmojiAdminList(...pair, options.output)
        .get(options.bustCache, options.since)),
    ));

    const [srcEmojiLists, dstEmojiLists] = await Promise.all(srcDstPromises);
    diffs = EmojiAdminList.diff(
      srcEmojiLists, options.srcSubdomains, dstEmojiLists, options.dstSubdomains,
    );
  } else {
    throw new Error('Invalid Input');
  }

  const uploadedDiffPromises = diffs.map((diffObj) => {
    const pathSlug = `to-${diffObj.dstSubdomain}.from-${diffObj.srcSubdomains.join('-')}`;
    if (options.output) FileUtils.writeJson(`./build/${pathSlug}.emojiAdminList.json`, diffObj.emojiList);
    if (options.dryRun) return { subdomain: diffObj.dstSubdomain, emojiList: diffObj.emojiList };

    const emojiAdd = new EmojiAdd(diffObj.dstSubdomain, _.find(
      authTuples,
      [0, diffObj.dstSubdomain],
    )[1], options.output);
    return emojiAdd.upload(diffObj.emojiList).then((results) => {
      if (results.errorList && results.errorList.length > 0 && options.output) FileUtils.writeJson(`./build/${pathSlug}.emojiUploadErrors.json`, results.errorList);
      return results;
    });
  });

  return Helpers.formatResultsHash(await Promise.all(uploadedDiffPromises));
}

function syncCli() {
  const program = new commander.Command();

  Cli.requireAuth(program);
  Cli.allowIoControl(program)
    .option('--src-subdomain [value]', 'subdomain from which to draw emoji for one way sync', Cli.list, null)
    .option('--src-token [value]', 'token with which to draw emoji for one way sync', Cli.list, null)
    .option('--src-cookie [value]', 'cookie with which to draw emoji for one way sync', Cli.list, null)
    .option('--dst-subdomain [value]', 'subdomain to which to emoji will be added is one way sync', Cli.list, null)
    .option('--dst-token [value]', 'token with which emoji will be added for one way sync', Cli.list, null)
    .option('--dst-cookie [value]', 'cookie with which emoji will be added for one way sync', Cli.list, null)
    // Notice that this is missing --force and --prefix. These have been
    // deemed TOO POWERFUL for mortal usage. If you _really_ want that
    // power, you can download then upload the adminlist you retrieve.
    .option('--dry-run', 'if set to true, nothing will be uploaded or synced', false)
    .parse(process.argv);

  Cli.unpackAuthJson(program);

  return sync(program.subdomain, program.token, program.cookie, {
    srcSubdomains: program.srcSubdomain,
    srcTokens: program.srcToken,
    srcCookies: program.srcCookie,
    dstSubdomains: program.dstSubdomain,
    dstTokens: program.dstToken,
    dstCookies: program.dstCookie,
    bustCache: program.bustCache,
    output: program.output,
    since: program.since,
    dryRun: program.dryRun,
  });
}

if (require.main === module) {
  syncCli();
}

module.exports = {
  sync,
  syncCli,
};

================================================ FILE: docs/emojme-upload.js.html ================================================ emojme-upload.js - Documentation

emojme-upload.js

const _ = require('lodash');
const fs = require('graceful-fs');
const commander = require('commander');

const EmojiAdminList = require('./lib/emoji-admin-list');
const EmojiAdd = require('./lib/emoji-add');

const Cli = require('./lib/util/cli');
const FileUtils = require('./lib/util/file-utils');
const Helpers = require('./lib/util/helpers');
/** @module upload */

/**
 * The upload response object, like other response objects, is organized by input subdomain.
 * @typedef {object} syncResponseObject
 * @property {object} subdomain each subdomain passed in to add will appear as a key in the response
 * @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.
 * @property {emojiList[]} subdomain.collisions if `options.avoidCollisions` is `false`, emoji that cannot be uploaded due to existing conflicting emoji names will exist here
 */

/**
 * The required format of a json file that can be used as the `options.src` for {@link upload}
 *
 * To see an example, use {@link download}, then look at `buidl/*.adminList.json`
 *
 * @typedef {Array} jsonEmojiListFormat
 * @property {Array} emojiList
 * @property {object} emojiList.emojiObject
 * @property {string} emojiList.emojiObject.name the name of the emoji
 * @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
 * @property {string} emojiList.emojiObject.alias_for the name of the emoji this emoji is apeing
 * @property {string} emojiList.emojiObject.url the remote url or local path of the emoji
 * @property {string} emojiList.emojiObject.user_display_name the name of the emoji creator
 *
 * @example
 * [
 *   {
 *      "name": "a_giving_lovely_generous_individual",
 *      "is_alias": 1,
 *      "alias_for": "caleb"
 *   },
 *   {
 *     "name": "gooddoggy",
 *     "is_alias": 0,
 *     "alias_for": null,
 *     "url": "https://emoji.slack-edge.com/T3T9KQULR/gooddoggy/849f53cf1de25f97.png"
 *   }
 * ]
 */

/**
 * The required format of a yaml file that can be used as the `options.src` for {@link upload}
 * @typedef {object} yamlEmojiListFormat
 * @property {object} topLevelYaml all keys execpt for `emojis` are ignored
 * @property {Array} emojis the array of emoji objects
 * @property {object} emojis.emojiObject
 * @property {string} emojis.emojiObject.name the name of the emoji
 * @property {string} emojis.emojiObject.src alias for `name`
 * @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
 * @property {string} emojis.emojiObject.alias_for the name of the emoji this emoji is apeing
 * @property {string} emojis.emojiObject.url the remote url or local path of the emoji
 * @property {string} emojis.emojiObject.user_display_name the name of the emoji creator
 *
 * @example
 *  title: animals
 *  emojis:
 *    - name: llama
 *      src: http://i.imgur.com/6bKXKUP.gif
 *    - name: alpaca
 *      src: http://i.imgur.com/c6QxTbM.gif
 */

/**
 * Upload multiple emoji described by an existing list on disk, either as a json emoji admin list or emojipacks-like yaml.
 *
 * @async
 * @param {string|string[]} subdomains a single or list of subdomains from which to download emoji. Must match respectively to `token`s and `cookie`s.
 * @param {string|string[]} tokens a single or list of tokens with which to authenticate. Must match respectively to `subdomain`s and `cookie`s.
 * @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.
 * @param {object} options contains singleton or arrays of emoji descriptors.
 * @param {string|string[]} options.src source emoji list files for the emoji to be added. Can either be in {@link jsonEmojiListFormat} or {@link yamlEmojiListFormat}
 * @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
 * @param {string} [options.prefix] string to prefix all emoji being uploaded
 * @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
 * @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
 * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file.
 *
 * @returns {Promise<uploadResponseObject>} uploadResponseObject result object
 *
 * @example
var uploadOptions = {
  src: './emoji-list.json', // upload all the emoji in this json array of objects
  avoidCollisions: true, // append '-1' or similar if we try to upload a dupe
  prefix: 'new-' // prepend every emoji in src with "new-", e.g. "emoji" becomes "new-emoji"
};
var uploadResults = await emojme.upload('mySubdomain', 'myToken', 'myCookie', uploadOptions);
console.log(uploadResults);
// {
//   mySubdomain: {
//     collisions: [
//       { name: an-emoji-that-already-exists-in-mySubdomain ... }
//     ],
//     emojiList: [
//       { name: emoji-from-emoji-list-json ... },
//       { name: emoji-from-emoji-list-json ... },
//       ...
//     ]
//   }
// }
 */
async function upload(subdomains, tokens, cookies, options) {
  subdomains = Helpers.arrayify(subdomains);
  tokens = Helpers.arrayify(tokens);
  cookies = Helpers.arrayify(cookies);
  options = options || {};
  let inputEmoji;

  // TODO this isn't handling --src file --src file correctly
  if (Array.isArray(options.src)) {
    inputEmoji = options.src;
  } else if (!fs.existsSync(options.src)) {
    throw new Error(`Emoji source file ${options.src} does not exist`);
  } else {
    const fileType = options.src.split('.').slice(-1)[0];
    if (fileType.match(/yaml|yml/)) {
      inputEmoji = FileUtils.readYaml(options.src);
    } else if (fileType.match(/json/)) {
      inputEmoji = FileUtils.readJson(options.src);
    } else {
      throw new Error(`Filetype ${fileType} is not supported`);
    }
  }

  const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies);

  const uploadPromises = authTuples.map(async (authTuple) => {
    let emojiToUpload = []; let
      collisions = [];

    if (options.prefix) {
      inputEmoji = Helpers.applyPrefix(inputEmoji, options.prefix);
    }

    if (options.allowCollisions) {
      emojiToUpload = inputEmoji;
    } else {
      const existingEmojiList = await new EmojiAdminList(...authTuple, options.output)
        .get(options.bustCache);
      const existingNameList = existingEmojiList.map(e => e.name);

      if (options.avoidCollisions) {
        inputEmoji = Helpers.avoidCollisions(inputEmoji, existingEmojiList);
      }

      [collisions, emojiToUpload] = _.partition(inputEmoji,
        emoji => existingNameList.includes(emoji.name));
    }

    const emojiAdd = new EmojiAdd(...authTuple);
    const uploadResult = await emojiAdd.upload(emojiToUpload);
    return Object.assign({}, uploadResult, { collisions });
  });

  return Helpers.formatResultsHash(await Promise.all(uploadPromises));
}

function uploadCli() {
  const program = new commander.Command();

  Cli.requireAuth(program);
  Cli.allowIoControl(program);
  Cli.allowEmojiAlterations(program)
    .option('--src <value>', 'source file(s) for emoji json or yaml you\'d like to upload')
    .parse(process.argv);
  Cli.unpackAuthJson(program);

  return upload(program.subdomain, program.token, program.cookie, {
    src: program.src,
    bustCache: program.bustCache,
    allowCollisions: program.allowCollisions,
    avoidCollisions: program.avoidCollisions,
    prefix: program.prefix,
    output: program.output,
  });
}

if (require.main === module) {
  uploadCli();
}

module.exports = {
  upload,
  uploadCli,
};

================================================ FILE: docs/emojme-user-stats.js.html ================================================ emojme-user-stats.js - Documentation

emojme-user-stats.js

const _ = require('lodash');
const commander = require('commander');

const EmojiAdminList = require('./lib/emoji-admin-list');

const Cli = require('./lib/util/cli');
const FileUtils = require('./lib/util/file-utils');
const Helpers = require('./lib/util/helpers');
/** @module userStats */

/**
 * The user-specific userStats response object, like other response objects, is organized by input subdomain.
 * @typedef {object} userStatsResponseObject
 * @property {object} subdomain each subdomain passed in to add will appear as a key in the response
 * @property {emojiList[]} subdomain.emojiList the list of emoji downloaded from `subdomain`
 * @property {object[]} subdomain.userStatsResults an array of user stats objects
 * @property {object} subdomain.userStatsResults.userStatsObject An object containing several maybe-useful statistics, separated by user
 * @property {string} subdomain.userStatsResults.userStatsObject.user the name of the user in question
 * @property {emojiList[]} subdomain.userStatsResults.userStatsObject.userEmoji the emojiList the user authored
 * @property {string} subdomain.userStatsResults.userStatsObject.subdomain redundant :shrug:
 * @property {Number} subdomain.userStatsResults.userStatsObject.originalCount the number of original emoji the user has created
 * @property {Number} subdomain.userStatsResults.userStatsObject.aliasCount the number of emoji aliases the user has defined
 * @property {Number} subdomain.userStatsResults.userStatsObject.totalCount the number of original and aliases the user has created
 * @property {Number} subdomain.userStatsResults.userStatsObject.percentage the percentage of emoji in the given subdomain that the user is responsible for
 */

/**
 * Get a few useful-ish statistics for either specific users, or the top-n emoji creators
 *
 * @async
 * @param {string|string[]} subdomains a single or list of subdomains to analyze. Must match respectively to `token`s and `cookie`s.
 * @param {string|string[]} tokens a single or list of tokens to add emoji to. Must match respectively to `subdomain`s and `cookie`s.
 * @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.
 * @param {object} options contains options for what stats to present
 * @param {string|string[]} [options.user] user name or array of user names you would like to retrieve user statistics on. If specified, ignores `top`
 * @param {Number} [options.top] count of top n emoji contriubtors you would like to retrieve user statistics on
 * @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
 * @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
 * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file.
 *
 * @returns {Promise<userStatsResponseObject>} userStatsResponseObject result object
 *
 * @example
var userStatsOptions = {
  user: ['username_1', 'username_2'] // get me some info on these two users
};
var userStatsResults = await emojme.userStats('mySubdomain', 'myToken', 'myCookie', userStatsOptions);
console.log(userStatsResults);
// {
//   mySubdomain: {
//     userStatsResults: [
//       {
//         user: 'username_1',
//         userEmoji: [{ all username_1's emoji }],
//         subdomain: mySubdomain,
//         originalCount: x,
//         aliasCount: y,
//         totalCount: x + y,
//         percentage: (x + y) / mySubdomain's total emoji count
//       },
//       {
//         user: 'username_2',
//         userEmoji: [{ all username_2's emoji }],
//         subdomain: mySubdomain,
//         originalCount: x,
//         aliasCount: y,
//         totalCount: x + y,
//         percentage: (x + y) / mySubdomain's total emoji count
//       }
//     ]
//   }
// }
 */
async function userStats(subdomains, tokens, cookies, options) {
  subdomains = Helpers.arrayify(subdomains);
  tokens = Helpers.arrayify(tokens);
  cookies = Helpers.arrayify(cookies);
  const users = Helpers.arrayify(options.user);
  options = options || {};

  const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies);

  const userStatsPromises = authTuples.map(async (authTuple) => {
    const emojiAdminList = new EmojiAdminList(...authTuple, options.output);
    const emojiList = await emojiAdminList.get(options.bustCache, options.since);
    if (users && users.length > 0) {
      const results = EmojiAdminList.summarizeUser(emojiList, authTuple[0], users);
      return results.map((result) => {
        const safeUserName = FileUtils.sanitize(result.user);
        FileUtils.writeJson(`./build/${safeUserName}.${result.subdomain}.adminList.json`, result.userEmoji, null, 3);
        return { subdomain: authTuple[0], userStatsResults: results, emojiList };
      });
    }
    const results = EmojiAdminList.summarizeSubdomain(emojiList, authTuple[0], options.top);
    results.forEach((result) => {
      const safeUserName = FileUtils.sanitize(result.user);
      FileUtils.writeJson(`./build/${safeUserName}.${result.subdomain}.adminList.json`, result.userEmoji, null, 3);
    });

    return { subdomain: authTuple[0], userStatsResults: results, emojiList };
  });

  return Helpers.formatResultsHash(_.flatten(await Promise.all(userStatsPromises)));
}

function userStatsCli() {
  const program = new commander.Command();

  Cli.requireAuth(program);
  Cli.allowIoControl(program)
    .option('--user <value>', 'slack user you\'d like to get stats on. Can be specified multiple times for multiple users.', Cli.list, null)
    .option('--top <value>', 'the top n users you\'d like user emoji statistics on', 10)
    .parse(process.argv);
  Cli.unpackAuthJson(program);

  return userStats(program.subdomain, program.token, program.cookie, {
    user: program.user,
    top: program.top,
    bustCache: program.bustCache,
    output: program.output,
    since: program.since,
  });
}

if (require.main === module) {
  userStatsCli();
}

module.exports = {
  userStats,
  userStatsCli,
};

================================================ FILE: docs/index.html ================================================ Home - Documentation

emojme - Documentation

Table of Contents

What it is

Emojme is a set of tools to manage your Slack emoji, either directly from the command line or from within your own Javascript project.

Primary features are:

  • Uploading new emoji
    • Individually, by passing a file or url
    • In bulk, by passing a json "adminList" or a yaml "emojipack" file
    • To one or many slack instances at once
  • Download existing emoji
    • From one or many slack instances
    • Download all emoji
    • Download some emoji
  • Sync emoji between mulitple slack instance
    • One to one, one to many, many to one, or many to many
  • Analyze emoji authorship
    • Who makes the most emoji in your slack instance?
  • Analyze emoji usage
    • Which emoji do you use most?

jsdocs are available at https://jackellenberger.github.io/emojme. Read em.

Breaking Changes

2.0.0

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?

  • 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).
  • 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.
  • Read on for examples and instructions on how to collect your cookie from the jar.

Requirements

To use emojme you don't need a bot or a workspace admin account. In fact, ~only regular user tokens 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:

  • Cookie tokens can be grabbed from any logged in slack webpage by following these instructions.
  • Auth Cookies are grabbed with even more difficulty, again from logged in slack pages, following these instructions.
  • 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.
  • 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.
    • 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 and can't be easily (or at all?) accessed via javascript.

Installation

Command Line

Via npm

$ (nvm use 10 || nvm install 10) && npm install emojme
$ npx emojme [command] [options]

Via github

$ git clone https://github.com/jackellenberger/emojme.git
$ cd emojme
$ node ./emojme [command] [options]

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 Emojme: Emoji Anywhere 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 if you don't enjoy pre-rolls.

Finding a slack token

Update 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. If use see an auth error, this is probably the reason. Cookie tokens follow a similar form, but not the c: xoxc-(\w{12}|\w{10})-(\w{12}|\w{11})-\w{12}-\w{64}.

Slack for Web

It's easyish! Open and sign into the slack customization page, e.g. https://my.slack.com/customize, right click anywhere > inspect element. Open the console and paste:

window.prompt("your api token is: ", TS.boot_data.api_token)

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?:

javascript:(function()%7Bwindow.prompt(%22your%20api%20token%20is%3A%20%22%2C%20TS.boot_data.api_token)%7D)()

(Slack has invalidated @curtisgibby's sage advice here, but we still appreciate them)

Slack for Desktop (Has anyone tried this?)

This is a similar process, but requires an extra step depending on your platform.

  • OSX: run or add to your .bashrc: export SLACK_DEVELOPER_MENU=true; open -a /Applications/Slack.app
  • Windows: create a shortcut: C:\Windows\System32\cmd.exe /c " SET SLACK_DEVELOPER_MENU=TRUE && start C:\existing\path\to\slack.exe"
  • Linux: honestly probably the same as OSX :shrug:

With that done and slack open, open View > Developer > Toggle Webapp DevTools (shortcut super+option+i). This will give you a chromium inspector into which you can paste

console.log(window.boot_data.api_token)

Finding a slack cookie

As cookies are now required, and 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.

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. Emojme: Emoji Anywhere is such an extension, and is available in the chrome web store (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.

So easy! So Fun! With just one chrome extension!

You 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.

I have an MFA in drawing with a mouse

Usage

Emojme can be used either as a command line tool or as a node module to be mixed in with your existing projects.

Complete CLI flags can be found in USAGe.md, but each command takes the --help option.

Module

In your project's directory

npm install --save emojme

In your project

var emojme = require('emojme');

// emojme-download
var downloadOptions = {
  save: ['username_1', 'username_2'], // Download the emoji source files for these two users
  bustCache: true, // make sure this data is fresh
  output: true // download the adminList to ./build
};
var downloadResults = await emojme.download('mySubdomain', 'myToken', 'myCookie', downloadOptions);
console.log(downloadResults);
/*
  {
    mySubdomain: {
      emojiList: [
        { name: 'emoji-from-mySubdomain', ... },
        ...
      ],
      saveResults: [
        './build/mySubdomain/username_1/an_emoji.jpg',
        './build/mySubdomain/username_1/another_emoji.gif',
        ... all of username_1's emoji
        './build/mySubdomain/username_2/some_emoji.jpg',
        './build/mySubdomain/username_2/some_other_emoji.gif',
        ... all of username_2's emoji
      ]
    }
  }
*/

// emojme-upload
var uploadOptions = {
  src: './emoji-list.json', // upload all the emoji in this json array of objects
  avoidCollisions: true, // append '-1' or similar if we try to upload a dupe
  prefix: 'new-' // prepend every emoji in src with "new-", e.g. "emoji" becomes "new-emoji"
};
var uploadResults = await emojme.upload('mySubdomain', 'myToken', 'myCookie', uploadOptions);
console.log(uploadResults);
/*
  {
    mySubdomain: {
      collisions: [
        { name: an-emoji-that-already-exists-in-mySubdomain ... }
      ],
      emojiList: [
        { name: emoji-from-emoji-list-json ... },
        { name: emoji-from-emoji-list-json ... },
        ...
      ]
    }
  }
*/

// emojme-add
var addOptions = {
  src: ['./emoji1.jpg', 'http://example.com/emoji2.png'], // upload these two images
  name: ['myLocalEmoji', 'myOnlineEmoji'], // call them these two names
  bustCache: false, // don't bother redownloading existing emoji
  avoidCollisions: true, // if there are similarly named emoji, change my new emoji names
  output: false // don't write any files
};
var subdomains = ['mySubdomain1', 'mySubdomain2'] // can add one or multiple
var tokens = ['myToken1', 'myToken2'] // can add one or multiple
var addResults = await emojme.add(subdomains, tokens, addOptions);
console.log(addResults);
/*
  {
    mySubomain1: {
      collisions: [], // only defined if avoidCollisons = false
      emojiList: [
        { name: 'myLocalEmoji', ... },
        { name: 'myOnlineEmoji', ... },
      ]
    },
    mySubomain2: {
      collisions: [], // only defined if avoidCollisons = false
      emojiList: [
        { name: 'myLocalEmoji', ... },
        { name: 'myOnlineEmoji', ... },
      ]
    }
  }
*/

// emojme-sync
var syncOptions = {
  srcSubdomains: ['srcSubdomain'], // copy all emoji from srcSubdomain...
  srcTokens: ['srcToken'],
  dstSubdomains: ['dstSubdomain1', 'dstSubdomain2'], // ...to dstSubdomain1 and dstSubdomain2
  dstTokens: ['dstToken1', 'dstToken2'],
  bustCache: true // get fresh lists to make sure we're not doing more lifting than we have to
};
var syncResults = await emojme.sync(null, null, syncOptions);
console.log(syncResults);
/*
  {
    dstSubdomain1: {
      emojiList: [
        { name: emoji-1-from-srcSubdomain ... },
        { name: emoji-2-from-srcSubdomain ... }
      ]
    },
    dstSubdomain2: {
      emojiList: [
        { name: emoji-1-from-srcSubdomain ... },
        { name: emoji-2-from-srcSubdomain ... }
      ]
    }
  }
*/

//emojme-user-stats
var userStatsOptions = {
  user: ['username_1', 'username_2'] // get me some info on these two users
};
var userStatsResults = await emojme.userStats('mySubdomain', 'myToken', 'myCookie', userStatsOptions);
console.log(userStatsResults);
/*
  {
    mySubdomain: {
      userStatsResults: [
        {
          user: 'username_1',
          userEmoji: [{ all username_1's emoji }],
          subdomain: mySubdomain,
          originalCount: x,
          aliasCount: y,
          totalCount: x + y,
          percentage: (x + y) / mySubdomain's total emoji count
        },
        {
          user: 'username_2',
          userEmoji: [{ all username_2's emoji }],
          subdomain: mySubdomain,
          originalCount: x,
          aliasCount: y,
          totalCount: x + y,
          percentage: (x + y) / mySubdomain's total emoji count
        }
      ]
    }
  }
*/

//emojme-favorites
var favoritesResult = await emojme.favorites('mySubdomain', 'myToken', 'myCookie', {});
console.log(favoritesResult);
/*
  {
    mySubdomain: {
      favoritesResult: {
          user: '{myToken's user}',
          favoriteEmoji: [
             emojiName,
             ...
          ],
          favoriteEmojiAdminList: [
            {emojiName}: {adminList-style emoji object, with additional `usage` value}
            ...
          ],
        }
    }
  }
*/

Build directory output

Okay you've run it, now what? Where are my dang emoji?

  • Diagnostic info and intermediate results are written to the build directory. Some might come in handy!
  • 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.
  • build/$SUBDOMAIN.adminList.json is the "master list" of a subdomain's emoji. Generated from download and sync calls.
  • build/$USER.$SUBDOMAIN.adminList.json is all the emoji created by a user. Generated from user-stats calls.
  • build/diff.to-$SUBDOMAIN.from-$SUBDOMAINLIST.adminList.json contains all emoji present in $SUBDOMAINLIST but not in $SUBDOMAIN. Generated from sync calls.

A closer look at options

  • Universal options:

    • requires at least one --subdomain/--token/--cookie auth tuple. Can accept multiple auth tuples.
      • exception: sync can use a source/destination pattern, see below.
    • optional: --bust-cache will force a redownload of emoji adminlist. If not supplied, a redownload is forced every 24 hours.
    • optional: --no-output will prevent writing of files in the ./build directory. It does not currently suppres stdout.
  • download

    • requires at least one --subdomain/--token/--cookie auth tuple. Can accept multiple auth tuples.
    • optional: --save $user will save actual emoji data for the specified user, rather than just adminList json. Find the emoji in ./build/subdomain/user/
    • optional: --bust-cache will force a redownload of emoji adminlist. If not supplied, a redownload is forced every 24 hours.
    • optional: --no-output will prevent writing of files in the ./build directory. It does not currently suppres stdout.
    • optional: --since timestamp will only download or save emoji created after the epoch time timestamp given, e.g. 1572064302751
  • upload
    • requires at least one --subdomain/--token/--cookie auth tuple. Can accept multiple auth tuples.
    • requires at least one --src source json file.
      • Src json should contain a list of objects where each object contains a "name" and "url" for image source
      • 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.
      • 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.
    • optional: --no-output will prevent writing of files in the ./build directory. It does not currently suppres stdout.
  • add
    • requires at least one --subdomain/--token/--cookie auth tuple. Can accept multiple auth tuples.
    • requires one of the following:
      1. --src path of local emoji file.
        • optional: --name name of the emoji being uploaded. If not provided, the file name will be used.
      2. --name and --alias-for to create an alias called $NAME with the same image as $ALIAS-FOR
    • Multiple --src's or --name/--alias-for pairs may be provided, but don't mix the patterns. You'll confuse yourself.
    • optional: --no-output will prevent writing of files in the ./build directory. It does not currently suppres stdout.
  • user-stats
    • requires at least one --subdomain/--token/--cookie auth tuple. Can accept multiple auth tuples.
    • With no optional parameters given, this will print the top 10 emoji contributors
    • optional: one of the following:
      1. --top will show the top $TOP emoji contributors
      2. --user will show statistics for $USER. Can accept multiple --user calls.
    • optional: --bust-cache will force a redownload of emoji adminlist. If not supplied, a redownload is forced every 24 hours.
    • optional: --no-output will prevent writing of files in the ./build directory. It does not currently suppres stdout.
    • optional: --since timestamp will count the author statistics of only those emoji created after the epoch time timestamp given, e.g. 1572064302751
  • sync
    • requires one of the following:
      1. at least two --subdomain/--token/--cookie auth tuple. Can accept more than two auth tuples.
      2. at least one --src-subdomain/--src-token auth tuple and at least one --dst-subdomain/--dst-token auth tuples for "one way" syncing.
    • optional: --bust-cache will force a redownload of emoji adminlist. If not supplied, a redownload is forced every 24 hours.
    • optional: --no-output will prevent writing of files in the ./build directory. It does not currently suppres stdout.
    • optional: --since timestamp will count the author statistics of only those emoji created after the epoch time timestamp given, e.g. 1572064302751
    • 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
  • favorites
    • requires at least one --subdomain/--token/--cookie auth tuple. Can accept multiple auth tuples.
    • With no optional parameters given, this will print the token's user's 10 most used emoji
    • optional: --top verbose cli usage only limits stdout to top N most used emoji
    • optional: --usage verbose cli usage only prints not only the user's favorite emoji, but also the usage numbers.
    • optional: --bust-cache will force a redownload of emoji adminlist and boot data. If not supplied, a redownload is forced every 24 hours.
    • optional: --no-output will prevent writing of files in the ./build directory. It does not currently suppres stdout.

What's the difference between Add and Upload?

Input type and use case! Technically (and behind the scenes) these commands do the same thing, which is post emoji to Slack.

The 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:

  • name (the name of the emoji duh)
  • url (the source content of the emoji. either a url, a file path, or a raw data: string)
  • is_alias (either 0 for non-aliases or 1 for aliases)
  • alias_for (name of the emoji to alias if the emoji being uploaded is an alias) There are other fields in an adminList, but no others are used at the current time.

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.

CLI Examples

It 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.

emojme download

  • Download all emoji from subdomain

    • npx emojme download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE
    • creates ./build/$SUBDOMAIN.adminList.json containing url references to all emoji, but not the files themselves.
  • Download all emoji from subdomain using an authjson

    • npx emojme download --auth-json '{"token":"$TOKEN","domain":"$SUBDOMAIN","cookie":"$COOKIE"}'
    • creates ./build/$SUBDOMAIN.adminList.json containing url references to all emoji, but not the files themselves.
  • Download all emoji from multiple subdomains

    • npx emojme download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2
    • creates ./build/$SUBDOMAIN1.adminList.json and ./build/$SUBDOMAIN2.adminList.json
  • download source content for emoji made by $USER1 and $USER2 in $SUBDOMAIN

    • npx emojme download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --save $USER1 --save $USER2
    • This will create directories ./build/$SUBDOMAIN/$USER1/ and ./build/$SUBDOMAIN/$USER2/, each containing that user's raw emoji image files
  • download source content for all emoji in $SUBDOMAIN, grouping by user

    • npx emojme download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --save-all
    • This will create directories ./build/$SUBDOMAIN/$USER/ for each user in $SUBDOMAIN that has created an emoji

emojme add

  • add $FILE as :$NAME: and $URL as :$NAME2: to subdomain

    • npx emojme add --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src $FILE --name $NAME --src $URL --name $NAME2
  • in $SUBDOMAIN1 and $SUBDOMAIN2, alias $ORIGINAL to $NAME

    • npx emojme add --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 ---subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2 --alias-for '$ORIGINAL' --name '$NAME'
  • Alias :$ORIGINAL: as :$NAME:, and if :$NAME: exists, alias as :$NAME-1: instead

    • npx emojme add --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --name $NAME --alias_for $ORIGINAL --avoid-collisions
    • 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.

emojme upload

  • upload emoji from source json to subdomain

    • npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src './myfile.json'
  • upload emoji from source emojipacks yaml to subdomain

    • npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src './emojipacks.yaml'
  • upload emoji from source json to multiple subdomains

    • npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2 --src './myfile.json'
  • upload emoji from source json to subdomain, with each emoji being prefixed by $PREFIX

    • npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src './myfile.json' --prefix '$PREFIX'
  • upload emoji from source json to subdomain, with each emoji being suffixed if it conficts with an existing emoji

    • npx emojme upload --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --src './myfile.json' --avoid-collisions

emojme-sync

  • sync emoji so that $SUBDOMAIN1 and $SUBDOMAIN2 have the same emoji*

    • *the same emoji names, that is. If :hi: is different on the two subdomains they will remain different
    • npx emojme sync --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 --subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2
  • sync emoji so that $SUBDOMAIN1, $SUBDOMAIN2, and $SUBDOMAIN3 have the same emoji

    • npx emojme sync --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 --subdomain $SUBDOMAIN2 --token $TOKEN2 --cookie $COOKIE2 --subdomain $SUBDOMAIN3 --token $TOKEN3 --cookie $COOKIE3
  • sync emoji from $SUBDOMAIN1 to $SUBDOMAIN2, so that $SUBDOMAIN1's emoji are a subset of $SUBDOMAIN2's emoji

    • npx emojme sync --src-subdomain $SUBDOMAIN1 --src-token $TOKEN1 --dst-subdomain $SUBDOMAIN2 --dst-token $TOKEN2
  • sync emoji from $SUBDOMAIN1 to $SUBDOMAIN2 and $SUBDOMAIN3

    • npx emojme sync --src-subdomain $SUBDOMAIN1 --src-token $TOKEN1 --dst-subdomain $SUBDOMAIN2 --dst-token $TOKEN2 --dst-subdomain $SUBDOMAIN3 --dst-token $TOKEN3
  • sync emoji from $SUBDOMAIN1 and $SUBDOMAIN2 to $SUBDOMAIN3

    • npx emojme sync --src-subdomain $SUBDOMAIN1 --src-token $TOKEN1 --src-subdomain $SUBDOMAIN2 --src-token $TOKEN2 --dst-subdomain $SUBDOMAIN3 --dst-token $TOKEN3

emojme user stats

These commands all write files to the build directory, but become more immediately useful with the --verbose flag.

  • get author statistics for user $USER (emoji upload count, etc)

    • npx emojme user-stats --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --user $USER --verbose
    • This will create json file ./build/$USER.$SUBDOMAIN.adminList.json
  • get user statistics for multiple users

    • npx emojme user-stats --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --user $USER --user $USER2 --user $USER3
    • This will create json files ./build/$USERX.$SUBDOMAIN.adminList.json for each user passed
  • get user statistics for top $N contributors

    • npx emojme user-stats --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --top $N
    • Defaults to top 10 users.

emojme-favorites

  • Print the token's user's top 20 most used emoji

    • npx emojme favorites --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 --top 20 --verbose
  • Print the usage numbers for the user's top 10 most used emoji

    • npx emojme favorites --subdomain $SUBDOMAIN1 --token $TOKEN1 --cookie $COOKIE1 --usage --verbose

Pro Moves :promoves:

Getting a list of single attributes from an adminList json:

Hey try this with $ATTRIBUTE of "url". You might need all those urls!

cat $ADMINLIST.json | jq '.[] | .["$ATTRIBUTE"]'

Rate limiting and you

Slack threatened to release then released 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.

Though 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.

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:

# 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
SLACK_REQUEST_CONCURRENCY
# 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.
SLACK_REQUEST_RATE
# The unit of time, in ms. The lower the number the faster.
SLACK_REQUEST_WINDOW

# So, an example that has 10 in-flight requests at a time at a maximum rate of 200 requests per minute would be:
SLACK_REQUEST_CONCURRENCY=10 \
SLACK_REQUEST_RATE=200 \
SLACK_REQUEST_WINDOW=60000 \
node emojme-download --subdomain $SUBDOMAIN --token $TOKEN --cookie $COOKIE --save-all --bust-cache

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.

FAQ

  • I'm getting invalid_auth errors? huh???

    • 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.
  • I don't see any progress when I run a cli command

    • Do you have --verbose in your command? that's pretty useful.
  • My network requests are slow and jerky

    • That's how we gotta live under rate limiting. 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.
  • I just want to upload this thing fast, but I have to download 20k emoji to upload one?

    • 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.

Contributing

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:

  • Add tests
  • Make your change
  • Run tests npm run test or npm run test:unit && npm run test:integration
    • pro move: add a debugger; and use it.only, then npm inspect node_modules/mocha/bin/_mocha spec/... to debug a failing test.
  • Run end to end tests (requires a real slack instance) npm run test:e2e -- --subdomain $YOUR_REAL_SUBDOMAIN --token $YOUR_REAL_TOKEN
  • Lint
  • Regenerate docs, if necessary

Inspirations

  • emojipacks is my OG. It mostly worked but seems rather undermaintained.
  • neutral-face-emoji-tools is a fantastic tool that has enabled me to make enough emoji that this tool became necessary.

Stupid ways to use this stupid library!

  • https://github.com/jackellenberger/allmyemojichildren
  • https://github.com/guyfedwards/emoji
  • https://github.com/jackellenberger/emojme-hubot-plugin
  • https://github.com/jackellenberger/emojme-emoji-anywhere
  • https://github.com/jackellenberger/infinite-emoji-discord-bot

================================================ FILE: docs/module-add.html ================================================ add - Documentation

add

Methods

(async, inner) add(subdomains, tokens, cookies, options) → {Promise.<addResponseObject>}

Add emoji described by parameters within options to the specified subdomain(s).

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 nulls for fields that are not relevant to the current emoji.

Parameters:
Name Type Description
subdomains string | Array.<string>

a single or list of subdomains to add emoji to. Must match respectively to tokens and cookies.

tokens string | Array.<string>

a single or list of tokens to add emoji to. Must match respectively to subdomains and cookies.

cookies string | Array.<string>

a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to subdomains and tokens.

options object

contains singleton or arrays of emoji descriptors.

Properties
Name Type Attributes Description
src string | Array.<string> <optional>

source image files for the emoji to be added. If no corresponding options.name is given, the filename will be used

name string | Array.<string> <optional>

names of the emoji to be added, overriding filenames if given, and becoming the alias name if an options.aliasFor is given

aliasFor string | Array.<string> <optional>

names of emoji to be aliased to options.name

allowCollisions boolean <optional>

if true, emoji being uploaded will not be checked against existing emoji. This will take less time up front but may cause more errors.

avoidCollisions boolean <optional>

if true, 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

prefix string <optional>

string to prefix all emoji being uploaded

bustCache boolean <optional>

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

output boolean <optional>

if false, no files will be written during execution. Prevents saving of adminList for future use

Source:
Example
var addOptions = {
  src: ['./emoji1.jpg', 'http://example.com/emoji2.png'], // upload these two images
  name: ['myLocalEmoji', 'myOnlineEmoji'], // call them these two names
  bustCache: false, // don't bother redownloading existing emoji
  avoidCollisions: true, // if there are similarly named emoji, change my new emoji names
  output: false // don't write any files
};
var subdomains = ['mySubdomain1', 'mySubdomain2'] // can add one or multiple
var tokens = ['myToken1', 'myToken2'] // can add one or multiple
var cookies = ['myCookie1', 'myCookie2'] // can add one or multiple
var addResults = await emojme.add(subdomains, tokens, cookies, addOptions);
console.log(userStatsResults);
// {
//   mySubomain1: {
//     collisions: [], // only defined if avoidCollisons = false
//     emojiList: [
//       { name: 'myLocalEmoji', ... },
//       { name: 'myOnlineEmoji', ... },
//     ]
//   },
//   mySubomain2: {
//     collisions: [], // only defined if avoidCollisons = false
//     emojiList: [
//       { name: 'myLocalEmoji', ... },
//       { name: 'myOnlineEmoji', ... },
//     ]
//   }
// }

Type Definitions

addResponseObject :object

The add response object, like other response objects, is organized by input subdomain.

Properties:
Name Type Description
subdomain object

each subdomain passed in to add will appear as a key in the response

Properties
Name Type Description
emojiList Array.<emojiList>

the list of emoji added to subdomain, with each element reflecting the parameters passed in to add

collisions Array.<emojiList>

if options.avoidCollisions is false, emoji that cannot be uploaded due to existing conflicting emoji names will exist here

Source:

================================================ FILE: docs/module-download.html ================================================ download - Documentation

download

Methods

(async, inner) download(subdomains, tokens, cookies, options) → {Promise.<downloadResponseObject>}

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.

Parameters:
Name Type Description
subdomains string | Array.<string>

a single or list of subdomains from which to download emoji. Must match respectively to tokens and cookies.

tokens string | Array.<string>

a single or list of tokens with which to authenticate. Must match respectively to subdomains and cookies.

cookies string | Array.<string>

a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to subdomains and tokens.

options object

contains singleton or arrays of emoji descriptors.

Properties
Name Type Attributes Description
save string | Array.<string> <optional>

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//

saveAll boolean <optional>

if true, download all emoji on slack instance from all users to disk in a single location.

saveAllByUser boolean <optional>

if true, download all emoji on slack instance from all users to disk, organized into directories by user.

bustCache boolean <optional>

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

output boolean <optional>

if false, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files

verbose boolean <optional>

if true, all messages will be written to stdout in addition to combined log file.

Source:
Example
var downloadOptions = {
  save: ['username_1', 'username_2'], // Download the emoji source files for these two users
  bustCache: true, // make sure this data is fresh
  output: true // download the adminList to ./build
};
var downloadResults = await emojme.download('mySubdomain', 'myToken', 'myCookie', downloadOptions);
console.log(downloadResults);
// {
//   mySubdomain: {
//     emojiList: [
//       { name: 'emoji-from-mySubdomain', ... },
//       ...
//     ],
//     saveResults: [
//       './build/mySubdomain/username_1/an_emoji.jpg',
//       './build/mySubdomain/username_1/another_emoji.gif',
//       ... all of username_1's emoji
//       './build/mySubdomain/username_2/some_emoji.jpg',
//       './build/mySubdomain/username_2/some_other_emoji.gif',
//       ... all of username_2's emoji
//     ]
//   }
// }

Type Definitions

downloadResponseObject :object

The download response object, like other response objects, is organized by input subdomain.

Properties:
Name Type Description
subdomain object

each subdomain passed in to add will appear as a key in the response

Properties
Name Type Description
emojiList Array.<emojiList>

the list of emoji downloaded from subdomain

saveResults Array.<string>

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.

Source:

================================================ FILE: docs/module-favorites.html ================================================ favorites - Documentation

favorites

Methods

(async, inner) favorites(subdomains, tokens, cookies, options) → {Promise.<favoritesResponseObject>}

Get the contents of the "Frequenly Used" box for your specified user

Parameters:
Name Type Description
subdomains string | Array.<string>

a single or list of subdomains from which to analyze emoji. Must match respectively to tokens and cookies.

tokens string | Array.<string>

a single or list of tokens to add emoji to. Must match respectively to subdomains and cookies.

cookies string | Array.<string>

a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to subdomains and tokens.

options object

contains options on what to present

Properties
Name Type Attributes Description
lite Number <optional>

do not attempt to marry favorites with complete adminlist content. Results will contain only emoji name and usage count.

top Number <optional>

(verbose cli only) count of top n emoji contriubtors you would like to retrieve user statistics on

usage Number <optional>

(verbose cli only) print not just the list of favorite emoji, but their usage count

bustCache boolean <optional>

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

output boolean <optional>

if false, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files

verbose boolean <optional>

if true, all messages will be written to stdout in addition to combined log file.

Source:
Example
var favoritesResult = await emojme.favorites('mySubdomain', 'myToken', 'myCookie', {});
console.log(favoritesResult);
// {
//   mySubdomain: {
//     favoritesResult: {
//         user: '{myToken's user}',
//         favoriteEmoji: [
//            emojiName,
//            ...
//         ],
//         favoriteEmojiAdminList: [
//           {emojiName}: {adminList-style emoji object, with additional `usage` value}
//           ...
//         ],
//       }
//   }
// }

Type Definitions

favoritesResponseObject :object

The user-specific favorites response object, like other response objects, is organized by input subdomain.

Properties:
Name Type Description
subdomain object

each subdomain passed in to add will appear as a key in the response

Properties
Name Type Description
favoritesResult.user string

the username associated with the given cookie token

favoritesResult.favoriteEmoji Array.<string>

the list of 'favorite' emoji as deemed by slack, in desc sorted order

favoritesResult.favoriteEmojiAdminList Array.<object>

an array of emoji objects, as organized by emojiAdminList

Source:

================================================ FILE: docs/module-sync.html ================================================ sync - Documentation

sync

Methods

(async, inner) sync(subdomains, tokens, cookies, options) → {Promise.<syncResponseObject>}

Sync emoji between slack subdomains

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.

Parameters:
Name Type Description
subdomains string | Array.<string> | null

Two ore more subdomains that you wish to have the same emoji pool

tokens string | Array.<string> | null

cookie tokens corresponding to the given subdomains

cookies string | Array.<string> | null

User cookies corresponding to the given subdomains

options object

contains src and dst information for "one way" sync configuration. Either specify subdomains and tokens, or srcSubdomains, srcTokens, dstSubdomains, and dstTokens, not both.

Properties
Name Type Attributes Description
srcSubdomains string | Array.<string> <optional>

slack instances from which to draw emoji. No additions will be made to these subdomains

srcTokens string | Array.<string> <optional>

tokens for the slack instances from which to draw emoji

srcCookies string | Array.<string> <optional>

cookies auth cookies for the slack instances from which to draw emoji

dstSubdomains string | Array.<string> <optional>

slack instances in which all source emoji will be deposited. None of dstSubdomain's emoji will end up in srcSubdomain

dstTokens string | Array.<string> <optional>

tokens for the slack instances where emoji will be deposited

dstCookies string | Array.<string> <optional>

cookies auth cookies for the slack instances from which to draw emoji

bustCache boolean <optional>

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

output boolean <optional>

if false, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files

verbose boolean <optional>

if true, all messages will be written to stdout in addition to combined log file.

Source:
Example
var syncOptions = {
  srcSubdomains: ['srcSubdomain'], // copy all emoji from srcSubdomain...
  srcTokens: ['srcToken'],
  srcCookies: ['srcCookie'],
  dstSubdomains: ['dstSubdomain1', 'dstSubdomain2'], // ...to dstSubdomain1 and dstSubdomain2
  dstTokens: ['dstToken1', 'dstToken2'],
  dstCookies: ['dstCookie1', 'dstCookie2'],
  bustCache: true // get fresh lists to make sure we're not doing more lifting than we have to
};
var syncResults = await emojme.sync(null, null, syncOptions);
console.log(syncResults);
// {
//   dstSubdomain1: {
//     emojiList: [
//       { name: emoji-1-from-srcSubdomain ... },
//       { name: emoji-2-from-srcSubdomain ... }
//     ]
//   },
//   dstSubdomain2: {
//     emojiList: [
//       { name: emoji-1-from-srcSubdomain ... },
//       { name: emoji-2-from-srcSubdomain ... }
//     ]
//   }
// }

Type Definitions

syncResponseObject :object

The sync response object, like other response objects, is organized by input subdomain.

Properties:
Name Type Description
subdomain object

each subdomain passed in to add will appear as a key in the response

Properties
Name Type Description
emojiList Array.<emojiList>

the list of emoji added to subdomain, with each element an emoji pulled from either srcSubdomain or subdomains less the subdomain in question.

Source:

================================================ FILE: docs/module-upload.html ================================================ upload - Documentation

upload

Methods

(async, inner) upload(subdomains, tokens, cookies, options) → {Promise.<uploadResponseObject>}

Upload multiple emoji described by an existing list on disk, either as a json emoji admin list or emojipacks-like yaml.

Parameters:
Name Type Description
subdomains string | Array.<string>

a single or list of subdomains from which to download emoji. Must match respectively to tokens and cookies.

tokens string | Array.<string>

a single or list of tokens with which to authenticate. Must match respectively to subdomains and cookies.

cookies string | Array.<string>

a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to subdomains and tokens.

options object

contains singleton or arrays of emoji descriptors.

Properties
Name Type Attributes Description
src string | Array.<string>

source emoji list files for the emoji to be added. Can either be in jsonEmojiListFormat or yamlEmojiListFormat

avoidCollisions boolean <optional>

if true, 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

prefix string <optional>

string to prefix all emoji being uploaded

bustCache boolean <optional>

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

output boolean <optional>

if false, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files

verbose boolean <optional>

if true, all messages will be written to stdout in addition to combined log file.

Source:
Example
var uploadOptions = {
  src: './emoji-list.json', // upload all the emoji in this json array of objects
  avoidCollisions: true, // append '-1' or similar if we try to upload a dupe
  prefix: 'new-' // prepend every emoji in src with "new-", e.g. "emoji" becomes "new-emoji"
};
var uploadResults = await emojme.upload('mySubdomain', 'myToken', 'myCookie', uploadOptions);
console.log(uploadResults);
// {
//   mySubdomain: {
//     collisions: [
//       { name: an-emoji-that-already-exists-in-mySubdomain ... }
//     ],
//     emojiList: [
//       { name: emoji-from-emoji-list-json ... },
//       { name: emoji-from-emoji-list-json ... },
//       ...
//     ]
//   }
// }

Type Definitions

jsonEmojiListFormat :Array

The required format of a json file that can be used as the options.src for upload

To see an example, use download, then look at buidl/*.adminList.json

Properties:
Name Type Description
emojiList Array
Properties
Name Type Description
emojiObject object
Properties
Name Type Description
name string

the name of the emoji

is_alias 1 | 0

whether or not the emoji is an alias. If 1, alias_for is require and url is ignored. If 0 vice versa

alias_for string

the name of the emoji this emoji is apeing

url string

the remote url or local path of the emoji

user_display_name string

the name of the emoji creator

Source:
Example
[
  {
     "name": "a_giving_lovely_generous_individual",
     "is_alias": 1,
     "alias_for": "caleb"
  },
  {
    "name": "gooddoggy",
    "is_alias": 0,
    "alias_for": null,
    "url": "https://emoji.slack-edge.com/T3T9KQULR/gooddoggy/849f53cf1de25f97.png"
  }
]

syncResponseObject :object

The upload response object, like other response objects, is organized by input subdomain.

Properties:
Name Type Description
subdomain object

each subdomain passed in to add will appear as a key in the response

Properties
Name Type Description
emojiList Array.<emojiList>

the list of emoji added to subdomain, with each element an emoji pulled from either srcSubdomain or subdomains less the subdomain in question.

collisions Array.<emojiList>

if options.avoidCollisions is false, emoji that cannot be uploaded due to existing conflicting emoji names will exist here

Source:

yamlEmojiListFormat :object

The required format of a yaml file that can be used as the options.src for upload

Properties:
Name Type Description
topLevelYaml object

all keys execpt for emojis are ignored

emojis Array

the array of emoji objects

Properties
Name Type Description
emojiObject object
Properties
Name Type Description
name string

the name of the emoji

src string

alias for name

is_alias 1 | 0

whether or not the emoji is an alias. If 1, alias_for is require and url is ignored. If 0 vice versa

alias_for string

the name of the emoji this emoji is apeing

url string

the remote url or local path of the emoji

user_display_name string

the name of the emoji creator

Source:
Example
title: animals
 emojis:
   - name: llama
     src: http://i.imgur.com/6bKXKUP.gif
   - name: alpaca
     src: http://i.imgur.com/c6QxTbM.gif

================================================ FILE: docs/module-userStats.html ================================================ userStats - Documentation

userStats

Methods

(async, inner) userStats(subdomains, tokens, cookies, options) → {Promise.<userStatsResponseObject>}

Get a few useful-ish statistics for either specific users, or the top-n emoji creators

Parameters:
Name Type Description
subdomains string | Array.<string>

a single or list of subdomains to analyze. Must match respectively to tokens and cookies.

tokens string | Array.<string>

a single or list of tokens to add emoji to. Must match respectively to subdomains and cookies.

cookies string | Array.<string>

a single or list of cookies used to authenticate access to the given subdomain. Must match respectively to subdomains and tokens.

options object

contains options for what stats to present

Properties
Name Type Attributes Description
user string | Array.<string> <optional>

user name or array of user names you would like to retrieve user statistics on. If specified, ignores top

top Number <optional>

count of top n emoji contriubtors you would like to retrieve user statistics on

bustCache boolean <optional>

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

output boolean <optional>

if false, no files will be written during execution. Prevents saving of adminList for future use, as well as the writing of log files

verbose boolean <optional>

if true, all messages will be written to stdout in addition to combined log file.

Source:
Example
var userStatsOptions = {
  user: ['username_1', 'username_2'] // get me some info on these two users
};
var userStatsResults = await emojme.userStats('mySubdomain', 'myToken', 'myCookie', userStatsOptions);
console.log(userStatsResults);
// {
//   mySubdomain: {
//     userStatsResults: [
//       {
//         user: 'username_1',
//         userEmoji: [{ all username_1's emoji }],
//         subdomain: mySubdomain,
//         originalCount: x,
//         aliasCount: y,
//         totalCount: x + y,
//         percentage: (x + y) / mySubdomain's total emoji count
//       },
//       {
//         user: 'username_2',
//         userEmoji: [{ all username_2's emoji }],
//         subdomain: mySubdomain,
//         originalCount: x,
//         aliasCount: y,
//         totalCount: x + y,
//         percentage: (x + y) / mySubdomain's total emoji count
//       }
//     ]
//   }
// }

Type Definitions

userStatsResponseObject :object

The user-specific userStats response object, like other response objects, is organized by input subdomain.

Properties:
Name Type Description
subdomain object

each subdomain passed in to add will appear as a key in the response

Properties
Name Type Description
emojiList Array.<emojiList>

the list of emoji downloaded from subdomain

userStatsResults Array.<object>

an array of user stats objects

Properties
Name Type Description
userStatsObject object

An object containing several maybe-useful statistics, separated by user

Properties
Name Type Description
user string

the name of the user in question

userEmoji Array.<emojiList>

the emojiList the user authored

subdomain string

redundant :shrug:

originalCount Number

the number of original emoji the user has created

aliasCount Number

the number of emoji aliases the user has defined

totalCount Number

the number of original and aliases the user has created

percentage Number

the percentage of emoji in the given subdomain that the user is responsible for

Source:

================================================ FILE: docs/scripts/linenumber.js ================================================ 'use strict'; /* global document */ (function () { var lineId, lines, totalLines, anchorHash; var source = document.getElementsByClassName('prettyprint source linenums'); var i = 0; var lineNumber = 0; if (source && source[0]) { anchorHash = document.location.hash.substring(1); lines = source[0].getElementsByTagName('li'); totalLines = lines.length; for (; i < totalLines; i++) { lineNumber++; lineId = 'line' + lineNumber; lines[i].id = lineId; if (lineId === anchorHash) { lines[i].className += ' selected'; } } } })(); ================================================ FILE: docs/scripts/pagelocation.js ================================================ 'use strict'; $(document).ready(function () { var currentSectionNav, target; // If an anchor hash is in the URL highlight the menu item highlightActiveHash(); // If a specific page section is in the URL highlight the menu item highlightActiveSection(); // If a specific page section is in the URL scroll that section up to the top currentSectionNav = $('#' + getCurrentSectionName() + '-nav'); if (currentSectionNav.position()) { $('nav').scrollTop(currentSectionNav.position().top); } // function to scroll to anchor when clicking an anchor linl $('a[href*="#"]:not([href="#"])').click(function () { /* eslint-disable no-invalid-this */ if (location.pathname.replace(/^\//, '') === this.pathname.replace(/^\//, '') && location.hostname === this.hostname) { target = $(this.hash); target = target.length ? target : $('[name=' + this.hash.slice(1) + ']'); if (target.length) { $('html, body').animate({ scrollTop: target.offset().top }, 1000); } } /* eslint-enable no-invalid-this */ }); }); // If a new anchor section is selected, change the hightlighted menu item $(window).bind('hashchange', function (event) { highlightActiveHash(event); }); function highlightActiveHash(event) { var oldUrl, oldSubSectionElement; // check for and remove old hash active state if (event && event.originalEvent.oldURL) { oldUrl = event.originalEvent.oldURL; if (oldUrl.indexOf('#') > -1) { oldSubSectionElement = $('#' + getCurrentSectionName() + '-' + oldUrl.substring(oldUrl.indexOf('#') + 1) + '-nav'); if (oldSubSectionElement) { oldSubSectionElement.removeClass('active'); } } } if (getCurrentHashName()) { $('#' + getCurrentSectionName() + '-' + getCurrentHashName() + '-nav').addClass('active'); } } function highlightActiveSection() { var pageId = getCurrentSectionName(); $('#' + pageId + '-nav').addClass('active'); } function getCurrentSectionName() { var path = window.location.pathname; var pageUrl = path.split('/').pop(); var sectionName = pageUrl.substring(0, pageUrl.indexOf('.')); // remove the wodr module- if its in the url sectionName = sectionName.replace('module-', ''); return sectionName; } function getCurrentHashName() { var pageSubSectionId; var pageSubSectionHash = window.location.hash; if (pageSubSectionHash) { pageSubSectionId = pageSubSectionHash.substring(1).replace('.', ''); return pageSubSectionId; } return false; } ================================================ FILE: docs/styles/jsdoc-default.css ================================================ @font-face { font-family: "Avenir Next W01"; font-style: normal; font-weight: 600; src: url("https://fast.fonts.net/dv2/14/14c73713-e4df-4dba-933b-057feeac8dd1.woff2?d44f19a684109620e484167ba790e8180fd9e29df91d80ce3d096f014db863074e1ea706cf5ed4e1c042492e76df291ce1d24ec684d3d9da9684f55406b9f22bce02f0f30f556681593dafea074d7bd44e28a680d083ccfd44ed4f8a3087a20c56147c11f917ed1dbd85c4a18cf38da25e6ac78f008f472262304d50e7e0cb7541ef1642c676db6e4bde4924846f5daf486fbde9335e98f6a20f6664bc4525253d1d4fca42cf1c490483c8daf0237f6a0fd292563417ad80ca3e69321417747bdc6f0969f34b2a0401b5e2b9a4dfd5b06d9710850900c66b34870aef&projectId=f750d5c7-baa2-4767-afd7-45484f47fe17") format('woff2'); } @font-face { font-family: "Avenir Next W01"; font-style: normal; font-weight: 500; src: url("https://fast.fonts.net/dv2/14/627fbb5a-3bae-4cd9-b617-2f923e29d55e.woff2?d44f19a684109620e484167ba790e8180fd9e29df91d80ce3d096f014db863074e1ea706cf5ed4e1c042492e76df291ce1d24ec684d3d9da9684f55406b9f22bce02f0f30f556681593dafea074d7bd44e28a680d083ccfd44ed4f8a3087a20c56147c11f917ed1dbd85c4a18cf38da25e6ac78f008f472262304d50e7e0cb7541ef1642c676db6e4bde4924846f5daf486fbde9335e98f6a20f6664bc4525253d1d4fca42cf1c490483c8daf0237f6a0fd292563417ad80ca3e69321417747bdc6f0969f34b2a0401b5e2b9a4dfd5b06d9710850900c66b34870aef&projectId=f750d5c7-baa2-4767-afd7-45484f47fe17") format('woff2'); } @font-face { font-family: "Avenir Next W01"; font-style: normal; font-weight: 400; src: url("https://fast.fonts.net/dv2/14/2cd55546-ec00-4af9-aeca-4a3cd186da53.woff2?d44f19a684109620e484167ba790e8180fd9e29df91d80ce3d096f014db863074e1ea706cf5ed4e1c042492e76df291ce1d24ec684d3d9da9684f55406b9f22bce02f0f30f556681593dafea074d7bd44e28a680d083ccfd44ed4f8a3087a20c56147c11f917ed1dbd85c4a18cf38da25e6ac78f008f472262304d50e7e0cb7541ef1642c676db6e4bde4924846f5daf486fbde9335e98f6a20f6664bc4525253d1d4fca42cf1c490483c8daf0237f6a0fd292563417ad80ca3e69321417747bdc6f0969f34b2a0401b5e2b9a4dfd5b06d9710850900c66b34870aef&projectId=f750d5c7-baa2-4767-afd7-45484f47fe17") format('woff2'); } @font-face { font-family: 'bt_mono'; font-style: normal; font-weight: 400; src: url('https://assets.braintreegateway.com/fonts/bt_mono_regular-webfont.eot'); 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'); } @font-face { font-family: 'bt_mono'; font-style: normal; font-weight: 500; src: url('https://assets.braintreegateway.com/fonts/bt_mono_medium-webfont.eot'); 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'); } @font-face { font-family: 'bt_mono'; font-style: normal; font-weight: 600; src: url('https://assets.braintreegateway.com/fonts/bt_mono_bold-webfont.eot'); 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'); } @font-face { font-family: 'bt_mono'; font-style: normal; font-weight: 900; src: url('https://assets.braintreegateway.com/fonts/bt_mono_heavy-webfont.eot'); 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'); } * { box-sizing: border-box } html, body { height: 100%; width: 100%; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: #3e3c42; text-rendering: optimizeLegibility; margin: 0; } body { color: #3e3c42; background-color: #f3f3f3; width: 100%; font: 16px/1.875 "Avenir Next W01", "Avenir Next", "Helvetica Neue", Helvetica, sans-serif; font-size: 16px; line-height: 160%; } a, a:active { color: #0095dd; text-decoration: none; } a:hover { text-decoration: underline } p, ul, ol, blockquote { margin-bottom: 1em; } p { max-width: 800px; } h1, h2, h3, h4, h5, h6 { color: #706d77; font-weight: 500; margin: 0; line-height: 1; } h1 { color: #4b484f; font-weight: 500; font-size: 40px; display: block; } h1 span { color: #999; font-size: 32px; display: block; line-height: 1.5; } h1.page-title { border-bottom: 1px dashed #ccc; margin-bottom: 20px; padding-bottom: 30px; } h2 { font-size: 30px; margin: 1.5em 0 0; } h3 { font-size: 20px; margin: 1.5em 0 0; text-transform: uppercase; } h3.reference-title { display: block; font-weight: 400; margin-top: 2em; max-width: 200px; } h3.reference-title small { display: inline-block; color: #0095dd; margin-left: 5px; font-weight: 500; } h3.subsection-title { border-bottom: 1px solid #ececec; padding-bottom: 20px; margin-top: 3em; margin-bottom: 1em; } h4 { font-size: 16px; margin: 1em 0 0; font-weight: bold; } h4.name { font-size: 20px; margin-top: 0; font-weight: 500; } h5 { margin: 2em 0 0.5em 0; font-size: 14px; font-weight: 500; text-transform: uppercase; } .container-overview .subsection-title { font-size: 14px; text-transform: uppercase; margin: 8px 0 15px 0; font-weight: bold; color: #4D4E53; padding-top: 10px; } h6 { font-size: 100%; letter-spacing: -0.01em; margin: 6px 0 3px 0; font-style: italic; text-transform: uppercase; font-weight: 500; } tt, code, kbd, samp { font-family: "Source Code Pro", monospace; background: #f4f4f4; padding: 1px 5px; border-radius: 5px; } .class-description { margin-bottom: 1em; margin-top: 1em; padding: 10px 20px; background-color: rgba(26, 159, 224, 0.1); } .class-description:empty { margin: 0 } #main { background-color: white; float: right; min-width: 360px; width: calc(100% - 300px); padding: 30px; z-index: 100; } header { display: block; max-width: 1400px; } section { display: block; max-width: 1400px; background-color: #fff; } .variation { display: none } .signature-attributes { font-size: 60%; color: #aaa; font-style: italic; font-weight: lighter; } .rule { width: 100%; margin-top: 20px; display: block; border-top: 1px solid #ccc; } ul { list-style-type: none; padding-left: 0; } ul li a { font-weight: 500; } ul ul { padding-top: 5px; } ul li ul { padding-left: 20px; } ul li ul li a { font-weight: normal; } nav { float: left; display: block; width: 300px; background: #f7f7f7; overflow-x: visible; overflow-y: auto; height: 100%; padding: 0px 30px 100px 30px; height: 100%; position: fixed; transition: left 0.2s; z-index: 998; margin-top: 0px; top: 43px; } .navicon-button { display: inline-block; position: fixed; bottom: 1.5em; right: 1.5em; z-index: 2; } nav h3 { font-size: 13px; text-transform: uppercase; letter-spacing: 1px; font-weight: bold; line-height: 24px; margin: 40px 0 10px 0; padding: 0; } nav ul { font-size: 100%; line-height: 17px; padding: 0; margin: 0; list-style-type: none; border: none; padding-left: 0; } nav ul a { font-size: 16px; } nav ul a, nav ul a:active { display: block; } nav ul a:hover, nav ul a:active { color: hsl(200, 100%, 43%); text-decoration: none; } nav>ul { padding: 0 10px; } nav>ul li:first-child { padding-top: 0; } nav ul li ul { padding-left: 0; } nav>ul>li { border-bottom: 1px solid #e2e2e2; padding: 10px 0 20px 0; } nav>ul>li.active ul { border-left: 3px solid #0095dd; padding-left: 15px; } nav>ul>li.active ul li.active a { font-weight: bold; } nav>ul>li.active a { color: #0095dd; } nav>ul>li>a { color: #706d77; padding: 20px 0; font-size: 18px; } nav ul ul { margin-bottom: 10px padding-left: 0; } nav ul ul a { color: #5f5c63; } nav ul ul a, nav ul ul a:active { font-family: 'bt_mono', monospace; font-size: 14px; padding-left: 20px; padding-top: 3px; padding-bottom: 9px; } nav h2 { font-size: 12px; margin: 0; padding: 0; } nav>h2>a { color: hsl(202, 71%, 50%); border-bottom: 1px solid hsl(202, 71%, 50%); padding-bottom: 5px; } nav>h2>a:hover { font-weight: 500; text-decoration: none; } footer { background-color: #fff; color: hsl(0, 0%, 28%); margin-left: 300px; display: block; font-style: italic; font-size: 12px; padding: 30px; text-align: center; } .ancestors { color: #999; } .ancestors a { color: #999 !important; text-decoration: none; } .clear { clear: both; } .important { font-weight: bold; color: #950B02; } .yes-def { text-indent: -1000px; } .type-signature { color: #aaa; } .name, .signature { font-family: 'bt_mono', monospace; word-wrap: break-word; } .details { margin-top: 14px; font-size: 13px; text-align: right; background: #ffffff; /* Old browsers */ background: -moz-linear-gradient(left, #ffffff 0%, #fafafa 100%); /* FF3.6-15 */ background: -webkit-linear-gradient(left, #ffffff 0%, #fafafa 100%); /* Chrome10-25,Safari5.1-6 */ background: linear-gradient(to right, #ffffff 0%, #fafafa 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ filter: progid: DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#fafafa', GradientType=1); padding-right: 5px; } .details dt { display: inline-block; } .details dd { display: inline-block; margin: 0; } .details dd a { font-style: italic; font-weight: normal; line-height: 1; } .details ul { margin: 0 } .details ul { list-style-type: none } .details li {} .details pre.prettyprint { margin: 0 } .details .object-value { padding-top: 0 } .description { margin-bottom: 1em; margin-top: 1em; } .code-caption { font-style: italic; margin: 0; font-size: 16px; color: #545454; } .prettyprint { font-size: 13px; border: 1px solid #ddd; border-radius: 3px; overflow: auto; background-color: #fbfbfb; } .prettyprint.source { width: inherit; } .prettyprint code { font-size: 100%; line-height: 18px; display: block; margin: 0 30px; background-color: #fbfbfb; color: #4D4E53; } .prettyprint>code { padding: 30px 15px; } .prettyprint .linenums code { padding: 0 15px; } .prettyprint .linenums li:first-of-type code { padding-top: 15px; } .prettyprint code span.line { display: inline-block; } .prettyprint.linenums { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .prettyprint.linenums ol { padding-left: 0 } .prettyprint.linenums li { border-left: 3px #ddd solid } .prettyprint.linenums li.selected, .prettyprint.linenums li.selected * { background-color: lightyellow } .prettyprint.linenums li * { -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text; user-select: text; } .readme .prettyprint { max-width: 800px; } .params, .props { border-spacing: 0; border: 1px solid #ddd; border-radius: 3px; width: 100%; font-size: 14px; } .params .name, .props .name, .name code { color: #4D4E53; font-family: 'bt_mono', monospace; font-size: 100%; } .params td, .params th, .props td, .props th { margin: 0px; text-align: left; vertical-align: top; padding: 10px; display: table-cell; } .params td { border-top: 1px solid #eee; } .params thead tr, .props thead tr { background-color: #fff; font-weight: bold; } .params .params thead tr, .props .props thead tr { background-color: #fff; font-weight: bold; } .params td.description>p:first-child, .props td.description>p:first-child { margin-top: 0; padding-top: 0; } .params td.description>p:last-child, .props td.description>p:last-child { margin-bottom: 0; padding-bottom: 0; } dl.param-type { margin-top: 5px; } .param-type dt, .param-type dd { display: inline-block } .param-type dd { font-family: Consolas, Monaco, 'Andale Mono', monospace } .disabled { color: #454545 } /* tag source style */ .tag-deprecated { padding-right: 5px; } .tag-source { border-bottom: 1px solid rgba(28, 160, 224, 0.35); } .tag-source:first-child { border-bottom: 1px solid rgba(28, 160, 224, 1); } /* navicon button */ .navicon-button { position: relative; transition: 0.25s; cursor: pointer; user-select: none; opacity: .8; background-color: white; border-radius: 100%; width: 50px; height: 50px; -webkit-box-shadow: 0px 2px 9px 0px rgba(0, 0, 0, 0.31); -moz-box-shadow: 0px 2px 9px 0px rgba(0, 0, 0, 0.31); box-shadow: 0px 2px 9px 0px rgba(0, 0, 0, 0.31); } .navicon-button .navicon:before, .navicon-button .navicon:after { transition: 0.25s; } .navicon-button:hover { transition: 0.5s; opacity: 1; } .navicon-button:hover .navicon:before, .navicon-button:hover .navicon:after { transition: 0.25s; } .navicon-button:hover .navicon:before { top: .425rem; } .navicon-button:hover .navicon:after { top: -.425rem; } /* navicon */ .navicon { position: relative; width: 1.5em; height: .195rem; background: #000; top: calc(50% - .09rem); left: calc(50% - .75rem); transition: 0.3s; border-radius: 5px; } .navicon:before, .navicon:after { display: block; content: ""; height: .195rem; width: 1.5rem; background: #000; position: absolute; z-index: -1; transition: 0.3s 0.25s; } .navicon:before { top: 0.425rem; height: .195rem; border-radius: 5px; } .navicon:after { top: -0.425rem; border-radius: 5px; } /* open */ .nav-trigger:checked+label:not(.steps) .navicon:before, .nav-trigger:checked+label:not(.steps) .navicon:after { top: 0 !important; } .nav-trigger:checked+label .navicon:before, .nav-trigger:checked+label .navicon:after { transition: 0.5s; } /* Minus */ .nav-trigger:checked+label { transform: scale(0.75); } /* × and + */ .nav-trigger:checked+label.plus .navicon, .nav-trigger:checked+label.x .navicon { background: transparent; } .nav-trigger:checked+label.plus .navicon:before, .nav-trigger:checked+label.x .navicon:before { transform: rotate(-45deg); background: #000; } .nav-trigger:checked+label.plus .navicon:after, .nav-trigger:checked+label.x .navicon:after { transform: rotate(45deg); background: #000; } .nav-trigger:checked+label.plus { transform: scale(0.75) rotate(45deg); } .nav-trigger:checked~nav { left: 0 !important; } .nav-trigger:checked~.overlay { display: block; } .nav-trigger { position: fixed; top: 0; clip: rect(0, 0, 0, 0); } .overlay { display: none; position: fixed; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; background: hsla(0, 0%, 0%, 0.5); z-index: 1; } table { border-collapse: separate; ; display: block; overflow-x: auto; /*table-layout:fixed;*/ } table tbody td { border-top: 1px solid hsl(207, 10%, 86%); border-right: 1px solid #eee; padding: 5px; /*word-wrap: break-word;*/ } td table.params, td table.props { border: 0; } @media only screen and (min-width: 320px) and (max-width: 680px) { body { overflow-x: hidden; } #main { padding: 30px 30px; width: 100%; min-width: 360px; } nav { background: #FFF; width: 300px; height: 100%; position: fixed; top: 0; right: 0; bottom: 0; left: -300px; z-index: 3; padding: 0 10px; transition: left 0.2s; margin-top: 0; } .navicon-button { display: inline-block; position: fixed; bottom: 1.5em; right: 20px; z-index: 1000; } .top-nav-wrapper { display: none; } #main h1.page-title { margin: 0.5em 0; } footer { margin-left: 0; margin-bottom: 30px; } } .top-nav-wrapper { background-color: #ececec; position: fixed; top: 0px; left: 0px; padding: 10px 10px 0 10px; z-index: 999; width: 300px; } .top-nav-wrapper ul { margin: 0; } .top-nav-wrapper ul li { display: inline-block; padding: 0 10px; vertical-align: top; } .top-nav-wrapper ul li.active { border-bottom: 2px solid rgba(28, 160, 224, 1); } .search-wrapper { display: inline-block; position: relative; } .search-wrapper svg { position: absolute; left: 0px; } input.search-input { background: transparent; box-shadow: 0; border: 0; border-bottom: 1px solid #c7c7c7; padding: 7px 15px 12px 35px; margin: 0 auto; } /* Smooth outline with box-shadow: */ input.search-input:focus { border-bottom: 2px solid rgba(28, 160, 224, 1); outline: none; } /* Hightlight JS Paradiso Light Theme */ .hljs-comment, .hljs-quote { color: #776e71 } .hljs-variable, .hljs-template-variable, .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-regexp, .hljs-link, .hljs-meta { color: #ef6155 } .hljs-number, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params, .hljs-deletion { color: #f99b15 } .hljs-title, .hljs-section, .hljs-attribute { color: #fec418 } .hljs-string, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #48b685 } .hljs-keyword, .hljs-selector-tag { color: #815ba4 } .hljs { display: block; overflow-x: auto; background: #e7e9db; color: #4f424c; padding: 0.5em } .hljs-emphasis { font-style: italic } .hljs-strong { font-weight: bold } .link-icon { opacity: 0; position: absolute; margin-left: -25px; padding-right: 5px; padding-top: 2px; } .example-container .link-icon { margin-top: -6px; } .example-container:hover .link-icon, .name-container:hover .link-icon { opacity: .5; } .name-container { display: flex; padding-top: 1em; } ================================================ FILE: docs/styles/prettify-jsdoc.css ================================================ /* JSDoc prettify.js theme */ /* plain text */ .pln { color: #000000; font-weight: normal; font-style: normal; } /* string content */ .str { color: hsl(104, 100%, 24%); font-weight: normal; font-style: normal; } /* a keyword */ .kwd { color: #000000; font-weight: bold; font-style: normal; } /* a comment */ .com { font-weight: normal; font-style: italic; } /* a type name */ .typ { color: #000000; font-weight: normal; font-style: normal; } /* a literal value */ .lit { color: #006400; font-weight: normal; font-style: normal; } /* punctuation */ .pun { color: #000000; font-weight: bold; font-style: normal; } /* lisp open bracket */ .opn { color: #000000; font-weight: bold; font-style: normal; } /* lisp close bracket */ .clo { color: #000000; font-weight: bold; font-style: normal; } /* a markup tag name */ .tag { color: #006400; font-weight: normal; font-style: normal; } /* a markup attribute name */ .atn { color: #006400; font-weight: normal; font-style: normal; } /* a markup attribute value */ .atv { color: #006400; font-weight: normal; font-style: normal; } /* a declaration */ .dec { color: #000000; font-weight: bold; font-style: normal; } /* a variable name */ .var { color: #000000; font-weight: normal; font-style: normal; } /* a function name */ .fun { color: #000000; font-weight: bold; font-style: normal; } /* Specify class=linenums on a pre to get line numbering */ ol.linenums { margin-top: 0; margin-bottom: 0; } ================================================ FILE: docs/styles/prettify-tomorrow.css ================================================ /* Tomorrow Theme */ /* Original theme - https://github.com/chriskempson/tomorrow-theme */ /* Pretty printing styles. Used with prettify.js. */ /* SPAN elements with the classes below are added by prettyprint. */ /* plain text */ .pln { color: #4d4d4c; } @media screen { /* string content */ .str { color: hsl(104, 100%, 24%); } /* a keyword */ .kwd { color: hsl(240, 100%, 50%); } /* a comment */ .com { color: hsl(0, 0%, 60%); } /* a type name */ .typ { color: hsl(240, 100%, 32%); } /* a literal value */ .lit { color: hsl(240, 100%, 40%); } /* punctuation */ .pun { color: #000000; } /* lisp open bracket */ .opn { color: #000000; } /* lisp close bracket */ .clo { color: #000000; } /* a markup tag name */ .tag { color: #c82829; } /* a markup attribute name */ .atn { color: #f5871f; } /* a markup attribute value */ .atv { color: #3e999f; } /* a declaration */ .dec { color: #f5871f; } /* a variable name */ .var { color: #c82829; } /* a function name */ .fun { color: #4271ae; } } /* Use higher contrast and text-weight for printable form. */ @media print, projection { .str { color: #060; } .kwd { color: #006; font-weight: bold; } .com { color: #600; font-style: italic; } .typ { color: #404; font-weight: bold; } .lit { color: #044; } .pun, .opn, .clo { color: #440; } .tag { color: #006; font-weight: bold; } .atn { color: #404; } .atv { color: #060; } } /* Style */ /* pre.prettyprint { background: white; font-family: Consolas, Monaco, 'Andale Mono', monospace; font-size: 12px; line-height: 1.5; border: 1px solid #ccc; padding: 10px; } */ /* Get LI elements to show when they are in the main article */ article ul li { list-style-type: circle; margin-left: 25px; } /* Specify class=linenums on a pre to get line numbering */ ol.linenums { margin-top: 0; margin-bottom: 0; } /* IE indents via margin-left */ li.L0, li.L1, li.L2, li.L3, li.L4, li.L5, li.L6, li.L7, li.L8, li.L9 { /* */ } /* Alternate shading for lines */ li.L1, li.L3, li.L5, li.L7, li.L9 { /* */ } ================================================ FILE: emojme-add.js ================================================ const _ = require('lodash'); const commander = require('commander'); const EmojiAdminList = require('./lib/emoji-admin-list'); const EmojiAdd = require('./lib/emoji-add'); const FileUtils = require('./lib/util/file-utils'); const Helpers = require('./lib/util/helpers'); const Cli = require('./lib/util/cli'); /** @module add */ /** * The add response object, like other response objects, is organized by input subdomain. * @typedef {object} addResponseObject * @property {object} subdomain each subdomain passed in to add will appear as a key in the response * @property {emojiList[]} subdomain.emojiList the list of emoji added to `subdomain`, with each element reflecting the parameters passed in to `add` * @property {emojiList[]} subdomain.collisions if `options.avoidCollisions` is `false`, emoji that cannot be uploaded due to existing conflicting emoji names will exist here */ /** * Add emoji described by parameters within options to the specified subdomain(s). * * 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. * * @async * @param {string|string[]} subdomains a single or list of subdomains to add emoji to. Must match respectively to `token`s and `cookie`s. * @param {string|string[]} tokens a single or list of tokens to add emoji to. Must match respectively to `subdomain`s and `cookie`s. * @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. * @param {object} options contains singleton or arrays of emoji descriptors. * @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 * @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 * @param {string|string[]} [options.aliasFor] names of emoji to be aliased to `options.name` * @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. * @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 * @param {string} [options.prefix] string to prefix all emoji being uploaded * @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 * @param {boolean} [options.output] if `false`, no files will be written during execution. Prevents saving of adminList for future use * * @returns {Promise} addResponseObject result object * * @example var addOptions = { src: ['./emoji1.jpg', 'http://example.com/emoji2.png'], // upload these two images name: ['myLocalEmoji', 'myOnlineEmoji'], // call them these two names bustCache: false, // don't bother redownloading existing emoji avoidCollisions: true, // if there are similarly named emoji, change my new emoji names output: false // don't write any files }; var subdomains = ['mySubdomain1', 'mySubdomain2'] // can add one or multiple var tokens = ['myToken1', 'myToken2'] // can add one or multiple var cookies = ['myCookie1', 'myCookie2'] // can add one or multiple var addResults = await emojme.add(subdomains, tokens, cookies, addOptions); console.log(userStatsResults); // { // mySubomain1: { // collisions: [], // only defined if avoidCollisons = false // emojiList: [ // { name: 'myLocalEmoji', ... }, // { name: 'myOnlineEmoji', ... }, // ] // }, // mySubomain2: { // collisions: [], // only defined if avoidCollisons = false // emojiList: [ // { name: 'myLocalEmoji', ... }, // { name: 'myOnlineEmoji', ... }, // ] // } // } */ async function add(subdomains, tokens, cookies, options) { subdomains = Helpers.arrayify(subdomains); tokens = Helpers.arrayify(tokens); cookies = Helpers.arrayify(cookies); options = options || {}; const aliases = Helpers.arrayify(options.aliasFor); const names = Helpers.arrayify(options.name); const sources = Helpers.arrayify(options.src); let inputEmoji = []; let name; let alias; let source; while (aliases.length || sources.length) { name = names.shift(); if (source = sources.shift()) { inputEmoji.push({ is_alias: 0, url: source, name: name || source.match(/(?:.*\/)?(.*).(jpg|jpeg|png|gif)/)[1], }); } else { alias = aliases.shift(); inputEmoji.push({ is_alias: 1, alias_for: alias, name, }); } } if (names.length || _.find(inputEmoji, ['name', undefined])) { 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.')); } const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies); const addPromises = authTuples.map(async (authTuple) => { let emojiToUpload = []; let collisions = []; if (options.prefix) { inputEmoji = Helpers.applyPrefix(inputEmoji, options.prefix); } if (options.allowCollisions) { emojiToUpload = inputEmoji; } else { const existingEmojiList = await new EmojiAdminList(...authTuple, options.output) .get(options.bustCache); const existingNameList = existingEmojiList.map(e => e.name); if (options.avoidCollisions) { inputEmoji = Helpers.avoidCollisions(inputEmoji, existingEmojiList); } [collisions, emojiToUpload] = _.partition(inputEmoji, emoji => existingNameList.includes(emoji.name)); } const emojiAdd = new EmojiAdd(...authTuple); return emojiAdd.upload(emojiToUpload).then((uploadResult) => { if (uploadResult.errorList && uploadResult.errorList.length > 1 && options.output) { FileUtils.writeJson(`./build/${this.subdomain}.emojiUploadErrors.json`, uploadResult.errorList); } return Object.assign({}, uploadResult, { collisions }); }); }); return Helpers.formatResultsHash(await Promise.all(addPromises)); } function addCli() { const program = new commander.Command(); Cli.requireAuth(program); Cli.allowIoControl(program); Cli.allowEmojiAlterations(program) .option('--src ', 'source image/gif/#content for emoji you\'d like to upload', Cli.list, null) .option('--name ', 'name of the emoji from --src that you\'d like to upload', Cli.list, null) .option('--alias-for ', 'name of the emoji you\'d like --name to be an alias of. Specifying this will negate --src', Cli.list, null) .parse(process.argv); Cli.unpackAuthJson(program); return add(program.subdomain, program.token, program.cookie, { src: program.src, name: program.name, aliasFor: program.aliasFor, bustCache: program.bustCache, allowCollisions: program.allowCollisions, avoidCollisions: program.avoidCollisions, prefix: program.prefix, output: program.output, }); } if (require.main === module) { addCli(); } module.exports = { add, addCli, }; ================================================ FILE: emojme-download.js ================================================ const commander = require('commander'); const EmojiAdminList = require('./lib/emoji-admin-list'); const Cli = require('./lib/util/cli'); const Helpers = require('./lib/util/helpers'); /** @module download */ /** * The download response object, like other response objects, is organized by input subdomain. * @typedef {object} downloadResponseObject * @property {object} subdomain each subdomain passed in to add will appear as a key in the response * @property {emojiList[]} subdomain.emojiList the list of emoji downloaded from `subdomain` * @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. */ /** * 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. * * @async * @param {string|string[]} subdomains a single or list of subdomains from which to download emoji. Must match respectively to `token`s and `cookie`s. * @param {string|string[]} tokens a single or list of tokens with which to authenticate. Must match respectively to `subdomain`s and `cookie`s. * @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. * @param {object} options contains singleton or arrays of emoji descriptors. * @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// * @param {boolean} [options.saveAll] if `true`, download all emoji on slack instance from all users to disk in a single location. * @param {boolean} [options.saveAllByUser] if `true`, download all emoji on slack instance from all users to disk, organized into directories by user. * @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 * @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 * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file. * * @returns {Promise} downloadResponseObject result object * * @example var downloadOptions = { save: ['username_1', 'username_2'], // Download the emoji source files for these two users bustCache: true, // make sure this data is fresh output: true // download the adminList to ./build }; var downloadResults = await emojme.download('mySubdomain', 'myToken', 'myCookie', downloadOptions); console.log(downloadResults); // { // mySubdomain: { // emojiList: [ // { name: 'emoji-from-mySubdomain', ... }, // ... // ], // saveResults: [ // './build/mySubdomain/username_1/an_emoji.jpg', // './build/mySubdomain/username_1/another_emoji.gif', // ... all of username_1's emoji // './build/mySubdomain/username_2/some_emoji.jpg', // './build/mySubdomain/username_2/some_other_emoji.gif', // ... all of username_2's emoji // ] // } // } */ async function download(subdomains, tokens, cookies, options) { subdomains = Helpers.arrayify(subdomains); tokens = Helpers.arrayify(tokens); cookies = Helpers.arrayify(cookies); options = options || {}; const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies); const downloadPromises = authTuples.map(async (authTuple) => { const subdomain = authTuple[0]; let saveResults = []; const adminList = new EmojiAdminList(...authTuple, options.output); const emojiList = await adminList.get(options.bustCache, options.since); if ((options.save && options.save.length) || options.saveAll || options.saveAllByUser) { saveResults = saveResults.concat(await EmojiAdminList.save(emojiList, subdomain, { save: options.save, saveAll: options.saveAll, saveAllByUser: options.saveAllByUser, })); } return { emojiList, subdomain, saveResults }; }); return Helpers.formatResultsHash(await Promise.all(downloadPromises)); } function downloadCli() { const program = new commander.Command(); Cli.requireAuth(program); Cli.allowIoControl(program) .option('--save ', 'save all of \'s emoji to disk at build/$subdomain/$user', Cli.list, []) .option('--save-all', 'save all emoji from all users to disk at build/$subdomain') .option('--save-all-by-user', 'save all emoji from all users to disk at build/$subdomain/$user') .parse(process.argv); Cli.unpackAuthJson(program); return download(program.subdomain, program.token, program.cookie, { save: program.save, saveAll: program.saveAll, saveAllByUser: program.saveAllByUser, bustCache: program.bustCache, output: program.output, since: program.since, }); } if (require.main === module) { downloadCli(); } module.exports = { download, downloadCli, }; ================================================ FILE: emojme-favorites.js ================================================ const _ = require('lodash'); const commander = require('commander'); const util = require('util'); const ClientBoot = require('./lib/client-boot'); const EmojiAdminList = require('./lib/emoji-admin-list'); const logger = require('./lib/logger'); const Cli = require('./lib/util/cli'); const FileUtils = require('./lib/util/file-utils'); const Helpers = require('./lib/util/helpers'); /** @module favorites */ /** * The user-specific favorites response object, like other response objects, is organized by input subdomain. * @typedef {object} favoritesResponseObject * @property {object} subdomain each subdomain passed in to add will appear as a key in the response * @property {string} subdomain.favoritesResult.user the username associated with the given cookie token * @property {string[]} subdomain.favoritesResult.favoriteEmoji the list of 'favorite' emoji as deemed by slack, in desc sorted order * @property {object[]} subdomain.favoritesResult.favoriteEmojiAdminList an array of emoji objects, as organized by emojiAdminList */ /** * Get the contents of the "Frequenly Used" box for your specified user * * @async * @param {string|string[]} subdomains a single or list of subdomains from which to analyze emoji. Must match respectively to `token`s and `cookie`s. * @param {string|string[]} tokens a single or list of tokens to add emoji to. Must match respectively to `subdomain`s and `cookie`s. * @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. * @param {object} options contains options on what to present * @param {Number} [options.lite] do not attempt to marry favorites with complete adminlist content. Results will contain only emoji name and usage count. * @param {Number} [options.top] (verbose cli only) count of top n emoji contriubtors you would like to retrieve user statistics on * @param {Number} [options.usage] (verbose cli only) print not just the list of favorite emoji, but their usage count * @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 * @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 * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file. * * @returns {Promise} fovoritesResponseObject result object * * @example var favoritesResult = await emojme.favorites('mySubdomain', 'myToken', 'myCookie', {}); console.log(favoritesResult); // { // mySubdomain: { // favoritesResult: { // user: '{myToken's user}', // favoriteEmoji: [ // emojiName, // ... // ], // favoriteEmojiAdminList: [ // {emojiName}: {adminList-style emoji object, with additional `usage` value} // ... // ], // } // } // } */ async function favorites(subdomains, tokens, cookies, options) { subdomains = Helpers.arrayify(subdomains); tokens = Helpers.arrayify(tokens); cookies = Helpers.arrayify(cookies); options = options || {}; const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies); const favoritesPromises = authTuples.map(async (authTuple) => { let emojiList = []; if (!options.lite) { const emojiAdminList = new EmojiAdminList(...authTuple, options.output); emojiList = await emojiAdminList.get(options.bustCache); } const bootClient = new ClientBoot(...authTuple, options.output); const bootData = await bootClient.get(options.bustCache); const user = ClientBoot.extractName(bootData); const favoriteEmojiUsage = ClientBoot.extractEmojiUse(bootData); const favoriteEmojiList = favoriteEmojiUsage.map(e => e.name); const favoriteEmojiAdminList = _.reduce(favoriteEmojiUsage, (acc, usageObj) => { acc.push({ [usageObj.name]: { ...EmojiAdminList.find(emojiList, usageObj.name), usage: usageObj.usage, }, }); return acc; }, []); const result = { user, subdomain: bootClient.subdomain, favoriteEmoji: favoriteEmojiList, favoriteEmojiAdminList, }; const safeUserName = FileUtils.sanitize(result.user); if (options.output) FileUtils.writeJson(`./build/${safeUserName}.${bootClient.subdomain}.favorites.json`, result.favoriteEmojiAdminList, null, 3); const topNFavorites = util.inspect( (options.usage ? favoriteEmojiList : favoriteEmojiUsage) .slice(0, options.top), ); logger.info(`[${bootClient.subdomain}] Favorite emoji for ${result.user}: ${topNFavorites}`); return { subdomain: bootClient.subdomain, favoritesResult: result }; }); return Helpers.formatResultsHash(_.flatten(await Promise.all(favoritesPromises))); } function favoritesCli() { const program = new commander.Command(); Cli.requireAuth(program); Cli.allowIoControl(program); program .option('--top ', '(verbose cli only) the top n favorites you\'d like to see', 10) .option('--usage', '(verbose cli only) print emoji usage of favorites in addition to their names', false) .option('--lite', 'do not attempt to marry favorites with complete adminlist content. Results will contain only emoji name and usage count.', false) .parse(process.argv); Cli.unpackAuthJson(program); return favorites(program.subdomain, program.token, program.cookie, { top: program.top, usage: program.usage, lite: program.lite, bustCache: program.bustCache, output: program.output, }).catch((err) => { console.error('An error occurred: ', err); }); } if (require.main === module) { favoritesCli(); } module.exports = { favorites, favoritesCli, }; ================================================ FILE: emojme-sync.js ================================================ const _ = require('lodash'); const commander = require('commander'); const EmojiAdminList = require('./lib/emoji-admin-list'); const EmojiAdd = require('./lib/emoji-add'); const Cli = require('./lib/util/cli'); const FileUtils = require('./lib/util/file-utils'); const Helpers = require('./lib/util/helpers'); /** @module sync */ /** * The sync response object, like other response objects, is organized by input subdomain. * @typedef {object} syncResponseObject * @property {object} subdomain each subdomain passed in to add will appear as a key in the response * @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. */ /** * Sync emoji between slack subdomains * * 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. * * @async * @param {string|string[]|null} subdomains Two ore more subdomains that you wish to have the same emoji pool * @param {string|string[]|null} tokens cookie tokens corresponding to the given subdomains * @param {string|string[]|null} cookies User cookies corresponding to the given subdomains * @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. * @param {string|string[]} [options.srcSubdomains] slack instances from which to draw emoji. No additions will be made to these subdomains * @param {string|string[]} [options.srcTokens] tokens for the slack instances from which to draw emoji * @param {string|string[]} [options.srcCookies] cookies auth cookies for the slack instances from which to draw emoji * @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` * @param {string|string[]} [options.dstTokens] tokens for the slack instances where emoji will be deposited * @param {string|string[]} [options.dstCookies] cookies auth cookies for the slack instances from which to draw emoji * @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 * @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 * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file. * * @returns {Promise} syncResponseObject result object * * @example var syncOptions = { srcSubdomains: ['srcSubdomain'], // copy all emoji from srcSubdomain... srcTokens: ['srcToken'], srcCookies: ['srcCookie'], dstSubdomains: ['dstSubdomain1', 'dstSubdomain2'], // ...to dstSubdomain1 and dstSubdomain2 dstTokens: ['dstToken1', 'dstToken2'], dstCookies: ['dstCookie1', 'dstCookie2'], bustCache: true // get fresh lists to make sure we're not doing more lifting than we have to }; var syncResults = await emojme.sync(null, null, syncOptions); console.log(syncResults); // { // dstSubdomain1: { // emojiList: [ // { name: emoji-1-from-srcSubdomain ... }, // { name: emoji-2-from-srcSubdomain ... } // ] // }, // dstSubdomain2: { // emojiList: [ // { name: emoji-1-from-srcSubdomain ... }, // { name: emoji-2-from-srcSubdomain ... } // ] // } // } */ async function sync(subdomains, tokens, cookies, options) { let diffs; subdomains = Helpers.arrayify(subdomains); tokens = Helpers.arrayify(tokens); cookies = Helpers.arrayify(cookies); options = options || {}; const [authTuples, srcPairs, dstPairs] = Helpers.zipAuthTuples( subdomains, tokens, cookies, options, ); if (subdomains.length > 0) { const emojiLists = await Promise.all( authTuples.map(async authTuple => new EmojiAdminList(...authTuple, options.output) .get(options.bustCache, options.since)), ); diffs = EmojiAdminList.diff(emojiLists, subdomains); } else if (srcPairs && dstPairs) { const srcDstPromises = [srcPairs, dstPairs].map(pairs => Promise.all( pairs.map(async pair => new EmojiAdminList(...pair, options.output) .get(options.bustCache, options.since)), )); const [srcEmojiLists, dstEmojiLists] = await Promise.all(srcDstPromises); diffs = EmojiAdminList.diff( srcEmojiLists, options.srcSubdomains, dstEmojiLists, options.dstSubdomains, ); } else { throw new Error('Invalid Input'); } const uploadedDiffPromises = diffs.map((diffObj) => { const pathSlug = `to-${diffObj.dstSubdomain}.from-${diffObj.srcSubdomains.join('-')}`; if (options.output) FileUtils.writeJson(`./build/${pathSlug}.emojiAdminList.json`, diffObj.emojiList); if (options.dryRun) return { subdomain: diffObj.dstSubdomain, emojiList: diffObj.emojiList }; const emojiAdd = new EmojiAdd(diffObj.dstSubdomain, _.find( authTuples, [0, diffObj.dstSubdomain], )[1], options.output); return emojiAdd.upload(diffObj.emojiList).then((results) => { if (results.errorList && results.errorList.length > 0 && options.output) FileUtils.writeJson(`./build/${pathSlug}.emojiUploadErrors.json`, results.errorList); return results; }); }); return Helpers.formatResultsHash(await Promise.all(uploadedDiffPromises)); } function syncCli() { const program = new commander.Command(); Cli.requireAuth(program); Cli.allowIoControl(program) .option('--src-subdomain [value]', 'subdomain from which to draw emoji for one way sync', Cli.list, null) .option('--src-token [value]', 'token with which to draw emoji for one way sync', Cli.list, null) .option('--src-cookie [value]', 'cookie with which to draw emoji for one way sync', Cli.list, null) .option('--dst-subdomain [value]', 'subdomain to which to emoji will be added is one way sync', Cli.list, null) .option('--dst-token [value]', 'token with which emoji will be added for one way sync', Cli.list, null) .option('--dst-cookie [value]', 'cookie with which emoji will be added for one way sync', Cli.list, null) // Notice that this is missing --force and --prefix. These have been // deemed TOO POWERFUL for mortal usage. If you _really_ want that // power, you can download then upload the adminlist you retrieve. .option('--dry-run', 'if set to true, nothing will be uploaded or synced', false) .parse(process.argv); Cli.unpackAuthJson(program); return sync(program.subdomain, program.token, program.cookie, { srcSubdomains: program.srcSubdomain, srcTokens: program.srcToken, srcCookies: program.srcCookie, dstSubdomains: program.dstSubdomain, dstTokens: program.dstToken, dstCookies: program.dstCookie, bustCache: program.bustCache, output: program.output, since: program.since, dryRun: program.dryRun, }); } if (require.main === module) { syncCli(); } module.exports = { sync, syncCli, }; ================================================ FILE: emojme-upload.js ================================================ const _ = require('lodash'); const fs = require('graceful-fs'); const commander = require('commander'); const EmojiAdminList = require('./lib/emoji-admin-list'); const EmojiAdd = require('./lib/emoji-add'); const Cli = require('./lib/util/cli'); const FileUtils = require('./lib/util/file-utils'); const Helpers = require('./lib/util/helpers'); /** @module upload */ /** * The upload response object, like other response objects, is organized by input subdomain. * @typedef {object} syncResponseObject * @property {object} subdomain each subdomain passed in to add will appear as a key in the response * @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. * @property {emojiList[]} subdomain.collisions if `options.avoidCollisions` is `false`, emoji that cannot be uploaded due to existing conflicting emoji names will exist here */ /** * The required format of a json file that can be used as the `options.src` for {@link upload} * * To see an example, use {@link download}, then look at `buidl/*.adminList.json` * * @typedef {Array} jsonEmojiListFormat * @property {Array} emojiList * @property {object} emojiList.emojiObject * @property {string} emojiList.emojiObject.name the name of the emoji * @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 * @property {string} emojiList.emojiObject.alias_for the name of the emoji this emoji is apeing * @property {string} emojiList.emojiObject.url the remote url or local path of the emoji * @property {string} emojiList.emojiObject.user_display_name the name of the emoji creator * * @example * [ * { * "name": "a_giving_lovely_generous_individual", * "is_alias": 1, * "alias_for": "caleb" * }, * { * "name": "gooddoggy", * "is_alias": 0, * "alias_for": null, * "url": "https://emoji.slack-edge.com/T3T9KQULR/gooddoggy/849f53cf1de25f97.png" * } * ] */ /** * The required format of a yaml file that can be used as the `options.src` for {@link upload} * @typedef {object} yamlEmojiListFormat * @property {object} topLevelYaml all keys execpt for `emojis` are ignored * @property {Array} emojis the array of emoji objects * @property {object} emojis.emojiObject * @property {string} emojis.emojiObject.name the name of the emoji * @property {string} emojis.emojiObject.src alias for `name` * @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 * @property {string} emojis.emojiObject.alias_for the name of the emoji this emoji is apeing * @property {string} emojis.emojiObject.url the remote url or local path of the emoji * @property {string} emojis.emojiObject.user_display_name the name of the emoji creator * * @example * title: animals * emojis: * - name: llama * src: http://i.imgur.com/6bKXKUP.gif * - name: alpaca * src: http://i.imgur.com/c6QxTbM.gif */ /** * Upload multiple emoji described by an existing list on disk, either as a json emoji admin list or emojipacks-like yaml. * * @async * @param {string|string[]} subdomains a single or list of subdomains from which to download emoji. Must match respectively to `token`s and `cookie`s. * @param {string|string[]} tokens a single or list of tokens with which to authenticate. Must match respectively to `subdomain`s and `cookie`s. * @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. * @param {object} options contains singleton or arrays of emoji descriptors. * @param {string|string[]} options.src source emoji list files for the emoji to be added. Can either be in {@link jsonEmojiListFormat} or {@link yamlEmojiListFormat} * @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 * @param {string} [options.prefix] string to prefix all emoji being uploaded * @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 * @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 * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file. * * @returns {Promise} uploadResponseObject result object * * @example var uploadOptions = { src: './emoji-list.json', // upload all the emoji in this json array of objects avoidCollisions: true, // append '-1' or similar if we try to upload a dupe prefix: 'new-' // prepend every emoji in src with "new-", e.g. "emoji" becomes "new-emoji" }; var uploadResults = await emojme.upload('mySubdomain', 'myToken', 'myCookie', uploadOptions); console.log(uploadResults); // { // mySubdomain: { // collisions: [ // { name: an-emoji-that-already-exists-in-mySubdomain ... } // ], // emojiList: [ // { name: emoji-from-emoji-list-json ... }, // { name: emoji-from-emoji-list-json ... }, // ... // ] // } // } */ async function upload(subdomains, tokens, cookies, options) { subdomains = Helpers.arrayify(subdomains); tokens = Helpers.arrayify(tokens); cookies = Helpers.arrayify(cookies); options = options || {}; let inputEmoji; // TODO this isn't handling --src file --src file correctly if (Array.isArray(options.src)) { inputEmoji = options.src; } else if (!fs.existsSync(options.src)) { throw new Error(`Emoji source file ${options.src} does not exist`); } else { const fileType = options.src.split('.').slice(-1)[0]; if (fileType.match(/yaml|yml/)) { inputEmoji = FileUtils.readYaml(options.src); } else if (fileType.match(/json/)) { inputEmoji = FileUtils.readJson(options.src); } else { throw new Error(`Filetype ${fileType} is not supported`); } } const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies); const uploadPromises = authTuples.map(async (authTuple) => { let emojiToUpload = []; let collisions = []; if (options.prefix) { inputEmoji = Helpers.applyPrefix(inputEmoji, options.prefix); } if (options.allowCollisions) { emojiToUpload = inputEmoji; } else { const existingEmojiList = await new EmojiAdminList(...authTuple, options.output) .get(options.bustCache); const existingNameList = existingEmojiList.map(e => e.name); if (options.avoidCollisions) { inputEmoji = Helpers.avoidCollisions(inputEmoji, existingEmojiList); } [collisions, emojiToUpload] = _.partition(inputEmoji, emoji => existingNameList.includes(emoji.name)); } const emojiAdd = new EmojiAdd(...authTuple); const uploadResult = await emojiAdd.upload(emojiToUpload); return Object.assign({}, uploadResult, { collisions }); }); return Helpers.formatResultsHash(await Promise.all(uploadPromises)); } function uploadCli() { const program = new commander.Command(); Cli.requireAuth(program); Cli.allowIoControl(program); Cli.allowEmojiAlterations(program) .option('--src ', 'source file(s) for emoji json or yaml you\'d like to upload') .parse(process.argv); Cli.unpackAuthJson(program); return upload(program.subdomain, program.token, program.cookie, { src: program.src, bustCache: program.bustCache, allowCollisions: program.allowCollisions, avoidCollisions: program.avoidCollisions, prefix: program.prefix, output: program.output, }); } if (require.main === module) { uploadCli(); } module.exports = { upload, uploadCli, }; ================================================ FILE: emojme-user-stats.js ================================================ const _ = require('lodash'); const commander = require('commander'); const EmojiAdminList = require('./lib/emoji-admin-list'); const Cli = require('./lib/util/cli'); const FileUtils = require('./lib/util/file-utils'); const Helpers = require('./lib/util/helpers'); /** @module userStats */ /** * The user-specific userStats response object, like other response objects, is organized by input subdomain. * @typedef {object} userStatsResponseObject * @property {object} subdomain each subdomain passed in to add will appear as a key in the response * @property {emojiList[]} subdomain.emojiList the list of emoji downloaded from `subdomain` * @property {object[]} subdomain.userStatsResults an array of user stats objects * @property {object} subdomain.userStatsResults.userStatsObject An object containing several maybe-useful statistics, separated by user * @property {string} subdomain.userStatsResults.userStatsObject.user the name of the user in question * @property {emojiList[]} subdomain.userStatsResults.userStatsObject.userEmoji the emojiList the user authored * @property {string} subdomain.userStatsResults.userStatsObject.subdomain redundant :shrug: * @property {Number} subdomain.userStatsResults.userStatsObject.originalCount the number of original emoji the user has created * @property {Number} subdomain.userStatsResults.userStatsObject.aliasCount the number of emoji aliases the user has defined * @property {Number} subdomain.userStatsResults.userStatsObject.totalCount the number of original and aliases the user has created * @property {Number} subdomain.userStatsResults.userStatsObject.percentage the percentage of emoji in the given subdomain that the user is responsible for */ /** * Get a few useful-ish statistics for either specific users, or the top-n emoji creators * * @async * @param {string|string[]} subdomains a single or list of subdomains to analyze. Must match respectively to `token`s and `cookie`s. * @param {string|string[]} tokens a single or list of tokens to add emoji to. Must match respectively to `subdomain`s and `cookie`s. * @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. * @param {object} options contains options for what stats to present * @param {string|string[]} [options.user] user name or array of user names you would like to retrieve user statistics on. If specified, ignores `top` * @param {Number} [options.top] count of top n emoji contriubtors you would like to retrieve user statistics on * @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 * @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 * @param {boolean} [options.verbose] if `true`, all messages will be written to stdout in addition to combined log file. * * @returns {Promise} userStatsResponseObject result object * * @example var userStatsOptions = { user: ['username_1', 'username_2'] // get me some info on these two users }; var userStatsResults = await emojme.userStats('mySubdomain', 'myToken', 'myCookie', userStatsOptions); console.log(userStatsResults); // { // mySubdomain: { // userStatsResults: [ // { // user: 'username_1', // userEmoji: [{ all username_1's emoji }], // subdomain: mySubdomain, // originalCount: x, // aliasCount: y, // totalCount: x + y, // percentage: (x + y) / mySubdomain's total emoji count // }, // { // user: 'username_2', // userEmoji: [{ all username_2's emoji }], // subdomain: mySubdomain, // originalCount: x, // aliasCount: y, // totalCount: x + y, // percentage: (x + y) / mySubdomain's total emoji count // } // ] // } // } */ async function userStats(subdomains, tokens, cookies, options) { subdomains = Helpers.arrayify(subdomains); tokens = Helpers.arrayify(tokens); cookies = Helpers.arrayify(cookies); const users = Helpers.arrayify(options.user); options = options || {}; const [authTuples] = Helpers.zipAuthTuples(subdomains, tokens, cookies); const userStatsPromises = authTuples.map(async (authTuple) => { const emojiAdminList = new EmojiAdminList(...authTuple, options.output); const emojiList = await emojiAdminList.get(options.bustCache, options.since); if (users && users.length > 0) { const results = EmojiAdminList.summarizeUser(emojiList, authTuple[0], users); return results.map((result) => { const safeUserName = FileUtils.sanitize(result.user); FileUtils.writeJson(`./build/${safeUserName}.${result.subdomain}.adminList.json`, result.userEmoji, null, 3); return { subdomain: authTuple[0], userStatsResults: results, emojiList }; }); } const results = EmojiAdminList.summarizeSubdomain(emojiList, authTuple[0], options.top); results.forEach((result) => { const safeUserName = FileUtils.sanitize(result.user); FileUtils.writeJson(`./build/${safeUserName}.${result.subdomain}.adminList.json`, result.userEmoji, null, 3); }); return { subdomain: authTuple[0], userStatsResults: results, emojiList }; }); return Helpers.formatResultsHash(_.flatten(await Promise.all(userStatsPromises))); } function userStatsCli() { const program = new commander.Command(); Cli.requireAuth(program); Cli.allowIoControl(program) .option('--user ', 'slack user you\'d like to get stats on. Can be specified multiple times for multiple users.', Cli.list, null) .option('--top ', 'the top n users you\'d like user emoji statistics on', 10) .parse(process.argv); Cli.unpackAuthJson(program); return userStats(program.subdomain, program.token, program.cookie, { user: program.user, top: program.top, bustCache: program.bustCache, output: program.output, since: program.since, }); } if (require.main === module) { userStatsCli(); } module.exports = { userStats, userStatsCli, }; ================================================ FILE: emojme.js ================================================ #!/usr/bin/env node /* eslint-disable global-require */ const program = require('commander'); if (require.main === module) { program .version(require('./package').version) .command('download', 'download all emoji from given subdomain') .command('upload', 'upload source emoji to given subdomain') .command('add', 'upload source emoji to given subdomain') .command('user-stats', 'get emoji statistics for given user on given subdomain') .command('sync', 'get emoji statistics for given user on given subdomain') .command('favorites', 'get favorite emoji and personal emoji usage statistics') .parse(process.argv); } else { module.exports = { add: require('./emojme-add').add, download: require('./emojme-download').download, upload: require('./emojme-upload').upload, sync: require('./emojme-sync').sync, userStats: require('./emojme-user-stats').userStats, favorites: require('./emojme-favorites').favorites, }; } ================================================ FILE: lib/client-boot.js ================================================ const _ = require('lodash'); const SlackClient = require('./slack-client'); const FileUtils = require('./util/file-utils'); const ENDPOINT = '/client.boot'; const FLANNEL_API_VER = '4'; // Methods to upload emoji to slack. // Instance methods require slack client. // Static methods do not. class ClientBoot { constructor(subdomain, token, cookie, output) { this.subdomain = subdomain; this.token = token; this.cookie = cookie; this.output = output || false; this.slack = new SlackClient(this.subdomain, this.cookie, SlackClient.rateLimitTier(2)); this.endpoint = ENDPOINT; this.flannel_api_ver = FLANNEL_API_VER; } async get(bustCache) { const path = `./build/${this.subdomain}.clientBoot.json`; if (bustCache || FileUtils.isExpired(path)) { const bootData = await this.slack.request(this.endpoint, this.createMultipart()); if (this.output) FileUtils.writeJson(path, bootData); return bootData; } return FileUtils.readJson(path); } createMultipart() { return { token: this.token, flannel_api_ver: this.flannel_api_ver, }; } static extractEmojiUse(data) { const emojiToUsageMap = JSON.parse(data.prefs.emoji_use); const emojiToUsageArray = _.reduce(emojiToUsageMap, (acc, usage, name) => { acc.push({ name, usage }); return acc; }, []); return _.orderBy(emojiToUsageArray, 'usage', 'desc'); } static extractName(data) { return data.self.name; } } module.exports = ClientBoot; ================================================ FILE: lib/emoji-add.js ================================================ const fs = require('graceful-fs'); const _ = require('lodash'); const logger = require('./logger'); const SlackClient = require('./slack-client'); const FileUtils = require('./util/file-utils'); const ENDPOINT = '/emoji.add'; // Methods to upload emoji to slack. // Instance methods require slack client. // Static methods do not. class EmojiAdd { constructor(subdomain, token, cookie, output) { this.subdomain = subdomain; this.token = token; this.cookie = cookie; this.output = output || false; this.slack = new SlackClient(this.subdomain, this.cookie, SlackClient.rateLimitTier(2)); this.endpoint = ENDPOINT; } async uploadSingle(emoji) { const parts = await this.constructor.createMultipart(emoji, this.token); try { const body = await this.slack.request(this.endpoint, parts); if (!body.ok) { throw new Error(body.error); } } catch (err) { logger.warning(`[${this.subdomain}] error on ${emoji.name}: ${err.message || err}`); return Object.assign({}, emoji, { error: err.message || err }); } logger.debug(`[${this.subdomain}] ${emoji.name} uploaded successfully`); return false; // no error } async upload(src) { let masterList; if (Array.isArray(src)) { masterList = src; } else if (!fs.existsSync(src)) { throw new Error(`Emoji source file ${masterList} does not exist`); } else { masterList = FileUtils.readJson(src); } logger.info(`[${this.subdomain}] Attempting to upload ${masterList.length} emoji to ${this.subdomain}`); const [aliasList, emojiList] = _.partition(masterList, 'is_alias'); const emojiResultArray = await Promise.all( emojiList.map(emoji => this.uploadSingle(emoji)), ); const aliasResultArray = await Promise.all( aliasList.map(emoji => this.uploadSingle(emoji)), ); const resultArray = emojiResultArray.concat(aliasResultArray); const errors = _.filter(resultArray); const totalCount = resultArray.length; const errorsCount = errors.length; const successCount = totalCount - errorsCount; logger.info(`\n[${this.subdomain}] Batch upload complete.\n total requests: ${totalCount} \n successes: ${successCount} \n errors: ${errorsCount}`); return { subdomain: this.subdomain, emojiList: masterList, errorList: errors }; } static async createMultipart(emoji, token) { if (emoji.is_alias === 1) { return { token, name: emoji.name, mode: 'alias', alias_for: emoji.alias_for, }; } const emojiData = await FileUtils.getData(emoji.url || emoji.src); return { token, name: emoji.name, mode: 'data', image: emojiData, }; } } module.exports = EmojiAdd; ================================================ FILE: lib/emoji-admin-list.js ================================================ const _ = require('lodash'); const Throttle = require('superagent-throttle'); const logger = require('./logger'); const SlackClient = require('./slack-client'); const FileUtils = require('./util/file-utils'); const Helpers = require('./util/helpers'); const ENDPOINT = '/emoji.adminList'; const PAGE_SIZE = 500; // Methods and datastructures to retrieve // parse, compare, and summarize results from // the "emoji admin list" endpoint. // Instace methods require slack client. // Static methods do not. class EmojiAdminList { constructor(subdomain, token, cookie, output) { this.subdomain = subdomain; this.token = token; this.cookie = cookie; this.output = output || false; this.slack = new SlackClient(this.subdomain, this.cookie, SlackClient.rateLimitTier(3)); this.endpoint = ENDPOINT; this.pageSize = PAGE_SIZE; } setPageSize(pageSize) { this.pageSize = pageSize || PAGE_SIZE; } createMultipart(page) { return { query: '', page, count: this.pageSize, token: this.token, }; } async get(bustCache, sinceTimestamp) { let emojiList; const sinceString = sinceTimestamp ? `${sinceTimestamp}.` : ''; const path = `./build/${sinceString}${this.subdomain}.adminList.json`; if (bustCache || FileUtils.isExpired(path)) { const emojiLists = await this.getAdminListPages(); emojiList = this.constructor.since([].concat(...emojiLists), sinceTimestamp); if (this.output) FileUtils.writeJson(path, emojiList); } else { emojiList = FileUtils.readJson(path); } logger.info(`[${this.subdomain}] Found ${emojiList.length} emoji`); return emojiList; } async getAdminListPages() { const firstPageBody = await this.slack.request(this.endpoint, this.createMultipart(1)); if (!firstPageBody.ok) { throw new Error(`Slack request failed with error ${firstPageBody.error}`); } if (!firstPageBody.emoji) { throw new Error('Unable to retrieve first page of emoji'); } const { pages } = firstPageBody.paging; const promiseArray = [Promise.resolve(firstPageBody.emoji)]; logger.info(`[${this.subdomain}] Found ${firstPageBody.custom_emoji_total_count} total emoji on ${firstPageBody.paging.pages} pages`); for (let i = 2; i <= pages; i++) { promiseArray.push( this.slack.request( this.endpoint, this.createMultipart(i), ).then((body) => { if (!body.ok || !body.emoji || body.emoji.length === 0) { throw new Error(`Failed to fetch page ${i - 1}`); } logger.debug(`[${this.subdomain}] retrieved page ${i - 1}`); return body.emoji; }).catch((err) => { logger.error(`[${this.subdomain}] AdminList page request failed with ${err}`); }), ); } return Promise.all(promiseArray).then(emojiLists => emojiLists.filter(Boolean)); } static find(emojiList, emojiName) { return _.find(emojiList, { name: emojiName }); } // give a count and percentage of all emoji created by [user] static summarizeUser(emojiList, subdomain, users) { users = Helpers.arrayify(users); const groupedEmoji = _.groupBy(emojiList, 'user_display_name'); return users.map((user) => { if (!(user in groupedEmoji)) { logger.warning(`[${subdomain}] Could not find ${user} in emoji contributors`); return false; } const userEmoji = groupedEmoji[user]; const userTotal = userEmoji.length; const originals = _.filter(userEmoji, { is_alias: 0 }).length; const aliases = userTotal - originals; const percentage = (((userTotal * 1.0) / emojiList.length) * 100.0).toPrecision(4); logger.info(`[${subdomain}] ${user}'s emoji: \n\ttotal: ${userTotal} \n\toriginals: ${originals} \n\taliases: ${aliases} \n\tpercentage:${percentage}%`); return { user, userEmoji, subdomain, originalCount: originals, aliasCount: aliases, totalCount: userTotal, percentage, }; }).filter(Boolean); } static summarizeSubdomain(emojiList, subdomain, n) { const groupedEmoji = _.countBy(emojiList, 'user_display_name'); const sortedUsers = _.chain(groupedEmoji) .map((count, user) => ({ user, count })) .orderBy('count', 'desc') .value(); logger.info(`[${subdomain}] The top ${n} contributors for ${subdomain}'s ${emojiList.length} emoji are:`); const topUsers = sortedUsers.slice(0, n); return this.summarizeUser(emojiList, subdomain, topUsers.map(e => e.user)); } static diff(srcLists, srcSubdomains, dstLists, dstSubdomains) { dstLists = dstLists || srcLists; dstSubdomains = dstSubdomains || srcSubdomains; const diffs = []; const uniqEmojiList = _.unionBy(...srcLists, 'name'); _.zipWith(dstSubdomains, dstLists, (dstSubdomain, dstEmojiList) => { const missingEmojiList = _.differenceBy(uniqEmojiList, dstEmojiList, 'name'); diffs.push({ dstSubdomain, srcSubdomains: _.without(srcSubdomains, dstSubdomains), emojiList: missingEmojiList, }); }); return diffs; } static since(emojiList, sinceTimestamp) { if (!sinceTimestamp) { return emojiList; } return _.filter(emojiList, ({ created }) => created > sinceTimestamp); } static async save(emojiList, subdomain, options) { const groupedEmoji = _.groupBy(emojiList, 'user_display_name'); const promiseArray = []; const specialTier = SlackClient.rateLimitTier('special'); const throttle = new Throttle({ concurrent: process.env.SLACK_REQUEST_CONCURRENCY || specialTier.slackRequestConcurrency, rate: process.env.SLACK_REQUEST_RATE || specialTier.slackRequestRate, ratePer: process.env.SLACK_REQUEST_WINDOW || specialTier.slackRequestWindow, }); let subdomainDirCreated = !options.saveAll; const users = (options.saveAll || options.saveAllByUser) ? Object.keys(groupedEmoji) : Helpers.arrayify(options.save); users.forEach((user) => { let dirPath = `build/${subdomain}`; if (!(user in groupedEmoji)) { logger.warning(`[${subdomain}] Could not find ${user} in emoji contributors`); return Promise.resolve(); } logger.info(`[${subdomain}] Found ${groupedEmoji[user].length} emoji to save by ${user}.`); if (!options.saveAll) { dirPath = `${dirPath}/${user}`; FileUtils.mkdirp(dirPath); } else if (!subdomainDirCreated) { FileUtils.mkdirp(dirPath); subdomainDirCreated = true; // prevent wasting time trying to make and remake this } return groupedEmoji[user].forEach((emoji) => { let path = `${dirPath}/${emoji.name}`; try { let fileType; if (emoji.url.match(/^.*\.(gif|png|jpg|jpg)$/)) { fileType = emoji.url.split('.').slice(-1); } else if (emoji.url.match(/^data:.*/)) { fileType = emoji.url.match(/^data:image\/(gif|png|jpg|jpeg).*/)[1]; } else { throw new Error('unable to retrieve emoji source url'); } path += `.${fileType}`; promiseArray.push(FileUtils.getData(emoji.url, { throttle }) .then(emojiData => FileUtils.saveData(emojiData, path)) .then(() => { logger.debug(`[${subdomain}] saved ${emoji.name} to ${path}`); return path; })); } catch (err) { // There is a bizarre case where you can make an alias for a default emoji // and all of a suddent it disappears? the url becomes `null` and `alias_for` = '1'??? logger.warning(`[${subdomain}] unable to save ${emoji.name} to ${path}`); } }); }); return Promise.all(promiseArray); } } module.exports = EmojiAdminList; ================================================ FILE: lib/logger.js ================================================ const winston = require('winston'); const logger = winston.createLogger({ levels: winston.config.syslog.levels, transports: [ // console log anything warning or worse new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.simple(), ), colorize: true, level: 'warning', }), // log everything to a file new winston.transports.File({ filename: 'log/combined.log', format: winston.format.combine( winston.format.timestamp({ format: 'YYY-MM-DD hh:mm:ss A ZZ', }), winston.format.json(), ), prettyPrint: true, timestamp: true, level: 'debug', }), ], }); module.exports = logger; ================================================ FILE: lib/slack-client.js ================================================ const _ = require('lodash'); const superagent = require('superagent'); const Throttle = require('superagent-throttle'); const sleep = require('util').promisify(setTimeout); const logger = require('./logger'); const RATE_LIMIT_TIER = { 1: { slackRequestConcurrency: 1, slackRequestRate: 1, slackRequestWindow: 60000, }, 2: { slackRequestConcurrency: 5, slackRequestRate: 20, slackRequestWindow: 60000, }, 3: { slackRequestConcurrency: 11, slackRequestRate: 49, slackRequestWindow: 60000, }, 4: { slackRequestConcurrency: 25, slackRequestRate: 100, slackRequestWindow: 60000, }, special: { slackRequestConcurrency: 200, slackRequestRate: 3000, slackRequestWindow: 60000, }, }; // slack why const cookieName = 'd'; class SlackClient { constructor(subdomain, cookie, options) { this.subdomain = subdomain; this.cookie = cookie; this.backoffTime = 0; this.backoffs = []; // If not otherwise specified, use tier 2 rate limiting this.options = Object.assign({}, this.constructor.rateLimitTier(2), options); // Throttle requests to Slacks "Tier 2" limit, // 20 requests per minute, or one request per 3000ms. // Run a few requests simultaneously and hope Slack treats // it as an acceptable "burst". this.throttle = new Throttle({ concurrent: process.env.SLACK_REQUEST_CONCURRENCY || this.options.slackRequestConcurrency, rate: process.env.SLACK_REQUEST_RATE || this.options.slackRequestRate, ratePer: process.env.SLACK_REQUEST_WINDOW || this.options.slackRequestWindow, }); } url(endpoint) { return `https://${this.subdomain}.slack.com/api${endpoint}`; } attachParts(req, hash, action) { _.each(hash, (val, key) => { if (key === '_attach') { this.attachParts(req, val, 'attach'); return; } if (typeof val.then === 'function') { val.then(() => { req.attach(key, './test.jpg'); }); } else { req[action](key, val); } }); } async request(endpoint, parts) { if (this.backoffTime > 0) { logger.debug(`[${this.subdomain}] Waiting for ${this.backoffTime} to begin next request to ${endpoint}.`); await sleep(this.backoffTime); } const req = superagent.post(this.url(endpoint)); req.use(this.throttle.plugin()); req.set('Cookie', `${cookieName}=${this.cookie}`); if (parts) { _.each(parts, (val, key) => { if (key === 'image') { req.attach(key, val, { filename: parts.name }); } else { req.field(key, val); } }); } try { const res = await req; if (this.backoffs.length) { this.backoffTime += (this.backoffs.pop() * this.options.slackRequestConcurrency); logger.debug(`[${this.subdomain}] ${this.backoffs.length} backoffs in the queue.`); } else { if (this.throttle.concurrent !== this.options.slackRequestConcurrency) { logger.debug(`[${this.subdomain}] Returning to default concurrency: ${this.options.slackRequestConcurrency} from previous ${this.throttle.concurrent}`); } // Once all backoffs have been satisfied, no longer apply any backoff or throttling. this.throttle.options({ concurrent: this.options.slackRequestConcurrency }); this.backoffTime = 0; } if (!res.body) { throw new Error('No body received from slack'); } if (res.statusCode >= 400) { throw new Error(`Error response: ${res.statusCode} for req ${req.body}`); } return res.body; } catch (err) { let retryAfter; // Sometimes throttling our requests isn't enough and we still get rate limited. // In those cases, retry the request after however long Slack asks us to wait. if ((err.status === 429) && (retryAfter = err.response.headers['retry-after'] * 1000)) { logger.info(`[${this.subdomain}] Rate limiting detected.`); logger.debug(`[${this.subdomain}] Endpoint ${endpoint} rate limited. Will retry request after ${retryAfter} ms backoff`); // Prevent sending multiple requests later and re-angering the rate limiter this.throttle.options({ concurrent: 1 }); this.backoffTime += retryAfter * this.options.slackRequestConcurrency; this.backoffs.push(retryAfter); return this.request(endpoint, parts); } throw new Error(`Caught error from Superagent "${err.message}" for req to ${endpoint}, ${parts.name}`); } } static rateLimitTier(tier) { return RATE_LIMIT_TIER[tier]; } } module.exports = SlackClient; ================================================ FILE: lib/util/cli.js ================================================ const _ = require('lodash'); const logger = require('../logger'); function list(val, memo) { memo = memo || []; memo.push(val === '' ? null : val); return memo; } function verbosity() { logger.transports[0].level = 'debug'; } function requireAuth(program) { return program .option('-s, --subdomain ', 'slack subdomain. Can be specified multiple times, paired with respective token.', list, []) .option('-d, --domain ', 'alias for --subdomain', list, []) .option('-t, --token ', 'slack cookie token. ususaly starts xoxc-... Can be specified multiple times, paired with respective subdomains. User tokens (xoxs-...) are no longer supported. :(', list, []) .option('-c, --cookie ', 'slack cookie. paired with respective subdomains and tokens.', list, []) .option('-a --auth-json ', '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, []); } function allowEmojiAlterations(program) { return program .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) .option('--avoid-collisions', 'instead of culling collisions, rename the emoji to be uploaded "intelligently"', false) .option('--prefix ', 'prefix all emoji to be uploaded with '); } function allowIoControl(program) { return program .option('--bust-cache', 'force a redownload of all cached info.', false) .option('--no-output', 'prevent writing of files in build/ and log/') .option('--since ', 'only consider emoji since the given epoch timestamp') .option('--verbose', 'log debug messages to console', verbosity); } function unpackAuthJson(program) { // Take a commander program, and if an authJson is included, split it into // constituent subdomain, token, and cookies if (!program.authJson || _.isEmpty(program.authJson)) { return; } _.each(program.authJson, (authJson) => { const auth = JSON.parse(authJson); if ((auth.domain || auth.subdomain) && auth.token && auth.cookie) { program.subdomain.push(auth.domain || auth.subdomain); program.token.push(auth.token); program.cookie.push(auth.cookie); } }); } module.exports = { list, requireAuth, allowIoControl, allowEmojiAlterations, unpackAuthJson, }; ================================================ FILE: lib/util/file-utils.js ================================================ const yaml = require('js-yaml'); const superagent = require('superagent'); const fs = require('graceful-fs'); const logger = require('../logger'); const MAX_AGE = (1000 * 60 * 60 * 24); // one day const VALID_FILENAME_CHARS = /[\w\-. ]+/; function mkdirp(path) { const dirs = path.split('/'); for (let i = 1; i <= dirs.length; i++) { const subPath = dirs.slice(0, i).join('/'); if (!fs.existsSync(subPath)) fs.mkdirSync(subPath); } } function writeJson(path, data) { this.mkdirp('build'); fs.writeFileSync( path, JSON.stringify(data, null, 2), ); } function readJson(path) { if (!fs.existsSync(path)) { return {}; } return JSON.parse(fs.readFileSync(path, 'utf8')); } function readYaml(path) { if (fs.existsSync(path)) { const contents = yaml.safeLoad(fs.readFileSync(path, 'utf8')); if (contents.emojis) { return contents.emojis; } } return {}; } function isExpired(path, expirationTimestamp) { const currentTime = Date.now(); let maxAge = currentTime - MAX_AGE; if (expirationTimestamp) { maxAge = Math.max(maxAge, expirationTimestamp); } if (!fs.existsSync(path) || fs.statSync(path).ctimeMs < maxAge) { return true; } logger.debug(`Cached request is still fresh. To force a new request, delete or move ${path}`); return false; } function saveData(data, path) { this.mkdirp('build'); return new Promise((resolve) => { resolve(fs.writeFileSync(path, data, { encoding: 'base64' })); }); } async function getData(path, options) { path = path || ''; options = options || {}; try { if (path.match(/^http.*/)) { const req = superagent.get(path); req.retry(3); req.buffer(true); if (options.throttle) { req.use(options.throttle.plugin()); } const res = await req.parse(superagent.parse.image); return res.body; } if (path.match(/^data:/)) { return path.replace(/^.*base64,/, ''); } if (path.match(/\.(gif|jpg|jpeg|png)/) && fs.existsSync(path)) { return fs.readFileSync(path); } throw new Error('Emoji Url does not contain acceptable data'); } catch (e) { throw e; } } function sanitize(str) { return str .split('') .filter(s => VALID_FILENAME_CHARS.test(s)) // remove illegal characters .join('') .replace(/ +(?= )/g, '') // remove repeated spaces .trim(); } module.exports = { mkdirp, writeJson, readJson, readYaml, isExpired, saveData, getData, sanitize, }; ================================================ FILE: lib/util/helpers.js ================================================ const _ = require('lodash'); function validAuthTuples(subdomains, tokens, cookies) { return subdomains.length === tokens.length && tokens.length === cookies.length && _.every(subdomains, _.isString) && _.every(tokens, _.isString) && _.every(cookies, _.isString); } function validSrcDstPairs(options) { return options.srcSubdomains && options.srcTokens && options.srcCookies && (options.srcSubdomains.length === options.srcTokens.length) && (options.srcTokens.length === options.srcCookies.length) && options.dstSubdomains && options.dstTokens && options.dstCookies && (options.dstSubdomains.length === options.dstTokens.length) && (options.dstTokens.length === options.dstCookies.length); } function atLeastOneValidInputType(subdomains, tokens, cookies, options) { return (validAuthTuples(subdomains, tokens, cookies) && subdomains.length > 0) || ( validSrcDstPairs(options) && options.srcSubdomains.length > 0 && options.dstSubdomains.length > 0 ); } function zipAuthTuples(subdomains, tokens, cookies, options) { let srcPairs = []; let dstPairs = []; options = options || {}; if (options && !_.isEmpty(options) && options.srcSubdomains && options.srcTokens && options.srcCookies) { srcPairs = _.zip(options.srcSubdomains, options.srcTokens, options.srcCookies); dstPairs = _.zip(options.dstSubdomains, options.dstTokens, options.dstCookies); } const authTuples = _.uniq(_.zip(subdomains, tokens, cookies).concat(srcPairs, dstPairs)); if (!atLeastOneValidInputType(subdomains, tokens, cookies, options)) { throw new Error('Invalid input. Ensure that every given "subdomain" has a matching "token" and "cookie"'); } return [authTuples, srcPairs, dstPairs]; } function applyPrefix(emojiList, prefix) { return emojiList.map(e => ({ ...e, name: prefix + e.name })); } // Given an emoji, return its slug-name, i.e. the name without delimiter or id function slugName(emoji, returnIdDelim) { let id; let idMatch; let delimiter; let delimiterMatch; let name = emoji.name; if (idMatch = name.match(/^[A-z-_]*([0-9])$/)) { id = parseInt(idMatch[1], 10); name = name.slice(0, -1); } else { id = 0; } if (delimiterMatch = name.match(/[-_]/g)) { delimiter = delimiterMatch.slice(-1); if (name.lastIndexOf(delimiter) === name.length - 1) { name = name.slice(0, -1); } } else { // If emoji1 collides, the next emoji is emoji2 // If emoji collides, the next emoji is emoji-1 delimiter = idMatch ? '' : '-'; } if (returnIdDelim) return [name, id, delimiter]; return name; } // Group array of emoji objects into hash of emojiSlug : emojiList // where emojiSlug is the emoji name without delimiter or id // e.g. emoji-1 and emoji_1 both have slug emoji function groupEmoji(emojiList) { let maxId = -1; return _(emojiList).sortBy('name').groupBy((e) => { const [nameSlug, id] = slugName(e, true); maxId = (id > maxId) ? id : maxId; return nameSlug; }).reduce((acc, val, key) => { acc[key] = { list: val, maxId: 0 }; return acc; }, {}); } // Given array of existing emoji and an array of emoji to add // rename new emoji to avoid colliding with existing emoji and themsevles. function avoidCollisions(newEmoji, existingEmoji) { const usedNameList = existingEmoji.map(e => e.name); const completeNameList = usedNameList.concat(newEmoji.map(e => e.name)); const groupedEmoji = groupEmoji(existingEmoji.concat(newEmoji)); return newEmoji.map((emoji) => { if (!usedNameList.includes(emoji.name)) { usedNameList.push(emoji.name); return emoji; } const [nameSlug, id, delimiter] = slugName(emoji, true); // eslint-disable-line no-unused-vars let maxId = groupedEmoji[nameSlug].maxId += 1; let newName = `${nameSlug}${delimiter}${maxId}`; while (completeNameList.includes(newName)) { maxId = groupedEmoji[nameSlug].maxId += 1; newName = `${nameSlug}${delimiter}${maxId}`; usedNameList.push(emoji.name); completeNameList.push(emoji.name); } return { ...emoji, name: newName, collision: emoji.name }; }); } function formatResultsHash(resultsArray) { return _.reduce(resultsArray, (acc, elem) => { if (!elem.subdomain) { throw new Error('Found results unattached from subdomain'); } acc[elem.subdomain] = _.omit(elem, 'subdomain'); return acc; }, {}); } function arrayify(elem) { return elem ? _.castArray(elem) : []; } module.exports = { arrayify, applyPrefix, avoidCollisions, groupEmoji, slugName, zipAuthTuples, formatResultsHash, }; ================================================ FILE: package.json ================================================ { "name": "emojme", "description": "The Emojartist's toolbox for spreading their work across the slackosphere", "version": "2.0.1", "keywords": [ "emoji", "slack", "sync", "download", "upload" ], "author": "Jack Ellenberger ", "repository": { "type": "git", "url": "https://github.com/jackellenberger/emojme" }, "main": "emojme.js", "files": [ "emojme*", "lib", "README.md" ], "bin": { "emojme": "./emojme.js" }, "engines": { "node": ">=10.0.0" }, "scripts": { "lint": "eslint . || true", "test": "mocha spec/unit/**/* && mocha spec/integration/**/*", "test:unit": "mocha spec/unit/**/*", "test:integration": "mocha spec/integration/**/*", "test:debug": "node inspect node_modules/mocha/bin/_mocha", "test:e2e": "mocha spec/e2e/**", "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", "generate-usage": "./scripts/usage.sh" }, "license": "ISC", "dependencies": { "commander": "^2.17.1", "graceful-fs": "^4.1.11", "js-yaml": "^3.13.1", "lodash": "^4.17.14", "superagent": "^3.8.3", "superagent-throttle": "^1.0.0", "winston": "^3.1.0" }, "devDependencies": { "chai": "^4.1.2", "chai-shallow-deep-equal": "^1.4.6", "eslint": "^5.13.0", "eslint-config-airbnb-base": "^13.1.0", "eslint-plugin-import": "^2.14.0", "jsdoc": "^3.6.3", "jsdoc-template": "braintree/jsdoc-template#3.2.0", "mocha": "^5.2.0", "sinon": "^7.2.3" } } ================================================ FILE: scripts/usage.sh ================================================ #!/bin/bash # Don't lose all our progress if we can avoid it mv USAGE.md USAGE.md.old # Get top level commands into USAGE.md echo "# Commands" > USAGE.md echo "\`\`\`" >> USAGE.md ./emojme.js >> USAGE.md echo "\`\`\`" >> USAGE.md # Parse back out the commands so we can run them # NOTE: thie head'ing depends on the above, be careful not to heck it up! for command in $(cat USAGE.md | sed -n '/^Commands:$/,$p' | head -n-2 | tail -n+2 | awk '{print $1}'); do echo >> USAGE.md echo "## emojme $command" >> USAGE.md echo "\`\`\`" >> USAGE.md node emojme-${command}.js --help >> USAGE.md echo "\`\`\`" >> USAGE.md done ================================================ FILE: spec/e2e/emojme-download.js ================================================ const chai = require('chai'); chai.use(require('chai-shallow-deep-equal')); const assert = chai.assert; const commander = require('commander'); const downloadCli = require('../../emojme-download').downloadCli; let subdomain; let token; let cookie; before(() => { const program = new commander.Command(); console.log('THIS WILL MAKE REAL REQUESTS AGAINST REAL SLACK. CONSIDER YOURSELF CHASTISED.'); program .option('-s --subdomain ', 'slack subdomain for testing.') .option('-t --token ', 'slack token for testing.') .option('-c --cookie ', 'slack cookie for testing.') .parse(process.argv); subdomain = program.subdomain; token = program.token; cookie = program.cookie; }); describe('emojme download', () => { it('fails when no authentication is specified', () => { process.argv = [ 'node', 'emojme', 'download', ]; return downloadCli().then(() => { throw Error('FAIL you should not be able to use emojme without a subdomain and token.'); }).catch((err) => { assert.equal(err.message, 'Invalid input. Ensure that every given "subdomain" has a matching "token" and "cookie"'); }); }); it('downloads an emoji.adminList when authenticated', (done) => { // Note that for this to work you may need to increase timeout process.argv = [ 'node', 'emojme', 'download', '--subdomain', subdomain, '--token', token, '--cookie', cookie, '--verbose', ]; return downloadCli().then(() => { // If we don't get an auth error, we're happy. done(); }); }); }); ================================================ FILE: spec/fixtures/clientBoot.json ================================================ { "ok": true, "self": { "id": "UUSERID", "name": "username", "prefs": { "user_colors": "", "color_names_in_list": true, "keyboard": null, "email_alerts": "none", "email_alerts_sleep_until": 0, "email_tips": true, "email_weekly": true, "email_offers": true, "email_research": true, "email_developer": true, "welcome_message_hidden": false, "search_sort": "not_set", "search_file_sort": "score", "search_channel_sort": "relevant", "search_people_sort": "relevant", "expand_inline_imgs": true, "expand_internal_inline_imgs": true, "expand_snippets": false, "posts_formatting_guide": true, "seen_welcome_2": true, "seen_ssb_prompt": false, "spaces_new_xp_banner_dismissed": false, "search_only_my_channels": false, "search_only_current_team": false, "search_hide_my_channels": false, "search_only_show_online": false, "search_hide_deactivated_users": false, "emoji_mode": "default", "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}", "has_invited": true, "has_uploaded": true, "has_created_channel": true, "has_searched": true, "search_exclude_channels": "", "messages_theme": "default", "webapp_spellcheck": true, "no_joined_overlays": true, "no_created_overlays": false, "dropbox_enabled": false, "seen_domain_invite_reminder": false, "seen_member_invite_reminder": false, "mute_sounds": false, "arrow_history": false, "tab_ui_return_selects": true, "obey_inline_img_limit": true, "require_at": false, "ssb_space_window": "", "mac_ssb_bounce": "", "mac_ssb_bullet": true, "expand_non_media_attachments": true, "show_typing": true, "pagekeys_handled": true, "last_snippet_type": "auto", "display_real_names_override": 0, "display_display_names": true, "time24": false, "enter_is_special_in_tbt": false, "msg_input_send_btn": false, "msg_input_send_btn_auto_set": false, "graphic_emoticons": false, "convert_emoticons": true, "ss_emojis": true, "seen_onboarding_start": false, "onboarding_cancelled": true, "seen_onboarding_slackbot_conversation": false, "seen_onboarding_channels": false, "seen_onboarding_direct_messages": false, "seen_onboarding_invites": false, "seen_onboarding_search": false, "seen_onboarding_recent_mentions": false, "seen_onboarding_starred_items": false, "seen_onboarding_private_groups": false, "seen_onboarding_banner": false, "onboarding_slackbot_conversation_step": 0, "set_tz_automatically": false, "dnd_enabled": true, "dnd_start_hour": "22:00", "dnd_end_hour": "8:00", "dnd_before_monday": "8:00", "dnd_after_monday": "22:00", "dnd_enabled_monday": "partial", "dnd_before_tuesday": "8:00", "dnd_after_tuesday": "22:00", "dnd_enabled_tuesday": "partial", "dnd_before_wednesday": "8:00", "dnd_after_wednesday": "22:00", "dnd_enabled_wednesday": "partial", "dnd_before_thursday": "8:00", "dnd_after_thursday": "22:00", "dnd_enabled_thursday": "partial", "dnd_before_friday": "8:00", "dnd_after_friday": "22:00", "dnd_enabled_friday": "partial", "dnd_before_saturday": "8:00", "dnd_after_saturday": "22:00", "dnd_enabled_saturday": "partial", "dnd_before_sunday": "8:00", "dnd_after_sunday": "22:00", "dnd_enabled_sunday": "partial", "dnd_days": "every_day", "dnd_custom_new_badge_seen": false, "dnd_notification_schedule_new_badge_seen": false, "unread_collapsed_channels": null, "sidebar_behavior": "", "channel_sort": "default", "separate_private_channels": false, "separate_shared_channels": true, "sidebar_theme": "default", "sidebar_theme_custom_values": "", "no_invites_widget_in_sidebar": false, "no_omnibox_in_channels": false, "k_key_omnibox_auto_hide_count": 5, "show_sidebar_quickswitcher_button": false, "ent_org_wide_channels_sidebar": true, "mark_msgs_read_immediately": true, "start_scroll_at_oldest": true, "snippet_editor_wrap_long_lines": false, "ls_disabled": false, "f_key_search": false, "k_key_omnibox": true, "prompted_for_email_disabling": false, "no_macelectron_banner": false, "no_macssb1_banner": true, "no_macssb2_banner": false, "no_winssb1_banner": true, "hide_user_group_info_pane": false, "mentions_exclude_at_user_groups": false, "mentions_exclude_reactions": false, "privacy_policy_seen": true, "enterprise_migration_seen": true, "last_tos_acknowledged": "tos_mar2018", "search_exclude_bots": false, "load_lato_2": false, "fuller_timestamps": false, "last_seen_at_channel_warning": 1520548435691, "emoji_autocomplete_big": false, "two_factor_auth_enabled": false, "two_factor_type": null, "two_factor_backup_type": null, "hide_hex_swatch": false, "show_jumper_scores": false, "enterprise_mdm_custom_msg": "", "enterprise_excluded_app_teams": null, "client_logs_pri": "", "flannel_server_pool": "random", "mentions_exclude_at_channels": true, "confirm_clear_all_unreads": true, "confirm_user_marked_away": true, "box_enabled": false, "seen_single_emoji_msg": true, "confirm_sh_call_start": true, "preferred_skin_tone": "", "show_all_skin_tones": false, "whats_new_read": 1560276001, "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]}}", "frecency_ent_jumper": "", "frecency_ent_jumper_backup": "", "jumbomoji": true, "newxp_seen_last_message": 0, "show_memory_instrument": false, "enable_unread_view": false, "seen_unread_view_coachmark": false, "enable_react_emoji_picker": true, "seen_custom_status_badge": false, "seen_custom_status_callout": false, "seen_custom_status_expiration_badge": false, "used_custom_status_kb_shortcut": false, "seen_guest_admin_slackbot_announcement": false, "seen_threads_notification_banner": false, "seen_name_tagging_coachmark": true, "all_unreads_sort_order": "", "locale": "en-US", "seen_intl_channel_names_coachmark": false, "seen_p2_locale_change_message": 1556807549196, "seen_locale_change_message": 3, "seen_japanese_locale_change_message": false, "seen_shared_channels_coachmark": false, "seen_shared_channels_opt_in_change_message": false, "has_recently_shared_a_channel": false, "seen_channel_browser_admin_coachmark": false, "seen_administration_menu": false, "seen_drafts_section_coachmark": true, "seen_emoji_update_overlay_coachmark": false, "seen_sonic_deluxe_toast": 2, "allow_calls_to_set_current_status": true, "in_interactive_mas_migration_flow": false, "sunset_interactive_message_views": 0, "shdep_promo_code_submitted": false, "seen_shdep_slackbot_message": false, "seen_calls_interactive_coachmark": false, "allow_cmd_tab_iss": false, "workflow_builder_coachmarks": "{}", "seen_gdrive_coachmark": false, "overloaded_message_enabled": true, "seen_highlights_coachmark": false, "seen_highlights_arrows_coachmark": false, "seen_highlights_warm_welcome": false, "seen_new_search_ui": true, "seen_channel_search": false, "seen_people_search": false, "a11y_animations": true, "seen_keyboard_shortcuts_coachmark": false, "needs_initial_password_set": false, "lessons_enabled": false, "tractor_enabled": false, "tractor_experiment_group": "", "opened_slackbot_dm": false, "newxp_suggested_channels": "", "onboarding_complete": true, "welcome_place_state": "none", "whocanseethis_dm_mpdm_badge": false, "highlight_words": "", "threads_everything": false, "no_text_in_notifications": false, "push_show_preview": true, "growls_enabled": true, "all_channels_loud": false, "push_dm_alert": true, "push_mention_alert": true, "push_everything": false, "push_idle_wait": 2, "push_sound": "b2.mp3", "new_msg_snd": "knock_brush.mp3", "push_loud_channels": "", "push_mention_channels": "CCHANNELID", "push_loud_channels_set": "CCHANNELID", "loud_channels": "", "never_channels": "", "loud_channels_set": "", "at_channel_suppressed_channels": "", "push_at_channel_suppressed_channels": "", "muted_channels": "", "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}}}", "growth_msg_limit_approaching_cta_count": 0, "growth_msg_limit_approaching_cta_ts": 0, "growth_msg_limit_reached_cta_count": 0, "growth_msg_limit_reached_cta_last_ts": 0, "growth_msg_limit_long_reached_cta_count": 0, "growth_msg_limit_long_reached_cta_last_ts": 0, "growth_msg_limit_sixty_day_banner_cta_count": 0, "growth_msg_limit_sixty_day_banner_cta_last_ts": 0, "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}", "analytics_upsell_coachmark_seen": false, "seen_app_space_coachmark": false, "seen_app_space_tutorial": false, "purchaser": false, "app_action_picker": "{ \"enabled\": false }", "show_ent_onboarding": true, "folders_enabled": false, "folder_data": "[]", "seen_corporate_export_alert": false, "show_autocomplete_help": 1, "deprecation_toast_last_seen": 0, "deprecation_modal_last_seen": 0, "failover_proxy_check_completed": 0, "edge_upload_proxy_check_completed": 3, "app_subdomain_check_completed": 8, "add_apps_prompt_dismissed": false, "add_channel_prompt_dismissed": false, "channel_sidebar_hide_invite": false, "in_prod_surveys_enabled": true, "dismissed_installed_app_dm_suggestions": "", "tz": "America/Chicago", "locales_enabled": { "de-DE": "Deutsch (Deutschland)", "en-GB": "English (UK)", "en-US": "English (US)", "es-ES": "Español (España)", "es-LA": "Español (Latinoamérica)", "fr-FR": "Français (France)", "pt-BR": "Português (Brasil)", "ja-JP": "日本語" } }, "created": 1510585552, "manual_presence": "active" }, "team": { "id": "TTEAMID", "name": "teamname", "email_domain": "", "domain": "domain", "msg_edit_window_mins": -1, "prefs": { "locale": "en-US", "invites_only_admins": true, "show_join_leave": true, "default_channels": [ "CCHANNEL" ], "display_email_addresses": false, "who_can_create_channels": "regular", "who_can_archive_channels": "regular", "who_can_create_groups": "regular", "who_can_kick_channels": "owner", "who_can_kick_groups": "regular", "gdrive_enabled_team": true, "slackbot_responses_disabled": false, "hide_referers": true, "msg_edit_window_mins": -1, "allow_message_deletion": true, "calling_app_name": "Slack", "allow_calls": true, "allow_calls_interactive_screen_sharing": true, "display_real_names": false, "who_can_at_everyone": "regular", "who_can_at_channel": "ra", "who_can_manage_channel_posting_prefs": "ra", "who_can_post_general": "ra", "retention_type": 0, "retention_duration": 0, "group_retention_type": 0, "group_retention_duration": 0, "dm_retention_type": 0, "dm_retention_duration": 0, "file_retention_type": 0, "file_retention_duration": 0, "allow_retention_override": false, "default_rxns": [ "simple_smile", "thumbsup", "white_check_mark", "heart", "eyes" ], "compliance_export_start": 0, "warn_before_at_channel": "always", "disallow_public_file_urls": false, "who_can_create_delete_user_groups": "admin", "who_can_edit_user_groups": "admin", "who_can_change_team_profile": "admin", "subteams_auto_create_owner": false, "subteams_auto_create_admin": false, "discoverable": "unlisted", "dnd_days": "every_day", "invite_requests_enabled": true, "disable_file_uploads": "allow_all", "disable_file_editing": false, "disable_file_deleting": false, "file_limit_whitelisted": false, "uses_customized_custom_status_presets": false, "disable_email_ingestion": false, "who_can_manage_guests": { "type": [ "admin" ] }, "who_can_create_shared_channels": "admin", "who_can_manage_shared_channels": { "type": [ "admin" ] }, "who_can_post_in_shared_channels": { "type": [ "ra" ] }, "allow_shared_channel_perms_override": false, "who_can_manage_ext_shared_channels": { "type": [ "ORG_ADMINS_AND_OWNERS" ] }, "dropbox_legacy_picker": false, "onedrive_enabled_team": false, "can_receive_shared_channels_invites": true, "enterprise_default_channels": [], "enterprise_mandatory_channels": [], "enterprise_mdm_disable_file_download": false, "mobile_passcode_timeout_in_seconds": -1, "has_hipaa_compliance": false, "all_users_can_purchase": true, "self_serve_select": false, "loud_channel_mentions_limit": 10000, "enable_shared_channels": 2, "enterprise_mobile_device_check": false, "disable_sidebar_connect_prompts": [], "disable_sidebar_install_prompts": [], "block_file_download": false, "custom_contact_email": null, "dnd_enabled": true, "dnd_start_hour": "22:00", "dnd_end_hour": "08:00", "dnd_before_monday": "08:00", "dnd_after_monday": "22:00", "dnd_before_tuesday": "08:00", "dnd_after_tuesday": "22:00", "dnd_before_wednesday": "08:00", "dnd_after_wednesday": "22:00", "dnd_before_thursday": "08:00", "dnd_after_thursday": "22:00", "dnd_before_friday": "08:00", "dnd_after_friday": "22:00", "dnd_before_saturday": "08:00", "dnd_after_saturday": "22:00", "dnd_before_sunday": "08:00", "dnd_after_sunday": "22:00", "dnd_enabled_monday": "partial", "dnd_enabled_tuesday": "partial", "dnd_enabled_wednesday": "partial", "dnd_enabled_thursday": "partial", "dnd_enabled_friday": "partial", "dnd_enabled_saturday": "partial", "dnd_enabled_sunday": "partial", "custom_status_presets": [ [ ":spiral_calendar_pad:", "In a meeting", "In a meeting", "1_hour" ], [ ":bus:", "Commuting", "Commuting", "30_minutes" ], [ ":face_with_thermometer:", "Out sick", "Out sick", "all_day" ], [ ":palm_tree:", "Vacationing", "Vacationing", "no_expiration" ], [ ":house_with_garden:", "Working remotely", "Working remotely", "all_day" ] ], "custom_status_default_emoji": ":speech_balloon:", "auth_mode": "normal", "who_can_manage_integrations": { "type": [ "regular" ] }, "app_whitelist_enabled": false, "invites_limit": true }, "icon": { "image_34": "https://avatars.slack-edge.com/2017-11-13/270775423888_61d633e513ed13fdbcd0_34.png", "image_44": "https://avatars.slack-edge.com/2017-11-13/270775423888_61d633e513ed13fdbcd0_44.png", "image_68": "https://avatars.slack-edge.com/2017-11-13/270775423888_61d633e513ed13fdbcd0_68.png", "image_88": "https://avatars.slack-edge.com/2017-11-13/270775423888_61d633e513ed13fdbcd0_88.png", "image_102": "https://avatars.slack-edge.com/2017-11-13/270775423888_61d633e513ed13fdbcd0_102.png", "image_132": "https://avatars.slack-edge.com/2017-11-13/270775423888_61d633e513ed13fdbcd0_132.png", "image_230": "https://avatars.slack-edge.com/2017-11-13/270775423888_61d633e513ed13fdbcd0_230.png", "image_original": "https://avatars.slack-edge.com/2017-11-13/270775423888_61d633e513ed13fdbcd0_original.png" }, "over_storage_limit": false, "messages_count": 51294, "plan": "", "onboarding_channel_id": "", "date_create": 1510365798, "limit_ts": 1553817600000000, "avatar_base_url": "https://ca.slack-edge.com/" }, "accept_tos_url": null, "latest_event_ts": "1565672021.000000", "channels": [ { "id": "CCHANELID", "name": "channel-name", "is_channel": true, "is_group": false, "is_im": false, "created": 1510539800, "is_archived": false, "is_general": false, "unlinked": 0, "name_normalized": "channel-name", "is_shared": false, "is_frozen": false, "parent_conversation": null, "creator": "UUSERID", "is_ext_shared": false, "is_org_shared": false, "shared_team_ids": [ "TTEAMID" ], "pending_shared": [], "pending_connected_team_ids": [], "is_pending_ext_shared": false, "is_member": false, "is_private": false, "is_mpim": false, "previous_names": [], "priority": 0 } ], "groups": [ { "id": "GGROUPID", "name": "group-name", "is_channel": false, "is_group": true, "is_im": false, "created": 1558839826, "is_archived": false, "is_general": false, "unlinked": 0, "name_normalized": "group-name", "is_shared": false, "is_frozen": false, "parent_conversation": null, "creator": "UUSERID", "is_ext_shared": false, "is_org_shared": false, "shared_team_ids": [ "TTEAMID" ], "pending_shared": [], "pending_connected_team_ids": [], "is_pending_ext_shared": false, "is_member": true, "is_private": true, "is_mpim": false, "last_read": "1565620503.000300", "latest": "1565620503.000300", "is_open": true, "members": [ "UUSERID1", "UUSERID2" ], "topic": { "value": "", "creator": "", "last_set": 0 }, "purpose": { "value": "", "creator": "", "last_set": 0 }, "priority": 0 } ], "ims": [ { "id": "DDIRECTMESSAGEID", "created": 1510585552, "is_frozen": false, "is_archived": false, "is_im": true, "is_org_shared": false, "user": "USLACKBOT", "last_read": "1564105295.000100", "latest": "1564105295.000100", "is_open": true, "priority": 0 } ], "cache_ts": 1565672621, "read_only_channels": [], "non_threadable_channels": [], "thread_only_channels": [], "can_manage_shared_channels": false, "mpims": [ { "id": "GGROUPID", "name": "mpdm-username1--username2--username3-1", "is_channel": false, "is_group": true, "is_im": false, "created": 1527272038, "is_archived": false, "is_general": false, "unlinked": 0, "name_normalized": "mpdm-username1--username2--username3-1", "is_shared": false, "is_frozen": false, "parent_conversation": null, "creator": "UUSERID", "is_ext_shared": false, "is_org_shared": false, "shared_team_ids": [ "TTEAMID" ], "pending_shared": [], "pending_connected_team_ids": [], "is_pending_ext_shared": false, "is_member": true, "is_private": true, "is_mpim": true, "last_read": "1527458159.000075", "latest": "1527458159.000075", "is_open": false, "members": [ "UUSERID1", "UUSERID2", "UUSERID3" ], "topic": { "value": "Group messaging", "creator": "UUSERID", "last_set": 1527272038 }, "purpose": { "value": "Group messaging with: @username1 @username2 @username3", "creator": "UUSERID", "last_set": 1527272038 }, "priority": 0 } ], "subteams": { "self": [], "all": [] }, "dnd": { "dnd_enabled": true, "next_dnd_start_ts": 1565665200, "next_dnd_end_ts": 1565701200, "snooze_enabled": false }, "cache_version": "v18-kudu", "cache_ts_version": "v2-bunny", "url": "wss://cerberus-xxxx.lb.slack-msgs.com/websocket/sha-of-socket-presumably=" } ================================================ FILE: spec/fixtures/emojiList.json ================================================ [ { "name": "emoji-1", "url": "./spec/fixtures/Example.jpg", "user_display_name": "test-user-0" }, { "name": "emoji-2", "is_alias": 1, "alias_for": "emoji-1", "user_display_name": "test-user-0" }, { "name": "emoji-3", "url": "./spec/fixtures/Example.jpg", "user_display_name": "test-user-1" }, { "name": "emoji-4", "is_alias": 1, "alias_for": "emoji-3", "user_display_name": "test-user-0" } ] ================================================ FILE: spec/fixtures/emojiList.yaml ================================================ --- title: 'some title it doesnt matter' anIgnoredKey: 'all keys outside "emojis" are ignored' emojis: - name: 'emoji-1' src: './spec/fixtures/Example.jpg' fullname: 'this key will be ignored' - name: 'emoji-2' is_alias: 1 alias_for: 'emoji-1' - name: 'emoji-3' src: './spec/fixtures/Example.jpg' - name: 'emoji-4' is_alias: 1 alias_for: 'emoji-3' ================================================ FILE: spec/integration/emojme-add-spec.js ================================================ const chai = require('chai'); chai.use(require('chai-shallow-deep-equal')); const assert = chai.assert; const sinon = require('sinon'); const EmojiAdd = require('../../lib/emoji-add'); const EmojiAdminList = require('../../lib/emoji-admin-list'); const SlackClient = require('../../lib/slack-client'); const FileUtils = require('../../lib/util/file-utils'); const add = require('../../emojme-add').add; const addCli = require('../../emojme-add').addCli; const logger = require('../../lib/logger'); let sandbox; let warningSpy; let infoSpy; let debugSpy; beforeEach(() => { sandbox = sinon.createSandbox(); warningSpy = sandbox.spy(logger, 'warning'); infoSpy = sandbox.spy(logger, 'info'); debugSpy = sandbox.spy(logger, 'debug'); }); afterEach(() => { sandbox.restore(); logger.transports[0].level = 'warning'; }); describe('add', () => { context('pre upload configuration', () => { beforeEach(() => { const uploadStub = sandbox.stub(EmojiAdd.prototype, 'upload'); uploadStub.callsFake(arg1 => Promise.resolve({ subdomain: 'subdomain', emojiList: arg1 })); sandbox.stub(EmojiAdminList.prototype, 'get').withArgs(sinon.match.any).resolves( [{ name: 'emoji-1' }], ); }); describe('renames emoji to avoid collisions when avoidCollisions is set', () => { const validateResults = ((results) => { assert.shallowDeepEqual(results, { subdomain: { collisions: [], emojiList: [ { name: 'emoji-5', collision: 'emoji-1', }, { name: 'emoji-2' }, { name: 'emoji-3' }, { name: 'emoji-4' }, ], }, }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'add', '--subdomain', 'subdomain', '--token', 'token', '--cookie', 'cookie', '--name', 'emoji-1', '--alias-for', 'emoji', '--name', 'emoji-2', '--alias-for', 'emoji', '--name', 'emoji-3', '--alias-for', 'emoji', '--name', 'emoji-4', '--alias-for', 'emoji', '--avoid-collisions', ]; return addCli().then(validateResults); }); it('using the module', () => { const options = { name: ['emoji-1', 'emoji-2', 'emoji-3', 'emoji-4'], aliasFor: ['emoji', 'emoji', 'emoji', 'emoji'], avoidCollisions: true, }; return add('subdomain', 'token', 'cookie', options).then(validateResults); }); }); describe('allows slack to return exceptions when allowCollisions is set', () => { beforeEach(() => { sandbox.restore(); sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves( { error: 'error_name_taken', alias_for: 'emoji', is_alias: 1, name: 'emoji-1', }, ); }); const validateResults = ((results) => { assert.shallowDeepEqual(results, { subdomain: { collisions: [], emojiList: [{ alias_for: 'emoji', is_alias: 1, name: 'emoji-1', }, { alias_for: 'emoji', is_alias: 1, name: 'emoji-2', }, { alias_for: 'emoji', is_alias: 1, name: 'emoji-3', }, { alias_for: 'emoji', is_alias: 1, name: 'emoji-4', }, ], errorList: [ { error: 'error_name_taken', alias_for: 'emoji', is_alias: 1, name: 'emoji-1', }, { error: 'error_name_taken', alias_for: 'emoji', is_alias: 1, name: 'emoji-2', }, { error: 'error_name_taken', alias_for: 'emoji', is_alias: 1, name: 'emoji-3', }, { error: 'error_name_taken', alias_for: 'emoji', is_alias: 1, name: 'emoji-4', }, ], }, }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'add', '--subdomain', 'subdomain', '--token', 'token', '--cookie', 'cookie', '--name', 'emoji-1', '--alias-for', 'emoji', '--name', 'emoji-2', '--alias-for', 'emoji', '--name', 'emoji-3', '--alias-for', 'emoji', '--name', 'emoji-4', '--alias-for', 'emoji', '--allow-collisions', ]; return addCli().then(validateResults); }); it('using the module', () => { const options = { name: ['emoji-1', 'emoji-2', 'emoji-3', 'emoji-4'], aliasFor: ['emoji', 'emoji', 'emoji', 'emoji'], allowCollisions: true, }; return add('subdomain', 'token', 'cookie', options).then(validateResults); }); }); describe('collects and does not attempt to upload collisions when avoidCollisions is false', () => { const validateResults = ((results) => { assert.shallowDeepEqual(results, { subdomain: { collisions: [ { name: 'emoji-1' }, ], emojiList: [ { name: 'emoji-2' }, { name: 'emoji-3' }, { name: 'emoji-4' }, ], }, }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'add', '--subdomain', 'subdomain', '--token', 'token', '--cookie', 'cookie', '--name', 'emoji-1', '--alias-for', 'emoji', '--name', 'emoji-2', '--alias-for', 'emoji', '--name', 'emoji-3', '--alias-for', 'emoji', '--name', 'emoji-4', '--alias-for', 'emoji', ]; return addCli().then(validateResults); }); it('using the module', () => { const options = { name: ['emoji-1', 'emoji-2', 'emoji-3', 'emoji-4'], aliasFor: ['emoji', 'emoji', 'emoji', 'emoji'], avoidCollisions: false, }; return add('subdomain', 'token', 'cookie', options).then(validateResults); }); }); }); context('upload behavior', () => { beforeEach(() => { sandbox.stub(EmojiAdminList.prototype, 'get').withArgs(sinon.match.any).resolves( [{ name: 'emoji-1' }], ); }); describe('returns array of subdomain specific results when uploading aliases', () => { beforeEach(() => { const requestStub = sandbox.stub(SlackClient.prototype, 'request'); requestStub.withArgs(sinon.match.any).resolves( { ok: true }, ); requestStub.withArgs(sinon.match.any).onFirstCall().resolves( { ok: false, error: 'an error message' }, ); }); const validateResults = ((results) => { assert.equal(warningSpy.callCount, 1); assert.equal(infoSpy.callCount, 4); assert.equal(debugSpy.callCount, 5); assert.equal(results.subdomain1.emojiList.length, 3); // 4 minus 1 collision assert.deepEqual(results.subdomain1.errorList, [{ name: 'emoji-2', is_alias: 1, alias_for: 'emoji', error: 'an error message', }]); // error on first call assert.equal(results.subdomain1.collisions.length, 1); // collision with emoji-1 assert.equal(results.subdomain2.emojiList.length, 3); // 4 minus 1 collision assert.equal(results.subdomain2.errorList.length, 0); // no errors assert.equal(results.subdomain2.collisions.length, 1); // collision with emoji-1 }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'add', '--subdomain', 'subdomain1', '--subdomain', 'subdomain2', '--token', 'token1', '--token', 'token2', '--cookie', 'cookie1', '--cookie', 'cookie2', '--name', 'emoji-1', '--alias-for', 'emoji', '--name', 'emoji-2', '--alias-for', 'emoji', '--name', 'emoji-3', '--alias-for', 'emoji', '--name', 'emoji-4', '--alias-for', 'emoji', '--verbose', ]; return addCli().then(validateResults); }); it('using the module', () => { const subdomains = ['subdomain1', 'subdomain2']; const tokens = ['token1', 'token2']; const cookies = ['cookie1', 'cookie2']; const options = { name: ['emoji-1', 'emoji-2', 'emoji-3', 'emoji-4'], aliasFor: ['emoji', 'emoji', 'emoji', 'emoji'], avoidCollisions: false, }; return add(subdomains, tokens, cookies, options).then(validateResults); }); }); describe('returns array of subdomain specific results when uploading new emoji', () => { beforeEach(() => { sandbox.stub(FileUtils, 'getData').withArgs(sinon.match.any).resolves('emoji data'); const requestStub = sandbox.stub(SlackClient.prototype, 'request'); requestStub.withArgs(sinon.match.any).resolves( { ok: true }, ); requestStub.withArgs(sinon.match.any).onFirstCall().resolves( { ok: false, error: 'an error message' }, ); }); const validateResults = ((results) => { assert.equal(results.subdomain1.emojiList.length, 3); // 4 minus 1 collision assert.deepEqual(results.subdomain1.errorList, [{ name: 'emoji-2', url: 'emoji-2.jpg', is_alias: 0, error: 'an error message', }]); // error on first call assert.equal(results.subdomain1.collisions.length, 1); // collision with emoji-1 assert.equal(results.subdomain2.emojiList.length, 3); // 4 minus 1 collision assert.equal(results.subdomain2.errorList.length, 0); // no errors assert.equal(results.subdomain2.collisions.length, 1); // collision with emoji-1 }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'add', '--subdomain', 'subdomain1', '--subdomain', 'subdomain2', '--token', 'token1', '--token', 'token2', '--cookie', 'cookie1', '--cookie', 'cookie2', '--src', 'emoji-1.jpg', '--src', 'emoji-2.jpg', '--src', 'emoji-3.jpg', '--src', 'emoji-4.jpg', ]; return addCli().then(validateResults); }); it('using the module', () => { const subdomains = ['subdomain1', 'subdomain2']; const tokens = ['token1', 'token2']; const cookies = ['cookie1', 'cookie2']; const options = { src: ['emoji-1.jpg', 'emoji-2.jpg', 'emoji-3.jpg', 'emoji-4.jpg'], avoidCollisions: false, }; return add(subdomains, tokens, cookies, options).then(validateResults); }); }); describe('allows mixed new / alias inputs when correctly formatted', () => { beforeEach(() => { sandbox.stub(EmojiAdd.prototype, 'upload').callsFake(arg1 => Promise.resolve({ subdomain: 'subdomain', emojiList: arg1 })); }); const validateResults = ((results) => { assert.deepEqual(results, { subdomain: { collisions: [], emojiList: [ { name: 'new-emoji-1', url: 'new-emoji-1.jpg', is_alias: 0, }, { name: 'alias-name-2', alias_for: 'alias-src-2', is_alias: 1, }, { name: 'alias-name-3', alias_for: 'alias-src-3', is_alias: 1, }, { name: 'emoji-name-4', url: 'new-emoji-4.gif', is_alias: 0, }, ], }, }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'add', '--subdomain', 'subdomain', '--token', 'token', '--cookie', 'cookie', '--src', 'new-emoji-1.jpg', '--name', '', '--src', '', '--name', 'alias-name-2', '--alias-for', 'alias-src-2', '--src', '', '--name', 'alias-name-3', '--alias-for', 'alias-src-3', '--src', 'new-emoji-4.gif', '--name', 'emoji-name-4', ]; return addCli().then(validateResults); }); it('using the module', () => { const options = { src: ['new-emoji-1.jpg', null, null, 'new-emoji-4.gif'], name: [null, 'alias-name-2', 'alias-name-3', 'emoji-name-4'], aliasFor: ['alias-src-2', 'alias-src-3'], }; return add('subdomain', 'tokens', 'cookies', options).then(validateResults); }); }); describe('rejects poorly formatted inputs', () => { const validateError = ((err) => { 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.'); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'add', '--subdomain', 'subdomain', '--token', 'token', '--cookie', 'cookie', '--src', 'emoji-1.jpg', '--name', 'emoji-1', '--name', 'emoji-2', '--alias-for', 'emoji-2-original', '--alias-for', 'unattached-alias', ]; return addCli().then(() => assert.fail()).catch(validateError); // eslint-disable-line no-undef }); it('using the module', () => { const options = { src: ['emoji-1.jpg'], name: ['emoji-1', 'emoji-2'], aliasFor: ['emoji-2-original', 'unattached-alias'], }; return add('subdomain', 'tokens', 'cookies', options) .then(() => assert.fail()).catch(validateError); // eslint-disable-line no-undef }); }); }); }); ================================================ FILE: spec/integration/emojme-download-spec.js ================================================ const chai = require('chai'); chai.use(require('chai-shallow-deep-equal')); const assert = chai.assert; const sinon = require('sinon'); const specHelper = require('../spec-helper'); const EmojiAdminList = require('../../lib/emoji-admin-list'); const FileUtils = require('../../lib/util/file-utils'); const download = require('../../emojme-download').download; const downloadCli = require('../../emojme-download').downloadCli; let sandbox; beforeEach(() => { sandbox = sinon.createSandbox(); }); afterEach(() => { sandbox.restore(); }); describe('download', () => { const subdomains = ['subdomain1', 'subdomain2']; const tokens = ['token1', 'token2']; const cookies = ['cookie1', 'cookie2']; beforeEach(() => { const getStub = sandbox.stub(EmojiAdminList.prototype, 'getAdminListPages'); getStub.callsFake(() => Promise.resolve(specHelper.testEmojiList(10))); // prevent writing during tests sandbox.stub(FileUtils, 'saveData').callsFake((arg1, arg2) => Promise.resolve(arg2)); sandbox.stub(FileUtils, 'mkdirp'); }); describe('downloads emojiList when save is not set', () => { const validateResults = ((results) => { assert.deepEqual(results.subdomain1.emojiList, specHelper.testEmojiList(10)); assert.deepEqual(results.subdomain2.emojiList, specHelper.testEmojiList(10)); assert.deepEqual(results.subdomain1.saveResults, []); assert.deepEqual(results.subdomain2.saveResults, []); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'download', '--subdomain', 'subdomain1', '--subdomain', 'subdomain2', '--token', 'token1', '--token', 'token2', '--cookie', 'cookie1', '--cookie', 'cookie2', ]; return downloadCli().then(validateResults); }); it('using the module', () => download(subdomains, tokens, cookies).then(validateResults)); }); describe('downloads emojiList containing only the emoji created since since_ts', () => { const validateResults = ((results) => { assert.equal(results.subdomain1.emojiList.length, 4); results.subdomain1.emojiList.forEach((emoji) => { assert.equal(emoji.created > 86400 * 5, true); }); assert.equal(results.subdomain2.emojiList.length, 4); results.subdomain2.emojiList.forEach((emoji) => { assert.equal(emoji.created > 86400 * 5, true); }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'download', '--subdomain', 'subdomain1', '--subdomain', 'subdomain2', '--token', 'token1', '--token', 'token2', '--cookie', 'cookie1', '--cookie', 'cookie2', '--since', 86400 * 5, // 5 minutes from epoch ]; return downloadCli().then(validateResults); }); it('using the module', () => download(subdomains, tokens, cookies, { since: 86400 * 5 }).then(validateResults)); }); describe('downloads emoji for specified users when save is set', () => { const validateResults = ((results) => { assert.deepEqual(results.subdomain1.emojiList, specHelper.testEmojiList(10)); assert.deepEqual(results.subdomain2.emojiList, specHelper.testEmojiList(10)); assert.equal(results.subdomain1.saveResults.length, 10); assert.equal(results.subdomain2.saveResults.length, 10); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'download', '--subdomain', 'subdomain1', '--subdomain', 'subdomain2', '--token', 'token1', '--token', 'token2', '--cookie', 'cookie1', '--cookie', 'cookie2', '--save', 'test-user-1', '--save', 'test-user-0', ]; return downloadCli().then(validateResults); }); it('using the module', () => download(subdomains, tokens, cookies, { save: ['test-user-1', 'test-user-0'] }).then(validateResults)); }); describe('downloads emoji for all users to a single location when saveAll is set', () => { const validateResults = ((results) => { assert.deepEqual(results.subdomain.emojiList, specHelper.testEmojiList(10)); assert.equal(results.subdomain.saveResults.length, 10); results.subdomain.saveResults.map(path => assert.match(path, /build\/subdomain\/emoji-[0-9]*.jpg/)); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'download', '--subdomain', 'subdomain', '--token', 'token', '--cookie', 'cookie', '--save-all', ]; return downloadCli().then(validateResults); }); it('using the module', () => { download('subdomain', 'token', 'cookie', { saveAll: true }).then(validateResults); }); }); describe('downloads emoji for all users to a user directories when saveAllByUser is set', () => { const validateResults = ((results) => { assert.deepEqual(results.subdomain.emojiList, specHelper.testEmojiList(10)); assert.equal(results.subdomain.saveResults.length, 10); results.subdomain.saveResults.map(path => assert.match(path, /build\/subdomain\/test-user-[0-9]\/emoji-[0-9]*.jpg/)); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'download', '--subdomain', 'subdomain', '--token', 'token', '--cookie', 'cookie', '--save-all-by-user', ]; return downloadCli().then(validateResults); }); it('using the module', () => { download('subdomain', 'token', 'cookie', { saveAllByUser: true }).then(validateResults); }); }); }); ================================================ FILE: spec/integration/emojme-favorites-spec.js ================================================ const chai = require('chai'); chai.use(require('chai-shallow-deep-equal')); const assert = chai.assert; const sinon = require('sinon'); const _ = require('lodash'); const specHelper = require('../spec-helper'); const ClientBoot = require('../../lib/client-boot'); const EmojiAdminList = require('../../lib/emoji-admin-list'); const FileUtils = require('../../lib/util/file-utils'); const favorites = require('../../emojme-favorites').favorites; const favoritesCli = require('../../emojme-favorites').favoritesCli; let sandbox; beforeEach(() => { sandbox = sinon.createSandbox(); }); afterEach(() => { sandbox.restore(); }); describe('favorites', () => { beforeEach(() => { const clientBootGetStub = sandbox.stub(ClientBoot.prototype, 'get'); clientBootGetStub.resolves( specHelper.mockedBootData(), ); const emojiAdminListGetStub = sandbox.stub(EmojiAdminList.prototype, 'get'); emojiAdminListGetStub.resolves( specHelper.testEmojiList(10), ); // prevent writing during tests sandbox.stub(FileUtils, 'saveData').callsFake((arg1, arg2) => Promise.resolve(arg2)); sandbox.stub(FileUtils, 'writeJson'); }); describe('returns favoritesResultObject', () => { const validateResults = ((result) => { let usage1 = 10; let usage2 = 10; assert.shallowDeepEqual(result, { subdomain1: { favoritesResult: { user: 'username', subdomain: 'subdomain1', favoriteEmoji: specHelper.testEmojiList(10).map(e => e.name), favoriteEmojiAdminList: _.reduce(specHelper.testEmojiList(10), (acc, e) => { acc.push({ [e.name]: { ...e, usage: usage1-- } }); return acc; }, []), }, }, subdomain2: { favoritesResult: { user: 'username', subdomain: 'subdomain2', favoriteEmoji: specHelper.testEmojiList(10).map(e => e.name), favoriteEmojiAdminList: _.reduce(specHelper.testEmojiList(10), (acc, e) => { acc.push({ [e.name]: { ...e, usage: usage2-- } }); return acc; }, []), }, }, }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'favorites', '--subdomain', 'subdomain1', '--subdomain', 'subdomain2', '--token', 'token1', '--token', 'token2', '--cookie', 'cookie1', '--cookie', 'cookie2', ]; return favoritesCli().then(validateResults); }); it('using the module', () => favorites(['subdomain1', 'subdomain1', 'subdomain2'], ['token1', 'token2', 'token3'], ['cookie1', 'cookie2', 'cookie3'], {}).then(validateResults)); }); }); ================================================ FILE: spec/integration/emojme-sync-spec.js ================================================ const chai = require('chai'); chai.use(require('chai-shallow-deep-equal')); const assert = chai.assert; const sinon = require('sinon'); const EmojiAdd = require('../../lib/emoji-add'); const EmojiAdminList = require('../../lib/emoji-admin-list'); const FileUtils = require('../../lib/util/file-utils'); const Helpers = require('../../lib/util/helpers'); const specHelper = require('../spec-helper'); const sync = require('../../emojme-sync').sync; const syncCli = require('../../emojme-sync').syncCli; let sandbox; beforeEach(() => { sandbox = sinon.createSandbox(); }); afterEach(() => { sandbox.restore(); }); describe('sync', () => { beforeEach(() => { const uploadStub = sandbox.stub(EmojiAdd.prototype, 'upload'); uploadStub.callsFake((arg1) => { const subdomain = uploadStub.thisValues[uploadStub.callCount - 1].subdomain; return Promise.resolve({ subdomain, emojiList: arg1 }); }); // Each subdomain will have 2 unique emoji and 2 emoji shared // between them. const getStub = sandbox.stub(EmojiAdminList.prototype, 'getAdminListPages'); getStub.callsFake(() => { const subdomain = (getStub.thisValues[getStub.callCount - 1].subdomain); const uniqEmoji = Helpers.applyPrefix(specHelper.testEmojiList(2), `${subdomain}-`); const sharedEmoji = specHelper.testEmojiList(2); return uniqEmoji.concat(sharedEmoji); }); // prevent writing during tests sandbox.stub(FileUtils, 'saveData').callsFake((arg1, arg2) => Promise.resolve(arg2)); sandbox.stub(FileUtils, 'writeJson'); }); describe('syncs one directionally when src and dst auth pairs are specified', () => { const validateResults = ((results) => { assert.shallowDeepEqual(results, { dstSubdomain: { emojiList: [ { name: 'srcSubdomain-emoji-0' }, { name: 'srcSubdomain-emoji-1' }, ], }, }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'sync', '--src-subdomain', 'srcSubdomain', '--src-token', 'srcToken', '--src-cookie', 'srcCookie', '--dst-subdomain', 'dstSubdomain', '--dst-token', 'dstToken', '--dst-cookie', 'dstcookie', ]; return syncCli().then(validateResults); }); it('using the module', () => sync([], [], [], { srcSubdomains: ['srcSubdomain'], srcTokens: ['srcToken'], srcCookies: ['srcCookie'], dstSubdomains: ['dstSubdomain'], dstTokens: ['dstToken'], dstCookies: ['dstCookie'], }).then(validateResults)); }); describe('syncs one directionally the emoji created since a certain time, if specified', () => { const validateResults = ((results) => { assert.shallowDeepEqual(results, { dstSubdomain: { emojiList: [ { name: 'srcSubdomain-emoji-1' }, ], }, }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'sync', '--src-subdomain', 'srcSubdomain', '--src-token', 'srcToken', '--src-cookie', 'srcCookie', '--dst-subdomain', 'dstSubdomain', '--dst-token', 'dstToken', '--dst-cookie', 'dstcookie', '--since', '1', ]; return syncCli().then(validateResults); }); it('using the module', () => sync([], [], [], { srcSubdomains: ['srcSubdomain'], srcTokens: ['srcToken'], srcCookies: ['srcCookie'], dstSubdomains: ['dstSubdomain'], dstTokens: ['dstToken'], dstCookies: ['dstCookie'], since: '1', }).then(validateResults)); }); describe('syncs one directionally from multiple sources to a single destionation when specified', () => { const validateResults = ((results) => { assert.shallowDeepEqual(results, { dstSubdomain: { emojiList: [ { name: 'srcSubdomain-1-emoji-0' }, { name: 'srcSubdomain-1-emoji-1' }, { name: 'srcSubdomain-2-emoji-0' }, { name: 'srcSubdomain-2-emoji-1' }, ], }, }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'sync', '--src-subdomain', 'srcSubdomain-1', '--src-subdomain', 'srcSubdomain-2', '--src-token', 'srcToken-1', '--src-token', 'srcToken-2', '--src-cookie', 'srcCookie-1', '--src-cookie', 'srcCookie-2', '--dst-subdomain', 'dstSubdomain', '--dst-token', 'dstToken', '--dst-cookie', 'dstcookie', ]; return syncCli().then(validateResults); }); it('using the module', () => sync([], [], [], { srcSubdomains: ['srcSubdomain-1', 'srcSubdomain-2'], srcTokens: ['srcToken-1', 'srcToken-2'], srcCookies: ['srcCookie-1', 'srcCookie-2'], dstSubdomains: ['dstSubdomain'], dstTokens: ['dstToken'], dstCookies: ['dstCookie'], }).then(validateResults)); }); describe('syncs one directionally from multiple sources to a multiple destionations when specified', () => { const validateResults = ((results) => { assert.shallowDeepEqual(results, { 'dstSubdomain-1': { emojiList: [ { name: 'srcSubdomain-1-emoji-0' }, { name: 'srcSubdomain-1-emoji-1' }, { name: 'srcSubdomain-2-emoji-0' }, { name: 'srcSubdomain-2-emoji-1' }, ], }, 'dstSubdomain-2': { emojiList: [ { name: 'srcSubdomain-1-emoji-0' }, { name: 'srcSubdomain-1-emoji-1' }, { name: 'srcSubdomain-2-emoji-0' }, { name: 'srcSubdomain-2-emoji-1' }, ], }, }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'sync', '--src-subdomain', 'srcSubdomain-1', '--src-subdomain', 'srcSubdomain-2', '--src-token', 'srcToken-1', '--src-token', 'srcToken-2', '--src-cookie', 'srcCookie-1', '--src-cookie', 'srcCookie-2', '--dst-subdomain', 'dstSubdomain-1', '--dst-subdomain', 'dstSubdomain-2', '--dst-token', 'dstToken-1', '--dst-token', 'dstToken-2', '--dst-cookie', 'dstcookie-1', '--dst-cookie', 'dstcookie-2', ]; return syncCli().then(validateResults); }); it('using the module', () => sync([], [], [], { srcSubdomains: ['srcSubdomain-1', 'srcSubdomain-2'], srcTokens: ['srcToken-1', 'srcToken-2'], srcCookies: ['srcCookie-1', 'srcCookie-2'], dstSubdomains: ['dstSubdomain-1', 'dstSubdomain-2'], dstTokens: ['dstToken-1', 'dstToken-2'], dstCookies: ['dstCookie-1', 'dstCookie-2'], }).then(validateResults)); }); describe('syncs all emoji across all auth pairs when mutliple subdomains and tokens are specified', () => { const validateResults = ((results) => { assert.shallowDeepEqual(results, { 'subdomain-1': { emojiList: [ { name: 'subdomain-2-emoji-0' }, { name: 'subdomain-2-emoji-1' }, { name: 'subdomain-3-emoji-0' }, { name: 'subdomain-3-emoji-1' }, ], }, 'subdomain-2': { emojiList: [ { name: 'subdomain-1-emoji-0' }, { name: 'subdomain-1-emoji-1' }, { name: 'subdomain-3-emoji-0' }, { name: 'subdomain-3-emoji-1' }, ], }, 'subdomain-3': { emojiList: [ { name: 'subdomain-1-emoji-0' }, { name: 'subdomain-1-emoji-1' }, { name: 'subdomain-2-emoji-0' }, { name: 'subdomain-2-emoji-1' }, ], }, }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'sync', '--subdomain', 'subdomain-1', '--subdomain', 'subdomain-2', '--subdomain', 'subdomain-3', '--token', 'token-1', '--token', 'token-2', '--token', 'token-3', '--cookie', 'cookie-1', '--cookie', 'cookie-2', '--cookie', 'cookie-3', ]; return syncCli().then(validateResults); }); it('using the module', () => sync( ['subdomain-1', 'subdomain-2', 'subdomain-3'], ['token-1', 'token-2', 'token-3'], ['cookie-1', 'cookie-2', 'cookie-3'], {}, ).then(validateResults)); }); }); ================================================ FILE: spec/integration/emojme-upload-spec.js ================================================ const chai = require('chai'); const assert = chai.assert; const sinon = require('sinon'); const fs = require('graceful-fs'); const EmojiAdd = require('../../lib/emoji-add'); const EmojiAdminList = require('../../lib/emoji-admin-list'); const SlackClient = require('../../lib/slack-client'); const FileUtils = require('../../lib/util/file-utils'); const upload = require('../../emojme-upload').upload; const uploadCli = require('../../emojme-upload').uploadCli; let sandbox; let uploadStub; beforeEach(() => { sandbox = sinon.createSandbox(); }); afterEach(() => { sandbox.restore(); }); describe('upload', () => { beforeEach(() => { uploadStub = sandbox.stub(EmojiAdd.prototype, 'upload'); uploadStub.callsFake(arg1 => Promise.resolve({ subdomain: 'subdomain', emojiList: arg1 })); sandbox.stub(EmojiAdminList.prototype, 'get').withArgs(sinon.match.any).resolves( [{ name: 'emoji-1' }], ); }); describe('uploads emoji from specified json', () => { const validateResults = ((results) => { const fixture = JSON.parse(fs.readFileSync('./spec/fixtures/emojiList.json', 'utf-8')); assert.deepEqual(results, { subdomain: { collisions: [ fixture[0], ], emojiList: [ fixture[1], fixture[2], fixture[3], ], }, }); assert.deepEqual(uploadStub.getCall(0).args, [ [ fixture[1], fixture[2], fixture[3], ], ]); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'upload', '--subdomain', 'subdomain', '--token', 'token', '--cookie', 'cookie', '--src', './spec/fixtures/emojiList.json', ]; return uploadCli().then(validateResults); }); it('using the module', () => { const options = { src: './spec/fixtures/emojiList.json' }; return upload('subdomain', 'token', 'cookie', options).then(validateResults); }); }); describe('uploads emoji from specified yaml', () => { const validateResults = ((results) => { const fixture = FileUtils.readYaml('./spec/fixtures/emojiList.yaml', 'utf-8'); assert.deepEqual(results, { subdomain: { collisions: [ fixture[0], ], emojiList: [ fixture[1], fixture[2], fixture[3], ], }, }); assert.deepEqual(uploadStub.getCall(0).args, [ [ fixture[1], fixture[2], fixture[3], ], ]); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'upload', '--subdomain', 'subdomain', '--token', 'token', '--cookie', 'cookie', '--src', './spec/fixtures/emojiList.yaml', ]; return uploadCli().then(validateResults); }); it('using the module', () => { const options = { src: './spec/fixtures/emojiList.yaml' }; return upload('subdomain', 'token', 'cookie', options).then(validateResults); }); }); describe('renames emoji to avoid collisions when avoidCollisions is set', () => { const validateResults = ((results) => { const fixture = JSON.parse(fs.readFileSync('./spec/fixtures/emojiList.json', 'utf-8')); assert.deepEqual(results, { subdomain: { collisions: [], emojiList: [ { ...fixture[0], name: 'emoji-5', collision: 'emoji-1' }, fixture[1], fixture[2], fixture[3], ], }, }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'upload', '--subdomain', 'subdomain', '--token', 'token', '--cookie', 'cookie', '--src', './spec/fixtures/emojiList.json', '--avoid-collisions', ]; return uploadCli().then(validateResults); }); it('using the module', () => { const options = { src: './spec/fixtures/emojiList.json', avoidCollisions: true, }; return upload('subdomain', 'token', 'cookie', options).then(validateResults); }); }); describe('collects and does not attempt to upload collisions when avoidCollisions is false', () => { const validateResults = ((results) => { const fixture = JSON.parse(fs.readFileSync('./spec/fixtures/emojiList.json', 'utf-8')); assert.deepEqual(results, { subdomain: { collisions: [ fixture[0], ], emojiList: [ fixture[1], fixture[2], fixture[3], ], }, }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'upload', '--subdomain', 'subdomain', '--token', 'token', '--cookie', 'cookie', '--src', './spec/fixtures/emojiList.json', ]; return uploadCli().then(validateResults); }); it('using the module', () => { const options = { src: './spec/fixtures/emojiList.json', }; return upload('subdomain', 'token', 'cookie', options).then(validateResults); }); }); describe('allows collision errors on the slack side when allowCollisions is set', () => { beforeEach(() => { sandbox.restore(); sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves( { error: 'error_name_taken', alias_for: 'emoji', is_alias: 1, name: 'emoji-1', }, ); }); const validateResults = ((results) => { const fixture = JSON.parse(fs.readFileSync('./spec/fixtures/emojiList.json', 'utf-8')); assert.deepEqual(results, { subdomain: { collisions: [], emojiList: [ fixture[0], fixture[1], fixture[2], fixture[3], ], errorList: [ { ...fixture[0], error: 'error_name_taken' }, { ...fixture[2], error: 'error_name_taken' }, { ...fixture[1], error: 'error_name_taken' }, { ...fixture[3], error: 'error_name_taken' }, ], }, }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'upload', '--subdomain', 'subdomain', '--token', 'token', '--cookie', 'cookie', '--src', './spec/fixtures/emojiList.json', '--allow-collisions', ]; return uploadCli().then(validateResults); }); it('using the module', () => { const options = { src: './spec/fixtures/emojiList.json', allowCollisions: true, }; return upload('subdomain', 'token', 'cookie', options).then(validateResults); }); }); }); ================================================ FILE: spec/integration/emojme-user-stats-spec.js ================================================ const chai = require('chai'); chai.use(require('chai-shallow-deep-equal')); const assert = chai.assert; const sinon = require('sinon'); const specHelper = require('../spec-helper'); const EmojiAdminList = require('../../lib/emoji-admin-list'); const FileUtils = require('../../lib/util/file-utils'); const userStats = require('../../emojme-user-stats').userStats; const userStatsCli = require('../../emojme-user-stats').userStatsCli; let sandbox; beforeEach(() => { sandbox = sinon.createSandbox(); }); afterEach(() => { sandbox.restore(); }); describe('user-stats', () => { beforeEach(() => { const getStub = sandbox.stub(EmojiAdminList.prototype, 'getAdminListPages'); getStub.resolves( specHelper.testEmojiList(10), ); // prevent writing during tests sandbox.stub(FileUtils, 'saveData').callsFake((arg1, arg2) => Promise.resolve(arg2)); sandbox.stub(FileUtils, 'writeJson'); }); describe('when one user is given it returns their user stats', () => { const validateResults = ((result) => { assert.shallowDeepEqual(result, { subdomain1: { userStatsResults: [{ user: 'test-user-0', // userEmoji: sinon.match.array, subdomain: 'subdomain1', originalCount: 5, aliasCount: 0, totalCount: 5, percentage: '50.00', }], }, subdomain2: { userStatsResults: [{ user: 'test-user-0', // userEmoji: sinon.match.array, subdomain: 'subdomain2', originalCount: 5, aliasCount: 0, totalCount: 5, percentage: '50.00', }], }, }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'user-stats', '--subdomain', 'subdomain1', '--subdomain', 'subdomain2', '--token', 'token1', '--token', 'token2', '--cookie', 'cookie1', '--cookie', 'cookie2', '--user', 'test-user-0', ]; return userStatsCli().then(validateResults); }); it('using the module', () => userStats(['subdomain1', 'subdomain2'], ['token1', 'token2'], ['cookie1', 'cookie2'], { user: ['test-user-0'] }).then(validateResults)); }); describe('when multiple users are given it returns all their user stats', () => { const validateResults = ((result) => { assert.shallowDeepEqual(result, { subdomain: { userStatsResults: [ { user: 'test-user-0', // userEmoji: sinon.match.array, subdomain: 'subdomain', originalCount: 5, aliasCount: 0, totalCount: 5, percentage: '50.00', }, { user: 'test-user-1', // userEmoji: sinon.match.array, subdomain: 'subdomain', originalCount: 0, aliasCount: 5, totalCount: 5, percentage: '50.00', }, ], }, }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'user-stats', '--subdomain', 'subdomain', '--token', 'token', '--cookie', 'cookie', '--user', 'test-user-0', '--user', 'test-user-1', ]; return userStatsCli().then(validateResults); }); it('using the module', () => userStats('subdomain', 'token', 'cookie', { user: ['test-user-0', 'test-user-1', 'non-existant-user'] }).then(validateResults)); }); describe('when no users are given, give the top n users', () => { const validateResults = ((result) => { assert.shallowDeepEqual(result, { subdomain: { userStatsResults: [ { user: 'test-user-0', // userEmoji: sinon.match.array, subdomain: 'subdomain', originalCount: 5, aliasCount: 0, totalCount: 5, percentage: '50.00', }, { user: 'test-user-1', // userEmoji: sinon.match.array, subdomain: 'subdomain', originalCount: 0, aliasCount: 5, totalCount: 5, percentage: '50.00', }, ], }, }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'user-stats', '--subdomain', 'subdomain', '--token', 'token', '--cookie', 'cookie', '--top', '2', ]; return userStatsCli().then(validateResults); }); it('using the module', () => userStats('subdomain', 'token', 'cookie', { top: 2 }).then(validateResults)); }); describe('gives user stats about emoji created after --since', () => { const validateResults = ((result) => { assert.equal(result.subdomain.emojiList.length, 4); assert.shallowDeepEqual(result, { subdomain: { userStatsResults: [ { user: 'test-user-0', // userEmoji: sinon.match.array, subdomain: 'subdomain', originalCount: 2, aliasCount: 0, totalCount: 2, percentage: '50.00', }, { user: 'test-user-1', // userEmoji: sinon.match.array, subdomain: 'subdomain', originalCount: 0, aliasCount: 2, totalCount: 2, percentage: '50.00', }, ], }, }); }); it('using the cli', () => { process.argv = [ 'node', 'emojme', 'user-stats', '--subdomain', 'subdomain', '--token', 'token', '--cookie', 'cookie', '--since', 86400 * 5, ]; return userStatsCli().then(validateResults); }); it('using the module', () => userStats('subdomain', 'token', 'cookie', { since: 86400 * 5 }).then(validateResults)); }); }); ================================================ FILE: spec/spec-helper.js ================================================ const fs = require('fs'); module.exports = { authTuple: ['subdomain1', 'token1'], authTuples(n) { return Array(n).map((x, i) => [`subdomain${i}`, `token${i}`]); }, emojiName(i) { return `emoji-${i}`; }, userName(i) { return `test-user-${i % 2}`; }, testEmoji(i) { return { name: this.emojiName(i), is_alias: i % 2, alias_for: this.emojiName(1), url: './spec/fixtures/Example.jpg', user_display_name: this.userName(i), created: i * 86400, }; }, testEmojiList(n) { return Array(n).fill(0).map((x, i) => this.testEmoji(i)); }, mockedSlackResponse(emojiCount, pageSize, page, ok) { return { ok: ok === undefined ? true : ok, emoji: this.testEmojiList(pageSize), custom_emoji_total_count: emojiCount, paging: { count: pageSize, total: emojiCount, page, pages: Math.ceil(emojiCount / pageSize), }, }; }, mockedBootData() { return JSON.parse(fs.readFileSync('spec/fixtures/clientBoot.json')); }, }; ================================================ FILE: spec/unit/lib/emoji-add-spec.js ================================================ const assert = require('chai').assert; const sinon = require('sinon'); const fs = require('graceful-fs'); const EmojiAdd = require('../../../lib/emoji-add'); const SlackClient = require('../../../lib/slack-client'); const logger = require('../../../lib/logger'); const specHelper = require('../../spec-helper'); let sandbox; let emojiAdd; let infoSpy; beforeEach(() => { sandbox = sinon.createSandbox(); emojiAdd = new EmojiAdd('subdomain', 'token'); infoSpy = sandbox.spy(logger, 'info'); }); afterEach(() => { sandbox.restore(); }); describe('EmojiAdd', () => { describe('createMultipart', () => { it('creates an alias multipart request', () => { const emoji = { name: 'name', is_alias: 1, alias_for: 'some other emoji', }; return EmojiAdd.createMultipart(emoji, 'token').then((result) => { assert.deepEqual(result, { token: 'token', name: emoji.name, mode: 'alias', alias_for: emoji.alias_for, }); }); }); it('creates a multipart emoji request', () => { const emoji = { name: 'name', url: './spec/fixtures/Example.jpg', }; return EmojiAdd.createMultipart(emoji, 'token').then((result) => { assert.deepEqual(result, { token: 'token', name: emoji.name, mode: 'data', image: fs.readFileSync(emoji.url), }); }); }); }); describe('uploadSingle', () => { const emoji = { name: 'name', url: './spec/fixtures/Example.jpg', }; it('adds error responses to result', () => { sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves( { ok: false, error: 'sample error' }, ); return emojiAdd.uploadSingle(emoji).then((result) => { assert.deepEqual(result, Object.assign({}, emoji, { error: 'sample error' })); }); }); it('does not return anything for successful responses', () => { sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves( { ok: true }, ); return emojiAdd.uploadSingle(emoji).then((result) => { assert.equal(result, false); }); }); }); describe('upload', () => { it('handles source file inputs', () => { sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves( specHelper.mockedSlackResponse(), ); return emojiAdd.upload('./spec/fixtures/emojiList.json').then((results) => { assert.deepEqual(results.errorList, []); }); }); it('handles array inputs', () => { sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves( specHelper.mockedSlackResponse(), ); return emojiAdd.upload(specHelper.testEmojiList(2)).then((results) => { assert.deepEqual(results.errorList, []); }); }); it('uploads new emoji first, then aliases', () => { sandbox.spy(emojiAdd, 'uploadSingle'); sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves( specHelper.mockedSlackResponse(), ); return emojiAdd.upload('./spec/fixtures/emojiList.json').then((results) => { assert.deepEqual(results.errorList, []); const calls = emojiAdd.uploadSingle.getCalls(); assert.equal(calls[0].args[0].name, 'emoji-1'); assert.equal(calls[1].args[0].name, 'emoji-3'); assert.equal(calls[2].args[0].name, 'emoji-2'); assert.equal(calls[3].args[0].name, 'emoji-4'); }); }); it('gathers unsuccessful results', () => { sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves( { ok: false, error: 'sample error' }, ); return emojiAdd.upload(specHelper.testEmojiList(2)).then((results) => { for (const result in results.errors) { assert.equal(result.error, 'sample error'); } }); }); it('gathers successful results', () => { sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves( specHelper.mockedSlackResponse(), ); return emojiAdd.upload('./spec/fixtures/emojiList.json').then((results) => { const infoCalls = infoSpy.getCalls(); assert.deepEqual(results.errorList, []); assert.equal(infoSpy.callCount, 2); assert.match(infoCalls[1].lastArg, /.*total requests: 4[\s\S]*successes: 4[\s\S]*errors: 0.*/); }); }); }); }); ================================================ FILE: spec/unit/lib/emoji-admin-list-spec.js ================================================ const assert = require('chai').assert; const sinon = require('sinon'); const _ = require('lodash'); const fs = require('graceful-fs'); const EmojiAdminList = require('../../../lib/emoji-admin-list'); const SlackClient = require('../../../lib/slack-client'); const FileUtils = require('../../../lib/util/file-utils'); const specHelper = require('../../spec-helper'); let sandbox; let adminList; beforeEach(() => { sandbox = sinon.createSandbox(); adminList = new EmojiAdminList(...specHelper.authTuple); }); afterEach(() => { sandbox.restore(); }); describe('EmojiAdminList', () => { describe('createMultipart', () => { it('creates multipart request for specified page', () => { let pageNum; for (pageNum in [0, 1, 10]) { const part = adminList.createMultipart(pageNum); assert.deepEqual(part, { query: '', page: pageNum, count: adminList.pageSize, token: specHelper.authTuple[1], }); } }); }); describe('get', () => { const testEmojiList = specHelper.testEmojiList(3); it('uses cached json file if it is not expired', (done) => { sandbox.stub(fs, 'existsSync').withArgs(sinon.match.any).returns(true); sandbox.stub(fs, 'statSync').withArgs(sinon.match.any).returns({ ctimeMs: Date.now() }); sandbox.stub(FileUtils, 'readJson').withArgs(sinon.match.any).returns(testEmojiList); sandbox.stub(EmojiAdminList.prototype, 'getAdminListPages').resolves(testEmojiList); adminList.get().then((emojiList) => { assert.deepEqual(emojiList, testEmojiList); done(); }); }); it('ignores cached json file if it is expired', (done) => { sandbox.stub(fs, 'existsSync').withArgs(sinon.match.any).returns(true); sandbox.stub(fs, 'statSync').withArgs(sinon.match.any).returns({ ctimeMs: 0 }); sandbox.stub(FileUtils, 'writeJson').withArgs(sinon.match.any); sandbox.stub(EmojiAdminList.prototype, 'getAdminListPages').resolves(testEmojiList); adminList.get().then((emojiList) => { assert.deepEqual(emojiList, testEmojiList); done(); }); }); it('generates new emojilist if no cache file exists', (done) => { sandbox.stub(FileUtils, 'isExpired').withArgs(sinon.match.any).returns(true); sandbox.stub(FileUtils, 'writeJson').withArgs(sinon.match.any); sandbox.stub(EmojiAdminList.prototype, 'getAdminListPages').resolves(testEmojiList); adminList.get().then((emojiList) => { assert.deepEqual(emojiList, testEmojiList); done(); }); }); }); describe('getAdminListPages', () => { it('pulls initial page with total number of pages', (done) => { sandbox.stub(SlackClient.prototype, 'request').withArgs(sinon.match.any).resolves( specHelper.mockedSlackResponse(1, 1, 1, true), ); adminList.getAdminListPages().then((emojiLists) => { assert.deepEqual(emojiLists[0], specHelper.testEmojiList(1)); assert.equal(emojiLists.length, 1); done(); }); }); it('generates as many requests as pages', (done) => { const req = sandbox.stub(SlackClient.prototype, 'request'); for (let i = 0; i <= 10; i++) { req.onCall(i).resolves( specHelper.mockedSlackResponse(10, 1, i + 1, true), ); } adminList.setPageSize(1); adminList.getAdminListPages().then((emojiLists) => { assert.equal(emojiLists.length, 10); done(); }); }); it('rejects when requests return errors in body', (done) => { const req = sandbox.stub(SlackClient.prototype, 'request'); req.onCall(0).resolves(specHelper.mockedSlackResponse(2, 1, 1, true)); req.onCall(1).resolves(specHelper.mockedSlackResponse(2, 1, 2, false)); adminList.setPageSize(1); adminList.getAdminListPages().then((emojiLists) => { assert.equal(emojiLists.length, 1); done(); }); }); }); describe('summarizeUser', () => { const emojiList = specHelper.testEmojiList(10); it('returns null if user is not a contributor', () => { const result = EmojiAdminList.summarizeUser(emojiList, 'subdomain', 'a non existent user'); assert.deepEqual(result, []); }); it('returns a user\'s emoji contributions', () => { const result = EmojiAdminList.summarizeUser(emojiList, 'subdomain', 'test-user-0'); assert.equal(result.length, 1); assert.equal(result[0].user, 'test-user-0'); }); it('returns multiple users\' contributions if provided', () => { const result = EmojiAdminList.summarizeUser(emojiList, 'subdomain', ['test-user-0', 'test-user-1']); assert.equal(result.length, 2); assert.equal(result[0].user, 'test-user-0'); assert.equal(result[1].user, 'test-user-1'); }); it('returns existent users and filters out non existent users', () => { const result = EmojiAdminList.summarizeUser(emojiList, 'subdomain', ['test-user-0', 'non existent user', 'test-user-1']); assert.equal(result.length, 2); assert.equal(result[0].user, 'test-user-0'); assert.equal(result[1].user, 'test-user-1'); }); }); describe('summarizeSubdomain', () => { const emojiList = specHelper.testEmojiList(11); it('returns sorted list of contributors', () => { const result = EmojiAdminList.summarizeSubdomain(emojiList, 'subdomain', 10); assert.isAbove(result[0].totalCount, result[1].totalCount); }); it('returns all contributors if count > number of contributors', () => { const result = EmojiAdminList.summarizeSubdomain(emojiList, 'subdomain', 10); assert.equal(result.length, _.uniqBy(emojiList, 'user_display_name').length); }); it('returns n contributors when n is provided', () => { const n = 1; const result = EmojiAdminList.summarizeSubdomain(emojiList, 'subdomain', n); assert.equal(result.length, n); }); }); describe('since', () => { const emojiList = specHelper.testEmojiList(10); context('when given a time in the future', () => { it('returns an empty array', () => { const result = EmojiAdminList.since(emojiList, Date.now() + 86400); assert.deepEqual(result, []); }); }); context('when given a time older than any emoji', () => { it('returns the passed in emojiList', () => { const result = EmojiAdminList.since(emojiList, -1); assert.deepEqual(result, emojiList); }); }); context('when given a time bisecting emoji creation dates', () => { it('returns the part of the emojiList that was created after the given time', () => { const result = EmojiAdminList.since(emojiList, 86400 * 5); assert.equal(result.length, 4); result.forEach((emoji) => { assert.equal(emoji.created > 86400 * 5, true); }); }); }); }); describe('diff', () => { context('when explicit source and destination are given', () => { it('creates upload diffs for every subdomain given', () => { const srcLists = [specHelper.testEmojiList(10)]; const srcSubdomains = ['src 1']; const dstLists = [specHelper.testEmojiList(5), specHelper.testEmojiList(10)]; const dstSubdomains = ['dst 1', 'dst 2']; const [diffTo1, diffTo2] = EmojiAdminList.diff( srcLists, srcSubdomains, dstLists, dstSubdomains, ); assert.equal(diffTo1.dstSubdomain, 'dst 1'); assert.equal(diffTo1.emojiList.length, 5); assert.equal(diffTo2.dstSubdomain, 'dst 2'); assert.equal(diffTo2.emojiList.length, 0); }); it('diffs contain emoji from all other subdomains', () => { const srcLists = [specHelper.testEmojiList(10), specHelper.testEmojiList(20)]; const srcSubdomains = ['src 1', 'src 2']; const dstLists = [specHelper.testEmojiList(1)]; const dstSubdomains = ['dst 1']; const [diffTo1] = EmojiAdminList.diff(srcLists, srcSubdomains, dstLists, dstSubdomains); assert.equal(diffTo1.dstSubdomain, 'dst 1'); assert.equal(diffTo1.emojiList.length, 19); }); }); context('when destination is not given', () => { it('makes the given subdomains and emoji both the src and dst', () => { const lists = [specHelper.testEmojiList(5), specHelper.testEmojiList(10)]; const subdomains = ['sub 1', 'sub 2']; const [diffTo1, diffTo2] = EmojiAdminList.diff(lists, subdomains); assert.equal(diffTo1.dstSubdomain, 'sub 1'); assert.equal(diffTo1.emojiList.length, 5); assert.equal(diffTo2.dstSubdomain, 'sub 2'); assert.equal(diffTo2.emojiList.length, 0); }); it('creates upload diffs for every given subdomain', () => { const lists = [ specHelper.testEmojiList(5), specHelper.testEmojiList(10), specHelper.testEmojiList(20), ]; const subdomains = ['sub 1', 'sub 2', 'sub 3']; const [diffTo1, diffTo2, diffTo3] = EmojiAdminList.diff(lists, subdomains); assert.equal(diffTo1.dstSubdomain, 'sub 1'); assert.equal(diffTo1.emojiList.length, 15); assert.equal(diffTo2.dstSubdomain, 'sub 2'); assert.equal(diffTo2.emojiList.length, 10); assert.equal(diffTo3.dstSubdomain, 'sub 3'); assert.equal(diffTo3.emojiList.length, 0); }); }); it('creates accurate diffs', () => { const subdomains = ['sub 1', 'sub 2', 'sub 3']; const lists = [ [ { name: 'present-in-all' }, { name: 'present-in-1-and-2' }, ], [ { name: 'present-in-all' }, { name: 'present-in-1-and-2' }, { name: 'present-in-2-and-3' }, ], [ { name: 'present-in-all' }, { name: 'present-in-2-and-3' }, { name: 'present-in-3' }, ], ]; const [diffTo1, diffTo2, diffTo3] = EmojiAdminList.diff(lists, subdomains); assert.equal(diffTo1.dstSubdomain, 'sub 1'); assert.deepEqual(diffTo1.emojiList, [ { name: 'present-in-2-and-3' }, { name: 'present-in-3' }, ]); assert.equal(diffTo2.dstSubdomain, 'sub 2'); assert.deepEqual(diffTo2.emojiList, [ { name: 'present-in-3' }, ]); assert.equal(diffTo3.dstSubdomain, 'sub 3'); assert.deepEqual(diffTo3.emojiList, [ { name: 'present-in-1-and-2' }, ]); }); }); }); ================================================ FILE: spec/unit/lib/file-utils-spec.js ================================================ const assert = require('chai').assert; const fs = require('graceful-fs'); const FileUtils = require('../../../lib/util/file-utils'); describe('FileUtils', () => { describe('getData', () => { const fileData = fs.readFileSync('./spec/fixtures/Example.jpg'); it('downloads links', (done) => { const path = 'https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg'; FileUtils.getData(path).then((urlData) => { assert.deepEqual(fileData, urlData); done(); }); }).timeout(10000); it('passes through existing data', (done) => { const path = `data:image/jpeg;${Buffer.from(fileData).toString('base64')}`; FileUtils.getData(path).then((urlData) => { assert.deepEqual(path, urlData); done(); }); }); it('reads in file paths', (done) => { const path = './spec/fixtures/Example.jpg'; FileUtils.getData(path).then((urlData) => { assert.deepEqual(fileData, urlData); done(); }); }); it('rejects with error if no data is gettable', (done) => { const path = 'malformed'; FileUtils.getData(path).then(() => { throw new Error('Should not get here'); }).catch((err) => { assert.isDefined(err); done(); }); }); }); describe('sanitize', () => { it('removes emoji', (done) => { const input = 'frog emoji 🐸is best'; const expectedOutput = 'frog emoji is best'; assert.equal(FileUtils.sanitize(input), expectedOutput); done(); }); it('replaces non alphanumeric chars', (done) => { const input = 'abc 123 ,/^ =+% \\|?'; const expectedOutput = 'abc 123'; assert.equal(FileUtils.sanitize(input), expectedOutput); done(); }); it('retains dashes and underscores', (done) => { const input = 'Jack_Skellenberger (2016-2020)'; const expectedOutput = 'Jack_Skellenberger 2016-2020'; assert.equal(FileUtils.sanitize(input), expectedOutput); done(); }); }); }); ================================================ FILE: spec/unit/lib/slack-client-spec.js ================================================ const assert = require('chai').assert; const SlackClient = require('../../../lib/slack-client'); describe('SlackClient', () => { describe('constructor', () => { const tierTwoLimits = SlackClient.rateLimitTier(2); const tierThreeLimits = SlackClient.rateLimitTier(3); it('defaults to using tier 2 rate limiting when no limits are specified', () => { const slackClient = new SlackClient('subdomain', 'cookie'); assert.deepEqual(slackClient.options, tierTwoLimits); }); it('allows rate limit tier overrides to be set', () => { const slackClient = new SlackClient('subdomain', 'cookie', tierThreeLimits); assert.deepEqual(slackClient.options, tierThreeLimits); }); it('overrides passed variables with environment variables when present', () => { process.env.SLACK_REQUEST_CONCURRENCY = 1; process.env.SLACK_REQUEST_RATE = 2; process.env.SLACK_REQUEST_WINDOW = 3; const slackClient = new SlackClient('subdomain', 'cookie', tierThreeLimits); assert.include(slackClient.throttle, { concurrent: '1', rate: '2', ratePer: '3', }); delete process.env.SLACK_REQUEST_CONCURRENCY; delete process.env.SLACK_REQUEST_RATE; delete process.env.SLACK_REQUEST_WINDOW; }); }); }); ================================================ FILE: spec/unit/lib/util/cli-spec.js ================================================ const chai = require('chai'); const assert = chai.assert; const commander = require('commander'); const Cli = require('../../../../lib/util/cli'); describe('Cli', () => { describe('unpackAuthJson', () => { let program; beforeEach(() => { program = new commander.Command(); Cli.requireAuth(program); }); it('is ignored if no auth json is specified', () => { process.argv = [ 'node', 'emojme', 'download', '--subdomain', 'subdomain', '--token', 'token', '--cookie', 'cookie', '--auth-json', '{}', ]; program.parse(process.argv); Cli.unpackAuthJson(program); assert.deepEqual(program.subdomain, ['subdomain']); assert.deepEqual(program.token, ['token']); assert.deepEqual(program.cookie, ['cookie']); }); it('is can be used once, alone', () => { process.argv = [ 'node', 'emojme', 'download', // arbitrary '--auth-json', '{"subdomain":"subdomain", "token":"token", "cookie":"cookie"}', ]; program.parse(process.argv); Cli.unpackAuthJson(program); assert.deepEqual(program.subdomain, ['subdomain']); assert.deepEqual(program.token, ['token']); assert.deepEqual(program.cookie, ['cookie']); }); it('can be used repeatedly', () => { process.argv = [ 'node', 'emojme', 'download', '--auth-json', '{"subdomain":"subdomain1", "token":"token1", "cookie":"cookie1"}', '--auth-json', '{"subdomain":"subdomain2", "token":"token2", "cookie":"cookie2"}', ]; program.parse(process.argv); Cli.unpackAuthJson(program); assert.deepEqual(program.subdomain, ['subdomain1', 'subdomain2']); assert.deepEqual(program.token, ['token1', 'token2']); assert.deepEqual(program.cookie, ['cookie1', 'cookie2']); }); it('can be used in conjunction with --subdomain, --token, and --cookie flags', () => { process.argv = [ 'node', 'emojme', 'download', '--subdomain', 'subdomain1', '--token', 'token1', '--cookie', 'cookie1', '--auth-json', '{"subdomain":"subdomain2", "token":"token2", "cookie":"cookie2"}', '--subdomain', 'subdomain3', '--token', 'token3', '--cookie', 'cookie3', ]; program.parse(process.argv); Cli.unpackAuthJson(program); assert.deepEqual(program.subdomain, ['subdomain1', 'subdomain3', 'subdomain2']); assert.deepEqual(program.token, ['token1', 'token3', 'token2']); assert.deepEqual(program.cookie, ['cookie1', 'cookie3', 'cookie2']); }); }); }); ================================================ FILE: spec/unit/lib/util/helpers-spec.js ================================================ const chai = require('chai'); const assert = chai.assert; const commander = require('commander'); const Helpers = require('../../../../lib/util/helpers'); const Cli = require('../../../../lib/util/cli'); describe('Helpers', () => { describe('zipAuthTuples', () => { it('zips together equal length subdomain and token lists', () => { const subdomains = ['subdomain 1']; const tokens = ['token 1']; const cookies = ['cookie 1']; const options = {}; const [authTuples, srcPairs, dstPairs] = Helpers.zipAuthTuples( subdomains, tokens, cookies, options, ); assert.deepEqual(authTuples, [['subdomain 1', 'token 1', 'cookie 1']]); assert.deepEqual(srcPairs, []); assert.deepEqual(dstPairs, []); }); it('zips src and dst auth pairs when given', () => { const subdomains = ['subdomain 1']; const tokens = ['token 1']; const cookies = ['cookie 1']; const options = { srcSubdomains: ['src subdomain 1', 'src subdomain 2'], srcTokens: ['src token 1', 'src token 2'], srcCookies: ['src cookie 1', 'src cookie 2'], dstSubdomains: ['dst subdomain 1'], dstTokens: ['dst token 1'], dstCookies: ['dst cookie 1'], }; const [authTuples, srcPairs, dstPairs] = Helpers.zipAuthTuples( subdomains, tokens, cookies, options, ); const expectedSrcPairs = [['src subdomain 1', 'src token 1', 'src cookie 1'], ['src subdomain 2', 'src token 2', 'src cookie 2']]; const expectedDstPairs = [['dst subdomain 1', 'dst token 1', 'dst cookie 1']]; assert.deepEqual(authTuples, [['subdomain 1', 'token 1', 'cookie 1']].concat(expectedSrcPairs, expectedDstPairs)); assert.deepEqual(srcPairs, expectedSrcPairs); assert.deepEqual(dstPairs, expectedDstPairs); }); it('throws an error when auth pairs are mismatched', () => { const subdomains = ['subdomain 1']; const tokens = []; const options = {}; assert.throws( (() => { Helpers.zipAuthTuples(subdomains, tokens, options); }), Error, /Invalid input/, ); }); it('throws an error when src/dst auth pairs are mismatched', () => { const subdomains = ['subdomain 1']; const tokens = []; const options = { srcSubdomains: ['src subdomain 1', 'src subdomain 2'], srcTokens: [], dstSubdomains: ['dst subdomain 1'], dstTokens: ['dst token 1'], }; assert.throws( (() => { Helpers.zipAuthTuples(subdomains, tokens, options); }), Error, /Invalid input/, ); }); }); describe('avoidCollisions', () => { it('does not add id when adding unique emoji, even when emoji name slug space overlaps', () => { const existingEmojiList = [ { name: 'emoji1' }, ]; const newEmojiList = [ { name: 'emoji' }, ]; const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList); assert.deepEqual(result, [{ name: 'emoji' }]); }); it('adds id when a direct emoji collision is detected', () => { const existingEmojiList = [ { name: 'emoji' }, ]; const newEmojiList = [ { name: 'emoji' }, ]; const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList); assert.deepEqual(result, [{ name: 'emoji-1', collision: 'emoji' }]); }); it('adapts to new emoji name delimiter when one is present', () => { const existingEmojiList = []; const newEmojiList = [ { name: 'e_m_o_j_i' }, { name: 'e_m_o_j_i' }, ]; const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList); assert.deepEqual(result, [ { name: 'e_m_o_j_i' }, { name: 'e_m_o_j_i_1', collision: 'e_m_o_j_i' }, ]); }); it('adapts to uploaded emoji name delimiter when one is present', () => { const existingEmojiList = [ { name: 'emoji1' }, ]; const newEmojiList = [ { name: 'emoji1' }, ]; const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList); assert.deepEqual(result, [{ name: 'emoji2', collision: 'emoji1' }]); }); it('adds id to all but first emoji when multiple identical emoji names are added', () => { const existingEmojiList = []; const newEmojiList = [ { name: 'emoji' }, { name: 'emoji' }, { name: 'emoji' }, { name: 'emoji' }, ]; const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList); assert.deepEqual(result, [ { name: 'emoji' }, { name: 'emoji-1', collision: 'emoji' }, { name: 'emoji-2', collision: 'emoji' }, { name: 'emoji-3', collision: 'emoji' }, ]); }); it('gracefully folds in existing id\'d emoji', () => { const existingEmojiList = [{ name: 'emoji-2' }]; const newEmojiList = [ { name: 'emoji' }, { name: 'emoji' }, { name: 'emoji' }, ]; const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList); assert.deepEqual(result, [ { name: 'emoji' }, { name: 'emoji-1', collision: 'emoji' }, { name: 'emoji-3', collision: 'emoji' }, ]); }); it('does not clobber id\'d new emoji names', () => { const existingEmojiList = [{ name: 'emoji-1' }]; const newEmojiList = [ { name: 'emoji-1' }, { name: 'emoji-2' }, { name: 'emoji-3' }, ]; const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList); assert.deepEqual(result, [ { collision: 'emoji-1', name: 'emoji-4', }, { name: 'emoji-2' }, { name: 'emoji-3' }, ]); }); it('gracefully folds in id\'d new emoji', () => { const existingEmojiList = []; const newEmojiList = [ { name: 'emoji' }, { name: 'emoji-2' }, { name: 'emoji' }, { name: 'emoji' }, ]; const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList); assert.deepEqual(result, [ { name: 'emoji' }, { name: 'emoji-2' }, { name: 'emoji-1', collision: 'emoji' }, { name: 'emoji-3', collision: 'emoji' }, ]); }); it('does not increment numberal emoji names', () => { const existingEmojiList = [ { name: '1984' }, ]; const newEmojiList = [ { name: '1984' }, { name: '1984' }, ]; const result = Helpers.avoidCollisions(newEmojiList, existingEmojiList); assert.deepEqual(result, [ { name: '1984-1', collision: '1984' }, { name: '1984-2', collision: '1984' }, ]); }); }); describe('formatResultHash', () => { it('organizes promise array output into more easily indexable hash', () => { const promiseArrayResult = [ { subdomain: 'subdomain1', result1: 'first part of results', result2: ['second', 'part', 'of', 'results'], result3: { third: 'part', of: 'results', }, }, { subdomain: 'subdomain2', result4: 'first part of results', result5: ['second', 'part', 'of', 'results'], result6: { third: 'part', of: 'results', }, }, ]; assert.deepEqual(Helpers.formatResultsHash(promiseArrayResult), { subdomain1: { result1: 'first part of results', result2: ['second', 'part', 'of', 'results'], result3: { third: 'part', of: 'results', }, }, subdomain2: { result4: 'first part of results', result5: ['second', 'part', 'of', 'results'], result6: { third: 'part', of: 'results', }, }, }); }); }); });