[
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n    \"env\": {\n        \"amd\": true,\n        \"node\": true,\n        \"es6\": true\n    },\n    \"extends\": \"eslint:recommended\",\n    \"parserOptions\": {\n        \"ecmaVersion\": 2016,\n        \"sourceType\": \"module\"\n    },\n    \"rules\": {\n        \"accessor-pairs\": \"error\",\n        \"array-bracket-newline\": \"error\",\n        \"array-bracket-spacing\": [\n            \"error\",\n            \"never\"\n        ],\n        \"array-callback-return\": \"error\",\n        \"array-element-newline\": \"off\",\n        \"arrow-body-style\": \"error\",\n        \"arrow-parens\": [\n            \"error\",\n            \"as-needed\"\n        ],\n        \"arrow-spacing\": [\n            \"error\",\n            {\n                \"after\": true,\n                \"before\": true\n            }\n        ],\n        \"block-scoped-var\": \"off\",\n        \"block-spacing\": [\n            \"error\",\n            \"always\"\n        ],\n        \"brace-style\": [\n            \"error\",\n            \"1tbs\",\n            {\n                \"allowSingleLine\": true\n            }\n        ],\n        \"callback-return\": \"error\",\n        \"camelcase\": \"off\",\n        \"capitalized-comments\": \"off\",\n        \"class-methods-use-this\": \"off\",\n        \"comma-dangle\": \"error\",\n        \"comma-spacing\": [\n            \"error\",\n            {\n                \"after\": true,\n                \"before\": false\n            }\n        ],\n        \"comma-style\": [\n            \"error\",\n            \"last\"\n        ],\n        \"complexity\": \"error\",\n        \"computed-property-spacing\": [\n            \"error\",\n            \"never\"\n        ],\n        \"consistent-return\": \"off\",\n        \"consistent-this\": \"off\",\n        \"curly\": \"error\",\n        \"default-case\": \"error\",\n        \"dot-location\": \"error\",\n        \"dot-notation\": \"error\",\n        \"eol-last\": \"error\",\n        \"eqeqeq\": \"off\",\n        \"func-call-spacing\": \"error\",\n        \"func-name-matching\": \"error\",\n        \"func-names\": \"error\",\n        \"func-style\": \"error\",\n        \"function-paren-newline\": \"off\",\n        \"generator-star-spacing\": \"error\",\n        \"global-require\": \"error\",\n        \"guard-for-in\": \"off\",\n        \"handle-callback-err\": \"error\",\n        \"id-blacklist\": \"error\",\n        \"id-length\": \"off\",\n        \"id-match\": \"error\",\n        \"implicit-arrow-linebreak\": [\n            \"error\",\n            \"beside\"\n        ],\n        \"indent\": \"off\",\n        \"indent-legacy\": \"off\",\n        \"init-declarations\": \"off\",\n        \"jsx-quotes\": \"error\",\n        \"key-spacing\": \"error\",\n        \"keyword-spacing\": [\n            \"error\",\n            {\n                \"after\": true,\n                \"before\": true\n            }\n        ],\n        \"line-comment-position\": \"off\",\n        \"linebreak-style\": [\n            \"error\",\n            \"unix\"\n        ],\n        \"lines-around-comment\": \"error\",\n        \"lines-around-directive\": \"error\",\n        \"lines-between-class-members\": [\n            \"error\",\n            \"always\"\n        ],\n        \"max-classes-per-file\": \"off\",\n        \"max-depth\": \"error\",\n        \"max-len\": \"off\",\n        \"max-lines\": \"off\",\n        \"max-lines-per-function\": \"error\",\n        \"max-nested-callbacks\": \"error\",\n        \"max-params\": \"off\",\n        \"max-statements\": \"off\",\n        \"max-statements-per-line\": \"off\",\n        \"multiline-comment-style\": [\n            \"error\",\n            \"separate-lines\"\n        ],\n        \"multiline-ternary\": [\n            \"error\",\n            \"always-multiline\"\n        ],\n        \"new-cap\": \"error\",\n        \"new-parens\": \"error\",\n        \"newline-after-var\": \"off\",\n        \"newline-before-return\": \"off\",\n        \"newline-per-chained-call\": \"error\",\n        \"no-alert\": \"error\",\n        \"no-array-constructor\": \"error\",\n        \"no-async-promise-executor\": \"error\",\n        \"no-await-in-loop\": \"error\",\n        \"no-bitwise\": \"error\",\n        \"no-buffer-constructor\": \"error\",\n        \"no-caller\": \"error\",\n        \"no-catch-shadow\": \"error\",\n        \"no-confusing-arrow\": \"error\",\n        \"no-continue\": \"error\",\n        \"no-div-regex\": \"error\",\n        \"no-duplicate-imports\": \"error\",\n        \"no-else-return\": \"error\",\n        \"no-empty-function\": \"error\",\n        \"no-eq-null\": \"off\",\n        \"no-eval\": \"error\",\n        \"no-extend-native\": \"error\",\n        \"no-extra-bind\": \"error\",\n        \"no-extra-label\": \"error\",\n        \"no-extra-parens\": \"off\",\n        \"no-floating-decimal\": \"error\",\n        \"no-implicit-coercion\": \"error\",\n        \"no-implicit-globals\": \"error\",\n        \"no-implied-eval\": \"error\",\n        \"no-inline-comments\": \"off\",\n        \"no-inner-declarations\": [\n            \"error\",\n            \"functions\"\n        ],\n        \"no-invalid-this\": \"error\",\n        \"no-iterator\": \"error\",\n        \"no-label-var\": \"error\",\n        \"no-labels\": \"error\",\n        \"no-lone-blocks\": \"error\",\n        \"no-lonely-if\": \"error\",\n        \"no-loop-func\": \"error\",\n        \"no-magic-numbers\": \"off\",\n        \"no-misleading-character-class\": \"error\",\n        \"no-mixed-operators\": \"error\",\n        \"no-mixed-requires\": \"error\",\n        \"no-multi-assign\": \"error\",\n        \"no-multi-spaces\": \"error\",\n        \"no-multi-str\": \"error\",\n        \"no-multiple-empty-lines\": \"error\",\n        \"no-native-reassign\": \"error\",\n        \"no-negated-condition\": \"off\",\n        \"no-negated-in-lhs\": \"error\",\n        \"no-nested-ternary\": \"off\",\n        \"no-new\": \"error\",\n        \"no-new-func\": \"error\",\n        \"no-new-object\": \"error\",\n        \"no-new-require\": \"error\",\n        \"no-new-wrappers\": \"error\",\n        \"no-octal-escape\": \"error\",\n        \"no-param-reassign\": \"off\",\n        \"no-path-concat\": \"error\",\n        \"no-plusplus\": \"error\",\n        \"no-process-env\": \"off\",\n        \"no-process-exit\": \"off\",\n        \"no-proto\": \"error\",\n        \"no-prototype-builtins\": \"error\",\n        \"no-restricted-globals\": \"error\",\n        \"no-restricted-imports\": \"error\",\n        \"no-restricted-modules\": \"error\",\n        \"no-restricted-properties\": \"error\",\n        \"no-restricted-syntax\": \"error\",\n        \"no-return-assign\": \"error\",\n        \"no-return-await\": \"error\",\n        \"no-script-url\": \"error\",\n        \"no-self-compare\": \"error\",\n        \"no-sequences\": \"error\",\n        \"no-shadow\": \"error\",\n        \"no-shadow-restricted-names\": \"error\",\n        \"no-spaced-func\": \"error\",\n        \"no-sync\": \"error\",\n        \"no-tabs\": \"error\",\n        \"no-template-curly-in-string\": \"error\",\n        \"no-ternary\": \"off\",\n        \"no-throw-literal\": \"error\",\n        \"no-trailing-spaces\": \"error\",\n        \"no-undef-init\": \"error\",\n        \"no-undefined\": \"off\",\n        \"no-underscore-dangle\": \"error\",\n        \"no-unmodified-loop-condition\": \"error\",\n        \"no-unneeded-ternary\": \"error\",\n        \"no-unused-expressions\": \"error\",\n        \"no-use-before-define\": \"error\",\n        \"no-useless-call\": \"error\",\n        \"no-useless-computed-key\": \"error\",\n        \"no-useless-concat\": \"error\",\n        \"no-useless-constructor\": \"error\",\n        \"no-useless-rename\": \"error\",\n        \"no-useless-return\": \"error\",\n        \"no-var\": \"off\",\n        \"no-void\": \"error\",\n        \"no-warning-comments\": \"error\",\n        \"no-whitespace-before-property\": \"error\",\n        \"no-with\": \"error\",\n        \"nonblock-statement-body-position\": \"error\",\n        \"object-curly-newline\": \"error\",\n        \"object-curly-spacing\": \"off\",\n        \"object-shorthand\": \"error\",\n        \"one-var\": \"off\",\n        \"one-var-declaration-per-line\": \"error\",\n        \"operator-assignment\": [\n            \"error\",\n            \"always\"\n        ],\n        \"operator-linebreak\": \"error\",\n        \"padded-blocks\": \"off\",\n        \"padding-line-between-statements\": \"error\",\n        \"prefer-arrow-callback\": \"error\",\n        \"prefer-const\": \"off\",\n        \"prefer-destructuring\": \"off\",\n        \"prefer-numeric-literals\": \"error\",\n        \"prefer-object-spread\": \"error\",\n        \"prefer-promise-reject-errors\": \"error\",\n        \"prefer-reflect\": \"off\",\n        \"prefer-rest-params\": \"error\",\n        \"prefer-spread\": \"error\",\n        \"prefer-template\": \"error\",\n        \"quote-props\": \"off\",\n        \"quotes\": \"off\",\n        \"radix\": \"error\",\n        \"require-atomic-updates\": \"error\",\n        \"require-await\": \"error\",\n        \"require-jsdoc\": \"error\",\n        \"require-unicode-regexp\": \"off\",\n        \"rest-spread-spacing\": [\n            \"error\",\n            \"never\"\n        ],\n        \"semi\": \"off\",\n        \"semi-spacing\": \"error\",\n        \"semi-style\": [\n            \"error\",\n            \"last\"\n        ],\n        \"sort-imports\": \"error\",\n        \"sort-keys\": \"off\",\n        \"sort-vars\": \"error\",\n        \"space-before-blocks\": \"error\",\n        \"space-before-function-paren\": \"off\",\n        \"space-in-parens\": \"off\",\n        \"space-infix-ops\": \"error\",\n        \"space-unary-ops\": \"error\",\n        \"spaced-comment\": [\n            \"error\",\n            \"always\"\n        ],\n        \"strict\": \"error\",\n        \"switch-colon-spacing\": \"error\",\n        \"symbol-description\": \"error\",\n        \"template-curly-spacing\": [\n            \"error\",\n            \"never\"\n        ],\n        \"template-tag-spacing\": \"error\",\n        \"unicode-bom\": [\n            \"error\",\n            \"never\"\n        ],\n        \"valid-jsdoc\": \"error\",\n        \"vars-on-top\": \"off\",\n        \"wrap-iife\": \"error\",\n        \"wrap-regex\": \"error\",\n        \"yield-star-spacing\": \"error\",\n        \"yoda\": [\n            \"error\",\n            \"never\"\n        ]\n    }\n};\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: \"npm\" # See documentation for possible values\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: CI\n\non: [push, pull_request]\n\njobs:\n\n  test:\n    name: Test on Node ${{ matrix.node }}\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node: [ '20' ]\n    steps:\n      - uses: actions/checkout@v1\n      - uses: actions/setup-node@v1\n        with:\n          node-version: ${{ matrix.node }}\n      - name: install dependencies\n        run: npm ci\n      - name: Project Tests\n        run: npm test\n\n  test_latest:\n    name: Test on latest Node\n    runs-on: ubuntu-latest\n    container: node:current\n    steps:\n      - uses: actions/checkout@v1\n      - name: install dependencies\n        run: npm ci\n      - name: Project Tests\n        run: npm test\n\n  eslint:\n    name: Check ESLint\n    runs-on: ubuntu-latest\n    container: node:current\n    steps:\n      - uses: actions/checkout@v1\n      - name: install dependencies\n        run: npm ci\n      - name: check lint\n        run: npm run lint\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\n\n# Runtime data\npids\n*.pid\n*.seed\n*.swp\ndist\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directory\n# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git\nnode_modules\n\n# IntelliJ\n**/*.iml\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:20-alpine\n\nARG hubot_owner\nARG hubot_description\nARG hubot_name\n\nRUN adduser -D -s /bin/bash hubot-matteruser\n\nRUN mkdir -p /usr/src/hubot-matteruser\nRUN chown hubot-matteruser:hubot-matteruser /usr/src/hubot-matteruser\nRUN chown hubot-matteruser:hubot-matteruser /usr/local/lib/node_modules/\nRUN chown hubot-matteruser:hubot-matteruser /usr/local/bin/\n\nWORKDIR /usr/src/hubot-matteruser\nUSER hubot-matteruser\nRUN npm install -g yo\nRUN npm install -g generator-hubot\n\nRUN echo \"No\" | yo hubot --adapter matteruser --owner=\"${hubot_owner}\" --name=\"${hubot_name}\" --description=\"${hubot_desciption}\" --defaults \\\n&& sed -i '/heroku/d' external-scripts.json\n\nRUN rm hubot-scripts.json\n\nCMD [\"-a\", \"matteruser\"]\nENTRYPOINT [\"./bin/hubot\"]\n\nEXPOSE 8080\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Andy Lo-A-Foe\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "README.md",
    "content": "[![Downloads](https://img.shields.io/npm/dm/hubot-matteruser.svg)](https://www.npmjs.com/package/hubot-matteruser)\n[![Version](https://img.shields.io/npm/v/hubot-matteruser.svg)](https://github.com/loafoe/hubot-matteruser/releases)\n[![Licence](https://img.shields.io/npm/l/express.svg)](https://github.com/loafoe/hubot-matteruser/blob/master/LICENSE)\n\n# hubot-matteruser\n\n**Hubot** is \"chat bot\" created by GitHub that listens for commands and executes actions based on your requests. \n\n`hubot-matteruser` is a Hubot adapter for [Mattermost](https://about.mattermost.com/) written in JavaScript that uses the Mattermost [Web Services API](https://api.mattermost.com/) and WebSockets to deliver Hubot functionality. \n\n- Learn more about [Hubot in Wired Magazine](https://www.wired.com/2015/10/the-most-important-startups-hardest-worker-isnt-a-person/)\n- Learn more about [Mattermost as an open source, self-hosted team communication server](https://about.mattermost.com/)\n\n## Description\n\nThis [Hubot](https://github.com/github/hubot) adapter connects to your Mattermost server. You can invite your bot to any channel just as a regular user. It listens and perform your commands. The adapter uses [mattermost-client](https://github.com/loafoe/mattermost-client) for all low level Mattermost communication.\n\nTwo authentication methods are supported:\n\n* login/password,\n* [personnal access token](https://docs.mattermost.com/developer/personal-access-tokens.html).\n\nThe second one is necessary if the Mattermost server delegates the authentication to another service (for example when using Mattermost shiped with [GitLab](http://www.gitlab.com)).\nSuch method is also probably prefered as the token does not reveals original credentials and can be revoked without any impact on the related account.\n\n## Docker usage\n\n### Standalone\n\nClone this repository, then build the Hubot-Matteruser container:\n\n```\n$ docker build --build-arg hubot_owner=<owner> \\\n             --build-arg hubot_name=<name> \\\n             --build-arg hubot_description=<desc> \\\n             --tag=hubot-matteruser \\\n             .\n```\n\nStart the container:\n\n```\n$ docker run -it \\\n           --env MATTERMOST_HOST=<mm_host> \\\n           --env MATTERMOST_GROUP=<mm_team> \\\n           --env MATTERMOST_USER=<mm_user_email> \\\n           --env MATTERMOST_PASSWORD=<mm_user_password> \\\n           -p 8080:8080 \\\n           --name hubot-matteruser \\\n           hubot-matteruser\n```\n\nor if you have a personal access token:\n\n```\n$ docker run -it \\\n           --env MATTERMOST_HOST=<mm_host> \\\n           --env MATTERMOST_GROUP=<mm_team> \\\n           --env MATTERMOST_ACCESS_TOKEN=<personal>\n           -p 8080:8080 \\\n           --name hubot-matteruser \\\n           hubot-matteruser\n```\n\n### Docker Compose\n\nTo integrate with a running Mattermost instance, update docker-compose.yml accordingly and launch the bot:\n\n``` \ndocker-compose build\ndocker-compose run -d\n```\n\nIf you just want to test locally, you can find [here](https://github.com/banzo/mattermost-docker/tree/feature/hubot-matteruser) a fork of the [official Mattermost Docker Compose stack](https://github.com/mattermost/mattermost-docker) plugged to Hubot-Matteruser: \n\n\n## Installation\n\n### 1) Install a Mattermost server\n\nFollow the [Mattermost install guides](https://docs.mattermost.com/guides/administrator.html#install-guides) to set up the latest version of Mattermost 5.4.x.\n\n**IMPORTANT:** Make sure your `hubot-matteruser` and `mattermost-client` versions **match** the major version of your Mattermost server so the API versions will match. \n\n### 2) Install hubot-matteruser\n\nOn a separate server, install `hubot-matteruser` using the following commands: \n\n  ```sh\nnpm install -g yo generator-hubot\nyo hubot --adapter matteruser\n  ```\n\nFollow the instructions to set up your bot, including setup of [`mattermost-client`](https://github.com/loafoe/mattermost-client). \n\n#### Environment variables\n\nThe adapter requires the following environment variables to be defined before your Hubot instance will start:\n\n| Variable | Required | Description |\n|----------|----------|-------------|\n| MATTERMOST\\_HOST | Yes | The Mattermost host e.g. _mm.yourcompany.com_ |\n| MATTERMOST\\_GROUP | Yes | The team/group on your Mattermost server e.g. _core_ |\n| MATTERMOST\\_USER | No | The Mattermost user account name e.g. _hubot@yourcompany.com_ |\n| MATTERMOST\\_PASSWORD | No | The password of the user e.g. _s3cr3tP@ssw0rd!_ |\n| MATTERMOST\\_ACCESS\\_TOKEN | No | The [personal access token](https://docs.mattermost.com/developer/personal-access-tokens.html) of the user |\n| MATTERMOST\\_WSS\\_PORT | No | Overrides the default port `443` for  websocket (`wss://`) connections |\n| MATTERMOST\\_HTTP\\_PORT | No | Overrides the default port (`80` or `443`) for `http://` or `https://` connections |\n| MATTERMOST\\_TLS\\_VERIFY | No | (default: true) set to 'false' to allow connections when certs can not be verified (ex: self-signed, internal CA, ... - MITM risks) |\n| MATTERMOST\\_USE\\_TLS | No | (default: true) set to 'false' to switch to http/ws protocols |\n| MATTERMOST\\_LOG\\_LEVEL | No | (default: info) set log level (also: debug, ...) |\n| MATTERMOST\\_REPLY | No | (default: true) set to 'false' to stop posting `reply` responses as comments |\n| MATTERMOST\\_IGNORE\\_USERS | No | (default: empty) Enter a comma-separated list of user senderi\\_names to ignore. |\n\n#### Example configuration\n\nThe below example assumes you have created a user `hubot@yourcompany.com` with username `hubot` and password `s3cr3tP@ssw0rd!` on your Mattermost server in the `core` team reachable on URL `https://mm.yourcompany.com/core`\n\n  ```sh\nexport MATTERMOST_HOST=mm.yourcompany.com \nexport MATTERMOST_GROUP=core\nexport MATTERMOST_USER=hubot@yourcompany.com\nexport MATTERMOST_PASSWORD=s3cr3tP@ssw0rd!\n  ```\n\n## Upgrade\n\nTo upgrade your Hubot for Mattermost 4.4.x, find the `package.json` file in your Hubot directory and look for the line in the `dependencies` section that references `hubot-matteruser`. Change the verion so it points to `^5.4.4` of the client. Example:\n\n  ```json\n    ...\n    \"dependencies\": {\n      \"hubot-matteruser\": \"^5.4.6\"\n    },\n    ...\n  ```\n\n## Try the Hubot demo\n\nYou can try out Hubot by joining the Mattermost community server and joining the Hubot channel: \n\n1. [Create an account](https://pre-release.mattermost.com/signup_user_complete/?id=f1924a8db44ff3bb41c96424cdc20676) on the Mattermost nightly builds server at https://pre-release.mattermost.com/\n2. Join the \"Hubot\" channel\n3. Type `hubot help` for instructions\n\n### Sample commands\n\nYou can try a simple command like `hubot the rules` to bring some static text stored in Hubot: \n\n![s](https://cloud.githubusercontent.com/assets/177788/20645776/b25da69a-b41c-11e6-81d2-a40d76947e60.png)\n\nTry `hubot animate me` to have Hubot reach out to Giphy and bring back a random animated image.\n\n![s](https://cloud.githubusercontent.com/assets/177788/20645764/88c267a8-b41c-11e6-96c9-529c3ca844f3.png)\n\nTry `hubot map me [NAME_OF_CITY]` to have Hubot reach out to Google Maps and bring back a map based on the name of a city you pass in as a parameter. For example, `hubot map me palo alto` brings back the below map of Palo Alto\n\n![s](https://cloud.githubusercontent.com/assets/177788/20645769/9d58a786-b41c-11e6-90b1-6a9e7ab19172.png)\n\n\n## License\n\nThe MIT License. See `LICENSE` file.\n\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: \"2\"\n\nservices:\n  hubot-matteruser:\n    build:\n     context: .\n     args:\n       hubot_owner: <CHANGEME>\n       hubot_name: <CHANGEME>\n       hubot_description: <CHANGEME>\n    restart: always\n    user: hubot-matteruser\n    ports:\n      - \"8080:8080\"\n    environment:\n      - MATTERMOST_HOST=<CHANGEME>\n      - MATTERMOST_GROUP=<CHANGEME>\n      - MATTERMOST_USER=<CHANGEME>\n      - MATTERMOST_PASSWORD=<CHANGEME>\n      - MATTERMOST_LOG_LEVEL=info\n      - MATTERMOST_USE_TLS=false\n      - MATTERMOST_TLS_VERIFY=false\n      - MATTERMOST_WSS_PORT=80\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"hubot-matteruser\",\n  \"version\": \"5.4.6\",\n  \"author\": {\n    \"name\": \"Andy Lo-A-Foe\",\n    \"url\": \"https://github.com/loafoe\"\n  },\n  \"contributors\": [],\n  \"description\": \"Mattermost Adapter\",\n  \"keywords\": [\n    \"hubot\",\n    \"adapter\",\n    \"mattermost\",\n    \"chat\"\n  ],\n  \"license\": \"MIT\",\n  \"module\": \"./src/matteruser.js\",\n  \"main\": \"dist/matteruser.js\",\n  \"files\": [\n    \"/dist\"\n  ],\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"lint\": \"yarn lint:js\",\n    \"lint:fix\": \"yarn lint:js:fix\",\n    \"lint:js\": \"eslint 'src/**/*.js'\",\n    \"lint:js:fix\": \"yarn lint:js --fix\",\n    \"build:dev\": \"webpack --mode=development\",\n    \"build:prod\": \"webpack --mode=production\",\n    \"prepublishOnly\": \"npm run build:prod\"\n  },\n  \"dependencies\": {\n    \"json-schema\": \"^0.4.0\",\n    \"mattermost-client\": \"^6.5.0\"\n  },\n  \"peerDependencies\": {\n    \"hubot\": \">=3.0.1\"\n  },\n  \"devDependencies\": {\n    \"eslint\": \"^8.4.0\",\n    \"eslint-config-airbnb-base\": \"^15.0.0\",\n    \"hubot\": \"^3.3.2\",\n    \"jest\": \"^29.1.2\",\n    \"webpack\": \"^5.35.1\",\n    \"webpack-cli\": \"^4.6.0\",\n    \"yarn\": \"^1.22.10\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/loafoe/hubot-matteruser.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/loafoe/hubot-matteruser/issues\",\n    \"email\": \"andy.loafoe@gmail.com\"\n  }\n}\n"
  },
  {
    "path": "src/matteruser.js",
    "content": "const {\n  Adapter,\n  TextMessage,\n  EnterMessage,\n  LeaveMessage\n} = require('hubot/es2015');\n\nconst MatterMostClient = require('mattermost-client');\n\nclass AttachmentMessage extends TextMessage {\n\n  constructor(user, text, file_ids, id) {\n    super(user, text, id);\n  }\n}\n\n// A TextMessage class that adds `msg.props` for Mattermost's properties.\n//\n// Text fields from message attachments are appended in @text for matching.\n// <https://docs.mattermost.com/developer/message-attachments.html>\n// The result is that `bot.hear()` will match against these attachment fields.\n//\n// As well, it is possible that some bot handlers could make use of other\n// fields on `msg.props`.\n//\n// Example raw props:\n//   {\n//       \"attachments\": [...],\n//       \"from_webhook\": \"true\",\n//       \"override_username\": \"trenthere\"\n//   }\nclass TextAndPropsMessage extends TextMessage {\n\n  constructor(user, text, props, id) {\n    super(user, text, id);\n    this.props = props;\n    this.origText = this.text;\n    if (this.props.attachments) {\n      const separator = '\\n\\n--\\n\\n';\n      for (let attachment of this.props.attachments) {\n        const parts = [];\n        for (let field of ['pretext', 'title', 'text']) {\n          if (attachment[field]) {\n            parts.push(attachment[field]);\n          }\n        }\n        if (parts.length) {\n          this.text += separator + parts.join('\\n\\n');\n        }\n      }\n    }\n  }\n\n  match(regex) {\n    return this.text.match(regex);\n  }\n}\n\nclass Matteruser extends Adapter {\n\n  constructor(...args) {\n    super(...args);\n\n    // Binding because async calls galore\n    this.open = this.open.bind(this);\n    this.error = this.error.bind(this);\n    this.onConnected = this.onConnected.bind(this);\n    this.onHello = this.onHello.bind(this);\n    this.userChange = this.userChange.bind(this);\n    this.loggedIn = this.loggedIn.bind(this);\n    this.profilesLoaded = this.profilesLoaded.bind(this);\n    this.brainLoaded = this.brainLoaded.bind(this);\n    this.message = this.message.bind(this);\n    this.userTyping = this.userTyping.bind(this);\n    this.userAdded = this.userAdded.bind(this);\n    this.userRemoved = this.userRemoved.bind(this);\n  }\n\n  run() {\n    const mmHost = process.env.MATTERMOST_HOST;\n    const mmUser = process.env.MATTERMOST_USER || null;\n    const mmPassword = process.env.MATTERMOST_PASSWORD;\n    const mmMFAToken = process.env.MATTERMOST_MFA_TOKEN || null;\n    const mmGroup = process.env.MATTERMOST_GROUP;\n    const mmWSSPort = process.env.MATTERMOST_WSS_PORT || '443';\n    const mmHTTPPort = process.env.MATTERMOST_HTTP_PORT || null;\n    const mmAccessToken = process.env.MATTERMOST_ACCESS_TOKEN || null;\n    const mmHTTPProxy = process.env.http_proxy || null;\n    this.mmNoReply = process.env.MATTERMOST_REPLY === 'false';\n    this.mmIgnoreUsers = (process.env.MATTERMOST_IGNORE_USERS != null\n      ? process.env.MATTERMOST_IGNORE_USERS.split(',')\n      : undefined) || [];\n\n    if (mmHost == null) {\n      this.robot.logger.emergency(\"MATTERMOST_HOST is required\");\n      process.exit(1);\n    }\n    if (mmUser == null && mmAccessToken == null) {\n      this.robot.logger.emergency(\"MATTERMOST_USER or MATTERMOST_ACCESS_TOKEN is required\");\n      process.exit(1);\n    }\n    if (mmPassword == null && mmAccessToken == null) {\n      this.robot.logger.emergency(\"MATTERMOST_PASSWORD is required\");\n      process.exit(1);\n    }\n    if (mmGroup == null) {\n      this.robot.logger.emergency(\"MATTERMOST_GROUP is required\");\n      process.exit(1);\n    }\n\n    this.client = new MatterMostClient(mmHost, mmGroup, {\n      wssPort: mmWSSPort, httpPort: mmHTTPPort, pingInterval: 30000, httpProxy: mmHTTPProxy,\n      logger: this.robot.logger\n    });\n\n    this.declareCallbacks();\n    if (mmAccessToken != null) {\n      return this.client.tokenLogin(mmAccessToken);\n    }\n\n    return this.client.login(mmUser, mmPassword, mmMFAToken);\n  }\n\n  declareCallbacks() {\n    this.client.on('open', this.open);\n    this.client.on('hello', this.onHello);\n    this.client.on('loggedIn', this.loggedIn);\n    this.client.on('connected', this.onConnected);\n    this.client.on('message', this.message);\n    this.client.on('profilesLoaded', this.profilesLoaded);\n    this.client.on('user_added', this.userAdded);\n    this.client.on('user_removed', this.userRemoved);\n    this.client.on('typing', this.userTyping);\n    this.client.on('error', this.error);\n\n    this.robot.brain.on('loaded', this.brainLoaded);\n  }\n\n  open() {\n    return true;\n  }\n\n  error(err) {\n    this.robot.logger.info('Error: %j', err);\n    return true;\n  }\n\n  onConnected() {\n    this.robot.logger.info('Connected to Mattermost.');\n    this.emit('connected');\n    return true;\n  }\n\n  onHello(event) {\n    this.robot.logger.info('Mattermost server: %s', event.data.server_version);\n    return true;\n  }\n\n  /**\n   *\n   * @param {User} user The user to be change\n   * @returns {User} The updated User\n   */\n  userChange(user) {\n    if (!user || (user.id == null)) {\n      return;\n    }\n    this.robot.logger.debug('Adding user %s', user.id);\n    const newUser = {\n      name: user.username,\n      real_name: `${user.first_name} ${user.last_name}`,\n      email_address: user.email,\n      mm: {}\n    };\n\n    // Preserve the DM channel ID if it exists\n    let user_obj = this.robot.brain.userForId(user.id);\n    newUser.mm.dm_channel_id = undefined;\n    if (\"mm\" in user_obj) {\n      newUser.mm.dm_channel_id = user_obj.mm.dm_channel_id\n    }\n\n    let value;\n    for (var key in user) {\n      value = user[key];\n      newUser.mm[key] = value;\n    }\n    if (user.id in this.robot.brain.data.users) {\n      for (key in this.robot.brain.data.users[user.id]) {\n        value = this.robot.brain.data.users[user.id][key];\n        if (!(key in newUser)) {\n          newUser[key] = value;\n        }\n      }\n    }\n    delete this.robot.brain.data.users[user.id];\n    return this.robot.brain.userForId(user.id, newUser);\n  }\n\n  /**\n   *\n   * @param {User} user The user to logged in\n   * @returns {boolean} True if the user is now logged in\n   */\n  loggedIn(user) {\n    this.robot.logger.info('Logged in as user \"%s\" but not connected yet.', user.username);\n    this.self = user;\n    return true;\n  }\n\n  /**\n   *\n   * @param {User[]} profiles The users profile loaded\n   * @returns {[]} The changed users\n   */\n  profilesLoaded(profiles) {\n    return (() => {\n      const result = [];\n      for (let id in profiles) {\n        const user = profiles[id];\n        result.push(this.userChange(user));\n      }\n      return result;\n    })();\n  }\n\n  brainLoaded() {\n    this.robot.logger.info('Brain loaded');\n    for (let id in this.client.users) {\n      const user = this.client.users[id];\n      this.userChange(user);\n    }\n    return true;\n  }\n\n  /**\n   *\n   * @param {Envelop} envelope containing the room to send strings\n   * @param {string} strings  The messages to send\n   * @return {undefined}\n   */\n  send(envelope, ...strings) {\n    // Check if the target room is also a user's username\n    let str;\n    const user = this.robot.brain.userForName(envelope.room);\n\n    // If it's not, continue as normal\n    if (!user) {\n      const channel = this.client.findChannelByName(envelope.room);\n      const channel_id = channel ? channel.id : undefined;\n\n      for (str of strings) {\n        this.client.postMessage(str,\n          (channel_id || envelope.room));\n      }\n    } else {\n      // If it is, we assume they want to DM that user\n      // Message their DM channel ID if it already exists.\n      let dm_channel_id = user.mm\n        ? (user.mm.dm_channel_id ? user.mm.dm_channel_id : undefined)\n        : undefined\n\n      if (dm_channel_id != null) {\n        for (str of strings) {\n          this.client.postMessage(str, user.mm.dm_channel_id);\n        }\n\n      } else {\n\n        let self = this\n\n        // Otherwise, create a new DM channel ID and message it.\n        this.client.getUserDirectMessageChannel(user.id, channel => {\n          if (!user.mm) {\n            user.mm = {};\n          }\n          user.mm.dm_channel_id = channel.id;\n\n          for (str of strings) {\n            self.client.postMessage(str, channel.id);\n          }\n        });\n      }\n    }\n  }\n\n  /**\n   *\n   * @param {Envelop} envelope The Message envelop\n   * @param {string} strings The message lines to reply\n   * @returns {void}\n   */\n  cmd(envelope, ...strings) {\n    // Check if the target room is also a user's username\n    let str;\n    const user = this.robot.brain.userForName(envelope.room);\n\n    // If it's not, continue as normal\n    if (!user) {\n      const channel = this.client.findChannelByName(envelope.room);\n      const channel_id = channel ? channel.id : undefined;\n\n      for (str of strings) {\n        this.client.postCommand((channel_id || envelope.room),\n          str);\n      }\n    } else {\n      // If it is, we assume they want to DM that user\n      // Message their DM channel ID if it already exists.\n      let dm_channel_id = user.mm\n        ? (user.mm.dm_channel_id ? user.mm.dm_channel_id : undefined)\n        : undefined\n\n      if (dm_channel_id != null) {\n        for (str of strings) {\n          this.client.postCommand(user.mm.dm_channel_id, str);\n        }\n\n      } else {\n\n        let self = this\n\n        // Otherwise, create a new DM channel ID and message it.\n        this.client.getUserDirectMessageChannel(user.id, channel => {\n          if (!user.mm) {\n            user.mm = {};\n          }\n          user.mm.dm_channel_id = channel.id;\n\n          for (str of strings) {\n            self.client.postCommand(channel.id, str);\n          }\n        });\n      }\n    }\n  }\n\n  /**\n   * Reply to a message\n   * @param {Envelop} envelope The Message envelop\n   * @param {string} strings The message lines to reply\n   * @returns {void}\n   */\n  reply(envelope, ...strings) {\n    if (this.mmNoReply) {\n      return this.send(envelope, ...strings);\n    }\n\n    const postData = {};\n    postData.message = strings[0];\n\n    // Set the comment relationship\n    postData.root_id = envelope.user.root_id || envelope.message.id;\n    postData.parent_id = envelope.message.id;\n\n    postData.create_at = 0;\n    postData.user_id = this.self.id;\n    postData.filename = [];\n    // Check if the target room is also a user's username\n    const user = this.robot.brain.userForName(envelope.room);\n\n    // If it's not, continue as normal\n    if (!user) {\n      const channel = this.client.findChannelByName(envelope.room);\n      postData.channel_id = (channel ? channel.id : undefined) || envelope.room;\n      this.client.customMessage(postData, postData.channel_id);\n      return;\n    }\n\n    // If it is, we assume they want to DM that user\n    // Message their DM channel ID if it already exists.\n    if ((user.mm ? user.mm.dm_channel_id : undefined) != null) {\n      postData.channel_id = user.mm.dm_channel_id;\n      this.client.customMessage(postData, postData.channel_id);\n      return;\n    }\n\n    // Otherwise, create a new DM channel ID and message it.\n    return this.client.getUserDirectMessageChannel(user.id, channel => {\n      if (!user.mm) {\n        user.mm = {};\n      }\n      user.mm.dm_channel_id = channel.id;\n      postData.channel_id = channel.id;\n      return this.client.customMessage(postData, postData.channel_id);\n    });\n  }\n\n  message(msg) {\n    if (this.mmIgnoreUsers.includes(msg.data.sender_name)) {\n      this.robot.logger.info('User %s is in MATTERMOST_IGNORE_USERS, ignoring them.', msg.data.sender_name);\n      return;\n    }\n\n    this.robot.logger.debug(msg);\n    const mmPost = JSON.parse(msg.data.post);\n    if (mmPost.user_id === this.self.id) {\n      return;\n    } // Ignore our own output\n    this.robot.logger.debug('From: %s, To: %s', mmPost.user_id, this.self.id);\n\n    const user = this.robot.brain.userForId(mmPost.user_id);\n    user.room = mmPost.channel_id;\n    user.room_name = msg.data.channel_name;\n    user.room_display_name = msg.data.channel_display_name;\n    user.channel_type = msg.data.channel_type;\n    user.root_id = mmPost.root_id;\n\n    let text = mmPost.message;\n    if (msg.data.channel_type === 'D') {\n      if (!new RegExp(`^@?${this.robot.name}`, 'i').test(text)) {\n        text = `${this.robot.name} ${text}`;\n      }\n      if (!user.mm) {\n        user.mm = {};\n      }\n      user.mm.dm_channel_id = mmPost.channel_id;\n    }\n    this.robot.logger.debug('Text: %s', text);\n\n    if (mmPost.file_ids) {\n      this.receive(new AttachmentMessage(user, text, mmPost.file_ids, mmPost.id));\n      // If there are interesting props, then include them for bot handlers.\n    } else if (mmPost.props ? mmPost.props.attachments : undefined) {\n      this.receive(new TextAndPropsMessage(user, text, mmPost.props, mmPost.id));\n    } else {\n      this.receive(new TextMessage(user, text, mmPost.id));\n    }\n    this.robot.logger.debug(\"Message sent to hubot brain.\");\n    return true;\n  }\n\n  userTyping(msg) {\n    this.robot.logger.info('Someone is typing -> %j', msg);\n    return true;\n  }\n\n  userAdded(msg) {\n    // update channels when this bot is added to a new channel\n    if (msg.data.user_id === this.self.id) {\n      this.client.loadChannels();\n    }\n    try {\n      const mmUser = this.client.getUserByID(msg.data.user_id);\n      this.userChange(mmUser);\n      const user = this.robot.brain.userForId(mmUser.id);\n      user.room = msg.broadcast.channel_id;\n      this.receive(new EnterMessage(user));\n      return true;\n    } catch (error) {\n      return false;\n    }\n  }\n\n  userRemoved(msg) {\n    // update channels when this bot is removed from a channel\n    if (msg.data.user_id === this.self.id) {\n      this.client.loadChannels();\n    }\n    try {\n      const mmUser = this.client.getUserByID(msg.data.user_id);\n      const user = this.robot.brain.userForId(mmUser.id);\n      user.room = msg.broadcast.channel_id;\n      this.receive(new LeaveMessage(user));\n      return true;\n    } catch (error) {\n      return false;\n    }\n  }\n\n  changeHeader(channel, header) {\n    if (channel == null || header == null) {\n      return;\n    }\n\n    const channelInfo = this.client.findChannelByName(channel);\n\n    if (channelInfo == null) {\n      return this.robot.logger.error('Channel not found');\n    }\n\n    return this.client.setChannelHeader(channelInfo.id, header);\n  }\n}\n\nmodule.exports.use = robot => new Matteruser(robot)\n"
  },
  {
    "path": "tests/Matteruser_message.test.js",
    "content": "const {TextMessage} = require(\"hubot/es2015\");\nconst {matterUserAfterEnv, matterUserBeforeEnv} = require(\"./helpers/test-helpers\");\nconst {HUBOT_SELF_USER} = require(\"./helpers/samples\");\n\nconst {use} = require('../src/matteruser.js');\njest.mock('mattermost-client');\n\nconst robot = require('robot');\nconst tested = use(robot);\n\n\nbeforeAll(matterUserBeforeEnv);\nafterAll(matterUserAfterEnv);\n\nbeforeEach(() => {\n  jest.resetAllMocks();\n  jest.resetModules() // Most important - it clears the cache\n\n  tested.run();\n  tested.emit = jest.fn();\n  tested.self = HUBOT_SELF_USER\n});\n\ndescribe('MatterUser message', () => {\n  test('should receive from self user', () => {\n    tested.message({\n      data: {\n        sender_name: 'dsidious',\n        post: JSON.stringify({\n          user_id: HUBOT_SELF_USER.id,\n        }),\n      }\n    });\n    expect(robot.receive).not.toHaveBeenCalled();\n  });\n\n  test('should receive from ignored user', () => {\n    tested.mmIgnoreUsers = ['dsidious'];\n    tested.message({\n      data: {\n        sender_name: 'dsidious',\n        post: JSON.stringify({\n          user_id: 'okenobi',\n          root_id: '42',\n          message: 'May the force',\n        }),\n        channel_name: 'jedi',\n        channel_display_name: 'Jedi Room',\n        channel_type: 'D'\n      }\n    });\n    expect(robot.receive).not.toHaveBeenCalled();\n  });\n\n  test('should receive direct message without files', () => {\n    tested.message({\n      data: {\n        sender_name: 'bfett',\n        post: JSON.stringify({\n          user_id: 'bfett',\n          root_id: '42',\n          message: 'May the force'\n        }),\n        channel_name: 'jedi',\n        channel_display_name: 'Jedi Room',\n        channel_type: 'D'\n      }\n    });\n\n    expect(robot.receive).toHaveBeenCalledWith({\n      done: false,\n      text: 'hubot May the force',\n      user: {\n        channel_type: 'D',\n        id: 'bfett',\n        mm: {},\n        room_display_name: 'Jedi Room',\n        room_name: 'jedi',\n        root_id: '42',\n        username: 'Boba Fett'\n      }\n    });\n  });\n\n  test('should receive direct message with files', () => {\n    tested.message({\n      data: {\n        sender_name: 'Obiwan Kenobi',\n        post: JSON.stringify({\n          user_id: 'okenobi',\n          root_id: '42',\n          message: 'May the force',\n          file_ids: [1, 2, 3]\n        }),\n        channel_name: 'jedi',\n        channel_display_name: 'Jedi Room',\n        channel_type: 'D'\n      }\n    });\n\n    expect(robot.receive).toHaveBeenCalled();\n    let actual = robot.receive.mock.calls[0][0];\n    expect(actual).toBeInstanceOf(TextMessage);\n    expect(actual).toEqual({\n      done: false,\n      text: 'hubot May the force',\n      user: {\n        channel_type: 'D',\n        faction: 'jedi',\n        id: 'okenobi',\n        mm: {},\n        room_display_name: 'Jedi Room',\n        room_name: 'jedi',\n        root_id: '42',\n        username: 'Obiwan Kenobi'\n      }\n    });\n  });\n\n  test('should receive direct message with attachments props', () => {\n    tested.message({\n      data: {\n        sender_name: 'Obiwan Kenobi',\n        post: JSON.stringify({\n          user_id: 'okenobi',\n          root_id: '42',\n          message: 'May the force',\n          props: {\n            attachments: [\n              {pretext: 'Yoda Says', title: 'Jedi Code', text: 'Emotion, yet peace'},\n              {pretext: 'Yoda Says', title: 'Ignorance, yet knowledge', text: 'Emotion, yet peace'},\n            ]\n          }\n        }),\n        channel_name: 'jedi',\n        channel_display_name: 'Jedi Room',\n        channel_type: 'D'\n      }\n    });\n\n    expect(robot.receive).toHaveBeenCalled();\n    let actual = robot.receive.mock.calls[0][0];\n    expect(actual).toBeInstanceOf(TextMessage);\n    expect(actual).toEqual({\n      done: false,\n      origText: 'hubot May the force',\n      props: {\n        attachments: [\n          {\n            pretext: 'Yoda Says',\n            text: 'Emotion, yet peace',\n            title: 'Jedi Code'\n          },\n          {\n            pretext: 'Yoda Says',\n            text: 'Emotion, yet peace',\n            title: 'Ignorance, yet knowledge'\n          }\n        ]\n      },\n      text: [\n        'hubot May the force',\n        '',\n        '--',\n        '',\n        'Yoda Says',\n        '',\n        'Jedi Code',\n        '',\n        'Emotion, yet peace',\n        '',\n        '--',\n        '',\n        'Yoda Says',\n        '',\n        'Ignorance, yet knowledge',\n        '',\n        'Emotion, yet peace'\n      ].join('\\n'),\n      user: {\n        channel_type: 'D',\n        faction: 'jedi',\n        id: 'okenobi',\n        mm: {\n          dm_channel_id: undefined\n        },\n        room: undefined,\n        room_display_name: 'Jedi Room',\n        room_name: 'jedi',\n        root_id: '42',\n        username: 'Obiwan Kenobi'\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "tests/Matteruser_reply.test.js",
    "content": "const {matterUserAfterEnv, matterUserBeforeEnv} = require(\"./helpers/test-helpers\");\nconst {HUBOT_SELF_USER, USER_WITH_CHANNEL, USER_WITHOUT_CHANNEL} = require(\"./helpers/samples\");\n\nconst {use} = require('../src/matteruser.js');\njest.mock('mattermost-client');\n\nconst robot = require('robot');\nconst tested = use(robot);\n\n\nbeforeAll(matterUserBeforeEnv);\nafterAll(matterUserAfterEnv);\n\nbeforeEach(() => {\n  jest.resetAllMocks();\n  jest.resetModules() // Most important - it clears the cache\n\n  tested.run();\n  tested.emit = jest.fn();\n  tested.mmNoReply = false;\n  tested.self = HUBOT_SELF_USER\n});\n\ndescribe('MatterUser reply', () => {\n  test('should reply with no reply', () => {\n    tested.mmNoReply = true;\n    const spy = jest.spyOn(tested, 'send');\n    const envelope = {\n      room: 'jedi',\n      user: USER_WITH_CHANNEL,\n      message: {id: '42'}\n    };\n    tested.reply(envelope, \"May the force\", \"Be with you\");\n\n    expect(spy).toHaveBeenCalledWith(envelope, \"May the force\", \"Be with you\");\n  });\n\n  test('should reply on existing direct message channel', () => {\n    tested.reply({\n      room: 'okenobi',\n      user: USER_WITH_CHANNEL,\n      message: {id: '42'}\n    }, \"May the force\", \"Be with you\");\n\n    expect(tested.client.findChannelByName).not.toHaveBeenCalled();\n    expect(tested.client.customMessage).toHaveBeenNthCalledWith(1, {\n      channel_id: '66',\n      create_at: 0,\n      filename: [],\n      message: 'May the force',\n      parent_id: '42',\n      root_id: '42',\n      user_id: 'matterbot',\n    }, '66');\n  });\n\n  test('should reply on public channel', () => {\n    tested.reply({\n      room: 'jedi',\n      user: USER_WITH_CHANNEL,\n      message: {id: '42'}\n    }, \"May the force\", \"Be with you\");\n\n    expect(tested.client.findChannelByName).toHaveBeenCalledWith('jedi');\n    expect(tested.client.customMessage).toHaveBeenNthCalledWith(1, {\n      channel_id: 'jedi',\n      create_at: 0,\n      filename: [],\n      message: 'May the force',\n      parent_id: '42',\n      root_id: '42',\n      user_id: 'matterbot',\n    }, 'jedi');\n  });\n\n  test('should reply on new direct message channel', () => {\n    tested.client.getUserDirectMessageChannel.mockImplementation((user_id, callback) => {\n      callback({id: 'bfett'});\n    });\n\n    tested.reply({\n      room: 'bfett',\n      user: USER_WITHOUT_CHANNEL,\n      message: {id: '42'}\n    }, \"May the force\", \"Be with you\");\n\n    expect(tested.client.findChannelByName).not.toHaveBeenCalled();\n    expect(tested.client.getUserDirectMessageChannel).toHaveBeenNthCalledWith(1, 'bfett', expect.anything());\n    expect(tested.client.customMessage).toHaveBeenNthCalledWith(1, {\n      channel_id: 'bfett',\n      create_at: 0,\n      filename: [],\n      message: 'May the force',\n      parent_id: '42',\n      root_id: '42',\n      user_id: 'matterbot',\n    }, 'bfett');\n  });\n});\n"
  },
  {
    "path": "tests/Matteruser_send_cmd.test.js",
    "content": "const {matterUserAfterEnv, matterUserBeforeEnv} = require(\"./helpers/test-helpers\");\n\nconst {use} = require('../src/matteruser.js');\njest.mock('mattermost-client');\n\nlet robot, tested;\n\nbeforeAll(matterUserBeforeEnv);\nafterAll(matterUserAfterEnv);\n\nbeforeEach(() => {\n  jest.resetAllMocks();\n  jest.resetModules() // Most important - it clears the cache\n\n  robot = require('robot');\n  tested = use(robot);\n  tested.run();\n  tested.emit = jest.fn();\n});\n\ndescribe('MatterUser send', () => {\n  test('should send envelop to mattermost user', () => {\n    tested.send({room: 'tatooine'}, 'May the', '4th Be', 'with you');\n\n    expect(tested.client.postMessage).toHaveBeenNthCalledWith(1, \"May the\", \"tatooine\");\n    expect(tested.client.postMessage).toHaveBeenNthCalledWith(2, \"4th Be\", \"tatooine\");\n    expect(tested.client.postMessage).toHaveBeenNthCalledWith(3, \"with you\", \"tatooine\");\n  });\n\n  test('should send envelop to mattermost channel with channel id', () => {\n    tested.client.findChannelByName.mockImplementation(room => ({id: 'tatooine_id'}));\n    tested.send({room: 'tatooine'}, 'May the', '4th Be', 'with you');\n\n    expect(tested.client.postMessage).toHaveBeenNthCalledWith(1, \"May the\", \"tatooine_id\");\n    expect(tested.client.postMessage).toHaveBeenNthCalledWith(2, \"4th Be\", \"tatooine_id\");\n    expect(tested.client.postMessage).toHaveBeenNthCalledWith(3, \"with you\", \"tatooine_id\");\n  });\n\n  test('should send envelop to mattermost user', () => {\n    tested.send({room: 'okenobi'}, 'May the', '4th Be', 'with you');\n    expect(tested.client.postMessage).toHaveBeenNthCalledWith(1, \"May the\", \"66\");\n    expect(tested.client.postMessage).toHaveBeenNthCalledWith(2, \"4th Be\", \"66\");\n    expect(tested.client.postMessage).toHaveBeenNthCalledWith(3, \"with you\", \"66\");\n  });\n\n  test('should send envelop to mattermost user', () => {\n    tested.client.getUserDirectMessageChannel.mockImplementation((user_id, callback) => {\n      callback({id: 'bfett'});\n    });\n\n    tested.send({room: 'bfett'}, 'May the', '4th Be', 'with you');\n    expect(tested.client.postMessage).toHaveBeenNthCalledWith(1, \"May the\", \"bfett\");\n    expect(tested.client.postMessage).toHaveBeenNthCalledWith(2, \"4th Be\", \"bfett\");\n    expect(tested.client.postMessage).toHaveBeenNthCalledWith(3, \"with you\", \"bfett\");\n  });\n});\n\ndescribe('MatterUser command', () => {\n  test('should command to mattermost user', () => {\n    tested.cmd({room: 'tatooine'}, 'May the', '4th Be', 'with you');\n\n    expect(tested.client.postCommand).toHaveBeenNthCalledWith(1, \"tatooine\", \"May the\");\n    expect(tested.client.postCommand).toHaveBeenNthCalledWith(2, \"tatooine\", \"4th Be\");\n    expect(tested.client.postCommand).toHaveBeenNthCalledWith(3, \"tatooine\", \"with you\");\n  });\n\n  test('should command to mattermost channel with channel id', () => {\n    tested.client.findChannelByName.mockImplementation(room => ({id: 'tatooine_id'}));\n    tested.cmd({room: 'tatooine'}, 'May the', '4th Be', 'with you');\n\n    expect(tested.client.postCommand).toHaveBeenNthCalledWith(1, \"tatooine_id\", \"May the\");\n    expect(tested.client.postCommand).toHaveBeenNthCalledWith(2, \"tatooine_id\", \"4th Be\");\n    expect(tested.client.postCommand).toHaveBeenNthCalledWith(3, \"tatooine_id\", \"with you\");\n  });\n\n  test('should command to mattermost user', () => {\n    tested.cmd({room: 'okenobi'}, 'May the', '4th Be', 'with you');\n    expect(tested.client.postCommand).toHaveBeenNthCalledWith(1, \"66\", \"May the\");\n    expect(tested.client.postCommand).toHaveBeenNthCalledWith(2, \"66\", \"4th Be\");\n    expect(tested.client.postCommand).toHaveBeenNthCalledWith(3, \"66\", \"with you\");\n  });\n\n  test('should command to mattermost user', () => {\n    tested.client.getUserDirectMessageChannel.mockImplementation((user_id, callback) => {\n      callback({id: 'bfett'});\n    });\n\n    tested.cmd({room: 'bfett'}, 'May the', '4th Be', 'with you');\n    expect(tested.client.getUserDirectMessageChannel).toHaveBeenCalled();\n\n    expect(tested.client.postCommand).toHaveBeenNthCalledWith(1, \"bfett\", \"May the\");\n    expect(tested.client.postCommand).toHaveBeenNthCalledWith(2, \"bfett\", \"4th Be\");\n    expect(tested.client.postCommand).toHaveBeenNthCalledWith(3, \"bfett\", \"with you\");\n  });\n});\n"
  },
  {
    "path": "tests/Matteruser_user_actions.test.js",
    "content": "const {HUBOT_SELF_USER} = require(\"./helpers/samples\");\nconst {LeaveMessage, EnterMessage} = require(\"hubot/es2015\");\nconst {use} = require('../src/matteruser.js');\njest.mock('mattermost-client');\n\nconst robot = require('robot');\nconst tested = use(robot);\n\nbeforeEach(() => {\n  jest.resetAllMocks();\n  jest.resetModules() // Most important - it clears the cache\n  tested.self = HUBOT_SELF_USER;\n  tested.client = jest.fn();\n  tested.client.loadChannels = jest.fn();\n  tested.emit = jest.fn();\n});\n\ndescribe('MatterUser userChenge', () => {\n  test('should change user', () => {\n    const actual = tested.userChange({\n      id: 'okenobi',\n      username: 'okenobi',\n      first_name: 'Obiwan',\n      last_name: 'Kenobi',\n      email: 'obiwan.kenobi@matteruser.com',\n    });\n\n    expect(actual).toEqual({\n      id: 'okenobi',\n      name: 'okenobi',\n      real_name: 'Obiwan Kenobi',\n      email_address: 'obiwan.kenobi@matteruser.com',\n      faction: 'jedi',\n      room: 'okenobi',\n      username: 'Obiwan Kenobi',\n      mm: {\n        dm_channel_id: '66',\n        id: 'okenobi',\n        username: 'okenobi',\n        first_name: 'Obiwan',\n        last_name: 'Kenobi',\n        email: 'obiwan.kenobi@matteruser.com'\n      }\n    });\n  });\n\n  test('should change user without user', () => {\n    const actual = tested.userChange({\n      username: 'okenobi',\n      first_name: 'Obiwan',\n      last_name: 'Kenobi',\n      email: 'obiwan.kenobi@matteruser.com',\n    });\n\n    expect(actual).toBeFalsy();\n  });\n});\n\ndescribe('MatterUser userTyping', () => {\n  test('should see user typing', () => {\n    const actual = tested.userTyping({});\n    expect(actual).toBeTruthy();\n  });\n});\n\ndescribe('MatterUser userAdded', () => {\n  test('should add user', () => {\n    tested.client.getUserByID = jest.fn().mockReturnValue(HUBOT_SELF_USER);\n    let spyUserChange = jest.spyOn(tested, 'userChange').mockImplementation(() => true);\n    let spyReceive = jest.spyOn(tested, 'receive');\n    const actual = tested.userAdded({\n      data: {\n        user_id: HUBOT_SELF_USER.id\n      },\n      broadcast: {\n        channel_id: 'jedi'\n      }\n    });\n\n    expect(actual).toBeTruthy();\n    expect(tested.client.loadChannels).toHaveBeenCalled();\n    expect(spyUserChange).toHaveBeenCalledWith(HUBOT_SELF_USER);\n    expect(spyReceive).toHaveBeenCalledWith({\n      room: 'jedi',\n      done: false,\n      user: {\n        id: \"matterbot\",\n        mm: {dm_channel_id: \"66\"},\n        username: \"Hubot\",\n        room: 'jedi',\n      }\n    });\n    expect(spyReceive.mock.calls[0][0]).toBeInstanceOf(EnterMessage)\n  });\n\n  test('should fail to add user', () => {\n    tested.client.getUserByID = jest.fn().mockImplementation(() => new Error());\n    const actual = tested.userAdded({\n      data: {\n        user_id: HUBOT_SELF_USER.id\n      },\n      broadcast: {\n        channel_id: 'jedi'\n      }\n    });\n\n    expect(actual).toBeFalsy();\n  });\n});\n\ndescribe('MatterUser userRemove', () => {\n  test('should remove user', () => {\n    tested.client.getUserByID = jest.fn().mockReturnValue(HUBOT_SELF_USER);\n    let spyReceive = jest.spyOn(tested, 'receive');\n    const actual = tested.userRemoved({\n      data: {\n        user_id: HUBOT_SELF_USER.id\n      },\n      broadcast: {\n        channel_id: 'jedi'\n      }\n    });\n\n    expect(actual).toBeTruthy();\n    expect(tested.client.loadChannels).toHaveBeenCalled();\n    expect(spyReceive).toHaveBeenCalledWith({\n      room: 'jedi',\n      done: false,\n      user: {\n        id: \"matterbot\",\n        mm: {dm_channel_id: \"66\"},\n        username: \"Hubot\",\n        room: 'jedi',\n      }\n    });\n    expect(spyReceive.mock.calls[0][0]).toBeInstanceOf(LeaveMessage)\n  });\n\n  test('should fail to remove user', () => {\n    tested.client.getUserByID = jest.fn().mockImplementation(() => new Error());\n    const actual = tested.userRemoved({\n      data: {\n        user_id: HUBOT_SELF_USER.id\n      },\n      broadcast: {\n        channel_id: 'jedi'\n      }\n    });\n\n    expect(actual).toBeFalsy();\n  });\n});\n"
  },
  {
    "path": "tests/TextMessage.test.js",
    "content": "const {use} = require('../src/matteruser.js');\n\ndescribe('TextMessage', () => {\n  test('should construct attachment message', () => {\n  });\n});\n"
  },
  {
    "path": "tests/__mocks__/robot.js",
    "content": "const {HUBOT_SELF_USER, USER_WITH_CHANNEL, USER_WITHOUT_CHANNEL} = require(\"../helpers/samples\");\n\nconst robot = {\n  name: 'hubot'\n};\nrobot.send = jest.fn();\nrobot.receive = jest.fn();\nrobot.on = jest.fn();\n\nrobot.brain = jest.fn();\nrobot.brain.on = jest.fn();\nrobot.brain.data = {};\nrobot.brain.data.users = {\n  'okenobi': USER_WITH_CHANNEL,\n  'bfett': USER_WITHOUT_CHANNEL,\n  [HUBOT_SELF_USER.id]: HUBOT_SELF_USER,\n};\nrobot.brain.userForId = (userId, newUser) => {\n  if (newUser !== undefined) {\n    robot.brain.data.users[userId] = newUser;\n  }\n  return robot.brain.data.users[userId];\n};\nrobot.brain.userForName = (userName) => {\n  if (userName === undefined) {\n    return null;\n  } else if (userName === 'okenobi') {\n    return robot.brain.data.users[userName];\n  } else if (userName === 'bfett') {\n    return robot.brain.data.users[userName];\n  } else {\n    return null;\n  }\n};\n\nrobot.http = jest.fn();\n// robot.http = jest.fn().mockImplementation(() => ScopedClient.create());\n\nrobot.logger = jest.fn();\nrobot.logger.info = jest.fn();\nrobot.logger.debug = jest.fn();\nrobot.logger.error = jest.fn();\nrobot.logger.emergency = jest.fn();\n\nmodule.exports = robot;\n"
  },
  {
    "path": "tests/helpers/samples.js",
    "content": "/**\n * @type {User}\n */\nexports.HUBOT_SELF_USER = {\n  id: 'matterbot',\n  username: 'Hubot',\n  room: 'bot-channel',\n  mm: {\n    dm_channel_id: '66'\n  },\n}\n\n/**\n * @type {User}\n */\nexports.USER_WITH_CHANNEL = {\n  id: 'okenobi',\n  username: 'Obiwan Kenobi',\n  room: 'okenobi',\n  mm: {\n    dm_channel_id: '66'\n  },\n  faction: 'jedi',\n}\n\n/**\n * @type {User}\n */\nexports.USER_WITHOUT_CHANNEL = {\n  id: 'bfett',\n  username: 'Boba Fett',\n  room: 'bfett',\n}\n"
  },
  {
    "path": "tests/helpers/test-helpers.js",
    "content": "let OLD_ENV = {}\nfunction matterUserBeforeEnv() {\n  OLD_ENV = process.env;\n  process.env = {...OLD_ENV}; // Make a copy\n  process.env.MATTERMOST_HOST = '';\n  process.env.MATTERMOST_USER = 'obiwan';\n  process.env.MATTERMOST_PASSWORD = '';\n  process.env.MATTERMOST_MFA_TOKEN = '';\n  process.env.MATTERMOST_GROUP = '';\n}\n\nfunction matterUserAfterEnv() {\n  process.env = OLD_ENV;\n}\n\nmodule.exports = {\n  matterUserBeforeEnv,\n  matterUserAfterEnv,\n};\n"
  },
  {
    "path": "tests/matteruser.doc.js",
    "content": "/**\n * Mattermost Specific user options\n * @typedef {Object} UserMattermostOptions\n * @property {string} dm_channel_id Direct Message channel ID for user\n */\n/**\n * @typedef {Object} User\n * @property {string} id\n * @property {string} username\n * @property {string} room\n * @property {UserMattermostOptions} [mm]\n * @property {string} [root_id]\n * @property {string} [room_name]\n * @property {string} [room_display_name]\n * @property {string} [channel_type]\n * @property {string} [last_name]\n * @property {string} [first_name]\n * @property {string} [email] - The mail of the user\n */\n/**\n * @typedef {Object} Message\n * @property {string} id\n */\n/**\n * @typedef {Object} Envelop\n * @property {string} room\n * @property {Message} message Initial message for reply\n * @property {User} user\n */\n"
  },
  {
    "path": "tests/matteruser.test.js",
    "content": "const {matterUserAfterEnv, matterUserBeforeEnv} = require(\"./helpers/test-helpers\");\n\nconst {use} = require('../src/matteruser.js');\nconst MatterMostClient = require('mattermost-client');\njest.mock('mattermost-client');\n\nconst robot = require('robot');\nconst tested = use(robot);\n\nbeforeEach(() => {\n  jest.resetAllMocks();\n  jest.resetModules() // Most important - it clears the cache\n  tested.client = jest.fn();\n  tested.emit = jest.fn();\n});\n\ndescribe('Matteruser', () => {\n  const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {\n  });\n  beforeEach(matterUserBeforeEnv);\n  afterEach(matterUserAfterEnv);\n\n  test('should create Matteruser', () => {\n    const actual = use(robot);\n    expect(actual).toBeDefined();\n    expect(mockExit).not.toHaveBeenCalled();\n  });\n\n  test('should I login Matteruser', () => {\n    use(robot).run();\n    expect(robot.brain.on).toBeCalledWith('loaded', expect.anything());\n    expect(mockExit).not.toHaveBeenCalled();\n    expect(MatterMostClient).toHaveBeenCalled();\n    expect(MatterMostClient.prototype.login).toHaveBeenCalled();\n  });\n\n  test('should I login Matteruser with token', () => {\n    process.env.MATTERMOST_ACCESS_TOKEN = 'token';\n    use(robot).run();\n    expect(robot.brain.on).toBeCalledWith('loaded', expect.anything());\n    expect(mockExit).not.toHaveBeenCalled();\n    expect(MatterMostClient).toHaveBeenCalled();\n    expect(MatterMostClient.prototype.tokenLogin).toHaveBeenCalled();\n  });\n\n  test.each([\n    ['MATTERMOST_HOST'],\n    ['MATTERMOST_USER'],\n    ['MATTERMOST_PASSWORD'],\n    ['MATTERMOST_GROUP'],\n  ])('should fail run Matteruser without %s', (envvar) => {\n    delete process.env[envvar];\n\n    use(robot).run();\n    expect(mockExit).toHaveBeenCalledWith(1);\n    expect(robot.logger.emergency)\n      .toHaveBeenNthCalledWith(1, expect.stringContaining(envvar))\n  });\n\n});\n\ndescribe('Matteruser Callbacks', () => {\n\n  test('onOpen', () => {\n    const actual = tested.open();\n    expect(actual).toBeTruthy();\n  });\n\n  test('onError', () => {\n    const actual = tested.error();\n    expect(actual).toBeTruthy();\n    expect(robot.logger.info).toBeCalled();\n  });\n\n  test('onConnected', () => {\n    const actual = tested.onConnected();\n    expect(actual).toBeTruthy();\n    expect(tested.emit).toBeCalledWith('connected');\n  });\n\n  test('onHello', () => {\n    const actual = tested.onHello({data: {server_version: '5'}});\n    expect(actual).toBeTruthy();\n    expect(robot.logger.info).toBeCalled();\n  });\n\n  test('loggedIn', () => {\n    const actual = tested.loggedIn({username: 'Obiwan'});\n    expect(actual).toBeTruthy();\n    expect(robot.logger.info).toBeCalled();\n    expect(tested.self).toEqual({username: 'Obiwan'});\n  });\n\n  test('profilesLoaded', () => {\n    tested.userChange = jest.fn();\n    const actual = tested.profilesLoaded([\n      {username: 'Obiwan'},\n      {username: 'Luke'},\n    ]);\n    expect(actual).toHaveLength(2);\n    expect(tested.userChange).toBeCalledTimes(2);\n  });\n\n  test('brainLoaded', () => {\n    tested.userChange = jest.fn();\n    tested.client = {};\n    tested.client.users = {\n      'okenobi': {id: 'okenobi', username: 'Obiwan'},\n      'lskywalker': {id: 'lskywalker', username: 'Luke'},\n    };\n    const actual = tested.brainLoaded();\n    expect(actual).toBeTruthy();\n    expect(tested.userChange).toBeCalledTimes(2);\n  });\n});\n\ndescribe('Matteruser misc', () => {\n  beforeEach(() => {\n    tested.client = {};\n    tested.client.setChannelHeader = jest.fn().mockReturnValue(true);\n    tested.client.findChannelByName = jest.fn().mockImplementation(channel => ({id: 42, name: channel}));\n  });\n\n  test('should change channel header', () => {\n    const actual = tested.changeHeader('jedi', 'May the force be with you');\n    expect(actual).toBeTruthy();\n    expect(tested.client.findChannelByName).toBeCalled();\n    expect(tested.client.setChannelHeader).toBeCalledWith(42, 'May the force be with you');\n  });\n\n  test('should failed to change channel header', () => {\n    tested.changeHeader('May the force be with you');\n    expect(tested.client.findChannelByName).not.toBeCalled();\n    expect(tested.client.setChannelHeader).not.toBeCalledWith(42, 'May the force be with you');\n  });\n});\n"
  },
  {
    "path": "webpack.config.js",
    "content": "const path = require('path');\n\nmodule.exports = {\n  entry: './src/matteruser.js',\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: 'matteruser.js',\n    library: {\n      name: 'HubotMatteruser',\n      type: 'umd',\n    },\n    globalObject: 'this',\n  },\n  externals: {\n    'mattermost-client': 'commonjs2 mattermost-client',\n    'hubot/es2015': 'commonjs2 hubot/es2015',\n  },\n};\n"
  }
]