Full Code of loafoe/hubot-matteruser for AI

master 47c08715ef2d cached
22 files
58.8 KB
16.2k tokens
28 symbols
1 requests
Download .txt
Repository: loafoe/hubot-matteruser
Branch: master
Commit: 47c08715ef2d
Files: 22
Total size: 58.8 KB

Directory structure:
gitextract_stp90dsr/

├── .editorconfig
├── .eslintrc.js
├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       └── main.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── docker-compose.yml
├── package.json
├── src/
│   └── matteruser.js
├── tests/
│   ├── Matteruser_message.test.js
│   ├── Matteruser_reply.test.js
│   ├── Matteruser_send_cmd.test.js
│   ├── Matteruser_user_actions.test.js
│   ├── TextMessage.test.js
│   ├── __mocks__/
│   │   └── robot.js
│   ├── helpers/
│   │   ├── samples.js
│   │   └── test-helpers.js
│   ├── matteruser.doc.js
│   └── matteruser.test.js
└── webpack.config.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# http://editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false


================================================
FILE: .eslintrc.js
================================================
module.exports = {
    "env": {
        "amd": true,
        "node": true,
        "es6": true
    },
    "extends": "eslint:recommended",
    "parserOptions": {
        "ecmaVersion": 2016,
        "sourceType": "module"
    },
    "rules": {
        "accessor-pairs": "error",
        "array-bracket-newline": "error",
        "array-bracket-spacing": [
            "error",
            "never"
        ],
        "array-callback-return": "error",
        "array-element-newline": "off",
        "arrow-body-style": "error",
        "arrow-parens": [
            "error",
            "as-needed"
        ],
        "arrow-spacing": [
            "error",
            {
                "after": true,
                "before": true
            }
        ],
        "block-scoped-var": "off",
        "block-spacing": [
            "error",
            "always"
        ],
        "brace-style": [
            "error",
            "1tbs",
            {
                "allowSingleLine": true
            }
        ],
        "callback-return": "error",
        "camelcase": "off",
        "capitalized-comments": "off",
        "class-methods-use-this": "off",
        "comma-dangle": "error",
        "comma-spacing": [
            "error",
            {
                "after": true,
                "before": false
            }
        ],
        "comma-style": [
            "error",
            "last"
        ],
        "complexity": "error",
        "computed-property-spacing": [
            "error",
            "never"
        ],
        "consistent-return": "off",
        "consistent-this": "off",
        "curly": "error",
        "default-case": "error",
        "dot-location": "error",
        "dot-notation": "error",
        "eol-last": "error",
        "eqeqeq": "off",
        "func-call-spacing": "error",
        "func-name-matching": "error",
        "func-names": "error",
        "func-style": "error",
        "function-paren-newline": "off",
        "generator-star-spacing": "error",
        "global-require": "error",
        "guard-for-in": "off",
        "handle-callback-err": "error",
        "id-blacklist": "error",
        "id-length": "off",
        "id-match": "error",
        "implicit-arrow-linebreak": [
            "error",
            "beside"
        ],
        "indent": "off",
        "indent-legacy": "off",
        "init-declarations": "off",
        "jsx-quotes": "error",
        "key-spacing": "error",
        "keyword-spacing": [
            "error",
            {
                "after": true,
                "before": true
            }
        ],
        "line-comment-position": "off",
        "linebreak-style": [
            "error",
            "unix"
        ],
        "lines-around-comment": "error",
        "lines-around-directive": "error",
        "lines-between-class-members": [
            "error",
            "always"
        ],
        "max-classes-per-file": "off",
        "max-depth": "error",
        "max-len": "off",
        "max-lines": "off",
        "max-lines-per-function": "error",
        "max-nested-callbacks": "error",
        "max-params": "off",
        "max-statements": "off",
        "max-statements-per-line": "off",
        "multiline-comment-style": [
            "error",
            "separate-lines"
        ],
        "multiline-ternary": [
            "error",
            "always-multiline"
        ],
        "new-cap": "error",
        "new-parens": "error",
        "newline-after-var": "off",
        "newline-before-return": "off",
        "newline-per-chained-call": "error",
        "no-alert": "error",
        "no-array-constructor": "error",
        "no-async-promise-executor": "error",
        "no-await-in-loop": "error",
        "no-bitwise": "error",
        "no-buffer-constructor": "error",
        "no-caller": "error",
        "no-catch-shadow": "error",
        "no-confusing-arrow": "error",
        "no-continue": "error",
        "no-div-regex": "error",
        "no-duplicate-imports": "error",
        "no-else-return": "error",
        "no-empty-function": "error",
        "no-eq-null": "off",
        "no-eval": "error",
        "no-extend-native": "error",
        "no-extra-bind": "error",
        "no-extra-label": "error",
        "no-extra-parens": "off",
        "no-floating-decimal": "error",
        "no-implicit-coercion": "error",
        "no-implicit-globals": "error",
        "no-implied-eval": "error",
        "no-inline-comments": "off",
        "no-inner-declarations": [
            "error",
            "functions"
        ],
        "no-invalid-this": "error",
        "no-iterator": "error",
        "no-label-var": "error",
        "no-labels": "error",
        "no-lone-blocks": "error",
        "no-lonely-if": "error",
        "no-loop-func": "error",
        "no-magic-numbers": "off",
        "no-misleading-character-class": "error",
        "no-mixed-operators": "error",
        "no-mixed-requires": "error",
        "no-multi-assign": "error",
        "no-multi-spaces": "error",
        "no-multi-str": "error",
        "no-multiple-empty-lines": "error",
        "no-native-reassign": "error",
        "no-negated-condition": "off",
        "no-negated-in-lhs": "error",
        "no-nested-ternary": "off",
        "no-new": "error",
        "no-new-func": "error",
        "no-new-object": "error",
        "no-new-require": "error",
        "no-new-wrappers": "error",
        "no-octal-escape": "error",
        "no-param-reassign": "off",
        "no-path-concat": "error",
        "no-plusplus": "error",
        "no-process-env": "off",
        "no-process-exit": "off",
        "no-proto": "error",
        "no-prototype-builtins": "error",
        "no-restricted-globals": "error",
        "no-restricted-imports": "error",
        "no-restricted-modules": "error",
        "no-restricted-properties": "error",
        "no-restricted-syntax": "error",
        "no-return-assign": "error",
        "no-return-await": "error",
        "no-script-url": "error",
        "no-self-compare": "error",
        "no-sequences": "error",
        "no-shadow": "error",
        "no-shadow-restricted-names": "error",
        "no-spaced-func": "error",
        "no-sync": "error",
        "no-tabs": "error",
        "no-template-curly-in-string": "error",
        "no-ternary": "off",
        "no-throw-literal": "error",
        "no-trailing-spaces": "error",
        "no-undef-init": "error",
        "no-undefined": "off",
        "no-underscore-dangle": "error",
        "no-unmodified-loop-condition": "error",
        "no-unneeded-ternary": "error",
        "no-unused-expressions": "error",
        "no-use-before-define": "error",
        "no-useless-call": "error",
        "no-useless-computed-key": "error",
        "no-useless-concat": "error",
        "no-useless-constructor": "error",
        "no-useless-rename": "error",
        "no-useless-return": "error",
        "no-var": "off",
        "no-void": "error",
        "no-warning-comments": "error",
        "no-whitespace-before-property": "error",
        "no-with": "error",
        "nonblock-statement-body-position": "error",
        "object-curly-newline": "error",
        "object-curly-spacing": "off",
        "object-shorthand": "error",
        "one-var": "off",
        "one-var-declaration-per-line": "error",
        "operator-assignment": [
            "error",
            "always"
        ],
        "operator-linebreak": "error",
        "padded-blocks": "off",
        "padding-line-between-statements": "error",
        "prefer-arrow-callback": "error",
        "prefer-const": "off",
        "prefer-destructuring": "off",
        "prefer-numeric-literals": "error",
        "prefer-object-spread": "error",
        "prefer-promise-reject-errors": "error",
        "prefer-reflect": "off",
        "prefer-rest-params": "error",
        "prefer-spread": "error",
        "prefer-template": "error",
        "quote-props": "off",
        "quotes": "off",
        "radix": "error",
        "require-atomic-updates": "error",
        "require-await": "error",
        "require-jsdoc": "error",
        "require-unicode-regexp": "off",
        "rest-spread-spacing": [
            "error",
            "never"
        ],
        "semi": "off",
        "semi-spacing": "error",
        "semi-style": [
            "error",
            "last"
        ],
        "sort-imports": "error",
        "sort-keys": "off",
        "sort-vars": "error",
        "space-before-blocks": "error",
        "space-before-function-paren": "off",
        "space-in-parens": "off",
        "space-infix-ops": "error",
        "space-unary-ops": "error",
        "spaced-comment": [
            "error",
            "always"
        ],
        "strict": "error",
        "switch-colon-spacing": "error",
        "symbol-description": "error",
        "template-curly-spacing": [
            "error",
            "never"
        ],
        "template-tag-spacing": "error",
        "unicode-bom": [
            "error",
            "never"
        ],
        "valid-jsdoc": "error",
        "vars-on-top": "off",
        "wrap-iife": "error",
        "wrap-regex": "error",
        "yield-star-spacing": "error",
        "yoda": [
            "error",
            "never"
        ]
    }
};


================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
  - package-ecosystem: "npm" # See documentation for possible values
    directory: "/"
    schedule:
      interval: "weekly"


================================================
FILE: .github/workflows/main.yml
================================================
name: CI

on: [push, pull_request]

jobs:

  test:
    name: Test on Node ${{ matrix.node }}
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: [ '20' ]
    steps:
      - uses: actions/checkout@v1
      - uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node }}
      - name: install dependencies
        run: npm ci
      - name: Project Tests
        run: npm test

  test_latest:
    name: Test on latest Node
    runs-on: ubuntu-latest
    container: node:current
    steps:
      - uses: actions/checkout@v1
      - name: install dependencies
        run: npm ci
      - name: Project Tests
        run: npm test

  eslint:
    name: Check ESLint
    runs-on: ubuntu-latest
    container: node:current
    steps:
      - uses: actions/checkout@v1
      - name: install dependencies
        run: npm ci
      - name: check lint
        run: npm run lint


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log

# Runtime data
pids
*.pid
*.seed
*.swp
dist

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules

# IntelliJ
**/*.iml


================================================
FILE: Dockerfile
================================================
FROM node:20-alpine

ARG hubot_owner
ARG hubot_description
ARG hubot_name

RUN adduser -D -s /bin/bash hubot-matteruser

RUN mkdir -p /usr/src/hubot-matteruser
RUN chown hubot-matteruser:hubot-matteruser /usr/src/hubot-matteruser
RUN chown hubot-matteruser:hubot-matteruser /usr/local/lib/node_modules/
RUN chown hubot-matteruser:hubot-matteruser /usr/local/bin/

WORKDIR /usr/src/hubot-matteruser
USER hubot-matteruser
RUN npm install -g yo
RUN npm install -g generator-hubot

RUN echo "No" | yo hubot --adapter matteruser --owner="${hubot_owner}" --name="${hubot_name}" --description="${hubot_desciption}" --defaults \
&& sed -i '/heroku/d' external-scripts.json

RUN rm hubot-scripts.json

CMD ["-a", "matteruser"]
ENTRYPOINT ["./bin/hubot"]

EXPOSE 8080


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2016 Andy Lo-A-Foe

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



================================================
FILE: README.md
================================================
[![Downloads](https://img.shields.io/npm/dm/hubot-matteruser.svg)](https://www.npmjs.com/package/hubot-matteruser)
[![Version](https://img.shields.io/npm/v/hubot-matteruser.svg)](https://github.com/loafoe/hubot-matteruser/releases)
[![Licence](https://img.shields.io/npm/l/express.svg)](https://github.com/loafoe/hubot-matteruser/blob/master/LICENSE)

# hubot-matteruser

**Hubot** is "chat bot" created by GitHub that listens for commands and executes actions based on your requests. 

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

- Learn more about [Hubot in Wired Magazine](https://www.wired.com/2015/10/the-most-important-startups-hardest-worker-isnt-a-person/)
- Learn more about [Mattermost as an open source, self-hosted team communication server](https://about.mattermost.com/)

## Description

This [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.

Two authentication methods are supported:

* login/password,
* [personnal access token](https://docs.mattermost.com/developer/personal-access-tokens.html).

The 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)).
Such method is also probably prefered as the token does not reveals original credentials and can be revoked without any impact on the related account.

## Docker usage

### Standalone

Clone this repository, then build the Hubot-Matteruser container:

```
$ docker build --build-arg hubot_owner=<owner> \
             --build-arg hubot_name=<name> \
             --build-arg hubot_description=<desc> \
             --tag=hubot-matteruser \
             .
```

Start the container:

```
$ docker run -it \
           --env MATTERMOST_HOST=<mm_host> \
           --env MATTERMOST_GROUP=<mm_team> \
           --env MATTERMOST_USER=<mm_user_email> \
           --env MATTERMOST_PASSWORD=<mm_user_password> \
           -p 8080:8080 \
           --name hubot-matteruser \
           hubot-matteruser
```

or if you have a personal access token:

```
$ docker run -it \
           --env MATTERMOST_HOST=<mm_host> \
           --env MATTERMOST_GROUP=<mm_team> \
           --env MATTERMOST_ACCESS_TOKEN=<personal>
           -p 8080:8080 \
           --name hubot-matteruser \
           hubot-matteruser
```

### Docker Compose

To integrate with a running Mattermost instance, update docker-compose.yml accordingly and launch the bot:

``` 
docker-compose build
docker-compose run -d
```

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


## Installation

### 1) Install a Mattermost server

Follow the [Mattermost install guides](https://docs.mattermost.com/guides/administrator.html#install-guides) to set up the latest version of Mattermost 5.4.x.

**IMPORTANT:** Make sure your `hubot-matteruser` and `mattermost-client` versions **match** the major version of your Mattermost server so the API versions will match. 

### 2) Install hubot-matteruser

On a separate server, install `hubot-matteruser` using the following commands: 

  ```sh
npm install -g yo generator-hubot
yo hubot --adapter matteruser
  ```

Follow the instructions to set up your bot, including setup of [`mattermost-client`](https://github.com/loafoe/mattermost-client). 

#### Environment variables

The adapter requires the following environment variables to be defined before your Hubot instance will start:

| Variable | Required | Description |
|----------|----------|-------------|
| MATTERMOST\_HOST | Yes | The Mattermost host e.g. _mm.yourcompany.com_ |
| MATTERMOST\_GROUP | Yes | The team/group on your Mattermost server e.g. _core_ |
| MATTERMOST\_USER | No | The Mattermost user account name e.g. _hubot@yourcompany.com_ |
| MATTERMOST\_PASSWORD | No | The password of the user e.g. _s3cr3tP@ssw0rd!_ |
| MATTERMOST\_ACCESS\_TOKEN | No | The [personal access token](https://docs.mattermost.com/developer/personal-access-tokens.html) of the user |
| MATTERMOST\_WSS\_PORT | No | Overrides the default port `443` for  websocket (`wss://`) connections |
| MATTERMOST\_HTTP\_PORT | No | Overrides the default port (`80` or `443`) for `http://` or `https://` connections |
| 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) |
| MATTERMOST\_USE\_TLS | No | (default: true) set to 'false' to switch to http/ws protocols |
| MATTERMOST\_LOG\_LEVEL | No | (default: info) set log level (also: debug, ...) |
| MATTERMOST\_REPLY | No | (default: true) set to 'false' to stop posting `reply` responses as comments |
| MATTERMOST\_IGNORE\_USERS | No | (default: empty) Enter a comma-separated list of user senderi\_names to ignore. |

#### Example configuration

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

  ```sh
export MATTERMOST_HOST=mm.yourcompany.com 
export MATTERMOST_GROUP=core
export MATTERMOST_USER=hubot@yourcompany.com
export MATTERMOST_PASSWORD=s3cr3tP@ssw0rd!
  ```

## Upgrade

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

  ```json
    ...
    "dependencies": {
      "hubot-matteruser": "^5.4.6"
    },
    ...
  ```

## Try the Hubot demo

You can try out Hubot by joining the Mattermost community server and joining the Hubot channel: 

1. [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/
2. Join the "Hubot" channel
3. Type `hubot help` for instructions

### Sample commands

You can try a simple command like `hubot the rules` to bring some static text stored in Hubot: 

![s](https://cloud.githubusercontent.com/assets/177788/20645776/b25da69a-b41c-11e6-81d2-a40d76947e60.png)

Try `hubot animate me` to have Hubot reach out to Giphy and bring back a random animated image.

![s](https://cloud.githubusercontent.com/assets/177788/20645764/88c267a8-b41c-11e6-96c9-529c3ca844f3.png)

Try `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

![s](https://cloud.githubusercontent.com/assets/177788/20645769/9d58a786-b41c-11e6-90b1-6a9e7ab19172.png)


## License

The MIT License. See `LICENSE` file.



================================================
FILE: docker-compose.yml
================================================
version: "2"

services:
  hubot-matteruser:
    build:
     context: .
     args:
       hubot_owner: <CHANGEME>
       hubot_name: <CHANGEME>
       hubot_description: <CHANGEME>
    restart: always
    user: hubot-matteruser
    ports:
      - "8080:8080"
    environment:
      - MATTERMOST_HOST=<CHANGEME>
      - MATTERMOST_GROUP=<CHANGEME>
      - MATTERMOST_USER=<CHANGEME>
      - MATTERMOST_PASSWORD=<CHANGEME>
      - MATTERMOST_LOG_LEVEL=info
      - MATTERMOST_USE_TLS=false
      - MATTERMOST_TLS_VERIFY=false
      - MATTERMOST_WSS_PORT=80


================================================
FILE: package.json
================================================
{
  "name": "hubot-matteruser",
  "version": "5.4.6",
  "author": {
    "name": "Andy Lo-A-Foe",
    "url": "https://github.com/loafoe"
  },
  "contributors": [],
  "description": "Mattermost Adapter",
  "keywords": [
    "hubot",
    "adapter",
    "mattermost",
    "chat"
  ],
  "license": "MIT",
  "module": "./src/matteruser.js",
  "main": "dist/matteruser.js",
  "files": [
    "/dist"
  ],
  "scripts": {
    "test": "jest",
    "lint": "yarn lint:js",
    "lint:fix": "yarn lint:js:fix",
    "lint:js": "eslint 'src/**/*.js'",
    "lint:js:fix": "yarn lint:js --fix",
    "build:dev": "webpack --mode=development",
    "build:prod": "webpack --mode=production",
    "prepublishOnly": "npm run build:prod"
  },
  "dependencies": {
    "json-schema": "^0.4.0",
    "mattermost-client": "^6.5.0"
  },
  "peerDependencies": {
    "hubot": ">=3.0.1"
  },
  "devDependencies": {
    "eslint": "^8.4.0",
    "eslint-config-airbnb-base": "^15.0.0",
    "hubot": "^3.3.2",
    "jest": "^29.1.2",
    "webpack": "^5.35.1",
    "webpack-cli": "^4.6.0",
    "yarn": "^1.22.10"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/loafoe/hubot-matteruser.git"
  },
  "bugs": {
    "url": "https://github.com/loafoe/hubot-matteruser/issues",
    "email": "andy.loafoe@gmail.com"
  }
}


================================================
FILE: src/matteruser.js
================================================
const {
  Adapter,
  TextMessage,
  EnterMessage,
  LeaveMessage
} = require('hubot/es2015');

const MatterMostClient = require('mattermost-client');

class AttachmentMessage extends TextMessage {

  constructor(user, text, file_ids, id) {
    super(user, text, id);
  }
}

// A TextMessage class that adds `msg.props` for Mattermost's properties.
//
// Text fields from message attachments are appended in @text for matching.
// <https://docs.mattermost.com/developer/message-attachments.html>
// The result is that `bot.hear()` will match against these attachment fields.
//
// As well, it is possible that some bot handlers could make use of other
// fields on `msg.props`.
//
// Example raw props:
//   {
//       "attachments": [...],
//       "from_webhook": "true",
//       "override_username": "trenthere"
//   }
class TextAndPropsMessage extends TextMessage {

  constructor(user, text, props, id) {
    super(user, text, id);
    this.props = props;
    this.origText = this.text;
    if (this.props.attachments) {
      const separator = '\n\n--\n\n';
      for (let attachment of this.props.attachments) {
        const parts = [];
        for (let field of ['pretext', 'title', 'text']) {
          if (attachment[field]) {
            parts.push(attachment[field]);
          }
        }
        if (parts.length) {
          this.text += separator + parts.join('\n\n');
        }
      }
    }
  }

  match(regex) {
    return this.text.match(regex);
  }
}

class Matteruser extends Adapter {

  constructor(...args) {
    super(...args);

    // Binding because async calls galore
    this.open = this.open.bind(this);
    this.error = this.error.bind(this);
    this.onConnected = this.onConnected.bind(this);
    this.onHello = this.onHello.bind(this);
    this.userChange = this.userChange.bind(this);
    this.loggedIn = this.loggedIn.bind(this);
    this.profilesLoaded = this.profilesLoaded.bind(this);
    this.brainLoaded = this.brainLoaded.bind(this);
    this.message = this.message.bind(this);
    this.userTyping = this.userTyping.bind(this);
    this.userAdded = this.userAdded.bind(this);
    this.userRemoved = this.userRemoved.bind(this);
  }

  run() {
    const mmHost = process.env.MATTERMOST_HOST;
    const mmUser = process.env.MATTERMOST_USER || null;
    const mmPassword = process.env.MATTERMOST_PASSWORD;
    const mmMFAToken = process.env.MATTERMOST_MFA_TOKEN || null;
    const mmGroup = process.env.MATTERMOST_GROUP;
    const mmWSSPort = process.env.MATTERMOST_WSS_PORT || '443';
    const mmHTTPPort = process.env.MATTERMOST_HTTP_PORT || null;
    const mmAccessToken = process.env.MATTERMOST_ACCESS_TOKEN || null;
    const mmHTTPProxy = process.env.http_proxy || null;
    this.mmNoReply = process.env.MATTERMOST_REPLY === 'false';
    this.mmIgnoreUsers = (process.env.MATTERMOST_IGNORE_USERS != null
      ? process.env.MATTERMOST_IGNORE_USERS.split(',')
      : undefined) || [];

    if (mmHost == null) {
      this.robot.logger.emergency("MATTERMOST_HOST is required");
      process.exit(1);
    }
    if (mmUser == null && mmAccessToken == null) {
      this.robot.logger.emergency("MATTERMOST_USER or MATTERMOST_ACCESS_TOKEN is required");
      process.exit(1);
    }
    if (mmPassword == null && mmAccessToken == null) {
      this.robot.logger.emergency("MATTERMOST_PASSWORD is required");
      process.exit(1);
    }
    if (mmGroup == null) {
      this.robot.logger.emergency("MATTERMOST_GROUP is required");
      process.exit(1);
    }

    this.client = new MatterMostClient(mmHost, mmGroup, {
      wssPort: mmWSSPort, httpPort: mmHTTPPort, pingInterval: 30000, httpProxy: mmHTTPProxy,
      logger: this.robot.logger
    });

    this.declareCallbacks();
    if (mmAccessToken != null) {
      return this.client.tokenLogin(mmAccessToken);
    }

    return this.client.login(mmUser, mmPassword, mmMFAToken);
  }

  declareCallbacks() {
    this.client.on('open', this.open);
    this.client.on('hello', this.onHello);
    this.client.on('loggedIn', this.loggedIn);
    this.client.on('connected', this.onConnected);
    this.client.on('message', this.message);
    this.client.on('profilesLoaded', this.profilesLoaded);
    this.client.on('user_added', this.userAdded);
    this.client.on('user_removed', this.userRemoved);
    this.client.on('typing', this.userTyping);
    this.client.on('error', this.error);

    this.robot.brain.on('loaded', this.brainLoaded);
  }

  open() {
    return true;
  }

  error(err) {
    this.robot.logger.info('Error: %j', err);
    return true;
  }

  onConnected() {
    this.robot.logger.info('Connected to Mattermost.');
    this.emit('connected');
    return true;
  }

  onHello(event) {
    this.robot.logger.info('Mattermost server: %s', event.data.server_version);
    return true;
  }

  /**
   *
   * @param {User} user The user to be change
   * @returns {User} The updated User
   */
  userChange(user) {
    if (!user || (user.id == null)) {
      return;
    }
    this.robot.logger.debug('Adding user %s', user.id);
    const newUser = {
      name: user.username,
      real_name: `${user.first_name} ${user.last_name}`,
      email_address: user.email,
      mm: {}
    };

    // Preserve the DM channel ID if it exists
    let user_obj = this.robot.brain.userForId(user.id);
    newUser.mm.dm_channel_id = undefined;
    if ("mm" in user_obj) {
      newUser.mm.dm_channel_id = user_obj.mm.dm_channel_id
    }

    let value;
    for (var key in user) {
      value = user[key];
      newUser.mm[key] = value;
    }
    if (user.id in this.robot.brain.data.users) {
      for (key in this.robot.brain.data.users[user.id]) {
        value = this.robot.brain.data.users[user.id][key];
        if (!(key in newUser)) {
          newUser[key] = value;
        }
      }
    }
    delete this.robot.brain.data.users[user.id];
    return this.robot.brain.userForId(user.id, newUser);
  }

  /**
   *
   * @param {User} user The user to logged in
   * @returns {boolean} True if the user is now logged in
   */
  loggedIn(user) {
    this.robot.logger.info('Logged in as user "%s" but not connected yet.', user.username);
    this.self = user;
    return true;
  }

  /**
   *
   * @param {User[]} profiles The users profile loaded
   * @returns {[]} The changed users
   */
  profilesLoaded(profiles) {
    return (() => {
      const result = [];
      for (let id in profiles) {
        const user = profiles[id];
        result.push(this.userChange(user));
      }
      return result;
    })();
  }

  brainLoaded() {
    this.robot.logger.info('Brain loaded');
    for (let id in this.client.users) {
      const user = this.client.users[id];
      this.userChange(user);
    }
    return true;
  }

  /**
   *
   * @param {Envelop} envelope containing the room to send strings
   * @param {string} strings  The messages to send
   * @return {undefined}
   */
  send(envelope, ...strings) {
    // Check if the target room is also a user's username
    let str;
    const user = this.robot.brain.userForName(envelope.room);

    // If it's not, continue as normal
    if (!user) {
      const channel = this.client.findChannelByName(envelope.room);
      const channel_id = channel ? channel.id : undefined;

      for (str of strings) {
        this.client.postMessage(str,
          (channel_id || envelope.room));
      }
    } else {
      // If it is, we assume they want to DM that user
      // Message their DM channel ID if it already exists.
      let dm_channel_id = user.mm
        ? (user.mm.dm_channel_id ? user.mm.dm_channel_id : undefined)
        : undefined

      if (dm_channel_id != null) {
        for (str of strings) {
          this.client.postMessage(str, user.mm.dm_channel_id);
        }

      } else {

        let self = this

        // Otherwise, create a new DM channel ID and message it.
        this.client.getUserDirectMessageChannel(user.id, channel => {
          if (!user.mm) {
            user.mm = {};
          }
          user.mm.dm_channel_id = channel.id;

          for (str of strings) {
            self.client.postMessage(str, channel.id);
          }
        });
      }
    }
  }

  /**
   *
   * @param {Envelop} envelope The Message envelop
   * @param {string} strings The message lines to reply
   * @returns {void}
   */
  cmd(envelope, ...strings) {
    // Check if the target room is also a user's username
    let str;
    const user = this.robot.brain.userForName(envelope.room);

    // If it's not, continue as normal
    if (!user) {
      const channel = this.client.findChannelByName(envelope.room);
      const channel_id = channel ? channel.id : undefined;

      for (str of strings) {
        this.client.postCommand((channel_id || envelope.room),
          str);
      }
    } else {
      // If it is, we assume they want to DM that user
      // Message their DM channel ID if it already exists.
      let dm_channel_id = user.mm
        ? (user.mm.dm_channel_id ? user.mm.dm_channel_id : undefined)
        : undefined

      if (dm_channel_id != null) {
        for (str of strings) {
          this.client.postCommand(user.mm.dm_channel_id, str);
        }

      } else {

        let self = this

        // Otherwise, create a new DM channel ID and message it.
        this.client.getUserDirectMessageChannel(user.id, channel => {
          if (!user.mm) {
            user.mm = {};
          }
          user.mm.dm_channel_id = channel.id;

          for (str of strings) {
            self.client.postCommand(channel.id, str);
          }
        });
      }
    }
  }

  /**
   * Reply to a message
   * @param {Envelop} envelope The Message envelop
   * @param {string} strings The message lines to reply
   * @returns {void}
   */
  reply(envelope, ...strings) {
    if (this.mmNoReply) {
      return this.send(envelope, ...strings);
    }

    const postData = {};
    postData.message = strings[0];

    // Set the comment relationship
    postData.root_id = envelope.user.root_id || envelope.message.id;
    postData.parent_id = envelope.message.id;

    postData.create_at = 0;
    postData.user_id = this.self.id;
    postData.filename = [];
    // Check if the target room is also a user's username
    const user = this.robot.brain.userForName(envelope.room);

    // If it's not, continue as normal
    if (!user) {
      const channel = this.client.findChannelByName(envelope.room);
      postData.channel_id = (channel ? channel.id : undefined) || envelope.room;
      this.client.customMessage(postData, postData.channel_id);
      return;
    }

    // If it is, we assume they want to DM that user
    // Message their DM channel ID if it already exists.
    if ((user.mm ? user.mm.dm_channel_id : undefined) != null) {
      postData.channel_id = user.mm.dm_channel_id;
      this.client.customMessage(postData, postData.channel_id);
      return;
    }

    // Otherwise, create a new DM channel ID and message it.
    return this.client.getUserDirectMessageChannel(user.id, channel => {
      if (!user.mm) {
        user.mm = {};
      }
      user.mm.dm_channel_id = channel.id;
      postData.channel_id = channel.id;
      return this.client.customMessage(postData, postData.channel_id);
    });
  }

  message(msg) {
    if (this.mmIgnoreUsers.includes(msg.data.sender_name)) {
      this.robot.logger.info('User %s is in MATTERMOST_IGNORE_USERS, ignoring them.', msg.data.sender_name);
      return;
    }

    this.robot.logger.debug(msg);
    const mmPost = JSON.parse(msg.data.post);
    if (mmPost.user_id === this.self.id) {
      return;
    } // Ignore our own output
    this.robot.logger.debug('From: %s, To: %s', mmPost.user_id, this.self.id);

    const user = this.robot.brain.userForId(mmPost.user_id);
    user.room = mmPost.channel_id;
    user.room_name = msg.data.channel_name;
    user.room_display_name = msg.data.channel_display_name;
    user.channel_type = msg.data.channel_type;
    user.root_id = mmPost.root_id;

    let text = mmPost.message;
    if (msg.data.channel_type === 'D') {
      if (!new RegExp(`^@?${this.robot.name}`, 'i').test(text)) {
        text = `${this.robot.name} ${text}`;
      }
      if (!user.mm) {
        user.mm = {};
      }
      user.mm.dm_channel_id = mmPost.channel_id;
    }
    this.robot.logger.debug('Text: %s', text);

    if (mmPost.file_ids) {
      this.receive(new AttachmentMessage(user, text, mmPost.file_ids, mmPost.id));
      // If there are interesting props, then include them for bot handlers.
    } else if (mmPost.props ? mmPost.props.attachments : undefined) {
      this.receive(new TextAndPropsMessage(user, text, mmPost.props, mmPost.id));
    } else {
      this.receive(new TextMessage(user, text, mmPost.id));
    }
    this.robot.logger.debug("Message sent to hubot brain.");
    return true;
  }

  userTyping(msg) {
    this.robot.logger.info('Someone is typing -> %j', msg);
    return true;
  }

  userAdded(msg) {
    // update channels when this bot is added to a new channel
    if (msg.data.user_id === this.self.id) {
      this.client.loadChannels();
    }
    try {
      const mmUser = this.client.getUserByID(msg.data.user_id);
      this.userChange(mmUser);
      const user = this.robot.brain.userForId(mmUser.id);
      user.room = msg.broadcast.channel_id;
      this.receive(new EnterMessage(user));
      return true;
    } catch (error) {
      return false;
    }
  }

  userRemoved(msg) {
    // update channels when this bot is removed from a channel
    if (msg.data.user_id === this.self.id) {
      this.client.loadChannels();
    }
    try {
      const mmUser = this.client.getUserByID(msg.data.user_id);
      const user = this.robot.brain.userForId(mmUser.id);
      user.room = msg.broadcast.channel_id;
      this.receive(new LeaveMessage(user));
      return true;
    } catch (error) {
      return false;
    }
  }

  changeHeader(channel, header) {
    if (channel == null || header == null) {
      return;
    }

    const channelInfo = this.client.findChannelByName(channel);

    if (channelInfo == null) {
      return this.robot.logger.error('Channel not found');
    }

    return this.client.setChannelHeader(channelInfo.id, header);
  }
}

module.exports.use = robot => new Matteruser(robot)


================================================
FILE: tests/Matteruser_message.test.js
================================================
const {TextMessage} = require("hubot/es2015");
const {matterUserAfterEnv, matterUserBeforeEnv} = require("./helpers/test-helpers");
const {HUBOT_SELF_USER} = require("./helpers/samples");

const {use} = require('../src/matteruser.js');
jest.mock('mattermost-client');

const robot = require('robot');
const tested = use(robot);


beforeAll(matterUserBeforeEnv);
afterAll(matterUserAfterEnv);

beforeEach(() => {
  jest.resetAllMocks();
  jest.resetModules() // Most important - it clears the cache

  tested.run();
  tested.emit = jest.fn();
  tested.self = HUBOT_SELF_USER
});

describe('MatterUser message', () => {
  test('should receive from self user', () => {
    tested.message({
      data: {
        sender_name: 'dsidious',
        post: JSON.stringify({
          user_id: HUBOT_SELF_USER.id,
        }),
      }
    });
    expect(robot.receive).not.toHaveBeenCalled();
  });

  test('should receive from ignored user', () => {
    tested.mmIgnoreUsers = ['dsidious'];
    tested.message({
      data: {
        sender_name: 'dsidious',
        post: JSON.stringify({
          user_id: 'okenobi',
          root_id: '42',
          message: 'May the force',
        }),
        channel_name: 'jedi',
        channel_display_name: 'Jedi Room',
        channel_type: 'D'
      }
    });
    expect(robot.receive).not.toHaveBeenCalled();
  });

  test('should receive direct message without files', () => {
    tested.message({
      data: {
        sender_name: 'bfett',
        post: JSON.stringify({
          user_id: 'bfett',
          root_id: '42',
          message: 'May the force'
        }),
        channel_name: 'jedi',
        channel_display_name: 'Jedi Room',
        channel_type: 'D'
      }
    });

    expect(robot.receive).toHaveBeenCalledWith({
      done: false,
      text: 'hubot May the force',
      user: {
        channel_type: 'D',
        id: 'bfett',
        mm: {},
        room_display_name: 'Jedi Room',
        room_name: 'jedi',
        root_id: '42',
        username: 'Boba Fett'
      }
    });
  });

  test('should receive direct message with files', () => {
    tested.message({
      data: {
        sender_name: 'Obiwan Kenobi',
        post: JSON.stringify({
          user_id: 'okenobi',
          root_id: '42',
          message: 'May the force',
          file_ids: [1, 2, 3]
        }),
        channel_name: 'jedi',
        channel_display_name: 'Jedi Room',
        channel_type: 'D'
      }
    });

    expect(robot.receive).toHaveBeenCalled();
    let actual = robot.receive.mock.calls[0][0];
    expect(actual).toBeInstanceOf(TextMessage);
    expect(actual).toEqual({
      done: false,
      text: 'hubot May the force',
      user: {
        channel_type: 'D',
        faction: 'jedi',
        id: 'okenobi',
        mm: {},
        room_display_name: 'Jedi Room',
        room_name: 'jedi',
        root_id: '42',
        username: 'Obiwan Kenobi'
      }
    });
  });

  test('should receive direct message with attachments props', () => {
    tested.message({
      data: {
        sender_name: 'Obiwan Kenobi',
        post: JSON.stringify({
          user_id: 'okenobi',
          root_id: '42',
          message: 'May the force',
          props: {
            attachments: [
              {pretext: 'Yoda Says', title: 'Jedi Code', text: 'Emotion, yet peace'},
              {pretext: 'Yoda Says', title: 'Ignorance, yet knowledge', text: 'Emotion, yet peace'},
            ]
          }
        }),
        channel_name: 'jedi',
        channel_display_name: 'Jedi Room',
        channel_type: 'D'
      }
    });

    expect(robot.receive).toHaveBeenCalled();
    let actual = robot.receive.mock.calls[0][0];
    expect(actual).toBeInstanceOf(TextMessage);
    expect(actual).toEqual({
      done: false,
      origText: 'hubot May the force',
      props: {
        attachments: [
          {
            pretext: 'Yoda Says',
            text: 'Emotion, yet peace',
            title: 'Jedi Code'
          },
          {
            pretext: 'Yoda Says',
            text: 'Emotion, yet peace',
            title: 'Ignorance, yet knowledge'
          }
        ]
      },
      text: [
        'hubot May the force',
        '',
        '--',
        '',
        'Yoda Says',
        '',
        'Jedi Code',
        '',
        'Emotion, yet peace',
        '',
        '--',
        '',
        'Yoda Says',
        '',
        'Ignorance, yet knowledge',
        '',
        'Emotion, yet peace'
      ].join('\n'),
      user: {
        channel_type: 'D',
        faction: 'jedi',
        id: 'okenobi',
        mm: {
          dm_channel_id: undefined
        },
        room: undefined,
        room_display_name: 'Jedi Room',
        room_name: 'jedi',
        root_id: '42',
        username: 'Obiwan Kenobi'
      }
    });
  });
});


================================================
FILE: tests/Matteruser_reply.test.js
================================================
const {matterUserAfterEnv, matterUserBeforeEnv} = require("./helpers/test-helpers");
const {HUBOT_SELF_USER, USER_WITH_CHANNEL, USER_WITHOUT_CHANNEL} = require("./helpers/samples");

const {use} = require('../src/matteruser.js');
jest.mock('mattermost-client');

const robot = require('robot');
const tested = use(robot);


beforeAll(matterUserBeforeEnv);
afterAll(matterUserAfterEnv);

beforeEach(() => {
  jest.resetAllMocks();
  jest.resetModules() // Most important - it clears the cache

  tested.run();
  tested.emit = jest.fn();
  tested.mmNoReply = false;
  tested.self = HUBOT_SELF_USER
});

describe('MatterUser reply', () => {
  test('should reply with no reply', () => {
    tested.mmNoReply = true;
    const spy = jest.spyOn(tested, 'send');
    const envelope = {
      room: 'jedi',
      user: USER_WITH_CHANNEL,
      message: {id: '42'}
    };
    tested.reply(envelope, "May the force", "Be with you");

    expect(spy).toHaveBeenCalledWith(envelope, "May the force", "Be with you");
  });

  test('should reply on existing direct message channel', () => {
    tested.reply({
      room: 'okenobi',
      user: USER_WITH_CHANNEL,
      message: {id: '42'}
    }, "May the force", "Be with you");

    expect(tested.client.findChannelByName).not.toHaveBeenCalled();
    expect(tested.client.customMessage).toHaveBeenNthCalledWith(1, {
      channel_id: '66',
      create_at: 0,
      filename: [],
      message: 'May the force',
      parent_id: '42',
      root_id: '42',
      user_id: 'matterbot',
    }, '66');
  });

  test('should reply on public channel', () => {
    tested.reply({
      room: 'jedi',
      user: USER_WITH_CHANNEL,
      message: {id: '42'}
    }, "May the force", "Be with you");

    expect(tested.client.findChannelByName).toHaveBeenCalledWith('jedi');
    expect(tested.client.customMessage).toHaveBeenNthCalledWith(1, {
      channel_id: 'jedi',
      create_at: 0,
      filename: [],
      message: 'May the force',
      parent_id: '42',
      root_id: '42',
      user_id: 'matterbot',
    }, 'jedi');
  });

  test('should reply on new direct message channel', () => {
    tested.client.getUserDirectMessageChannel.mockImplementation((user_id, callback) => {
      callback({id: 'bfett'});
    });

    tested.reply({
      room: 'bfett',
      user: USER_WITHOUT_CHANNEL,
      message: {id: '42'}
    }, "May the force", "Be with you");

    expect(tested.client.findChannelByName).not.toHaveBeenCalled();
    expect(tested.client.getUserDirectMessageChannel).toHaveBeenNthCalledWith(1, 'bfett', expect.anything());
    expect(tested.client.customMessage).toHaveBeenNthCalledWith(1, {
      channel_id: 'bfett',
      create_at: 0,
      filename: [],
      message: 'May the force',
      parent_id: '42',
      root_id: '42',
      user_id: 'matterbot',
    }, 'bfett');
  });
});


================================================
FILE: tests/Matteruser_send_cmd.test.js
================================================
const {matterUserAfterEnv, matterUserBeforeEnv} = require("./helpers/test-helpers");

const {use} = require('../src/matteruser.js');
jest.mock('mattermost-client');

let robot, tested;

beforeAll(matterUserBeforeEnv);
afterAll(matterUserAfterEnv);

beforeEach(() => {
  jest.resetAllMocks();
  jest.resetModules() // Most important - it clears the cache

  robot = require('robot');
  tested = use(robot);
  tested.run();
  tested.emit = jest.fn();
});

describe('MatterUser send', () => {
  test('should send envelop to mattermost user', () => {
    tested.send({room: 'tatooine'}, 'May the', '4th Be', 'with you');

    expect(tested.client.postMessage).toHaveBeenNthCalledWith(1, "May the", "tatooine");
    expect(tested.client.postMessage).toHaveBeenNthCalledWith(2, "4th Be", "tatooine");
    expect(tested.client.postMessage).toHaveBeenNthCalledWith(3, "with you", "tatooine");
  });

  test('should send envelop to mattermost channel with channel id', () => {
    tested.client.findChannelByName.mockImplementation(room => ({id: 'tatooine_id'}));
    tested.send({room: 'tatooine'}, 'May the', '4th Be', 'with you');

    expect(tested.client.postMessage).toHaveBeenNthCalledWith(1, "May the", "tatooine_id");
    expect(tested.client.postMessage).toHaveBeenNthCalledWith(2, "4th Be", "tatooine_id");
    expect(tested.client.postMessage).toHaveBeenNthCalledWith(3, "with you", "tatooine_id");
  });

  test('should send envelop to mattermost user', () => {
    tested.send({room: 'okenobi'}, 'May the', '4th Be', 'with you');
    expect(tested.client.postMessage).toHaveBeenNthCalledWith(1, "May the", "66");
    expect(tested.client.postMessage).toHaveBeenNthCalledWith(2, "4th Be", "66");
    expect(tested.client.postMessage).toHaveBeenNthCalledWith(3, "with you", "66");
  });

  test('should send envelop to mattermost user', () => {
    tested.client.getUserDirectMessageChannel.mockImplementation((user_id, callback) => {
      callback({id: 'bfett'});
    });

    tested.send({room: 'bfett'}, 'May the', '4th Be', 'with you');
    expect(tested.client.postMessage).toHaveBeenNthCalledWith(1, "May the", "bfett");
    expect(tested.client.postMessage).toHaveBeenNthCalledWith(2, "4th Be", "bfett");
    expect(tested.client.postMessage).toHaveBeenNthCalledWith(3, "with you", "bfett");
  });
});

describe('MatterUser command', () => {
  test('should command to mattermost user', () => {
    tested.cmd({room: 'tatooine'}, 'May the', '4th Be', 'with you');

    expect(tested.client.postCommand).toHaveBeenNthCalledWith(1, "tatooine", "May the");
    expect(tested.client.postCommand).toHaveBeenNthCalledWith(2, "tatooine", "4th Be");
    expect(tested.client.postCommand).toHaveBeenNthCalledWith(3, "tatooine", "with you");
  });

  test('should command to mattermost channel with channel id', () => {
    tested.client.findChannelByName.mockImplementation(room => ({id: 'tatooine_id'}));
    tested.cmd({room: 'tatooine'}, 'May the', '4th Be', 'with you');

    expect(tested.client.postCommand).toHaveBeenNthCalledWith(1, "tatooine_id", "May the");
    expect(tested.client.postCommand).toHaveBeenNthCalledWith(2, "tatooine_id", "4th Be");
    expect(tested.client.postCommand).toHaveBeenNthCalledWith(3, "tatooine_id", "with you");
  });

  test('should command to mattermost user', () => {
    tested.cmd({room: 'okenobi'}, 'May the', '4th Be', 'with you');
    expect(tested.client.postCommand).toHaveBeenNthCalledWith(1, "66", "May the");
    expect(tested.client.postCommand).toHaveBeenNthCalledWith(2, "66", "4th Be");
    expect(tested.client.postCommand).toHaveBeenNthCalledWith(3, "66", "with you");
  });

  test('should command to mattermost user', () => {
    tested.client.getUserDirectMessageChannel.mockImplementation((user_id, callback) => {
      callback({id: 'bfett'});
    });

    tested.cmd({room: 'bfett'}, 'May the', '4th Be', 'with you');
    expect(tested.client.getUserDirectMessageChannel).toHaveBeenCalled();

    expect(tested.client.postCommand).toHaveBeenNthCalledWith(1, "bfett", "May the");
    expect(tested.client.postCommand).toHaveBeenNthCalledWith(2, "bfett", "4th Be");
    expect(tested.client.postCommand).toHaveBeenNthCalledWith(3, "bfett", "with you");
  });
});


================================================
FILE: tests/Matteruser_user_actions.test.js
================================================
const {HUBOT_SELF_USER} = require("./helpers/samples");
const {LeaveMessage, EnterMessage} = require("hubot/es2015");
const {use} = require('../src/matteruser.js');
jest.mock('mattermost-client');

const robot = require('robot');
const tested = use(robot);

beforeEach(() => {
  jest.resetAllMocks();
  jest.resetModules() // Most important - it clears the cache
  tested.self = HUBOT_SELF_USER;
  tested.client = jest.fn();
  tested.client.loadChannels = jest.fn();
  tested.emit = jest.fn();
});

describe('MatterUser userChenge', () => {
  test('should change user', () => {
    const actual = tested.userChange({
      id: 'okenobi',
      username: 'okenobi',
      first_name: 'Obiwan',
      last_name: 'Kenobi',
      email: 'obiwan.kenobi@matteruser.com',
    });

    expect(actual).toEqual({
      id: 'okenobi',
      name: 'okenobi',
      real_name: 'Obiwan Kenobi',
      email_address: 'obiwan.kenobi@matteruser.com',
      faction: 'jedi',
      room: 'okenobi',
      username: 'Obiwan Kenobi',
      mm: {
        dm_channel_id: '66',
        id: 'okenobi',
        username: 'okenobi',
        first_name: 'Obiwan',
        last_name: 'Kenobi',
        email: 'obiwan.kenobi@matteruser.com'
      }
    });
  });

  test('should change user without user', () => {
    const actual = tested.userChange({
      username: 'okenobi',
      first_name: 'Obiwan',
      last_name: 'Kenobi',
      email: 'obiwan.kenobi@matteruser.com',
    });

    expect(actual).toBeFalsy();
  });
});

describe('MatterUser userTyping', () => {
  test('should see user typing', () => {
    const actual = tested.userTyping({});
    expect(actual).toBeTruthy();
  });
});

describe('MatterUser userAdded', () => {
  test('should add user', () => {
    tested.client.getUserByID = jest.fn().mockReturnValue(HUBOT_SELF_USER);
    let spyUserChange = jest.spyOn(tested, 'userChange').mockImplementation(() => true);
    let spyReceive = jest.spyOn(tested, 'receive');
    const actual = tested.userAdded({
      data: {
        user_id: HUBOT_SELF_USER.id
      },
      broadcast: {
        channel_id: 'jedi'
      }
    });

    expect(actual).toBeTruthy();
    expect(tested.client.loadChannels).toHaveBeenCalled();
    expect(spyUserChange).toHaveBeenCalledWith(HUBOT_SELF_USER);
    expect(spyReceive).toHaveBeenCalledWith({
      room: 'jedi',
      done: false,
      user: {
        id: "matterbot",
        mm: {dm_channel_id: "66"},
        username: "Hubot",
        room: 'jedi',
      }
    });
    expect(spyReceive.mock.calls[0][0]).toBeInstanceOf(EnterMessage)
  });

  test('should fail to add user', () => {
    tested.client.getUserByID = jest.fn().mockImplementation(() => new Error());
    const actual = tested.userAdded({
      data: {
        user_id: HUBOT_SELF_USER.id
      },
      broadcast: {
        channel_id: 'jedi'
      }
    });

    expect(actual).toBeFalsy();
  });
});

describe('MatterUser userRemove', () => {
  test('should remove user', () => {
    tested.client.getUserByID = jest.fn().mockReturnValue(HUBOT_SELF_USER);
    let spyReceive = jest.spyOn(tested, 'receive');
    const actual = tested.userRemoved({
      data: {
        user_id: HUBOT_SELF_USER.id
      },
      broadcast: {
        channel_id: 'jedi'
      }
    });

    expect(actual).toBeTruthy();
    expect(tested.client.loadChannels).toHaveBeenCalled();
    expect(spyReceive).toHaveBeenCalledWith({
      room: 'jedi',
      done: false,
      user: {
        id: "matterbot",
        mm: {dm_channel_id: "66"},
        username: "Hubot",
        room: 'jedi',
      }
    });
    expect(spyReceive.mock.calls[0][0]).toBeInstanceOf(LeaveMessage)
  });

  test('should fail to remove user', () => {
    tested.client.getUserByID = jest.fn().mockImplementation(() => new Error());
    const actual = tested.userRemoved({
      data: {
        user_id: HUBOT_SELF_USER.id
      },
      broadcast: {
        channel_id: 'jedi'
      }
    });

    expect(actual).toBeFalsy();
  });
});


================================================
FILE: tests/TextMessage.test.js
================================================
const {use} = require('../src/matteruser.js');

describe('TextMessage', () => {
  test('should construct attachment message', () => {
  });
});


================================================
FILE: tests/__mocks__/robot.js
================================================
const {HUBOT_SELF_USER, USER_WITH_CHANNEL, USER_WITHOUT_CHANNEL} = require("../helpers/samples");

const robot = {
  name: 'hubot'
};
robot.send = jest.fn();
robot.receive = jest.fn();
robot.on = jest.fn();

robot.brain = jest.fn();
robot.brain.on = jest.fn();
robot.brain.data = {};
robot.brain.data.users = {
  'okenobi': USER_WITH_CHANNEL,
  'bfett': USER_WITHOUT_CHANNEL,
  [HUBOT_SELF_USER.id]: HUBOT_SELF_USER,
};
robot.brain.userForId = (userId, newUser) => {
  if (newUser !== undefined) {
    robot.brain.data.users[userId] = newUser;
  }
  return robot.brain.data.users[userId];
};
robot.brain.userForName = (userName) => {
  if (userName === undefined) {
    return null;
  } else if (userName === 'okenobi') {
    return robot.brain.data.users[userName];
  } else if (userName === 'bfett') {
    return robot.brain.data.users[userName];
  } else {
    return null;
  }
};

robot.http = jest.fn();
// robot.http = jest.fn().mockImplementation(() => ScopedClient.create());

robot.logger = jest.fn();
robot.logger.info = jest.fn();
robot.logger.debug = jest.fn();
robot.logger.error = jest.fn();
robot.logger.emergency = jest.fn();

module.exports = robot;


================================================
FILE: tests/helpers/samples.js
================================================
/**
 * @type {User}
 */
exports.HUBOT_SELF_USER = {
  id: 'matterbot',
  username: 'Hubot',
  room: 'bot-channel',
  mm: {
    dm_channel_id: '66'
  },
}

/**
 * @type {User}
 */
exports.USER_WITH_CHANNEL = {
  id: 'okenobi',
  username: 'Obiwan Kenobi',
  room: 'okenobi',
  mm: {
    dm_channel_id: '66'
  },
  faction: 'jedi',
}

/**
 * @type {User}
 */
exports.USER_WITHOUT_CHANNEL = {
  id: 'bfett',
  username: 'Boba Fett',
  room: 'bfett',
}


================================================
FILE: tests/helpers/test-helpers.js
================================================
let OLD_ENV = {}
function matterUserBeforeEnv() {
  OLD_ENV = process.env;
  process.env = {...OLD_ENV}; // Make a copy
  process.env.MATTERMOST_HOST = '';
  process.env.MATTERMOST_USER = 'obiwan';
  process.env.MATTERMOST_PASSWORD = '';
  process.env.MATTERMOST_MFA_TOKEN = '';
  process.env.MATTERMOST_GROUP = '';
}

function matterUserAfterEnv() {
  process.env = OLD_ENV;
}

module.exports = {
  matterUserBeforeEnv,
  matterUserAfterEnv,
};


================================================
FILE: tests/matteruser.doc.js
================================================
/**
 * Mattermost Specific user options
 * @typedef {Object} UserMattermostOptions
 * @property {string} dm_channel_id Direct Message channel ID for user
 */
/**
 * @typedef {Object} User
 * @property {string} id
 * @property {string} username
 * @property {string} room
 * @property {UserMattermostOptions} [mm]
 * @property {string} [root_id]
 * @property {string} [room_name]
 * @property {string} [room_display_name]
 * @property {string} [channel_type]
 * @property {string} [last_name]
 * @property {string} [first_name]
 * @property {string} [email] - The mail of the user
 */
/**
 * @typedef {Object} Message
 * @property {string} id
 */
/**
 * @typedef {Object} Envelop
 * @property {string} room
 * @property {Message} message Initial message for reply
 * @property {User} user
 */


================================================
FILE: tests/matteruser.test.js
================================================
const {matterUserAfterEnv, matterUserBeforeEnv} = require("./helpers/test-helpers");

const {use} = require('../src/matteruser.js');
const MatterMostClient = require('mattermost-client');
jest.mock('mattermost-client');

const robot = require('robot');
const tested = use(robot);

beforeEach(() => {
  jest.resetAllMocks();
  jest.resetModules() // Most important - it clears the cache
  tested.client = jest.fn();
  tested.emit = jest.fn();
});

describe('Matteruser', () => {
  const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
  });
  beforeEach(matterUserBeforeEnv);
  afterEach(matterUserAfterEnv);

  test('should create Matteruser', () => {
    const actual = use(robot);
    expect(actual).toBeDefined();
    expect(mockExit).not.toHaveBeenCalled();
  });

  test('should I login Matteruser', () => {
    use(robot).run();
    expect(robot.brain.on).toBeCalledWith('loaded', expect.anything());
    expect(mockExit).not.toHaveBeenCalled();
    expect(MatterMostClient).toHaveBeenCalled();
    expect(MatterMostClient.prototype.login).toHaveBeenCalled();
  });

  test('should I login Matteruser with token', () => {
    process.env.MATTERMOST_ACCESS_TOKEN = 'token';
    use(robot).run();
    expect(robot.brain.on).toBeCalledWith('loaded', expect.anything());
    expect(mockExit).not.toHaveBeenCalled();
    expect(MatterMostClient).toHaveBeenCalled();
    expect(MatterMostClient.prototype.tokenLogin).toHaveBeenCalled();
  });

  test.each([
    ['MATTERMOST_HOST'],
    ['MATTERMOST_USER'],
    ['MATTERMOST_PASSWORD'],
    ['MATTERMOST_GROUP'],
  ])('should fail run Matteruser without %s', (envvar) => {
    delete process.env[envvar];

    use(robot).run();
    expect(mockExit).toHaveBeenCalledWith(1);
    expect(robot.logger.emergency)
      .toHaveBeenNthCalledWith(1, expect.stringContaining(envvar))
  });

});

describe('Matteruser Callbacks', () => {

  test('onOpen', () => {
    const actual = tested.open();
    expect(actual).toBeTruthy();
  });

  test('onError', () => {
    const actual = tested.error();
    expect(actual).toBeTruthy();
    expect(robot.logger.info).toBeCalled();
  });

  test('onConnected', () => {
    const actual = tested.onConnected();
    expect(actual).toBeTruthy();
    expect(tested.emit).toBeCalledWith('connected');
  });

  test('onHello', () => {
    const actual = tested.onHello({data: {server_version: '5'}});
    expect(actual).toBeTruthy();
    expect(robot.logger.info).toBeCalled();
  });

  test('loggedIn', () => {
    const actual = tested.loggedIn({username: 'Obiwan'});
    expect(actual).toBeTruthy();
    expect(robot.logger.info).toBeCalled();
    expect(tested.self).toEqual({username: 'Obiwan'});
  });

  test('profilesLoaded', () => {
    tested.userChange = jest.fn();
    const actual = tested.profilesLoaded([
      {username: 'Obiwan'},
      {username: 'Luke'},
    ]);
    expect(actual).toHaveLength(2);
    expect(tested.userChange).toBeCalledTimes(2);
  });

  test('brainLoaded', () => {
    tested.userChange = jest.fn();
    tested.client = {};
    tested.client.users = {
      'okenobi': {id: 'okenobi', username: 'Obiwan'},
      'lskywalker': {id: 'lskywalker', username: 'Luke'},
    };
    const actual = tested.brainLoaded();
    expect(actual).toBeTruthy();
    expect(tested.userChange).toBeCalledTimes(2);
  });
});

describe('Matteruser misc', () => {
  beforeEach(() => {
    tested.client = {};
    tested.client.setChannelHeader = jest.fn().mockReturnValue(true);
    tested.client.findChannelByName = jest.fn().mockImplementation(channel => ({id: 42, name: channel}));
  });

  test('should change channel header', () => {
    const actual = tested.changeHeader('jedi', 'May the force be with you');
    expect(actual).toBeTruthy();
    expect(tested.client.findChannelByName).toBeCalled();
    expect(tested.client.setChannelHeader).toBeCalledWith(42, 'May the force be with you');
  });

  test('should failed to change channel header', () => {
    tested.changeHeader('May the force be with you');
    expect(tested.client.findChannelByName).not.toBeCalled();
    expect(tested.client.setChannelHeader).not.toBeCalledWith(42, 'May the force be with you');
  });
});


================================================
FILE: webpack.config.js
================================================
const path = require('path');

module.exports = {
  entry: './src/matteruser.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'matteruser.js',
    library: {
      name: 'HubotMatteruser',
      type: 'umd',
    },
    globalObject: 'this',
  },
  externals: {
    'mattermost-client': 'commonjs2 mattermost-client',
    'hubot/es2015': 'commonjs2 hubot/es2015',
  },
};
Download .txt
gitextract_stp90dsr/

├── .editorconfig
├── .eslintrc.js
├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       └── main.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── docker-compose.yml
├── package.json
├── src/
│   └── matteruser.js
├── tests/
│   ├── Matteruser_message.test.js
│   ├── Matteruser_reply.test.js
│   ├── Matteruser_send_cmd.test.js
│   ├── Matteruser_user_actions.test.js
│   ├── TextMessage.test.js
│   ├── __mocks__/
│   │   └── robot.js
│   ├── helpers/
│   │   ├── samples.js
│   │   └── test-helpers.js
│   ├── matteruser.doc.js
│   └── matteruser.test.js
└── webpack.config.js
Download .txt
SYMBOL INDEX (28 symbols across 2 files)

FILE: src/matteruser.js
  class AttachmentMessage (line 10) | class AttachmentMessage extends TextMessage {
    method constructor (line 12) | constructor(user, text, file_ids, id) {
  class TextAndPropsMessage (line 32) | class TextAndPropsMessage extends TextMessage {
    method constructor (line 34) | constructor(user, text, props, id) {
    method match (line 54) | match(regex) {
  class Matteruser (line 59) | class Matteruser extends Adapter {
    method constructor (line 61) | constructor(...args) {
    method run (line 79) | run() {
    method declareCallbacks (line 124) | declareCallbacks() {
    method open (line 139) | open() {
    method error (line 143) | error(err) {
    method onConnected (line 148) | onConnected() {
    method onHello (line 154) | onHello(event) {
    method userChange (line 164) | userChange(user) {
    method loggedIn (line 205) | loggedIn(user) {
    method profilesLoaded (line 216) | profilesLoaded(profiles) {
    method brainLoaded (line 227) | brainLoaded() {
    method send (line 242) | send(envelope, ...strings) {
    method cmd (line 293) | cmd(envelope, ...strings) {
    method reply (line 344) | reply(envelope, ...strings) {
    method message (line 389) | message(msg) {
    method userTyping (line 433) | userTyping(msg) {
    method userAdded (line 438) | userAdded(msg) {
    method userRemoved (line 455) | userRemoved(msg) {
    method changeHeader (line 471) | changeHeader(channel, header) {

FILE: tests/helpers/test-helpers.js
  constant OLD_ENV (line 1) | let OLD_ENV = {}
  function matterUserBeforeEnv (line 2) | function matterUserBeforeEnv() {
  function matterUserAfterEnv (line 12) | function matterUserAfterEnv() {
Condensed preview — 22 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (65K chars).
[
  {
    "path": ".editorconfig",
    "chars": 197,
    "preview": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace"
  },
  {
    "path": ".eslintrc.js",
    "chars": 9321,
    "preview": "module.exports = {\n    \"env\": {\n        \"amd\": true,\n        \"node\": true,\n        \"es6\": true\n    },\n    \"extends\": \"es"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 470,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/workflows/main.yml",
    "chars": 905,
    "preview": "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"
  },
  {
    "path": ".gitignore",
    "chars": 558,
    "preview": "# Logs\nlogs\n*.log\n\n# Runtime data\npids\n*.pid\n*.seed\n*.swp\ndist\n\n# Directory for instrumented libs generated by jscoverag"
  },
  {
    "path": "Dockerfile",
    "chars": 758,
    "preview": "FROM node:20-alpine\n\nARG hubot_owner\nARG hubot_description\nARG hubot_name\n\nRUN adduser -D -s /bin/bash hubot-matteruser\n"
  },
  {
    "path": "LICENSE",
    "chars": 1081,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Andy Lo-A-Foe\n\nPermission is hereby granted, free of charge, to any person obt"
  },
  {
    "path": "README.md",
    "chars": 7369,
    "preview": "[![Downloads](https://img.shields.io/npm/dm/hubot-matteruser.svg)](https://www.npmjs.com/package/hubot-matteruser)\n[![Ve"
  },
  {
    "path": "docker-compose.yml",
    "chars": 554,
    "preview": "version: \"2\"\n\nservices:\n  hubot-matteruser:\n    build:\n     context: .\n     args:\n       hubot_owner: <CHANGEME>\n       "
  },
  {
    "path": "package.json",
    "chars": 1299,
    "preview": "{\n  \"name\": \"hubot-matteruser\",\n  \"version\": \"5.4.6\",\n  \"author\": {\n    \"name\": \"Andy Lo-A-Foe\",\n    \"url\": \"https://git"
  },
  {
    "path": "src/matteruser.js",
    "chars": 14259,
    "preview": "const {\n  Adapter,\n  TextMessage,\n  EnterMessage,\n  LeaveMessage\n} = require('hubot/es2015');\n\nconst MatterMostClient = "
  },
  {
    "path": "tests/Matteruser_message.test.js",
    "chars": 4828,
    "preview": "const {TextMessage} = require(\"hubot/es2015\");\nconst {matterUserAfterEnv, matterUserBeforeEnv} = require(\"./helpers/test"
  },
  {
    "path": "tests/Matteruser_reply.test.js",
    "chars": 2842,
    "preview": "const {matterUserAfterEnv, matterUserBeforeEnv} = require(\"./helpers/test-helpers\");\nconst {HUBOT_SELF_USER, USER_WITH_C"
  },
  {
    "path": "tests/Matteruser_send_cmd.test.js",
    "chars": 4227,
    "preview": "const {matterUserAfterEnv, matterUserBeforeEnv} = require(\"./helpers/test-helpers\");\n\nconst {use} = require('../src/matt"
  },
  {
    "path": "tests/Matteruser_user_actions.test.js",
    "chars": 3997,
    "preview": "const {HUBOT_SELF_USER} = require(\"./helpers/samples\");\nconst {LeaveMessage, EnterMessage} = require(\"hubot/es2015\");\nco"
  },
  {
    "path": "tests/TextMessage.test.js",
    "chars": 144,
    "preview": "const {use} = require('../src/matteruser.js');\n\ndescribe('TextMessage', () => {\n  test('should construct attachment mess"
  },
  {
    "path": "tests/__mocks__/robot.js",
    "chars": 1167,
    "preview": "const {HUBOT_SELF_USER, USER_WITH_CHANNEL, USER_WITHOUT_CHANNEL} = require(\"../helpers/samples\");\n\nconst robot = {\n  nam"
  },
  {
    "path": "tests/helpers/samples.js",
    "chars": 449,
    "preview": "/**\n * @type {User}\n */\nexports.HUBOT_SELF_USER = {\n  id: 'matterbot',\n  username: 'Hubot',\n  room: 'bot-channel',\n  mm:"
  },
  {
    "path": "tests/helpers/test-helpers.js",
    "chars": 446,
    "preview": "let OLD_ENV = {}\nfunction matterUserBeforeEnv() {\n  OLD_ENV = process.env;\n  process.env = {...OLD_ENV}; // Make a copy\n"
  },
  {
    "path": "tests/matteruser.doc.js",
    "chars": 792,
    "preview": "/**\n * Mattermost Specific user options\n * @typedef {Object} UserMattermostOptions\n * @property {string} dm_channel_id D"
  },
  {
    "path": "tests/matteruser.test.js",
    "chars": 4200,
    "preview": "const {matterUserAfterEnv, matterUserBeforeEnv} = require(\"./helpers/test-helpers\");\n\nconst {use} = require('../src/matt"
  },
  {
    "path": "webpack.config.js",
    "chars": 396,
    "preview": "const path = require('path');\n\nmodule.exports = {\n  entry: './src/matteruser.js',\n  output: {\n    path: path.resolve(__d"
  }
]

About this extraction

This page contains the full source code of the loafoe/hubot-matteruser GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 22 files (58.8 KB), approximately 16.2k tokens, and a symbol index with 28 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!