diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 00000000..acdc612e --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,213 @@ +# For full documentation of the configuration options please +# see: https://github.com/golangci/golangci-lint#config-file. + +# options for analysis running +run: + # default concurrency is the available CPU number + # concurrency: 4 + + # timeout for analysis, e.g. 30s, 5m, default is 1m + deadline: 1m + + # exit code when at least one issue was found, default is 1 + issues-exit-code: 1 + + # include test files or not, default is true + tests: true + + # list of build tags, all linters use it. Default is empty list. + build-tags: + + # which dirs to skip: they won't be analyzed; + # can use regexp here: generated.*, regexp is applied on full path; + # default value is empty list, but next dirs are always skipped independently + # from this option's value: + # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ + skip-dirs: + + # which files to skip: they will be analyzed, but issues from them + # won't be reported. Default value is empty list, but there is + # no need to include all autogenerated files, we confidently recognize + # autogenerated files. If it's not please let us know. + skip-files: + + +# output configuration options +output: + # colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number" + format: colored-line-number + + # print lines of code with issue, default is true + print-issued-lines: true + + # print linter name in the end of issue text, default is true + print-linter-name: true + + +# all available settings of specific linters, we can set an option for +# a given linter even if we deactivate that same linter at runtime +linters-settings: + errcheck: + # report about not checking of errors in type assertions: `a := b.(MyStruct)`; + # default is false: such cases aren't reported by default. + check-type-assertions: false + + # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; + # default is false: such cases aren't reported by default. + check-blank: false + govet: + # report about shadowed variables + check-shadowing: true + golint: + # minimal confidence for issues, default is 0.8 + min-confidence: 0.8 + gofmt: + # simplify code: gofmt with `-s` option, true by default + simplify: true + goimports: + # put imports beginning with prefix after 3rd-party packages; + # it's a comma-separated list of prefixes + local-prefixes: github.com + gocyclo: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 15 + maligned: + # print struct with more effective memory layout or not, false by default + suggest-new: true + dupl: + # tokens count to trigger issue, 150 by default + threshold: 150 + goconst: + # minimal length of string constant, 3 by default + min-len: 3 + # minimal occurrences count to trigger, 3 by default + min-occurrences: 3 + depguard: + list-type: blacklist + include-go-root: false + packages: + # List of packages that we would want to blacklist for... reasons. + misspell: + # Correct spellings using locale preferences for US or UK. + # Default is to use a neutral variety of English. + # Setting locale to US will correct the British spelling of 'colour' to 'color'. + locale: US + lll: + # max line length, lines longer will be reported. Default is 120. + # '\t' is counted as 1 character by default, and can be changed with the tab-width option + line-length: 150 + # tab width in spaces. Default to 1. + tab-width: 1 + unused: + # treat code as a program (not a library) and report unused exported identifiers; default is false. + # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: + # if it's called for subdir of a project it can't find funcs usages. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + unparam: + # call graph construction algorithm (cha, rta). In general, use cha for libraries, + # and rta for programs with main packages. Default is cha. + algo: rta + + # Inspect exported functions, default is false. Set to true if no external program/library imports your code. + # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: + # if it's called for subdir of a project it can't find external interfaces. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + nakedret: + # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 + max-func-lines: 0 # Warn on all naked returns. + prealloc: + # XXX: we don't recommend using this linter before doing performance profiling. + # For most programs usage of prealloc will be a premature optimization. + + # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. + # True by default. + simple: true + range-loops: true # Report preallocation suggestions on range loops, true by default + for-loops: false # Report preallocation suggestions on for loops, false by default + gocritic: + # which checks should be enabled; can't be combined with 'disabled-checks'; + # default are: [appendAssign assignOp caseOrder dupArg dupBranchBody dupCase flagDeref + # ifElseChain regexpMust singleCaseSwitch sloppyLen switchTrue typeSwitchVar underef + # unlambda unslice rangeValCopy defaultCaseOrder]; + # all checks list: https://github.com/go-critic/checkers + enabled-checks: + - appendAssign + - assignOp + - boolExprSimplify + - builtinShadow + - captLocal + - caseOrder + - commentedOutImport + - defaultCaseOrder + - dupArg + - dupBranchBody + - dupCase + - dupSubExpr + - elseif + - emptyFallthrough + - hugeParam + - ifElseChain + - importShadow + - indexAlloc + - methodExprCall + - nestingReduce + - offBy1 + - ptrToRefParam + - regexpMust + - singleCaseSwitch + - sloppyLen + - sloppyReassign + - switchTrue + - typeSwitchVar + - typeUnparen + - underef + - unlambda + - unnecessaryBlock + - unslice + - valSwap + - wrapperFunc + - yodaStyleExpr + + +# linters that we should / shouldn't run +linters: + enable-all: true + disable: + - gochecknoglobals + - lll + - maligned + - prealloc + + +# rules to deal with reported isues +issues: + # List of regexps of issue texts to exclude, empty list by default. + # But independently from this option we use default exclude patterns, + # it can be disabled by `exclude-use-default: false`. To list all + # excluded by default patterns execute `golangci-lint run --help` + exclude: + + # Independently from option `exclude` we use default exclude patterns, + # it can be disabled by this option. To list all + # excluded by default patterns execute `golangci-lint run --help`. + # Default value for this option is true. + exclude-use-default: true + + # Maximum issues count per one linter. Set to 0 to disable. Default is 50. + max-per-linter: 0 + + # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. + max-same-issues: 0 + + # Show only new issues: if there are unstaged changes or untracked files, + # only those changes are analyzed, else only changes in HEAD~ are analyzed. + # It's a super-useful option for integration of golangci-lint into existing + # large codebase. It's not practical to fix all existing issues at the moment + # of integration: much better don't allow issues in new code. + # Default is false. + new: false + + # Show only new issues created after git revision `REV` + new-from-rev: "HEAD~1" diff --git a/.travis.yml b/.travis.yml index 2ed49274..4dd02ee5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,18 @@ language: go go: - #- 1.7.x - - 1.10.x - # - tip + - 1.11.x +go_import_path: github.com/42wim/matterbridge # we have everything vendored install: true +git: + depth: 200 + env: + global: - GOOS=linux GOARCH=amd64 - # - GOOS=windows GOARCH=amd64 - #- GOOS=linux GOARCH=arm + - GOLANGCI_VERSION="v1.12.3" matrix: # It's ok if our code fails on unstable development versions of Go. @@ -24,22 +26,26 @@ notifications: email: false before_script: - - MY_VERSION=$(git describe --tags) - - GO_FILES=$(find . -iname '*.go' | grep -v /vendor/) # All the .go files, excluding vendor/ - - PKGS=$(go list ./... | grep -v /vendor/) # All the import paths, excluding vendor/ -# - go get github.com/golang/lint/golint # Linter - - go get honnef.co/go/tools/cmd/megacheck # Badass static analyzer/linter + # Get version info from tags. + - MY_VERSION="$(git describe --tags)" + # Retrieve the golangci-lint linter binary. + - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b ${GOPATH}/bin ${GOLANGCI_VERSION} + # Retrieve and prepare CodeClimate's test coverage reporter. + - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + - chmod +x ./cc-test-reporter + - ./cc-test-reporter before-build -# Anything in before_script: that returns a nonzero exit code will -# flunk the build and immediately stop. It's sorta like having -# set -e enabled in bash. script: - #- test -z $(gofmt -s -l $GO_FILES) # Fail if a .go file hasn't been formatted with gofmt - - go test -v -race $PKGS # Run all the tests with the race detector enabled - # - go vet $PKGS # go vet is the official Go static analyzer - - megacheck $PKGS # "go vet on steroids" + linter + # Run the linter. + - golangci-lint run + # Run all the tests with the race detector and generate coverage. + - go test -v -race -coverprofile c.out ./... + # Run the build script to generate the necessary binaries and images. - /bin/bash ci/bintray.sh - #- golint -set_exit_status $PKGS # one last linter + +after_script: + # Upload test coverage to CodeClimate. + - ./cc-test-reporter after-build --exit-code ${TRAVIS_TEST_RESULT} deploy: provider: bintray @@ -48,4 +54,4 @@ deploy: file: ci/deploy.json user: 42wim key: - secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI=" + secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI=" \ No newline at end of file diff --git a/README.md b/README.md index 68cca9c2..7d3d304e 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,40 @@ +
+ # matterbridge -Click on one of the badges below to join the chat -[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?colorB=42f4242)](https://gitter.im/42wim/matterbridge) [![Join the IRC chat at https://webchat.freenode.net/?channels=matterbridgechat](https://img.shields.io/badge/IRC-matterbridgechat-green.svg?colorB=42f4242)](https://webchat.freenode.net/?channels=matterbridgechat) [![Discord](https://img.shields.io/badge/discord-matterbridge-green.svg?colorB=42f4242)](https://discord.gg/AkKPtrQ) [![Matrix](https://img.shields.io/badge/matrix-matterbridge-green.svg?colorB=42f4242)](https://riot.im/app/#/room/#matterbridge:matrix.org) [![Slack](https://img.shields.io/badge/slack-matterbridgechat-green.svg?colorB=42f4242)](https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA) [![Mattermost](https://img.shields.io/badge/mattermost-matterbridge-green.svg?colorB=42f4242)](https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e) [![Xmpp](https://img.shields.io/badge/xmpp-matterbridge@conference.jabber.de-green.svg?colorB=42f4242)](https://inverse.chat) [![Twitch](https://img.shields.io/badge/twitch-matterbridge-green.svg?colorB=42f4242)](https://www.twitch.tv/matterbridge) [![Zulip](https://img.shields.io/badge/zulip-matterbridge-green.svg?colorB=42f4242)](https://matterbridge.zulipchat.com/register/) +![Matterbridge Logo](img/matterbridge-notext.gif)
+ **A simple chat bridge**
+ Letting people be where they want to be.
+ Bridges between a growing number of protocols. Click below to demo. -[![Download stable](https://img.shields.io/github/release/42wim/matterbridge.svg?label=download%20stable)](https://github.com/42wim/matterbridge/releases/latest) [![Download dev](https://img.shields.io/bintray/v/42wim/nightly/Matterbridge.svg?label=download%20dev&colorB=007ec6)](https://bintray.com/42wim/nightly/Matterbridge/_latestVersion) + -![matterbridge.gif](https://github.com/42wim/matterbridge/blob/master/img/matterbridge.gif) + [Gitter][mb-gitter] | + [IRC][mb-irc] | + [Discord][mb-discord] | + [Matrix][mb-matrix] | + [Slack][mb-slack] | + [Mattermost][mb-mattermost] | + [XMPP][mb-xmpp] | + [Twitch][mb-twitch] | + [Zulip][mb-zulip] | + And more... + -Simple bridge between IRC, XMPP, Gitter, Mattermost, Slack, Discord, Telegram, Rocket.Chat, Hipchat(via xmpp), Matrix, Steam, ssh-chat and Zulip -Has a REST API. -Minecraft server chat support via [MatterLink](https://github.com/elytra/MatterLink) +---- +[![Download stable](https://img.shields.io/github/release/42wim/matterbridge.svg?label=download%20stable)](https://github.com/42wim/matterbridge/releases/latest) + [![Download dev](https://img.shields.io/bintray/v/42wim/nightly/Matterbridge.svg?label=download%20dev&colorB=007ec6)](https://bintray.com/42wim/nightly/Matterbridge/_latestVersion) + [![Maintainability](https://api.codeclimate.com/v1/badges/82dff70ef2ba85a6173a/maintainability)](https://codeclimate.com/github/42wim/matterbridge/maintainability) + [![Test Coverage](https://api.codeclimate.com/v1/badges/82dff70ef2ba85a6173a/test_coverage)](https://codeclimate.com/github/42wim/matterbridge/test_coverage)
+
+
+
-**Mattermost isn't required to run matterbridge. It bridges between any supported protocol.** -(The name matterbridge is a remnant when it was only bridging mattermost) +**Note:** Mattermost isn't required to run matterbridge.
-# Table of Contents +### Table of Contents * [Features](https://github.com/42wim/matterbridge/wiki/Features) + * [API](#API) * [Requirements](#requirements) * [Screenshots](https://github.com/42wim/matterbridge/wiki/) * [Installing](#installing) @@ -23,23 +42,26 @@ Minecraft server chat support via [MatterLink](https://github.com/elytra/MatterL * [Building](#building) * [Configuration](#configuration) * [Howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) - * [Examples](#examples) + * [Examples](#examples) * [Running](#running) * [Docker](#docker) * [Changelog](#changelog) * [FAQ](#faq) + * [Related projects](#related-projects) + * [Articles](#articles) * [Thanks](#thanks) -# Features +## Features * [Support bridging between any protocols](https://github.com/42wim/matterbridge/wiki/Features#support-bridging-between-any-protocols) * [Support multiple gateways(bridges) for your protocols](https://github.com/42wim/matterbridge/wiki/Features#support-multiple-gatewaysbridges-for-your-protocols) * [Message edits and deletes](https://github.com/42wim/matterbridge/wiki/Features#message-edits-and-deletes) +* Preserves threading when possible * [Attachment / files handling](https://github.com/42wim/matterbridge/wiki/Features#attachment--files-handling) * [Username and avatar spoofing](https://github.com/42wim/matterbridge/wiki/Features#username-and-avatar-spoofing) * [Private groups](https://github.com/42wim/matterbridge/wiki/Features#private-groups) * [API](https://github.com/42wim/matterbridge/wiki/Features#api) -## API +### API The API is very basic at the moment and rather undocumented. Used by at least 3 projects. Feel free to make a PR to add your project to this list. @@ -48,9 +70,9 @@ Used by at least 3 projects. Feel free to make a PR to add your project to this * [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot) * [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support) -# Requirements +## Requirements Accounts to one of the supported bridges -* [Mattermost](https://github.com/mattermost/platform/) 3.8.x - 3.10.x, 4.x, 5.x +* [Mattermost](https://github.com/mattermost/mattermost-server/) 4.x, 5.x * [IRC](http://www.mirc.com/servers.html) * [XMPP](https://jabber.org) * [Gitter](https://gitter.im) @@ -65,18 +87,21 @@ Accounts to one of the supported bridges * [Ssh-chat](https://github.com/shazow/ssh-chat) * [Zulip](https://zulipchat.com) -# Screenshots +## Screenshots See https://github.com/42wim/matterbridge/wiki -# Installing -## Binaries -* Latest stable release [v1.11.3](https://github.com/42wim/matterbridge/releases/latest) -* Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/) +## Installing +### Binaries +* Latest stable release [v1.12.2](https://github.com/42wim/matterbridge/releases/latest) +* Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/) -## Building +### Packages +* [Overview](https://repology.org/metapackage/matterbridge/versions) + +### Building Go 1.8+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH](https://golang.org/doc/code.html#GOPATH). -After Go is setup, download matterbridge to your $GOPATH directory. +After Go is setup, download matterbridge to your $GOPATH directory. ``` cd $GOPATH @@ -90,16 +115,16 @@ $ ls bin/ matterbridge ``` -# Configuration -## Basic configuration +## Configuration +### Basic configuration See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration. -## Advanced configuration +### Advanced configuration * [matterbridge.toml.sample](https://github.com/42wim/matterbridge/blob/master/matterbridge.toml.sample) for documentation and an example. -## Examples -### Bridge mattermost (off-topic) - irc (#testing) -``` +### Examples +#### Bridge mattermost (off-topic) - irc (#testing) +```toml [irc] [irc.freenode] Server="irc.freenode.net:6667" @@ -126,8 +151,8 @@ enable=true channel="off-topic" ``` -### Bridge slack (#general) - discord (general) -``` +#### Bridge slack (#general) - discord (general) +```toml [slack] [slack.test] Token="yourslacktoken" @@ -154,7 +179,7 @@ RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> " channel = "general" ``` -# Running +## Running See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration. @@ -170,24 +195,43 @@ Usage of ./matterbridge: show version ``` -## Docker -Create your matterbridge.toml file locally eg in ```/tmp/matterbridge.toml``` +### Docker +Create your matterbridge.toml file locally eg in `/tmp/matterbridge.toml` ``` docker run -ti -v /tmp/matterbridge.toml:/matterbridge.toml 42wim/matterbridge ``` -# Changelog +## Changelog See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.md) -# FAQ +## FAQ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ) -Want to tip ? +Want to tip ? * eth: 0xb3f9b5387c66ad6be892bcb7bbc67862f3abc16f * btc: 1N7cKHj5SfqBHBzDJ6kad4BzeqUBBS2zhs -# Thanks +## Related projects +* [FOSSRIT/infrastructure - roles/matterbridge](https://github.com/FOSSRIT/infrastructure/tree/master/roles/matterbridge) (Ansible role used to automate deployments of Matterbridge) +* [matterbridge autoconfig](https://github.com/patcon/matterbridge-autoconfig) +* [matterbridge config viewer](https://github.com/patcon/matterbridge-heroku-viewer) +* [matterbridge-heroku](https://github.com/cadecairos/matterbridge-heroku) +* [mattereddit](https://github.com/bonehurtingjuice/mattereddit) +* [matterlink](https://github.com/elytra/MatterLink) +* [mattermost-plugin](https://github.com/matterbridge/mattermost-plugin) - Run matterbridge as a plugin in mattermost +* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot) + +## Articles +* https://mattermost.com/blog/connect-irc-to-mattermost/ +* https://blog.valvin.fr/2016/09/17/mattermost-et-un-channel-irc-cest-possible/ +* https://blog.brightscout.com/top-10-mattermost-integrations/ +* http://bencey.co.nz/2018/09/17/bridge/ +* https://www.algoo.fr/blog/2018/01/19/recouvrez-votre-liberte-en-quittant-slack-pour-un-mattermost-auto-heberge/ +* https://kopano.com/blog/matterbridge-bridging-mattermost-chat/ +* https://www.stitcher.com/s/?eid=52382713 + +## Thanks [![Digitalocean](https://snag.gy/3LVifX.jpg)](https://www.digitalocean.com/) for sponsoring demo/testing droplets. Matterbridge wouldn't exist without these libraries: @@ -197,10 +241,22 @@ Matterbridge wouldn't exist without these libraries: * gops - https://github.com/google/gops * gozulipbot - https://github.com/ifo/gozulipbot * irc - https://github.com/lrstanley/girc -* mattermost - https://github.com/mattermost/platform +* mattermost - https://github.com/mattermost/mattermost-server * matrix - https://github.com/matrix-org/gomatrix * slack - https://github.com/nlopes/slack * steam - https://github.com/Philipp15b/go-steam * telegram - https://github.com/go-telegram-bot-api/telegram-bot-api * xmpp - https://github.com/mattn/go-xmpp * zulip - https://github.com/ifo/gozulipbot + + + + [mb-gitter]: https://gitter.im/42wim/matterbridge + [mb-irc]: https://webchat.freenode.net/?channels=matterbridgechat + [mb-discord]: https://discord.gg/AkKPtrQ + [mb-matrix]: https://riot.im/app/#/room/#matterbridge:matrix.org + [mb-slack]: https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA + [mb-mattermost]: https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e + [mb-xmpp]: https://inverse.chat/ + [mb-twitch]: https://www.twitch.tv/matterbridge + [mb-zulip]: https://matterbridge.zulipchat.com/register/ diff --git a/bridge/api/api.go b/bridge/api/api.go index 28c57106..f8a7c04c 100644 --- a/bridge/api/api.go +++ b/bridge/api/api.go @@ -13,13 +13,13 @@ import ( "github.com/zfjagann/golang-ring" ) -type Api struct { +type API struct { Messages ring.Ring sync.RWMutex *bridge.Config } -type ApiMessage struct { +type Message struct { Text string `json:"text"` Username string `json:"username"` UserID string `json:"userid"` @@ -28,17 +28,20 @@ type ApiMessage struct { } func New(cfg *bridge.Config) bridge.Bridger { - b := &Api{Config: cfg} + b := &API{Config: cfg} e := echo.New() e.HideBanner = true e.HidePort = true b.Messages = ring.Ring{} - b.Messages.SetCapacity(b.GetInt("Buffer")) + if b.GetInt("Buffer") != 0 { + b.Messages.SetCapacity(b.GetInt("Buffer")) + } if b.GetString("Token") != "" { e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) { return key == b.GetString("Token"), nil })) } + e.GET("/api/health", b.handleHealthcheck) e.GET("/api/messages", b.handleMessages) e.GET("/api/stream", b.handleStream) e.POST("/api/message", b.handlePostMessage) @@ -52,30 +55,34 @@ func New(cfg *bridge.Config) bridge.Bridger { return b } -func (b *Api) Connect() error { +func (b *API) Connect() error { return nil } -func (b *Api) Disconnect() error { +func (b *API) Disconnect() error { return nil } -func (b *Api) JoinChannel(channel config.ChannelInfo) error { +func (b *API) JoinChannel(channel config.ChannelInfo) error { return nil } -func (b *Api) Send(msg config.Message) (string, error) { +func (b *API) Send(msg config.Message) (string, error) { b.Lock() defer b.Unlock() // ignore delete messages - if msg.Event == config.EVENT_MSG_DELETE { + if msg.Event == config.EventMsgDelete { return "", nil } b.Messages.Enqueue(&msg) return "", nil } -func (b *Api) handlePostMessage(c echo.Context) error { +func (b *API) handleHealthcheck(c echo.Context) error { + return c.String(http.StatusOK, "OK") +} + +func (b *API) handlePostMessage(c echo.Context) error { message := config.Message{} if err := c.Bind(&message); err != nil { return err @@ -91,7 +98,7 @@ func (b *Api) handlePostMessage(c echo.Context) error { return c.JSON(http.StatusOK, message) } -func (b *Api) handleMessages(c echo.Context) error { +func (b *API) handleMessages(c echo.Context) error { b.Lock() defer b.Unlock() c.JSONPretty(http.StatusOK, b.Messages.Values(), " ") @@ -99,9 +106,17 @@ func (b *Api) handleMessages(c echo.Context) error { return nil } -func (b *Api) handleStream(c echo.Context) error { +func (b *API) handleStream(c echo.Context) error { c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON) c.Response().WriteHeader(http.StatusOK) + greet := config.Message{ + Event: config.EventAPIConnected, + Timestamp: time.Now(), + } + if err := json.NewEncoder(c.Response()).Encode(greet); err != nil { + return err + } + c.Response().Flush() closeNotifier := c.Response().CloseNotify() for { select { diff --git a/bridge/bridge.go b/bridge/bridge.go index 0436eeb6..debe2d62 100644 --- a/bridge/bridge.go +++ b/bridge/bridge.go @@ -22,7 +22,7 @@ type Bridge struct { Channels map[string]config.ChannelInfo Joined map[string]bool Log *log.Entry - Config *config.Config + Config config.Config General *config.Protocol } @@ -69,36 +69,41 @@ func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map } func (b *Bridge) GetBool(key string) bool { - if b.Config.GetBool(b.Account + "." + key) { - return b.Config.GetBool(b.Account + "." + key) + val, ok := b.Config.GetBool(b.Account + "." + key) + if !ok { + val, _ = b.Config.GetBool("general." + key) } - return b.Config.GetBool("general." + key) + return val } func (b *Bridge) GetInt(key string) int { - if b.Config.GetInt(b.Account+"."+key) != 0 { - return b.Config.GetInt(b.Account + "." + key) + val, ok := b.Config.GetInt(b.Account + "." + key) + if !ok { + val, _ = b.Config.GetInt("general." + key) } - return b.Config.GetInt("general." + key) + return val } func (b *Bridge) GetString(key string) string { - if b.Config.GetString(b.Account+"."+key) != "" { - return b.Config.GetString(b.Account + "." + key) + val, ok := b.Config.GetString(b.Account + "." + key) + if !ok { + val, _ = b.Config.GetString("general." + key) } - return b.Config.GetString("general." + key) + return val } func (b *Bridge) GetStringSlice(key string) []string { - if len(b.Config.GetStringSlice(b.Account+"."+key)) != 0 { - return b.Config.GetStringSlice(b.Account + "." + key) + val, ok := b.Config.GetStringSlice(b.Account + "." + key) + if !ok { + val, _ = b.Config.GetStringSlice("general." + key) } - return b.Config.GetStringSlice("general." + key) + return val } func (b *Bridge) GetStringSlice2D(key string) [][]string { - if len(b.Config.GetStringSlice2D(b.Account+"."+key)) != 0 { - return b.Config.GetStringSlice2D(b.Account + "." + key) + val, ok := b.Config.GetStringSlice2D(b.Account + "." + key) + if !ok { + val, _ = b.Config.GetStringSlice2D("general." + key) } - return b.Config.GetStringSlice2D("general." + key) + return val } diff --git a/bridge/config/config.go b/bridge/config/config.go index 7f8fc3ee..eb34912d 100644 --- a/bridge/config/config.go +++ b/bridge/config/config.go @@ -2,7 +2,7 @@ package config import ( "bytes" - "os" + "io/ioutil" "strings" "sync" "time" @@ -14,14 +14,16 @@ import ( ) const ( - EVENT_JOIN_LEAVE = "join_leave" - EVENT_TOPIC_CHANGE = "topic_change" - EVENT_FAILURE = "failure" - EVENT_FILE_FAILURE_SIZE = "file_failure_size" - EVENT_AVATAR_DOWNLOAD = "avatar_download" - EVENT_REJOIN_CHANNELS = "rejoin_channels" - EVENT_USER_ACTION = "user_action" - EVENT_MSG_DELETE = "msg_delete" + EventJoinLeave = "join_leave" + EventTopicChange = "topic_change" + EventFailure = "failure" + EventFileFailureSize = "file_failure_size" + EventAvatarDownload = "avatar_download" + EventRejoinChannels = "rejoin_channels" + EventUserAction = "user_action" + EventMsgDelete = "msg_delete" + EventAPIConnected = "api_connected" + EventUserTyping = "user_typing" ) type Message struct { @@ -34,6 +36,7 @@ type Message struct { Event string `json:"event"` Protocol string `json:"protocol"` Gateway string `json:"gateway"` + ParentID string `json:"parent_id"` Timestamp time.Time `json:"timestamp"` ID string `json:"id"` Extra map[string][]interface{} @@ -69,6 +72,7 @@ type Protocol struct { EditSuffix string // mattermost, slack, discord, telegram, gitter EditDisable bool // mattermost, slack, discord, telegram, gitter IconURL string // mattermost, slack + IgnoreFailureOnStart bool // general IgnoreNicks string // all protocols IgnoreMessages string // all protocols Jid string // xmpp @@ -97,6 +101,7 @@ type Protocol struct { NoTLS bool // mattermost Password string // IRC,mattermost,XMPP,matrix PrefixMessagesWithNick bool // mattemost, slack + PreserveThreading bool // slack Protocol string // all protocols QuoteDisable bool // telegram QuoteFormat string // telegram @@ -104,12 +109,15 @@ type Protocol struct { ReplaceMessages [][]string // all protocols ReplaceNicks [][]string // all protocols RemoteNickFormat string // all protocols + RunCommands []string // irc Server string // IRC,mattermost,XMPP,discord ShowJoinPart bool // all protocols ShowTopicChange bool // slack + ShowUserTyping bool // slack ShowEmbeds bool // discord SkipTLSVerify bool // IRC, mattermost StripNick bool // all protocols + SyncTopic bool // slack Team string // mattermost Token string // gitter, slack, discord, api Topic string // zulip @@ -152,113 +160,129 @@ type SameChannelGateway struct { Accounts []string } -type ConfigValues struct { - Api map[string]Protocol - Irc map[string]Protocol +type BridgeValues struct { + API map[string]Protocol + IRC map[string]Protocol Mattermost map[string]Protocol Matrix map[string]Protocol Slack map[string]Protocol + SlackLegacy map[string]Protocol Steam map[string]Protocol Gitter map[string]Protocol - Xmpp map[string]Protocol + XMPP map[string]Protocol Discord map[string]Protocol Telegram map[string]Protocol Rocketchat map[string]Protocol - Sshchat map[string]Protocol + SSHChat map[string]Protocol Zulip map[string]Protocol General Protocol Gateway []Gateway SameChannelGateway []SameChannelGateway } -type Config struct { - v *viper.Viper - *ConfigValues - sync.RWMutex +type Config interface { + BridgeValues() *BridgeValues + GetBool(key string) (bool, bool) + GetInt(key string) (int, bool) + GetString(key string) (string, bool) + GetStringSlice(key string) ([]string, bool) + GetStringSlice2D(key string) ([][]string, bool) } -func NewConfig(cfgfile string) *Config { +type config struct { + v *viper.Viper + sync.RWMutex + + cv *BridgeValues +} + +func NewConfig(cfgfile string) Config { log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true, FullTimestamp: false}) flog := log.WithFields(log.Fields{"prefix": "config"}) - var cfg ConfigValues - viper.SetConfigType("toml") viper.SetConfigFile(cfgfile) - viper.SetEnvPrefix("matterbridge") - viper.AddConfigPath(".") - viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - viper.AutomaticEnv() - f, err := os.Open(cfgfile) + input, err := getFileContents(cfgfile) if err != nil { log.Fatal(err) } - err = viper.ReadConfig(f) - if err != nil { - log.Fatal(err) - } - err = viper.Unmarshal(&cfg) - if err != nil { - log.Fatal("blah", err) - } - mycfg := new(Config) - mycfg.v = viper.GetViper() - if cfg.General.MediaDownloadSize == 0 { - cfg.General.MediaDownloadSize = 1000000 + mycfg := newConfigFromString(input) + if mycfg.cv.General.MediaDownloadSize == 0 { + mycfg.cv.General.MediaDownloadSize = 1000000 } viper.WatchConfig() viper.OnConfigChange(func(e fsnotify.Event) { flog.Println("Config file changed:", e.Name) }) - - mycfg.ConfigValues = &cfg return mycfg } -func NewConfigFromString(input []byte) *Config { - var cfg ConfigValues +func getFileContents(filename string) ([]byte, error) { + input, err := ioutil.ReadFile(filename) + if err != nil { + log.Fatal(err) + return []byte(nil), err + } + return input, nil +} + +func NewConfigFromString(input []byte) Config { + return newConfigFromString(input) +} + +func newConfigFromString(input []byte) *config { viper.SetConfigType("toml") + viper.SetEnvPrefix("matterbridge") + viper.AddConfigPath(".") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) + viper.AutomaticEnv() err := viper.ReadConfig(bytes.NewBuffer(input)) if err != nil { log.Fatal(err) } - err = viper.Unmarshal(&cfg) + + cfg := &BridgeValues{} + err = viper.Unmarshal(cfg) if err != nil { log.Fatal(err) } - mycfg := new(Config) - mycfg.v = viper.GetViper() - mycfg.ConfigValues = &cfg - return mycfg + return &config{ + v: viper.GetViper(), + cv: cfg, + } } -func (c *Config) GetBool(key string) bool { +func (c *config) BridgeValues() *BridgeValues { + return c.cv +} + +func (c *config) GetBool(key string) (bool, bool) { c.RLock() defer c.RUnlock() // log.Debugf("getting bool %s = %#v", key, c.v.GetBool(key)) - return c.v.GetBool(key) + return c.v.GetBool(key), c.v.IsSet(key) } -func (c *Config) GetInt(key string) int { +func (c *config) GetInt(key string) (int, bool) { c.RLock() defer c.RUnlock() // log.Debugf("getting int %s = %d", key, c.v.GetInt(key)) - return c.v.GetInt(key) + return c.v.GetInt(key), c.v.IsSet(key) } -func (c *Config) GetString(key string) string { +func (c *config) GetString(key string) (string, bool) { c.RLock() defer c.RUnlock() // log.Debugf("getting String %s = %s", key, c.v.GetString(key)) - return c.v.GetString(key) + return c.v.GetString(key), c.v.IsSet(key) } -func (c *Config) GetStringSlice(key string) []string { +func (c *config) GetStringSlice(key string) ([]string, bool) { c.RLock() defer c.RUnlock() // log.Debugf("getting StringSlice %s = %#v", key, c.v.GetStringSlice(key)) - return c.v.GetStringSlice(key) + return c.v.GetStringSlice(key), c.v.IsSet(key) } -func (c *Config) GetStringSlice2D(key string) [][]string { +func (c *config) GetStringSlice2D(key string) ([][]string, bool) { c.RLock() defer c.RUnlock() result := [][]string{} @@ -270,9 +294,9 @@ func (c *Config) GetStringSlice2D(key string) [][]string { } result = append(result, result2) } - return result + return result, true } - return result + return result, false } func GetIconURL(msg *Message, iconURL string) string { @@ -284,3 +308,45 @@ func GetIconURL(msg *Message, iconURL string) string { iconURL = strings.Replace(iconURL, "{PROTOCOL}", protocol, -1) return iconURL } + +type TestConfig struct { + Config + + Overrides map[string]interface{} +} + +func (c *TestConfig) GetBool(key string) (bool, bool) { + val, ok := c.Overrides[key] + if ok { + return val.(bool), true + } + return c.Config.GetBool(key) +} + +func (c *TestConfig) GetInt(key string) (int, bool) { + if val, ok := c.Overrides[key]; ok { + return val.(int), true + } + return c.Config.GetInt(key) +} + +func (c *TestConfig) GetString(key string) (string, bool) { + if val, ok := c.Overrides[key]; ok { + return val.(string), true + } + return c.Config.GetString(key) +} + +func (c *TestConfig) GetStringSlice(key string) ([]string, bool) { + if val, ok := c.Overrides[key]; ok { + return val.([]string), true + } + return c.Config.GetStringSlice(key) +} + +func (c *TestConfig) GetStringSlice2D(key string) ([][]string, bool) { + if val, ok := c.Overrides[key]; ok { + return val.([][]string), true + } + return c.Config.GetStringSlice2D(key) +} diff --git a/bridge/discord/discord.go b/bridge/discord/discord.go index b424e4b0..16ce8c05 100644 --- a/bridge/discord/discord.go +++ b/bridge/discord/discord.go @@ -3,35 +3,41 @@ package bdiscord import ( "bytes" "fmt" - "regexp" "strings" "sync" "github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/helper" - "github.com/matterbridge/discordgo" + "github.com/bwmarrin/discordgo" ) const MessageLength = 1950 type Bdiscord struct { - c *discordgo.Session - Channels []*discordgo.Channel - Nick string - UseChannelID bool - userMemberMap map[string]*discordgo.Member - guildID string - webhookID string - webhookToken string - channelInfoMap map[string]*config.ChannelInfo - sync.RWMutex *bridge.Config + + c *discordgo.Session + + nick string + useChannelID bool + guildID string + webhookID string + webhookToken string + + channelsMutex sync.RWMutex + channels []*discordgo.Channel + channelInfoMap map[string]*config.ChannelInfo + + membersMutex sync.RWMutex + userMemberMap map[string]*discordgo.Member + nickMemberMap map[string]*discordgo.Member } func New(cfg *bridge.Config) bridge.Bridger { b := &Bdiscord{Config: cfg} b.userMemberMap = make(map[string]*discordgo.Member) + b.nickMemberMap = make(map[string]*discordgo.Member) b.channelInfoMap = make(map[string]*config.ChannelInfo) if b.GetString("WebhookURL") != "" { b.Log.Debug("Configuring Discord Incoming Webhook") @@ -73,19 +79,47 @@ func (b *Bdiscord) Connect() error { if err != nil { return err } - b.Nick = userinfo.Username + serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1) + b.nick = userinfo.Username + b.channelsMutex.Lock() for _, guild := range guilds { - if guild.Name == b.GetString("Server") { - b.Channels, err = b.c.GuildChannels(guild.ID) + if guild.Name == serverName || guild.ID == serverName { + b.channels, err = b.c.GuildChannels(guild.ID) b.guildID = guild.ID if err != nil { - return err + break } } } - for _, channel := range b.Channels { + b.channelsMutex.Unlock() + if err != nil { + return err + } + b.channelsMutex.RLock() + for _, channel := range b.channels { b.Log.Debugf("found channel %#v", channel) } + b.channelsMutex.RUnlock() + + // Obtaining guild members and initializing nickname mapping. + b.membersMutex.Lock() + defer b.membersMutex.Unlock() + members, err := b.c.GuildMembers(b.guildID, "", 1000) + if err != nil { + b.Log.Error("Error obtaining guild members", err) + return err + } + for _, member := range members { + if member == nil { + b.Log.Warnf("Skipping missing information for a user.") + continue + } + b.userMemberMap[member.User.ID] = member + b.nickMemberMap[member.User.Username] = member + if member.Nick != "" { + b.nickMemberMap[member.Nick] = member + } + } return nil } @@ -94,10 +128,13 @@ func (b *Bdiscord) Disconnect() error { } func (b *Bdiscord) JoinChannel(channel config.ChannelInfo) error { + b.channelsMutex.Lock() + defer b.channelsMutex.Unlock() + b.channelInfoMap[channel.ID] = &channel idcheck := strings.Split(channel.Name, "ID:") if len(idcheck) > 1 { - b.UseChannelID = true + b.useChannelID = true } return nil } @@ -111,7 +148,7 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) { } // Make a action /me of the message - if msg.Event == config.EVENT_USER_ACTION { + if msg.Event == config.EventUserAction { msg.Text = "_" + msg.Text + "_" } @@ -120,16 +157,18 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) { wToken := b.webhookToken // check if have a channel specific webhook + b.channelsMutex.RLock() if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok { if ci.Options.WebhookURL != "" { wID, wToken = b.splitURL(ci.Options.WebhookURL) } } + b.channelsMutex.RUnlock() // Use webhook to send the message if wID != "" { // skip events - if msg.Event != "" && msg.Event != config.EVENT_JOIN_LEAVE && msg.Event != config.EVENT_TOPIC_CHANGE { + if msg.Event != "" && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange { return "", nil } b.Log.Debugf("Broadcasting using Webhook") @@ -145,6 +184,11 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) { } msg.Text = helper.ClipMessage(msg.Text, MessageLength) + msg.Text = b.replaceUserMentions(msg.Text) + // discord username must be [0..32] max + if len(msg.Username) > 32 { + msg.Username = msg.Username[0:32] + } err := b.c.WebhookExecute( wID, wToken, @@ -160,7 +204,7 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) { b.Log.Debugf("Broadcasting using token (API)") // Delete message - if msg.Event == config.EVENT_MSG_DELETE { + if msg.Event == config.EventMsgDelete { if msg.ID == "" { return "", nil } @@ -172,7 +216,9 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) { if msg.Extra != nil { for _, rmsg := range helper.HandleExtra(&msg, b.General) { rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength) - b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text) + if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil { + b.Log.Errorf("Could not send message %#v: %v", rmsg, err) + } } // check if we have files to upload (from slack, telegram or mattermost) if len(msg.Extra["file"]) > 0 { @@ -181,6 +227,8 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) { } msg.Text = helper.ClipMessage(msg.Text, MessageLength) + msg.Text = b.replaceUserMentions(msg.Text) + // Edit message if msg.ID != "" { _, err := b.c.ChannelMessageEdit(channelID, msg.ID, msg.Username+msg.Text) @@ -195,202 +243,15 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) { return res.ID, err } -func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { - rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EVENT_MSG_DELETE, Text: config.EVENT_MSG_DELETE} - rmsg.Channel = b.getChannelName(m.ChannelID) - if b.UseChannelID { - rmsg.Channel = "ID:" + m.ChannelID - } - b.Log.Debugf("<= Sending message from %s to gateway", b.Account) - b.Log.Debugf("<= Message is %#v", rmsg) - b.Remote <- rmsg -} - -func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) { - if b.GetBool("EditDisable") { - return - } - // only when message is actually edited - if m.Message.EditedTimestamp != "" { - b.Log.Debugf("Sending edit message") - m.Content = m.Content + b.GetString("EditSuffix") - b.messageCreate(s, (*discordgo.MessageCreate)(m)) - } -} - -func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { - var err error - - // not relay our own messages - if m.Author.Username == b.Nick { - return - } - // if using webhooks, do not relay if it's ours - if b.useWebhook() && m.Author.Bot && b.isWebhookID(m.Author.ID) { - return - } - - // add the url of the attachments to content - if len(m.Attachments) > 0 { - for _, attach := range m.Attachments { - m.Content = m.Content + "\n" + attach.URL - } - } - - rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", UserID: m.Author.ID, ID: m.ID} - - if m.Content != "" { - b.Log.Debugf("== Receiving event %#v", m.Message) - m.Message.Content = b.stripCustomoji(m.Message.Content) - m.Message.Content = b.replaceChannelMentions(m.Message.Content) - rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c) - if err != nil { - b.Log.Errorf("ContentWithMoreMentionsReplaced failed: %s", err) - rmsg.Text = m.ContentWithMentionsReplaced() - } - } - - // set channel name - rmsg.Channel = b.getChannelName(m.ChannelID) - if b.UseChannelID { - rmsg.Channel = "ID:" + m.ChannelID - } - - // set username - if !b.GetBool("UseUserName") { - rmsg.Username = b.getNick(m.Author) - } else { - rmsg.Username = m.Author.Username - } - - // if we have embedded content add it to text - if b.GetBool("ShowEmbeds") && m.Message.Embeds != nil { - for _, embed := range m.Message.Embeds { - rmsg.Text = rmsg.Text + "embed: " + embed.Title + " - " + embed.Description + " - " + embed.URL + "\n" - } - } - - // no empty messages - if rmsg.Text == "" { - return - } - - // do we have a /me action - var ok bool - rmsg.Text, ok = b.replaceAction(rmsg.Text) - if ok { - rmsg.Event = config.EVENT_USER_ACTION - } - - b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account) - b.Log.Debugf("<= Message is %#v", rmsg) - b.Remote <- rmsg -} - -func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) { - b.Lock() - if _, ok := b.userMemberMap[m.Member.User.ID]; ok { - b.Log.Debugf("%s: memberupdate: user %s (nick %s) changes nick to %s", b.Account, m.Member.User.Username, b.userMemberMap[m.Member.User.ID].Nick, m.Member.Nick) - } - b.userMemberMap[m.Member.User.ID] = m.Member - b.Unlock() -} - -func (b *Bdiscord) getNick(user *discordgo.User) string { - var err error - b.Lock() - defer b.Unlock() - if _, ok := b.userMemberMap[user.ID]; ok { - if b.userMemberMap[user.ID] != nil { - if b.userMemberMap[user.ID].Nick != "" { - // only return if nick is set - return b.userMemberMap[user.ID].Nick - } - // otherwise return username - return user.Username - } - } - // if we didn't find nick, search for it - member, err := b.c.GuildMember(b.guildID, user.ID) - if err != nil { - return user.Username - } - b.userMemberMap[user.ID] = member - // only return if nick is set - if b.userMemberMap[user.ID].Nick != "" { - return b.userMemberMap[user.ID].Nick - } - return user.Username -} - -func (b *Bdiscord) getChannelID(name string) string { - idcheck := strings.Split(name, "ID:") - if len(idcheck) > 1 { - return idcheck[1] - } - for _, channel := range b.Channels { - if channel.Name == name { - return channel.ID - } - } - return "" -} - -func (b *Bdiscord) getChannelName(id string) string { - for _, channel := range b.Channels { - if channel.ID == id { - return channel.Name - } - } - return "" -} - -func (b *Bdiscord) replaceChannelMentions(text string) string { - var err error - re := regexp.MustCompile("<#[0-9]+>") - text = re.ReplaceAllStringFunc(text, func(m string) string { - channel := b.getChannelName(m[2 : len(m)-1]) - // if at first don't succeed, try again - if channel == "" { - b.Channels, err = b.c.GuildChannels(b.guildID) - if err != nil { - return "#unknownchannel" - } - channel = b.getChannelName(m[2 : len(m)-1]) - return "#" + channel - } - return "#" + channel - }) - return text -} - -func (b *Bdiscord) replaceAction(text string) (string, bool) { - if strings.HasPrefix(text, "_") && strings.HasSuffix(text, "_") { - return strings.Replace(text, "_", "", -1), true - } - return text, false -} - -func (b *Bdiscord) stripCustomoji(text string) string { - // <:doge:302803592035958784> - re := regexp.MustCompile("<(:.*?:)[0-9]+>") - return re.ReplaceAllString(text, `$1`) -} - -// splitURL splits a webhookURL and returns the id and token -func (b *Bdiscord) splitURL(url string) (string, string) { - webhookURLSplit := strings.Split(url, "/") - if len(webhookURLSplit) != 7 { - b.Log.Fatalf("%s is no correct discord WebhookURL", url) - } - return webhookURLSplit[len(webhookURLSplit)-2], webhookURLSplit[len(webhookURLSplit)-1] -} - // useWebhook returns true if we have a webhook defined somewhere func (b *Bdiscord) useWebhook() bool { if b.GetString("WebhookURL") != "" { return true } + + b.channelsMutex.RLock() + defer b.channelsMutex.RUnlock() + for _, channel := range b.channelInfoMap { if channel.Options.WebhookURL != "" { return true @@ -407,6 +268,10 @@ func (b *Bdiscord) isWebhookID(id string) bool { return true } } + + b.channelsMutex.RLock() + defer b.channelsMutex.RUnlock() + for _, channel := range b.channelInfoMap { if channel.Options.WebhookURL != "" { wID, _ := b.splitURL(channel.Options.WebhookURL) @@ -423,9 +288,16 @@ func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (stri var err error for _, f := range msg.Extra["file"] { fi := f.(config.FileInfo) - files := []*discordgo.File{} - files = append(files, &discordgo.File{fi.Name, "", bytes.NewReader(*fi.Data)}) - _, err = b.c.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{Content: msg.Username + fi.Comment, Files: files}) + file := discordgo.File{ + Name: fi.Name, + ContentType: "", + Reader: bytes.NewReader(*fi.Data), + } + m := discordgo.MessageSend{ + Content: msg.Username + fi.Comment, + Files: []*discordgo.File{&file}, + } + _, err = b.c.ChannelMessageSendComplex(channelID, &m) if err != nil { return "", fmt.Errorf("file upload failed: %#v", err) } diff --git a/bridge/discord/handlers.go b/bridge/discord/handlers.go new file mode 100644 index 00000000..8691a612 --- /dev/null +++ b/bridge/discord/handlers.go @@ -0,0 +1,125 @@ +package bdiscord + +import ( + "github.com/42wim/matterbridge/bridge/config" + "github.com/bwmarrin/discordgo" +) + +func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { //nolint:unparam + rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EventMsgDelete, Text: config.EventMsgDelete} + rmsg.Channel = b.getChannelName(m.ChannelID) + if b.useChannelID { + rmsg.Channel = "ID:" + m.ChannelID + } + b.Log.Debugf("<= Sending message from %s to gateway", b.Account) + b.Log.Debugf("<= Message is %#v", rmsg) + b.Remote <- rmsg +} + +func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) { //nolint:unparam + if b.GetBool("EditDisable") { + return + } + // only when message is actually edited + if m.Message.EditedTimestamp != "" { + b.Log.Debugf("Sending edit message") + m.Content += b.GetString("EditSuffix") + b.messageCreate(s, (*discordgo.MessageCreate)(m)) + } +} + +func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { //nolint:unparam + var err error + + // not relay our own messages + if m.Author.Username == b.nick { + return + } + // if using webhooks, do not relay if it's ours + if b.useWebhook() && m.Author.Bot && b.isWebhookID(m.Author.ID) { + return + } + + // add the url of the attachments to content + if len(m.Attachments) > 0 { + for _, attach := range m.Attachments { + m.Content = m.Content + "\n" + attach.URL + } + } + + rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", UserID: m.Author.ID, ID: m.ID} + + if m.Content != "" { + b.Log.Debugf("== Receiving event %#v", m.Message) + m.Message.Content = b.stripCustomoji(m.Message.Content) + m.Message.Content = b.replaceChannelMentions(m.Message.Content) + rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c) + if err != nil { + b.Log.Errorf("ContentWithMoreMentionsReplaced failed: %s", err) + rmsg.Text = m.ContentWithMentionsReplaced() + } + } + + // set channel name + rmsg.Channel = b.getChannelName(m.ChannelID) + if b.useChannelID { + rmsg.Channel = "ID:" + m.ChannelID + } + + // set username + if !b.GetBool("UseUserName") { + rmsg.Username = b.getNick(m.Author) + } else { + rmsg.Username = m.Author.Username + } + + // if we have embedded content add it to text + if b.GetBool("ShowEmbeds") && m.Message.Embeds != nil { + for _, embed := range m.Message.Embeds { + rmsg.Text = rmsg.Text + "embed: " + embed.Title + " - " + embed.Description + " - " + embed.URL + "\n" + } + } + + // no empty messages + if rmsg.Text == "" { + return + } + + // do we have a /me action + var ok bool + rmsg.Text, ok = b.replaceAction(rmsg.Text) + if ok { + rmsg.Event = config.EventUserAction + } + + b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account) + b.Log.Debugf("<= Message is %#v", rmsg) + b.Remote <- rmsg +} + +func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) { + if m.Member == nil { + b.Log.Warnf("Received member update with no member information: %#v", m) + } + + b.membersMutex.Lock() + defer b.membersMutex.Unlock() + + if currMember, ok := b.userMemberMap[m.Member.User.ID]; ok { + b.Log.Debugf( + "%s: memberupdate: user %s (nick %s) changes nick to %s", + b.Account, + m.Member.User.Username, + b.userMemberMap[m.Member.User.ID].Nick, + m.Member.Nick, + ) + delete(b.nickMemberMap, currMember.User.Username) + delete(b.nickMemberMap, currMember.Nick) + delete(b.userMemberMap, m.Member.User.ID) + } + b.userMemberMap[m.Member.User.ID] = m.Member + b.nickMemberMap[m.Member.User.Username] = m.Member + if m.Member.Nick != "" { + b.nickMemberMap[m.Member.Nick] = m.Member + } +} diff --git a/bridge/discord/helpers.go b/bridge/discord/helpers.go new file mode 100644 index 00000000..e693498c --- /dev/null +++ b/bridge/discord/helpers.go @@ -0,0 +1,189 @@ +package bdiscord + +import ( + "errors" + "regexp" + "strings" + "unicode" + + "github.com/bwmarrin/discordgo" +) + +func (b *Bdiscord) getNick(user *discordgo.User) string { + b.membersMutex.RLock() + defer b.membersMutex.RUnlock() + + if member, ok := b.userMemberMap[user.ID]; ok { + if member.Nick != "" { + // Only return if nick is set. + return member.Nick + } + // Otherwise return username. + return user.Username + } + + // If we didn't find nick, search for it. + member, err := b.c.GuildMember(b.guildID, user.ID) + if err != nil { + b.Log.Warnf("Failed to fetch information for member %#v: %#v", user, err) + return user.Username + } else if member == nil { + b.Log.Warnf("Got no information for member %#v", user) + return user.Username + } + b.userMemberMap[user.ID] = member + b.nickMemberMap[member.User.Username] = member + if member.Nick != "" { + b.nickMemberMap[member.Nick] = member + return member.Nick + } + return user.Username +} + +func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error) { + b.membersMutex.RLock() + defer b.membersMutex.RUnlock() + + if member, ok := b.nickMemberMap[nick]; ok { + return member, nil + } + return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller +} + +func (b *Bdiscord) getChannelID(name string) string { + b.channelsMutex.RLock() + defer b.channelsMutex.RUnlock() + + idcheck := strings.Split(name, "ID:") + if len(idcheck) > 1 { + return idcheck[1] + } + for _, channel := range b.channels { + if channel.Name == name { + return channel.ID + } + } + return "" +} + +func (b *Bdiscord) getChannelName(id string) string { + b.channelsMutex.RLock() + defer b.channelsMutex.RUnlock() + + for _, channel := range b.channels { + if channel.ID == id { + return channel.Name + } + } + return "" +} + +var ( + // See https://discordapp.com/developers/docs/reference#message-formatting. + channelMentionRE = regexp.MustCompile("<#[0-9]+>") + emojiRE = regexp.MustCompile("<(:.*?:)[0-9]+>") + userMentionRE = regexp.MustCompile("@[^@\n]{1,32}") +) + +func (b *Bdiscord) replaceChannelMentions(text string) string { + replaceChannelMentionFunc := func(match string) string { + var err error + channelID := match[2 : len(match)-1] + + channelName := b.getChannelName(channelID) + // If we don't have the channel refresh our list. + if channelName == "" { + b.channels, err = b.c.GuildChannels(b.guildID) + if err != nil { + return "#unknownchannel" + } + channelName = b.getChannelName(channelID) + } + return "#" + channelName + } + return channelMentionRE.ReplaceAllStringFunc(text, replaceChannelMentionFunc) +} + +func (b *Bdiscord) replaceUserMentions(text string) string { + replaceUserMentionFunc := func(match string) string { + var ( + err error + member *discordgo.Member + username string + ) + + usernames := enumerateUsernames(match[1:]) + for _, username = range usernames { + b.Log.Debugf("Testing mention: '%s'", username) + member, err = b.getGuildMemberByNick(username) + if err == nil { + break + } + } + if member == nil { + return match + } + return strings.Replace(match, "@"+username, member.User.Mention(), 1) + } + return userMentionRE.ReplaceAllStringFunc(text, replaceUserMentionFunc) +} + +func (b *Bdiscord) stripCustomoji(text string) string { + return emojiRE.ReplaceAllString(text, `$1`) +} + +func (b *Bdiscord) replaceAction(text string) (string, bool) { + if strings.HasPrefix(text, "_") && strings.HasSuffix(text, "_") { + return text[1:], true + } + return text, false +} + +// splitURL splits a webhookURL and returns the ID and token. +func (b *Bdiscord) splitURL(url string) (string, string) { + const ( + expectedWebhookSplitCount = 7 + webhookIdxID = 5 + webhookIdxToken = 6 + ) + webhookURLSplit := strings.Split(url, "/") + if len(webhookURLSplit) != expectedWebhookSplitCount { + b.Log.Fatalf("%s is no correct discord WebhookURL", url) + } + return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken] +} + +func enumerateUsernames(s string) []string { + onlySpace := true + for _, r := range s { + if !unicode.IsSpace(r) { + onlySpace = false + break + } + } + if onlySpace { + return nil + } + + var username, endSpace string + var usernames []string + skippingSpace := true + for _, r := range s { + if unicode.IsSpace(r) { + if !skippingSpace { + usernames = append(usernames, username) + skippingSpace = true + } + endSpace += string(r) + username += string(r) + } else { + endSpace = "" + username += string(r) + skippingSpace = false + } + } + if endSpace == "" { + usernames = append(usernames, username) + } + return usernames +} diff --git a/bridge/discord/helpers_test.go b/bridge/discord/helpers_test.go new file mode 100644 index 00000000..0f196d9a --- /dev/null +++ b/bridge/discord/helpers_test.go @@ -0,0 +1,46 @@ +package bdiscord + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEnumerateUsernames(t *testing.T) { + testcases := map[string]struct { + match string + expectedUsernames []string + }{ + "only space": { + match: " \t\n \t", + expectedUsernames: nil, + }, + "single word": { + match: "veni", + expectedUsernames: []string{"veni"}, + }, + "single word with preceeding space": { + match: " vidi", + expectedUsernames: []string{" vidi"}, + }, + "single word with suffixed space": { + match: "vici ", + expectedUsernames: []string{"vici"}, + }, + "multi-word with varying whitespace": { + match: "just me and\tmy friends \t", + expectedUsernames: []string{ + "just", + "just me", + "just me and", + "just me and\tmy", + "just me and\tmy friends", + }, + }, + } + + for testname, testcase := range testcases { + foundUsernames := enumerateUsernames(testcase.match) + assert.Equalf(t, testcase.expectedUsernames, foundUsernames, "Should have found the expected usernames for testcase %s", testname) + } +} diff --git a/bridge/gitter/gitter.go b/bridge/gitter/gitter.go index f47ca7ea..486fe433 100644 --- a/bridge/gitter/gitter.go +++ b/bridge/gitter/gitter.go @@ -77,7 +77,7 @@ func (b *Bgitter) JoinChannel(channel config.ChannelInfo) error { Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username), UserID: ev.Message.From.ID, ID: ev.Message.ID} if strings.HasPrefix(ev.Message.Text, "@"+ev.Message.From.Username) { - rmsg.Event = config.EVENT_USER_ACTION + rmsg.Event = config.EventUserAction rmsg.Text = strings.Replace(rmsg.Text, "@"+ev.Message.From.Username+" ", "", -1) } b.Log.Debugf("<= Message is %#v", rmsg) @@ -100,7 +100,7 @@ func (b *Bgitter) Send(msg config.Message) (string, error) { } // Delete message - if msg.Event == config.EVENT_MSG_DELETE { + if msg.Event == config.EventMsgDelete { if msg.ID == "" { return "", nil } diff --git a/bridge/helper/helper.go b/bridge/helper/helper.go index a13e02af..c1a48ce8 100644 --- a/bridge/helper/helper.go +++ b/bridge/helper/helper.go @@ -40,29 +40,52 @@ func DownloadFileAuth(url string, auth string) (*[]byte, error) { return &data, nil } -func SplitStringLength(input string, length int) string { - a := []rune(input) - str := "" - for i, r := range a { - str = str + string(r) - if i > 0 && (i+1)%length == 0 { - str += "\n" +// GetSubLines splits messages in newline-delimited lines. If maxLineLength is +// specified as non-zero GetSubLines will and also clip long lines to the +// maximum length and insert a warning marker that the line was clipped. +// +// TODO: The current implementation has the inconvenient that it disregards +// word boundaries when splitting but this is hard to solve without potentially +// breaking formatting and other stylistic effects. +func GetSubLines(message string, maxLineLength int) []string { + const clippingMessage = " " + + var lines []string + for _, line := range strings.Split(strings.TrimSpace(message), "\n") { + if maxLineLength == 0 || len([]byte(line)) <= maxLineLength { + lines = append(lines, line) + continue } + + // !!! WARNING !!! + // Before touching the splitting logic below please ensure that you PROPERLY + // understand how strings, runes and range loops over strings work in Go. + // A good place to start is to read https://blog.golang.org/strings. :-) + var splitStart int + var startOfPreviousRune int + for i := range line { + if i-splitStart > maxLineLength-len([]byte(clippingMessage)) { + lines = append(lines, line[splitStart:startOfPreviousRune]+clippingMessage) + splitStart = startOfPreviousRune + } + startOfPreviousRune = i + } + // This last append is safe to do without looking at the remaining byte-length + // as we assume that the byte-length of the last rune will never exceed that of + // the byte-length of the clipping message. + lines = append(lines, line[splitStart:]) } - return str + return lines } // handle all the stuff we put into extra func HandleExtra(msg *config.Message, general *config.Protocol) []config.Message { extra := msg.Extra rmsg := []config.Message{} - if len(extra[config.EVENT_FILE_FAILURE_SIZE]) > 0 { - for _, f := range extra[config.EVENT_FILE_FAILURE_SIZE] { - fi := f.(config.FileInfo) - text := fmt.Sprintf("file %s too big to download (%#v > allowed size: %#v)", fi.Name, fi.Size, general.MediaDownloadSize) - rmsg = append(rmsg, config.Message{Text: text, Username: " ", Channel: msg.Channel, Account: msg.Account}) - } - return rmsg + for _, f := range extra[config.EventFileFailureSize] { + fi := f.(config.FileInfo) + text := fmt.Sprintf("file %s too big to download (%#v > allowed size: %#v)", fi.Name, fi.Size, general.MediaDownloadSize) + rmsg = append(rmsg, config.Message{Text: text, Username: " ", Channel: msg.Channel, Account: msg.Account}) } return rmsg } @@ -90,7 +113,7 @@ func HandleDownloadSize(flog *log.Entry, msg *config.Message, name string, size } flog.Debugf("Trying to download %#v with size %#v", name, size) if int(size) > general.MediaDownloadSize { - msg.Event = config.EVENT_FILE_FAILURE_SIZE + msg.Event = config.EventFileFailureSize msg.Extra[msg.Event] = append(msg.Extra[msg.Event], config.FileInfo{Name: name, Comment: msg.Text, Size: size}) return fmt.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, size, general.MediaDownloadSize) } @@ -100,7 +123,7 @@ func HandleDownloadSize(flog *log.Entry, msg *config.Message, name string, size func HandleDownloadData(flog *log.Entry, msg *config.Message, name, comment, url string, data *[]byte, general *config.Protocol) { var avatar bool flog.Debugf("Download OK %#v %#v", name, len(*data)) - if msg.Event == config.EVENT_AVATAR_DOWNLOAD { + if msg.Event == config.EventAvatarDownload { avatar = true } msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: data, URL: url, Comment: comment, Avatar: avatar}) diff --git a/bridge/helper/helper_test.go b/bridge/helper/helper_test.go new file mode 100644 index 00000000..1770acd9 --- /dev/null +++ b/bridge/helper/helper_test.go @@ -0,0 +1,105 @@ +package helper + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +const testLineLength = 64 + +var ( + lineSplittingTestCases = map[string]struct { + input string + splitOutput []string + nonSplitOutput []string + }{ + "Short single-line message": { + input: "short", + splitOutput: []string{"short"}, + nonSplitOutput: []string{"short"}, + }, + "Long single-line message": { + input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + splitOutput: []string{ + "Lorem ipsum dolor sit amet, consectetur adipis ", + "cing elit, sed do eiusmod tempor incididunt ut ", + " labore et dolore magna aliqua.", + }, + nonSplitOutput: []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."}, + }, + "Short multi-line message": { + input: "I\ncan't\nget\nno\nsatisfaction!", + splitOutput: []string{ + "I", + "can't", + "get", + "no", + "satisfaction!", + }, + nonSplitOutput: []string{ + "I", + "can't", + "get", + "no", + "satisfaction!", + }, + }, + "Long multi-line message": { + input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" + + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" + + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n" + + "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + splitOutput: []string{ + "Lorem ipsum dolor sit amet, consectetur adipis ", + "cing elit, sed do eiusmod tempor incididunt ut ", + " labore et dolore magna aliqua.", + "Ut enim ad minim veniam, quis nostrud exercita ", + "tion ullamco laboris nisi ut aliquip ex ea com ", + "modo consequat.", + "Duis aute irure dolor in reprehenderit in volu ", + "ptate velit esse cillum dolore eu fugiat nulla ", + " pariatur.", + "Excepteur sint occaecat cupidatat non proident ", + ", sunt in culpa qui officia deserunt mollit an ", + "im id est laborum.", + }, + nonSplitOutput: []string{ + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", + "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + }, + }, + "Message ending with new-line.": { + input: "Newline ending\n", + splitOutput: []string{"Newline ending"}, + nonSplitOutput: []string{"Newline ending"}, + }, + "Long message containing UTF-8 multi-byte runes": { + input: "不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說", + splitOutput: []string{ + "不布人個我此而及單石業喜資富下 ", + "我河下日沒一我臺空達的常景便物 ", + "沒為……子大我別名解成?生賣的 ", + "全直黑,我自我結毛分洲了世當, ", + "是政福那是東;斯說", + }, + nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"}, + }, + } +) + +func TestGetSubLines(t *testing.T) { + for testname, testcase := range lineSplittingTestCases { + splitLines := GetSubLines(testcase.input, testLineLength) + assert.Equalf(t, testcase.splitOutput, splitLines, "'%s' testcase should give expected lines with splitting.", testname) + for _, splitLine := range splitLines { + byteLength := len([]byte(splitLine)) + assert.True(t, byteLength <= testLineLength, "Splitted line '%s' of testcase '%s' should not exceed the maximum byte-length (%d vs. %d).", splitLine, testcase, byteLength, testLineLength) + } + + nonSplitLines := GetSubLines(testcase.input, 0) + assert.Equalf(t, testcase.nonSplitOutput, nonSplitLines, "'%s' testcase should give expected lines without splitting.", testname) + } +} diff --git a/bridge/irc/handlers.go b/bridge/irc/handlers.go new file mode 100644 index 00000000..c94ea892 --- /dev/null +++ b/bridge/irc/handlers.go @@ -0,0 +1,235 @@ +package birc + +import ( + "bytes" + "fmt" + "io/ioutil" + "regexp" + "strconv" + "strings" + "time" + + "github.com/42wim/matterbridge/bridge/config" + "github.com/42wim/matterbridge/bridge/helper" + "github.com/dfordsoft/golib/ic" + "github.com/lrstanley/girc" + "github.com/paulrosania/go-charset/charset" + "github.com/saintfish/chardet" + + // We need to import the 'data' package as an implicit dependency. + // See: https://godoc.org/github.com/paulrosania/go-charset/charset + _ "github.com/paulrosania/go-charset/data" +) + +func (b *Birc) handleCharset(msg *config.Message) error { + if b.GetString("Charset") != "" { + switch b.GetString("Charset") { + case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp": + msg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), msg.Text) + default: + buf := new(bytes.Buffer) + w, err := charset.NewWriter(b.GetString("Charset"), buf) + if err != nil { + b.Log.Errorf("charset from utf-8 conversion failed: %s", err) + return err + } + fmt.Fprint(w, msg.Text) + w.Close() + msg.Text = buf.String() + } + } + return nil +} + +// handleFiles returns true if we have handled the files, otherwise return false +func (b *Birc) handleFiles(msg *config.Message) bool { + if msg.Extra == nil { + return false + } + for _, rmsg := range helper.HandleExtra(msg, b.General) { + b.Local <- rmsg + } + if len(msg.Extra["file"]) == 0 { + return false + } + for _, f := range msg.Extra["file"] { + fi := f.(config.FileInfo) + if fi.Comment != "" { + msg.Text += fi.Comment + ": " + } + if fi.URL != "" { + msg.Text = fi.URL + if fi.Comment != "" { + msg.Text = fi.Comment + ": " + fi.URL + } + } + b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event} + } + return true +} + +func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) { + if len(event.Params) == 0 { + b.Log.Debugf("handleJoinPart: empty Params? %#v", event) + return + } + channel := strings.ToLower(event.Params[0]) + if event.Command == "KICK" && event.Params[1] == b.Nick { + b.Log.Infof("Got kicked from %s by %s", channel, event.Source.Name) + time.Sleep(time.Duration(b.GetInt("RejoinDelay")) * time.Second) + b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EventRejoinChannels} + return + } + if event.Command == "QUIT" { + if event.Source.Name == b.Nick && strings.Contains(event.Trailing, "Ping timeout") { + b.Log.Infof("%s reconnecting ..", b.Account) + b.Remote <- config.Message{Username: "system", Text: "reconnect", Channel: channel, Account: b.Account, Event: config.EventFailure} + return + } + } + if event.Source.Name != b.Nick { + if b.GetBool("nosendjoinpart") { + return + } + b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account) + msg := config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave} + b.Log.Debugf("<= Message is %#v", msg) + b.Remote <- msg + return + } + b.Log.Debugf("handle %#v", event) +} + +func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) { + b.Log.Debug("Registering callbacks") + i := b.i + b.Nick = event.Params[0] + + i.Handlers.Add("PRIVMSG", b.handlePrivMsg) + i.Handlers.Add("CTCP_ACTION", b.handlePrivMsg) + i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime) + i.Handlers.Add(girc.NOTICE, b.handleNotice) + i.Handlers.Add("JOIN", b.handleJoinPart) + i.Handlers.Add("PART", b.handleJoinPart) + i.Handlers.Add("QUIT", b.handleJoinPart) + i.Handlers.Add("KICK", b.handleJoinPart) +} + +func (b *Birc) handleNickServ() { + if !b.GetBool("UseSASL") && b.GetString("NickServNick") != "" && b.GetString("NickServPassword") != "" { + b.Log.Debugf("Sending identify to nickserv %s", b.GetString("NickServNick")) + b.i.Cmd.Message(b.GetString("NickServNick"), "IDENTIFY "+b.GetString("NickServPassword")) + } + if strings.EqualFold(b.GetString("NickServNick"), "Q@CServe.quakenet.org") { + b.Log.Debugf("Authenticating %s against %s", b.GetString("NickServUsername"), b.GetString("NickServNick")) + b.i.Cmd.Message(b.GetString("NickServNick"), "AUTH "+b.GetString("NickServUsername")+" "+b.GetString("NickServPassword")) + } + // give nickserv some slack + time.Sleep(time.Second * 5) + b.authDone = true +} + +func (b *Birc) handleNotice(client *girc.Client, event girc.Event) { + if strings.Contains(event.String(), "This nickname is registered") && event.Source.Name == b.GetString("NickServNick") { + b.handleNickServ() + } else { + b.handlePrivMsg(client, event) + } +} + +func (b *Birc) handleOther(client *girc.Client, event girc.Event) { + if b.GetInt("DebugLevel") == 1 { + if event.Command != "CLIENT_STATE_UPDATED" && + event.Command != "CLIENT_GENERAL_UPDATED" { + b.Log.Debugf("%#v", event.String()) + } + return + } + switch event.Command { + case "372", "375", "376", "250", "251", "252", "253", "254", "255", "265", "266", "002", "003", "004", "005": + return + } + b.Log.Debugf("%#v", event.String()) +} + +func (b *Birc) handleOtherAuth(client *girc.Client, event girc.Event) { + b.handleNickServ() + b.handleRunCommands() + // we are now fully connected + b.connected <- nil +} + +func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) { + if b.skipPrivMsg(event) { + return + } + rmsg := config.Message{Username: event.Source.Name, Channel: strings.ToLower(event.Params[0]), Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host} + b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Trailing, event) + + // set action event + if event.IsAction() { + rmsg.Event = config.EventUserAction + } + + // strip action, we made an event if it was an action + rmsg.Text += event.StripAction() + + // strip IRC colors + re := regexp.MustCompile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`) + rmsg.Text = re.ReplaceAllString(rmsg.Text, "") + + // start detecting the charset + mycharset := b.GetString("Charset") + if mycharset == "" { + // detect what were sending so that we convert it to utf-8 + detector := chardet.NewTextDetector() + result, err := detector.DetectBest([]byte(rmsg.Text)) + if err != nil { + b.Log.Infof("detection failed for rmsg.Text: %#v", rmsg.Text) + return + } + b.Log.Debugf("detected %s confidence %#v", result.Charset, result.Confidence) + mycharset = result.Charset + // if we're not sure, just pick ISO-8859-1 + if result.Confidence < 80 { + mycharset = "ISO-8859-1" + } + } + switch mycharset { + case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp": + rmsg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), rmsg.Text) + default: + r, err := charset.NewReader(mycharset, strings.NewReader(rmsg.Text)) + if err != nil { + b.Log.Errorf("charset to utf-8 conversion failed: %s", err) + return + } + output, _ := ioutil.ReadAll(r) + rmsg.Text = string(output) + } + + b.Log.Debugf("<= Sending message from %s on %s to gateway", event.Params[0], b.Account) + b.Remote <- rmsg +} + +func (b *Birc) handleRunCommands() { + for _, cmd := range b.GetStringSlice("RunCommands") { + if err := b.i.Cmd.SendRaw(cmd); err != nil { + b.Log.Errorf("RunCommands %s failed: %s", cmd, err) + } + time.Sleep(time.Second) + } +} + +func (b *Birc) handleTopicWhoTime(client *girc.Client, event girc.Event) { + parts := strings.Split(event.Params[2], "!") + t, err := strconv.ParseInt(event.Params[3], 10, 64) + if err != nil { + b.Log.Errorf("Invalid time stamp: %s", event.Params[3]) + } + user := parts[0] + if len(parts) > 1 { + user += " [" + parts[1] + "]" + } + b.Log.Debugf("%s: Topic set by %s [%s]", event.Command, user, time.Unix(t, 0)) +} diff --git a/bridge/irc/helper.go b/bridge/irc/helper.go deleted file mode 100644 index dd1cc468..00000000 --- a/bridge/irc/helper.go +++ /dev/null @@ -1,61 +0,0 @@ -package birc - -import ( - "strings" -) - -/* -func tableformatter(nicks []string, nicksPerRow int, continued bool) string { - result := "|IRC users" - if continued { - result = "|(continued)" - } - for i := 0; i < 2; i++ { - for j := 1; j <= nicksPerRow && j <= len(nicks); j++ { - if i == 0 { - result += "|" - } else { - result += ":-|" - } - } - result += "\r\n|" - } - result += nicks[0] + "|" - for i := 1; i < len(nicks); i++ { - if i%nicksPerRow == 0 { - result += "\r\n|" + nicks[i] + "|" - } else { - result += nicks[i] + "|" - } - } - return result -} -*/ - -func plainformatter(nicks []string, nicksPerRow int) string { - return strings.Join(nicks, ", ") + " currently on IRC" -} - -func IsMarkup(message string) bool { - switch message[0] { - case '|': - fallthrough - case '#': - fallthrough - case '_': - fallthrough - case '*': - fallthrough - case '~': - fallthrough - case '-': - fallthrough - case ':': - fallthrough - case '>': - fallthrough - case '=': - return true - } - return false -} diff --git a/bridge/irc/irc.go b/bridge/irc/irc.go index cd37c0b4..cad0bdf8 100644 --- a/bridge/irc/irc.go +++ b/bridge/irc/irc.go @@ -1,37 +1,32 @@ package birc import ( - "bytes" "crypto/tls" "fmt" "hash/crc32" - "io" - "io/ioutil" "net" - "regexp" "sort" "strconv" "strings" "time" - "unicode/utf8" "github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/helper" - "github.com/dfordsoft/golib/ic" "github.com/lrstanley/girc" - "github.com/paulrosania/go-charset/charset" + + // We need to import the 'data' package as an implicit dependency. + // See: https://godoc.org/github.com/paulrosania/go-charset/charset _ "github.com/paulrosania/go-charset/data" - "github.com/saintfish/chardet" ) type Birc struct { i *girc.Client Nick string names map[string][]string - connected chan struct{} + connected chan error Local chan config.Message // local queue for flood control - FirstConnection bool + FirstConnection, authDone bool MessageDelay, MessageQueue, MessageLength int *bridge.Config @@ -42,7 +37,7 @@ func New(cfg *bridge.Config) bridge.Bridger { b.Config = cfg b.Nick = b.GetString("Nick") b.names = make(map[string][]string) - b.connected = make(chan struct{}) + b.connected = make(chan error) if b.GetInt("MessageDelay") == 0 { b.MessageDelay = 1300 } else { @@ -63,11 +58,10 @@ func New(cfg *bridge.Config) bridge.Bridger { } func (b *Birc) Command(msg *config.Message) string { - switch msg.Text { - case "!users": + if msg.Text == "!users" { b.i.Handlers.Add(girc.RPL_NAMREPLY, b.storeNames) b.i.Handlers.Add(girc.RPL_ENDOFNAMES, b.endNames) - b.i.Cmd.SendRaw("NAMES " + msg.Channel) + b.i.Cmd.SendRaw("NAMES " + msg.Channel) //nolint:errcheck } return "" } @@ -75,13 +69,163 @@ func (b *Birc) Command(msg *config.Message) string { func (b *Birc) Connect() error { b.Local = make(chan config.Message, b.MessageQueue+10) b.Log.Infof("Connecting %s", b.GetString("Server")) - server, portstr, err := net.SplitHostPort(b.GetString("Server")) + + i, err := b.getClient() if err != nil { return err } + + if b.GetBool("UseSASL") { + i.Config.SASL = &girc.SASLPlain{ + User: b.GetString("NickServNick"), + Pass: b.GetString("NickServPassword"), + } + } + + i.Handlers.Add(girc.RPL_WELCOME, b.handleNewConnection) + i.Handlers.Add(girc.RPL_ENDOFMOTD, b.handleOtherAuth) + i.Handlers.Add(girc.ALL_EVENTS, b.handleOther) + + go b.doConnect() + + b.i = i + err = <-b.connected + if err != nil { + return fmt.Errorf("connection failed %s", err) + } + b.Log.Info("Connection succeeded") + b.FirstConnection = false + if b.GetInt("DebugLevel") == 0 { + i.Handlers.Clear(girc.ALL_EVENTS) + } + go b.doSend() + return nil +} + +func (b *Birc) Disconnect() error { + b.i.Close() + close(b.Local) + return nil +} + +func (b *Birc) JoinChannel(channel config.ChannelInfo) error { + // need to check if we have nickserv auth done before joining channels + for { + if b.authDone { + break + } + time.Sleep(time.Second) + } + if channel.Options.Key != "" { + b.Log.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name) + b.i.Cmd.JoinKey(channel.Name, channel.Options.Key) + } else { + b.i.Cmd.Join(channel.Name) + } + return nil +} + +func (b *Birc) Send(msg config.Message) (string, error) { + // ignore delete messages + if msg.Event == config.EventMsgDelete { + return "", nil + } + + b.Log.Debugf("=> Receiving %#v", msg) + + // we can be in between reconnects #385 + if !b.i.IsConnected() { + b.Log.Error("Not connected to server, dropping message") + } + + // Execute a command + if strings.HasPrefix(msg.Text, "!") { + b.Command(&msg) + } + + // convert to specified charset + if err := b.handleCharset(&msg); err != nil { + return "", err + } + + // handle files, return if we're done here + if ok := b.handleFiles(&msg); ok { + return "", nil + } + + var msgLines []string + if b.GetBool("MessageSplit") { + msgLines = helper.GetSubLines(msg.Text, b.MessageLength) + } else { + msgLines = helper.GetSubLines(msg.Text, 0) + } + for i := range msgLines { + if len(b.Local) >= b.MessageQueue { + b.Log.Debugf("flooding, dropping message (queue at %d)", len(b.Local)) + return "", nil + } + + b.Local <- config.Message{ + Text: msgLines[i], + Username: msg.Username, + Channel: msg.Channel, + Event: msg.Event, + } + } + return "", nil +} + +func (b *Birc) doConnect() { + for { + if err := b.i.Connect(); err != nil { + b.Log.Errorf("disconnect: error: %s", err) + if b.FirstConnection { + b.connected <- err + return + } + } else { + b.Log.Info("disconnect: client requested quit") + } + b.Log.Info("reconnecting in 30 seconds...") + time.Sleep(30 * time.Second) + b.i.Handlers.Clear(girc.RPL_WELCOME) + b.i.Handlers.Add(girc.RPL_WELCOME, func(client *girc.Client, event girc.Event) { + b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EventRejoinChannels} + // set our correct nick on reconnect if necessary + b.Nick = event.Source.Name + }) + } +} + +func (b *Birc) doSend() { + rate := time.Millisecond * time.Duration(b.MessageDelay) + throttle := time.NewTicker(rate) + for msg := range b.Local { + <-throttle.C + username := msg.Username + if b.GetBool("Colornicks") { + checksum := crc32.ChecksumIEEE([]byte(msg.Username)) + colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes + username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username) + } + if msg.Event == config.EventUserAction { + b.i.Cmd.Action(msg.Channel, username+msg.Text) + } else { + b.Log.Debugf("Sending to channel %s", msg.Channel) + b.i.Cmd.Message(msg.Channel, username+msg.Text) + } + } +} + +// validateInput validates the server/port/nick configuration. Returns a *girc.Client if successful +func (b *Birc) getClient() (*girc.Client, error) { + server, portstr, err := net.SplitHostPort(b.GetString("Server")) + if err != nil { + return nil, err + } port, err := strconv.Atoi(portstr) if err != nil { - return err + return nil, err } // fix strict user handling of girc user := b.GetString("Nick") @@ -101,268 +245,28 @@ func (b *Birc) Connect() error { User: user, Name: b.GetString("Nick"), SSL: b.GetBool("UseTLS"), - TLSConfig: &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, + TLSConfig: &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec PingDelay: time.Minute, }) - - if b.GetBool("UseSASL") { - i.Config.SASL = &girc.SASLPlain{b.GetString("NickServNick"), b.GetString("NickServPassword")} - } - - i.Handlers.Add(girc.RPL_WELCOME, b.handleNewConnection) - i.Handlers.Add(girc.RPL_ENDOFMOTD, b.handleOtherAuth) - i.Handlers.Add(girc.ALL_EVENTS, b.handleOther) - go func() { - for { - if err := i.Connect(); err != nil { - b.Log.Errorf("disconnect: error: %s", err) - } else { - b.Log.Info("disconnect: client requested quit") - } - - b.Log.Info("reconnecting in 30 seconds...") - time.Sleep(30 * time.Second) - i.Handlers.Clear(girc.RPL_WELCOME) - i.Handlers.Add(girc.RPL_WELCOME, func(client *girc.Client, event girc.Event) { - b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS} - // set our correct nick on reconnect if necessary - b.Nick = event.Source.Name - }) - } - }() - b.i = i - select { - case <-b.connected: - b.Log.Info("Connection succeeded") - case <-time.After(time.Second * 30): - return fmt.Errorf("connection timed out") - } - //i.Debug = false - if b.GetInt("DebugLevel") == 0 { - i.Handlers.Clear(girc.ALL_EVENTS) - } - go b.doSend() - return nil -} - -func (b *Birc) Disconnect() error { - b.i.Close() - close(b.Local) - return nil -} - -func (b *Birc) JoinChannel(channel config.ChannelInfo) error { - if channel.Options.Key != "" { - b.Log.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name) - b.i.Cmd.JoinKey(channel.Name, channel.Options.Key) - } else { - b.i.Cmd.Join(channel.Name) - } - return nil -} - -func (b *Birc) Send(msg config.Message) (string, error) { - // ignore delete messages - if msg.Event == config.EVENT_MSG_DELETE { - return "", nil - } - - b.Log.Debugf("=> Receiving %#v", msg) - - // we can be in between reconnects #385 - if !b.i.IsConnected() { - b.Log.Error("Not connected to server, dropping message") - } - - // Execute a command - if strings.HasPrefix(msg.Text, "!") { - b.Command(&msg) - } - - // convert to specified charset - if b.GetString("Charset") != "" { - switch b.GetString("Charset") { - case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp": - msg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), msg.Text) - default: - buf := new(bytes.Buffer) - w, err := charset.NewWriter(b.GetString("Charset"), buf) - if err != nil { - b.Log.Errorf("charset from utf-8 conversion failed: %s", err) - return "", err - } - fmt.Fprint(w, msg.Text) - w.Close() - msg.Text = buf.String() - } - } - - // Handle files - if msg.Extra != nil { - for _, rmsg := range helper.HandleExtra(&msg, b.General) { - b.Local <- rmsg - } - if len(msg.Extra["file"]) > 0 { - for _, f := range msg.Extra["file"] { - fi := f.(config.FileInfo) - if fi.Comment != "" { - msg.Text += fi.Comment + ": " - } - if fi.URL != "" { - msg.Text = fi.URL - if fi.Comment != "" { - msg.Text = fi.Comment + ": " + fi.URL - } - } - b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event} - } - return "", nil - } - } - - // split long messages on messageLength, to avoid clipped messages #281 - if b.GetBool("MessageSplit") { - msg.Text = helper.SplitStringLength(msg.Text, b.MessageLength) - } - for _, text := range strings.Split(msg.Text, "\n") { - if len(text) > b.MessageLength { - text = text[:b.MessageLength-len(" ")] - if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError { - text = text[:len(text)-size] - } - text += " " - } - if len(b.Local) < b.MessageQueue { - if len(b.Local) == b.MessageQueue-1 { - text = text + " " - } - b.Local <- config.Message{Text: text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event} - } else { - b.Log.Debugf("flooding, dropping message (queue at %d)", len(b.Local)) - } - } - return "", nil -} - -func (b *Birc) doSend() { - rate := time.Millisecond * time.Duration(b.MessageDelay) - throttle := time.NewTicker(rate) - for msg := range b.Local { - <-throttle.C - username := msg.Username - if b.GetBool("Colornicks") { - checksum := crc32.ChecksumIEEE([]byte(msg.Username)) - colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes - username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username) - } - if msg.Event == config.EVENT_USER_ACTION { - b.i.Cmd.Action(msg.Channel, username+msg.Text) - } else { - b.Log.Debugf("Sending to channel %s", msg.Channel) - b.i.Cmd.Message(msg.Channel, username+msg.Text) - } - } + return i, nil } func (b *Birc) endNames(client *girc.Client, event girc.Event) { channel := event.Params[1] sort.Strings(b.names[channel]) maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow() - continued := false for len(b.names[channel]) > maxNamesPerPost { - b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost], continued), + b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]), Channel: channel, Account: b.Account} b.names[channel] = b.names[channel][maxNamesPerPost:] - continued = true } - b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel], continued), + b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel]), Channel: channel, Account: b.Account} b.names[channel] = nil b.i.Handlers.Clear(girc.RPL_NAMREPLY) b.i.Handlers.Clear(girc.RPL_ENDOFNAMES) } -func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) { - b.Log.Debug("Registering callbacks") - i := b.i - b.Nick = event.Params[0] - - i.Handlers.Add(girc.RPL_ENDOFMOTD, b.handleOtherAuth) - i.Handlers.Add("PRIVMSG", b.handlePrivMsg) - i.Handlers.Add("CTCP_ACTION", b.handlePrivMsg) - i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime) - i.Handlers.Add(girc.NOTICE, b.handleNotice) - i.Handlers.Add("JOIN", b.handleJoinPart) - i.Handlers.Add("PART", b.handleJoinPart) - i.Handlers.Add("QUIT", b.handleJoinPart) - i.Handlers.Add("KICK", b.handleJoinPart) - // we are now fully connected - b.connected <- struct{}{} -} - -func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) { - if len(event.Params) == 0 { - b.Log.Debugf("handleJoinPart: empty Params? %#v", event) - return - } - channel := strings.ToLower(event.Params[0]) - if event.Command == "KICK" && event.Params[1] == b.Nick { - b.Log.Infof("Got kicked from %s by %s", channel, event.Source.Name) - time.Sleep(time.Duration(b.GetInt("RejoinDelay")) * time.Second) - b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS} - return - } - if event.Command == "QUIT" { - if event.Source.Name == b.Nick && strings.Contains(event.Trailing, "Ping timeout") { - b.Log.Infof("%s reconnecting ..", b.Account) - b.Remote <- config.Message{Username: "system", Text: "reconnect", Channel: channel, Account: b.Account, Event: config.EVENT_FAILURE} - return - } - } - if event.Source.Name != b.Nick { - if b.GetBool("nosendjoinpart") { - return - } - b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account) - msg := config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE} - b.Log.Debugf("<= Message is %#v", msg) - b.Remote <- msg - return - } - b.Log.Debugf("handle %#v", event) -} - -func (b *Birc) handleNotice(client *girc.Client, event girc.Event) { - if strings.Contains(event.String(), "This nickname is registered") && event.Source.Name == b.GetString("NickServNick") { - b.Log.Debugf("Sending identify to nickserv %s", b.GetString("NickServNick")) - b.i.Cmd.Message(b.GetString("NickServNick"), "IDENTIFY "+b.GetString("NickServPassword")) - } else { - b.handlePrivMsg(client, event) - } -} - -func (b *Birc) handleOther(client *girc.Client, event girc.Event) { - if b.GetInt("DebugLevel") == 1 { - if event.Command != "CLIENT_STATE_UPDATED" && - event.Command != "CLIENT_GENERAL_UPDATED" { - b.Log.Debugf("%#v", event.String()) - } - return - } - switch event.Command { - case "372", "375", "376", "250", "251", "252", "253", "254", "255", "265", "266", "002", "003", "004", "005": - return - } - b.Log.Debugf("%#v", event.String()) -} - -func (b *Birc) handleOtherAuth(client *girc.Client, event girc.Event) { - if strings.EqualFold(b.GetString("NickServNick"), "Q@CServe.quakenet.org") { - b.Log.Debugf("Authenticating %s against %s", b.GetString("NickServUsername"), b.GetString("NickServNick")) - b.i.Cmd.Message(b.GetString("NickServNick"), "AUTH "+b.GetString("NickServUsername")+" "+b.GetString("NickServPassword")) - } -} - func (b *Birc) skipPrivMsg(event girc.Event) bool { // Our nick can be changed b.Nick = b.i.GetNick() @@ -382,74 +286,6 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool { return false } -func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) { - if b.skipPrivMsg(event) { - return - } - rmsg := config.Message{Username: event.Source.Name, Channel: strings.ToLower(event.Params[0]), Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host} - b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Trailing, event) - - // set action event - if event.IsAction() { - rmsg.Event = config.EVENT_USER_ACTION - } - - // strip action, we made an event if it was an action - rmsg.Text += event.StripAction() - - // strip IRC colors - re := regexp.MustCompile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`) - rmsg.Text = re.ReplaceAllString(rmsg.Text, "") - - // start detecting the charset - var r io.Reader - var err error - mycharset := b.GetString("Charset") - if mycharset == "" { - // detect what were sending so that we convert it to utf-8 - detector := chardet.NewTextDetector() - result, err := detector.DetectBest([]byte(rmsg.Text)) - if err != nil { - b.Log.Infof("detection failed for rmsg.Text: %#v", rmsg.Text) - return - } - b.Log.Debugf("detected %s confidence %#v", result.Charset, result.Confidence) - mycharset = result.Charset - // if we're not sure, just pick ISO-8859-1 - if result.Confidence < 80 { - mycharset = "ISO-8859-1" - } - } - switch mycharset { - case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp": - rmsg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), rmsg.Text) - default: - r, err = charset.NewReader(mycharset, strings.NewReader(rmsg.Text)) - if err != nil { - b.Log.Errorf("charset to utf-8 conversion failed: %s", err) - return - } - output, _ := ioutil.ReadAll(r) - rmsg.Text = string(output) - } - - b.Log.Debugf("<= Sending message from %s on %s to gateway", event.Params[0], b.Account) - b.Remote <- rmsg -} - -func (b *Birc) handleTopicWhoTime(client *girc.Client, event girc.Event) { - parts := strings.Split(event.Params[2], "!") - t, err := strconv.ParseInt(event.Params[3], 10, 64) - if err != nil { - b.Log.Errorf("Invalid time stamp: %s", event.Params[3]) - } - user := parts[0] - if len(parts) > 1 { - user += " [" + parts[1] + "]" - } - b.Log.Debugf("%s: Topic set by %s [%s]", event.Command, user, time.Unix(t, 0)) -} - func (b *Birc) nicksPerRow() int { return 4 } @@ -461,6 +297,6 @@ func (b *Birc) storeNames(client *girc.Client, event girc.Event) { strings.Split(strings.TrimSpace(event.Trailing), " ")...) } -func (b *Birc) formatnicks(nicks []string, continued bool) string { - return plainformatter(nicks, b.nicksPerRow()) +func (b *Birc) formatnicks(nicks []string) string { + return strings.Join(nicks, ", ") + " currently on IRC" } diff --git a/bridge/matrix/matrix.go b/bridge/matrix/matrix.go index 95ba6b42..6ac41d53 100644 --- a/bridge/matrix/matrix.go +++ b/bridge/matrix/matrix.go @@ -72,9 +72,12 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) { b.Log.Debugf("Channel %s maps to channel id %s", msg.Channel, channel) // Make a action /me of the message - if msg.Event == config.EVENT_USER_ACTION { - resp, err := b.mc.SendMessageEvent(channel, "m.room.message", - matrix.TextMessage{"m.emote", msg.Username + msg.Text}) + if msg.Event == config.EventUserAction { + m := matrix.TextMessage{ + MsgType: "m.emote", + Body: msg.Username + msg.Text, + } + resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m) if err != nil { return "", err } @@ -82,7 +85,7 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) { } // Delete message - if msg.Event == config.EVENT_MSG_DELETE { + if msg.Event == config.EventMsgDelete { if msg.ID == "" { return "", nil } @@ -96,11 +99,13 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) { // Upload a file if it exists if msg.Extra != nil { for _, rmsg := range helper.HandleExtra(&msg, b.General) { - b.mc.SendText(channel, rmsg.Username+rmsg.Text) + if _, err := b.mc.SendText(channel, rmsg.Username+rmsg.Text); err != nil { + b.Log.Errorf("sendText failed: %s", err) + } } // check if we have files to upload (from slack, telegram or mattermost) if len(msg.Extra["file"]) > 0 { - return b.handleUploadFile(&msg, channel) + return b.handleUploadFiles(&msg, channel) } } @@ -126,7 +131,7 @@ func (b *Bmatrix) getRoomID(channel string) string { return "" } -func (b *Bmatrix) handlematrix() error { +func (b *Bmatrix) handlematrix() { syncer := b.mc.Syncer.(*matrix.DefaultSyncer) syncer.OnEventType("m.room.redaction", b.handleEvent) syncer.OnEventType("m.room.message", b.handleEvent) @@ -137,7 +142,6 @@ func (b *Bmatrix) handlematrix() error { } } }() - return nil } func (b *Bmatrix) handleEvent(ev *matrix.Event) { @@ -158,7 +162,8 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) { // Text must be a string if rmsg.Text, ok = ev.Content["body"].(string); !ok { - b.Log.Errorf("Content[body] wasn't a %T ?", rmsg.Text) + b.Log.Errorf("Content[body] is not a string: %T\n%#v", + ev.Content["body"], ev.Content) return } @@ -170,16 +175,16 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) { // Delete event if ev.Type == "m.room.redaction" { - rmsg.Event = config.EVENT_MSG_DELETE + rmsg.Event = config.EventMsgDelete rmsg.ID = ev.Redacts - rmsg.Text = config.EVENT_MSG_DELETE + rmsg.Text = config.EventMsgDelete b.Remote <- rmsg return } // Do we have a /me action if ev.Content["msgtype"].(string) == "m.emote" { - rmsg.Event = config.EVENT_USER_ACTION + rmsg.Event = config.EventUserAction } // Do we have attachments @@ -231,11 +236,11 @@ func (b *Bmatrix) handleDownloadFile(rmsg *config.Message, content map[string]in if msgtype == "m.image" { mext, _ := mime.ExtensionsByType(mtype) if len(mext) > 0 { - name = name + mext[0] + name += mext[0] } } else { // just a default .png extension if we don't have mime info - name = name + ".png" + name += ".png" } } @@ -254,47 +259,54 @@ func (b *Bmatrix) handleDownloadFile(rmsg *config.Message, content map[string]in return nil } -// handleUploadFile handles native upload of files -func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string) (string, error) { +// handleUploadFiles handles native upload of files. +func (b *Bmatrix) handleUploadFiles(msg *config.Message, channel string) (string, error) { for _, f := range msg.Extra["file"] { - fi := f.(config.FileInfo) - content := bytes.NewReader(*fi.Data) - sp := strings.Split(fi.Name, ".") - mtype := mime.TypeByExtension("." + sp[len(sp)-1]) - if strings.Contains(mtype, "image") || - strings.Contains(mtype, "video") { - if fi.Comment != "" { - _, err := b.mc.SendText(channel, msg.Username+fi.Comment) - if err != nil { - b.Log.Errorf("file comment failed: %#v", err) - } - } - b.Log.Debugf("uploading file: %s %s", fi.Name, mtype) - res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data))) - if err != nil { - b.Log.Errorf("file upload failed: %#v", err) - continue - } - if strings.Contains(mtype, "video") { - b.Log.Debugf("sendVideo %s", res.ContentURI) - _, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI) - if err != nil { - b.Log.Errorf("sendVideo failed: %#v", err) - } - } - if strings.Contains(mtype, "image") { - b.Log.Debugf("sendImage %s", res.ContentURI) - _, err = b.mc.SendImage(channel, fi.Name, res.ContentURI) - if err != nil { - b.Log.Errorf("sendImage failed: %#v", err) - } - } - b.Log.Debugf("result: %#v", res) + if fi, ok := f.(config.FileInfo); ok { + b.handleUploadFile(msg, channel, &fi) } } return "", nil } +// handleUploadFile handles native upload of a file. +func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *config.FileInfo) { + content := bytes.NewReader(*fi.Data) + sp := strings.Split(fi.Name, ".") + mtype := mime.TypeByExtension("." + sp[len(sp)-1]) + if !strings.Contains(mtype, "image") && !strings.Contains(mtype, "video") { + return + } + if fi.Comment != "" { + _, err := b.mc.SendText(channel, msg.Username+fi.Comment) + if err != nil { + b.Log.Errorf("file comment failed: %#v", err) + } + } + b.Log.Debugf("uploading file: %s %s", fi.Name, mtype) + res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data))) + if err != nil { + b.Log.Errorf("file upload failed: %#v", err) + return + } + + switch { + case strings.Contains(mtype, "video"): + b.Log.Debugf("sendVideo %s", res.ContentURI) + _, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI) + if err != nil { + b.Log.Errorf("sendVideo failed: %#v", err) + } + case strings.Contains(mtype, "image"): + b.Log.Debugf("sendImage %s", res.ContentURI) + _, err = b.mc.SendImage(channel, fi.Name, res.ContentURI) + if err != nil { + b.Log.Errorf("sendImage failed: %#v", err) + } + } + b.Log.Debugf("result: %#v", res) +} + // skipMessages returns true if this message should not be handled func (b *Bmatrix) containsAttachment(content map[string]interface{}) bool { // Skip empty messages diff --git a/bridge/mattermost/handlers.go b/bridge/mattermost/handlers.go new file mode 100644 index 00000000..d75e8d40 --- /dev/null +++ b/bridge/mattermost/handlers.go @@ -0,0 +1,194 @@ +package bmattermost + +import ( + "github.com/42wim/matterbridge/bridge/config" + "github.com/42wim/matterbridge/bridge/helper" + "github.com/42wim/matterbridge/matterclient" + "github.com/mattermost/mattermost-server/model" +) + +// handleDownloadAvatar downloads the avatar of userid from channel +// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful. +// logs an error message if it fails +func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) { + rmsg := config.Message{ + Username: "system", + Text: "avatar", + Channel: channel, + Account: b.Account, + UserID: userid, + Event: config.EventAvatarDownload, + Extra: make(map[string][]interface{}), + } + if _, ok := b.avatarMap[userid]; !ok { + data, resp := b.mc.Client.GetProfileImage(userid, "") + if resp.Error != nil { + b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error) + return + } + err := helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General) + if err != nil { + b.Log.Error(err) + return + } + helper.HandleDownloadData(b.Log, &rmsg, userid+".png", rmsg.Text, "", &data, b.General) + b.Remote <- rmsg + } +} + +// handleDownloadFile handles file download +func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error { + url, _ := b.mc.Client.GetFileLink(id) + finfo, resp := b.mc.Client.GetFileInfo(id) + if resp.Error != nil { + return resp.Error + } + err := helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General) + if err != nil { + return err + } + data, resp := b.mc.Client.DownloadFile(id, true) + if resp.Error != nil { + return resp.Error + } + helper.HandleDownloadData(b.Log, rmsg, finfo.Name, rmsg.Text, url, &data, b.General) + return nil +} + +func (b *Bmattermost) handleMatter() { + messages := make(chan *config.Message) + if b.GetString("WebhookBindAddress") != "" { + b.Log.Debugf("Choosing webhooks based receiving") + go b.handleMatterHook(messages) + } else { + if b.GetString("Token") != "" { + b.Log.Debugf("Choosing token based receiving") + } else { + b.Log.Debugf("Choosing login/password based receiving") + } + go b.handleMatterClient(messages) + } + var ok bool + for message := range messages { + message.Avatar = helper.GetAvatar(b.avatarMap, message.UserID, b.General) + message.Account = b.Account + message.Text, ok = b.replaceAction(message.Text) + if ok { + message.Event = config.EventUserAction + } + b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account) + b.Log.Debugf("<= Message is %#v", message) + b.Remote <- *message + } +} + +func (b *Bmattermost) handleMatterClient(messages chan *config.Message) { + for message := range b.mc.MessageChan { + b.Log.Debugf("%#v", message.Raw.Data) + + if b.skipMessage(message) { + b.Log.Debugf("Skipped message: %#v", message) + continue + } + + // only download avatars if we have a place to upload them (configured mediaserver) + if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" { + b.handleDownloadAvatar(message.UserID, message.Channel) + } + + b.Log.Debugf("== Receiving event %#v", message) + + rmsg := &config.Message{ + Username: message.Username, + UserID: message.UserID, + Channel: message.Channel, + Text: message.Text, + ID: message.Post.Id, + Extra: make(map[string][]interface{}), + } + + // handle mattermost post properties (override username and attachments) + b.handleProps(rmsg, message) + + // create a text for bridges that don't support native editing + if message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED && !b.GetBool("EditDisable") { + rmsg.Text = message.Text + b.GetString("EditSuffix") + } + + if message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED { + rmsg.Event = config.EventMsgDelete + } + + for _, id := range message.Post.FileIds { + err := b.handleDownloadFile(rmsg, id) + if err != nil { + b.Log.Errorf("download failed: %s", err) + } + } + + // Use nickname instead of username if defined + if nick := b.mc.GetNickName(rmsg.UserID); nick != "" { + rmsg.Username = nick + } + + messages <- rmsg + } +} + +func (b *Bmattermost) handleMatterHook(messages chan *config.Message) { + for { + message := b.mh.Receive() + b.Log.Debugf("Receiving from matterhook %#v", message) + messages <- &config.Message{ + UserID: message.UserID, + Username: message.UserName, + Text: message.Text, + Channel: message.ChannelName, + } + } +} + +// handleUploadFile handles native upload of files +func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) { + var err error + var res, id string + channelID := b.mc.GetChannelId(msg.Channel, b.TeamID) + for _, f := range msg.Extra["file"] { + fi := f.(config.FileInfo) + id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name) + if err != nil { + return "", err + } + msg.Text = fi.Comment + if b.GetBool("PrefixMessagesWithNick") { + msg.Text = msg.Username + msg.Text + } + res, err = b.mc.PostMessageWithFiles(channelID, msg.Text, []string{id}) + } + return res, err +} + +func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Message) { + props := message.Post.Props + if props == nil { + return + } + if _, ok := props["override_username"].(string); ok { + rmsg.Username = props["override_username"].(string) + } + if _, ok := props["attachments"].([]interface{}); ok { + rmsg.Extra["attachments"] = props["attachments"].([]interface{}) + if rmsg.Text == "" { + for _, attachment := range rmsg.Extra["attachments"] { + attach := attachment.(map[string]interface{}) + if attach["text"].(string) != "" { + rmsg.Text += attach["text"].(string) + continue + } + if attach["fallback"].(string) != "" { + rmsg.Text += attach["fallback"].(string) + } + } + } + } +} diff --git a/bridge/mattermost/helpers.go b/bridge/mattermost/helpers.go new file mode 100644 index 00000000..c6117624 --- /dev/null +++ b/bridge/mattermost/helpers.go @@ -0,0 +1,218 @@ +package bmattermost + +import ( + "strings" + + "github.com/42wim/matterbridge/bridge/config" + "github.com/42wim/matterbridge/bridge/helper" + "github.com/42wim/matterbridge/matterclient" + "github.com/42wim/matterbridge/matterhook" + "github.com/mattermost/mattermost-server/model" +) + +func (b *Bmattermost) doConnectWebhookBind() error { + switch { + case b.GetString("WebhookURL") != "": + b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)") + b.mh = matterhook.New(b.GetString("WebhookURL"), + matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), + BindAddress: b.GetString("WebhookBindAddress")}) + case b.GetString("Token") != "": + b.Log.Info("Connecting using token (sending)") + err := b.apiLogin() + if err != nil { + return err + } + case b.GetString("Login") != "": + b.Log.Info("Connecting using login/password (sending)") + err := b.apiLogin() + if err != nil { + return err + } + default: + b.Log.Info("Connecting using webhookbindaddress (receiving)") + b.mh = matterhook.New(b.GetString("WebhookURL"), + matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), + BindAddress: b.GetString("WebhookBindAddress")}) + } + return nil +} + +func (b *Bmattermost) doConnectWebhookURL() error { + b.Log.Info("Connecting using webhookurl (sending)") + b.mh = matterhook.New(b.GetString("WebhookURL"), + matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), + DisableServer: true}) + if b.GetString("Token") != "" { + b.Log.Info("Connecting using token (receiving)") + err := b.apiLogin() + if err != nil { + return err + } + } else if b.GetString("Login") != "" { + b.Log.Info("Connecting using login/password (receiving)") + err := b.apiLogin() + if err != nil { + return err + } + } + return nil +} + +func (b *Bmattermost) apiLogin() error { + password := b.GetString("Password") + if b.GetString("Token") != "" { + password = "token=" + b.GetString("Token") + } + + b.mc = matterclient.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server")) + if b.GetBool("debug") { + b.mc.SetLogLevel("debug") + } + b.mc.SkipTLSVerify = b.GetBool("SkipTLSVerify") + b.mc.NoTLS = b.GetBool("NoTLS") + b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server")) + err := b.mc.Login() + if err != nil { + return err + } + b.Log.Info("Connection succeeded") + b.TeamID = b.mc.GetTeamId() + go b.mc.WsReceiver() + go b.mc.StatusLoop() + return nil +} + +// replaceAction replace the message with the correct action (/me) code +func (b *Bmattermost) replaceAction(text string) (string, bool) { + if strings.HasPrefix(text, "*") && strings.HasSuffix(text, "*") { + return strings.Replace(text, "*", "", -1), true + } + return text, false +} + +func (b *Bmattermost) cacheAvatar(msg *config.Message) (string, error) { + fi := msg.Extra["file"][0].(config.FileInfo) + /* if we have a sha we have successfully uploaded the file to the media server, + so we can now cache the sha */ + if fi.SHA != "" { + b.Log.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID) + b.avatarMap[msg.UserID] = fi.SHA + } + return "", nil +} + +// sendWebhook uses the configured WebhookURL to send the message +func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) { + // skip events + if msg.Event != "" { + return "", nil + } + + if b.GetBool("PrefixMessagesWithNick") { + msg.Text = msg.Username + msg.Text + } + if msg.Extra != nil { + // this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE + for _, rmsg := range helper.HandleExtra(&msg, b.General) { + rmsg := rmsg // scopelint + iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl")) + matterMessage := matterhook.OMessage{ + IconURL: iconURL, + Channel: rmsg.Channel, + UserName: rmsg.Username, + Text: rmsg.Text, + Props: make(map[string]interface{}), + } + matterMessage.Props["matterbridge_"+b.uuid] = true + if err := b.mh.Send(matterMessage); err != nil { + b.Log.Errorf("sendWebhook failed: %s ", err) + } + } + + // webhook doesn't support file uploads, so we add the url manually + if len(msg.Extra["file"]) > 0 { + for _, f := range msg.Extra["file"] { + fi := f.(config.FileInfo) + if fi.URL != "" { + msg.Text += fi.URL + } + } + } + } + + iconURL := config.GetIconURL(&msg, b.GetString("iconurl")) + matterMessage := matterhook.OMessage{ + IconURL: iconURL, + Channel: msg.Channel, + UserName: msg.Username, + Text: msg.Text, + Props: make(map[string]interface{}), + } + if msg.Avatar != "" { + matterMessage.IconURL = msg.Avatar + } + matterMessage.Props["matterbridge_"+b.uuid] = true + err := b.mh.Send(matterMessage) + if err != nil { + b.Log.Info(err) + return "", err + } + return "", nil +} + +// skipMessages returns true if this message should not be handled +func (b *Bmattermost) skipMessage(message *matterclient.Message) bool { + // Handle join/leave + if message.Type == "system_join_leave" || + message.Type == "system_join_channel" || + message.Type == "system_leave_channel" { + if b.GetBool("nosendjoinpart") { + return true + } + b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account) + b.Remote <- config.Message{ + Username: "system", + Text: message.Text, + Channel: message.Channel, + Account: b.Account, + Event: config.EventJoinLeave, + } + return true + } + + // Handle edited messages + if (message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED) && b.GetBool("EditDisable") { + return true + } + + // Ignore messages sent from matterbridge + if message.Post.Props != nil { + if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok { + b.Log.Debugf("sent by matterbridge, ignoring") + return true + } + } + + // Ignore messages sent from a user logged in as the bot + if b.mc.User.Username == message.Username { + return true + } + + // if the message has reactions don't repost it (for now, until we can correlate reaction with message) + if message.Post.HasReactions { + return true + } + + // ignore messages from other teams than ours + if message.Raw.Data["team_id"].(string) != b.TeamID { + return true + } + + // only handle posted, edited or deleted events + if !(message.Raw.Event == "posted" || message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED || + message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED) { + return true + } + return false +} diff --git a/bridge/mattermost/mattermost.go b/bridge/mattermost/mattermost.go index 12b5097f..b0dca0ba 100644 --- a/bridge/mattermost/mattermost.go +++ b/bridge/mattermost/mattermost.go @@ -3,7 +3,6 @@ package bmattermost import ( "errors" "fmt" - "strings" "github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge/config" @@ -22,6 +21,8 @@ type Bmattermost struct { avatarMap map[string]string } +const mattermostPlugin = "mattermost.plugin" + func New(cfg *bridge.Config) bridge.Bridger { b := &Bmattermost{Config: cfg, avatarMap: make(map[string]string)} b.uuid = xid.New().String() @@ -33,62 +34,31 @@ func (b *Bmattermost) Command(cmd string) string { } func (b *Bmattermost) Connect() error { + if b.Account == mattermostPlugin { + return nil + } if b.GetString("WebhookBindAddress") != "" { - if b.GetString("WebhookURL") != "" { - b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)") - b.mh = matterhook.New(b.GetString("WebhookURL"), - matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), - BindAddress: b.GetString("WebhookBindAddress")}) - } else if b.GetString("Token") != "" { - b.Log.Info("Connecting using token (sending)") - err := b.apiLogin() - if err != nil { - return err - } - } else if b.GetString("Login") != "" { - b.Log.Info("Connecting using login/password (sending)") - err := b.apiLogin() - if err != nil { - return err - } - } else { - b.Log.Info("Connecting using webhookbindaddress (receiving)") - b.mh = matterhook.New(b.GetString("WebhookURL"), - matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), - BindAddress: b.GetString("WebhookBindAddress")}) + if err := b.doConnectWebhookBind(); err != nil { + return err } go b.handleMatter() return nil } - if b.GetString("WebhookURL") != "" { - b.Log.Info("Connecting using webhookurl (sending)") - b.mh = matterhook.New(b.GetString("WebhookURL"), - matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), - DisableServer: true}) - if b.GetString("Token") != "" { - b.Log.Info("Connecting using token (receiving)") - err := b.apiLogin() - if err != nil { - return err - } - go b.handleMatter() - } else if b.GetString("Login") != "" { - b.Log.Info("Connecting using login/password (receiving)") - err := b.apiLogin() - if err != nil { - return err - } - go b.handleMatter() + switch { + case b.GetString("WebhookURL") != "": + if err := b.doConnectWebhookURL(); err != nil { + return err } + go b.handleMatter() return nil - } else if b.GetString("Token") != "" { + case b.GetString("Token") != "": b.Log.Info("Connecting using token (sending and receiving)") err := b.apiLogin() if err != nil { return err } go b.handleMatter() - } else if b.GetString("Login") != "" { + case b.GetString("Login") != "": b.Log.Info("Connecting using login/password (sending and receiving)") err := b.apiLogin() if err != nil { @@ -96,7 +66,8 @@ func (b *Bmattermost) Connect() error { } go b.handleMatter() } - if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" && b.GetString("Login") == "" && b.GetString("Token") == "" { + if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" && + b.GetString("Login") == "" && b.GetString("Token") == "" { return errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Token/Login/Password/Server/Team configured") } return nil @@ -107,9 +78,12 @@ func (b *Bmattermost) Disconnect() error { } func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error { + if b.Account == mattermostPlugin { + return nil + } // we can only join channels using the API if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" { - id := b.mc.GetChannelId(channel.Name, "") + id := b.mc.GetChannelId(channel.Name, b.TeamID) if id == "" { return fmt.Errorf("Could not find channel ID for channel %s", channel.Name) } @@ -119,15 +93,18 @@ func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error { } func (b *Bmattermost) Send(msg config.Message) (string, error) { + if b.Account == mattermostPlugin { + return "", nil + } b.Log.Debugf("=> Receiving %#v", msg) // Make a action /me of the message - if msg.Event == config.EVENT_USER_ACTION { + if msg.Event == config.EventUserAction { msg.Text = "*" + msg.Text + "*" } // map the file SHA to our user (caches the avatar) - if msg.Event == config.EVENT_AVATAR_DOWNLOAD { + if msg.Event == config.EventAvatarDownload { return b.cacheAvatar(&msg) } @@ -137,7 +114,7 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) { } // Delete message - if msg.Event == config.EVENT_MSG_DELETE { + if msg.Event == config.EventMsgDelete { if msg.ID == "" { return "", nil } @@ -147,7 +124,9 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) { // Upload a file if it exists if msg.Extra != nil { for _, rmsg := range helper.HandleExtra(&msg, b.General) { - b.mc.PostMessage(b.mc.GetChannelId(rmsg.Channel, ""), rmsg.Username+rmsg.Text) + if _, err := b.mc.PostMessage(b.mc.GetChannelId(rmsg.Channel, b.TeamID), rmsg.Username+rmsg.Text); err != nil { + b.Log.Errorf("PostMessage failed: %s", err) + } } if len(msg.Extra["file"]) > 0 { return b.handleUploadFile(&msg) @@ -165,303 +144,5 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) { } // Post normal message - return b.mc.PostMessage(b.mc.GetChannelId(msg.Channel, ""), msg.Text) -} - -func (b *Bmattermost) handleMatter() { - messages := make(chan *config.Message) - if b.GetString("WebhookBindAddress") != "" { - b.Log.Debugf("Choosing webhooks based receiving") - go b.handleMatterHook(messages) - } else { - if b.GetString("Token") != "" { - b.Log.Debugf("Choosing token based receiving") - } else { - b.Log.Debugf("Choosing login/password based receiving") - } - go b.handleMatterClient(messages) - } - var ok bool - for message := range messages { - message.Avatar = helper.GetAvatar(b.avatarMap, message.UserID, b.General) - message.Account = b.Account - message.Text, ok = b.replaceAction(message.Text) - if ok { - message.Event = config.EVENT_USER_ACTION - } - b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account) - b.Log.Debugf("<= Message is %#v", message) - b.Remote <- *message - } -} - -func (b *Bmattermost) handleMatterClient(messages chan *config.Message) { - for message := range b.mc.MessageChan { - b.Log.Debugf("%#v", message.Raw.Data) - - if b.skipMessage(message) { - b.Log.Debugf("Skipped message: %#v", message) - continue - } - - // only download avatars if we have a place to upload them (configured mediaserver) - if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" { - b.handleDownloadAvatar(message.UserID, message.Channel) - } - - b.Log.Debugf("== Receiving event %#v", message) - - rmsg := &config.Message{Username: message.Username, UserID: message.UserID, Channel: message.Channel, Text: message.Text, ID: message.Post.Id, Extra: make(map[string][]interface{})} - - // handle mattermost post properties (override username and attachments) - props := message.Post.Props - if props != nil { - if _, ok := props["override_username"].(string); ok { - rmsg.Username = props["override_username"].(string) - } - if _, ok := props["attachments"].([]interface{}); ok { - rmsg.Extra["attachments"] = props["attachments"].([]interface{}) - if rmsg.Text == "" { - for _, attachment := range rmsg.Extra["attachments"] { - attach := attachment.(map[string]interface{}) - if attach["text"].(string) != "" { - rmsg.Text += attach["text"].(string) - continue - } - if attach["fallback"].(string) != "" { - rmsg.Text += attach["fallback"].(string) - } - } - } - } - } - - // create a text for bridges that don't support native editing - if message.Raw.Event == "post_edited" && !b.GetBool("EditDisable") { - rmsg.Text = message.Text + b.GetString("EditSuffix") - } - - if message.Raw.Event == "post_deleted" { - rmsg.Event = config.EVENT_MSG_DELETE - } - - if len(message.Post.FileIds) > 0 { - for _, id := range message.Post.FileIds { - err := b.handleDownloadFile(rmsg, id) - if err != nil { - b.Log.Errorf("download failed: %s", err) - } - } - } - // Use nickname instead of username if defined - if nick := b.mc.GetNickName(rmsg.UserID); nick != "" { - rmsg.Username = nick - } - - messages <- rmsg - } -} - -func (b *Bmattermost) handleMatterHook(messages chan *config.Message) { - for { - message := b.mh.Receive() - b.Log.Debugf("Receiving from matterhook %#v", message) - messages <- &config.Message{UserID: message.UserID, Username: message.UserName, Text: message.Text, Channel: message.ChannelName} - } -} - -func (b *Bmattermost) apiLogin() error { - password := b.GetString("Password") - if b.GetString("Token") != "" { - password = "MMAUTHTOKEN=" + b.GetString("Token") - } - - b.mc = matterclient.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server")) - if b.GetBool("debug") { - b.mc.SetLogLevel("debug") - } - b.mc.SkipTLSVerify = b.GetBool("SkipTLSVerify") - b.mc.NoTLS = b.GetBool("NoTLS") - b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server")) - err := b.mc.Login() - if err != nil { - return err - } - b.Log.Info("Connection succeeded") - b.TeamID = b.mc.GetTeamId() - go b.mc.WsReceiver() - go b.mc.StatusLoop() - return nil -} - -// replaceAction replace the message with the correct action (/me) code -func (b *Bmattermost) replaceAction(text string) (string, bool) { - if strings.HasPrefix(text, "*") && strings.HasSuffix(text, "*") { - return strings.Replace(text, "*", "", -1), true - } - return text, false -} - -func (b *Bmattermost) cacheAvatar(msg *config.Message) (string, error) { - fi := msg.Extra["file"][0].(config.FileInfo) - /* if we have a sha we have successfully uploaded the file to the media server, - so we can now cache the sha */ - if fi.SHA != "" { - b.Log.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID) - b.avatarMap[msg.UserID] = fi.SHA - } - return "", nil -} - -// handleDownloadAvatar downloads the avatar of userid from channel -// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful. -// logs an error message if it fails -func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) { - rmsg := config.Message{Username: "system", Text: "avatar", Channel: channel, Account: b.Account, UserID: userid, Event: config.EVENT_AVATAR_DOWNLOAD, Extra: make(map[string][]interface{})} - if _, ok := b.avatarMap[userid]; !ok { - data, resp := b.mc.Client.GetProfileImage(userid, "") - if resp.Error != nil { - b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error) - return - } - err := helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General) - if err != nil { - b.Log.Error(err) - return - } - helper.HandleDownloadData(b.Log, &rmsg, userid+".png", rmsg.Text, "", &data, b.General) - b.Remote <- rmsg - } -} - -// handleDownloadFile handles file download -func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error { - url, _ := b.mc.Client.GetFileLink(id) - finfo, resp := b.mc.Client.GetFileInfo(id) - if resp.Error != nil { - return resp.Error - } - err := helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General) - if err != nil { - return err - } - data, resp := b.mc.Client.DownloadFile(id, true) - if resp.Error != nil { - return resp.Error - } - helper.HandleDownloadData(b.Log, rmsg, finfo.Name, rmsg.Text, url, &data, b.General) - return nil -} - -// handleUploadFile handles native upload of files -func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) { - var err error - var res, id string - channelID := b.mc.GetChannelId(msg.Channel, "") - for _, f := range msg.Extra["file"] { - fi := f.(config.FileInfo) - id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name) - if err != nil { - return "", err - } - msg.Text = fi.Comment - if b.GetBool("PrefixMessagesWithNick") { - msg.Text = msg.Username + msg.Text - } - res, err = b.mc.PostMessageWithFiles(channelID, msg.Text, []string{id}) - } - return res, err -} - -// sendWebhook uses the configured WebhookURL to send the message -func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) { - // skip events - if msg.Event != "" { - return "", nil - } - - if b.GetBool("PrefixMessagesWithNick") { - msg.Text = msg.Username + msg.Text - } - if msg.Extra != nil { - // this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE - for _, rmsg := range helper.HandleExtra(&msg, b.General) { - iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl")) - matterMessage := matterhook.OMessage{IconURL: iconURL, Channel: rmsg.Channel, UserName: rmsg.Username, Text: rmsg.Text, Props: make(map[string]interface{})} - matterMessage.Props["matterbridge_"+b.uuid] = true - b.mh.Send(matterMessage) - } - - // webhook doesn't support file uploads, so we add the url manually - if len(msg.Extra["file"]) > 0 { - for _, f := range msg.Extra["file"] { - fi := f.(config.FileInfo) - if fi.URL != "" { - msg.Text += fi.URL - } - } - } - } - - iconURL := config.GetIconURL(&msg, b.GetString("iconurl")) - matterMessage := matterhook.OMessage{IconURL: iconURL, Channel: msg.Channel, UserName: msg.Username, Text: msg.Text, Props: make(map[string]interface{})} - if msg.Avatar != "" { - matterMessage.IconURL = msg.Avatar - } - matterMessage.Props["matterbridge_"+b.uuid] = true - err := b.mh.Send(matterMessage) - if err != nil { - b.Log.Info(err) - return "", err - } - return "", nil -} - -// skipMessages returns true if this message should not be handled -func (b *Bmattermost) skipMessage(message *matterclient.Message) bool { - // Handle join/leave - if message.Type == "system_join_leave" || - message.Type == "system_join_channel" || - message.Type == "system_leave_channel" { - if b.GetBool("nosendjoinpart") { - return true - } - b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account) - b.Remote <- config.Message{Username: "system", Text: message.Text, Channel: message.Channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE} - return true - } - - // Handle edited messages - if (message.Raw.Event == "post_edited") && b.GetBool("EditDisable") { - return true - } - - // Ignore messages sent from matterbridge - if message.Post.Props != nil { - if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok { - b.Log.Debugf("sent by matterbridge, ignoring") - return true - } - } - - // Ignore messages sent from a user logged in as the bot - if b.mc.User.Username == message.Username { - return true - } - - // if the message has reactions don't repost it (for now, until we can correlate reaction with message) - if message.Post.HasReactions { - return true - } - - // ignore messages from other teams than ours - if message.Raw.Data["team_id"].(string) != b.TeamID { - return true - } - - // only handle posted, edited or deleted events - if !(message.Raw.Event == "posted" || message.Raw.Event == "post_edited" || message.Raw.Event == "post_deleted") { - return true - } - return false + return b.mc.PostMessage(b.mc.GetChannelId(msg.Channel, b.TeamID), msg.Text) } diff --git a/bridge/rocketchat/rocketchat.go b/bridge/rocketchat/rocketchat.go index 27930188..1dbc7be0 100644 --- a/bridge/rocketchat/rocketchat.go +++ b/bridge/rocketchat/rocketchat.go @@ -8,13 +8,9 @@ import ( "github.com/42wim/matterbridge/matterhook" ) -type MMhook struct { +type Brocketchat struct { mh *matterhook.Client rh *rockethook.Client -} - -type Brocketchat struct { - MMhook *bridge.Config } @@ -47,12 +43,13 @@ func (b *Brocketchat) JoinChannel(channel config.ChannelInfo) error { func (b *Brocketchat) Send(msg config.Message) (string, error) { // ignore delete messages - if msg.Event == config.EVENT_MSG_DELETE { + if msg.Event == config.EventMsgDelete { return "", nil } b.Log.Debugf("=> Receiving %#v", msg) if msg.Extra != nil { for _, rmsg := range helper.HandleExtra(&msg, b.General) { + rmsg := rmsg // scopelint iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl")) matterMessage := matterhook.OMessage{IconURL: iconURL, Channel: rmsg.Channel, UserName: rmsg.Username, Text: rmsg.Text} b.mh.Send(matterMessage) diff --git a/bridge/slack/handlers.go b/bridge/slack/handlers.go new file mode 100644 index 00000000..89c800da --- /dev/null +++ b/bridge/slack/handlers.go @@ -0,0 +1,316 @@ +package bslack + +import ( + "fmt" + "html" + "time" + + "github.com/42wim/matterbridge/bridge/config" + "github.com/42wim/matterbridge/bridge/helper" + "github.com/nlopes/slack" +) + +func (b *Bslack) handleSlack() { + messages := make(chan *config.Message) + if b.GetString(incomingWebhookConfig) != "" { + b.Log.Debugf("Choosing webhooks based receiving") + go b.handleMatterHook(messages) + } else { + b.Log.Debugf("Choosing token based receiving") + go b.handleSlackClient(messages) + } + time.Sleep(time.Second) + b.Log.Debug("Start listening for Slack messages") + for message := range messages { + if message.Event != config.EventUserTyping { + b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account) + } + + // cleanup the message + message.Text = b.replaceMention(message.Text) + message.Text = b.replaceVariable(message.Text) + message.Text = b.replaceChannel(message.Text) + message.Text = b.replaceURL(message.Text) + message.Text = html.UnescapeString(message.Text) + + // Add the avatar + message.Avatar = b.getAvatar(message.UserID) + + b.Log.Debugf("<= Message is %#v", message) + b.Remote <- *message + } +} + +func (b *Bslack) handleSlackClient(messages chan *config.Message) { + for msg := range b.rtm.IncomingEvents { + if msg.Type != sUserTyping && msg.Type != sLatencyReport { + b.Log.Debugf("== Receiving event %#v", msg.Data) + } + switch ev := msg.Data.(type) { + case *slack.UserTypingEvent: + if !b.GetBool("ShowUserTyping") { + continue + } + rmsg, err := b.handleTypingEvent(ev) + if err != nil { + b.Log.Errorf("%#v", err) + continue + } + + messages <- rmsg + case *slack.MessageEvent: + if b.skipMessageEvent(ev) { + b.Log.Debugf("Skipped message: %#v", ev) + continue + } + rmsg, err := b.handleMessageEvent(ev) + if err != nil { + b.Log.Errorf("%#v", err) + continue + } + messages <- rmsg + case *slack.OutgoingErrorEvent: + b.Log.Debugf("%#v", ev.Error()) + case *slack.ChannelJoinedEvent: + // When we join a channel we update the full list of users as + // well as the information for the channel that we joined as this + // should now tell that we are a member of it. + b.populateUsers() + + b.channelsMutex.Lock() + b.channelsByID[ev.Channel.ID] = &ev.Channel + b.channelsByName[ev.Channel.Name] = &ev.Channel + b.channelsMutex.Unlock() + case *slack.ConnectedEvent: + b.si = ev.Info + b.populateChannels() + b.populateUsers() + case *slack.InvalidAuthEvent: + b.Log.Fatalf("Invalid Token %#v", ev) + case *slack.ConnectionErrorEvent: + b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj) + default: + } + } +} + +func (b *Bslack) handleMatterHook(messages chan *config.Message) { + for { + message := b.mh.Receive() + b.Log.Debugf("receiving from matterhook (slack) %#v", message) + if message.UserName == "slackbot" { + continue + } + messages <- &config.Message{ + Username: message.UserName, + Text: message.Text, + Channel: message.ChannelName, + } + } +} + +// skipMessageEvent skips event that need to be skipped :-) +func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool { + switch ev.SubType { + case sChannelLeave, sChannelJoin: + return b.GetBool(noSendJoinConfig) + case sPinnedItem, sUnpinnedItem: + return true + case sChannelTopic, sChannelPurpose: + // Skip the event if our bot/user account changed the topic/purpose + if ev.User == b.si.User.ID { + return true + } + } + + // Skip any messages that we made ourselves or from 'slackbot' (see #527). + if ev.Username == sSlackBotUser || + (b.rtm != nil && ev.Username == b.si.User.Name) || + (len(ev.Attachments) > 0 && ev.Attachments[0].CallbackID == "matterbridge_"+b.uuid) { + return true + } + + // It seems ev.SubMessage.Edited == nil when slack unfurls. + // Do not forward these messages. See Github issue #266. + if ev.SubMessage != nil && + ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp && + ev.SubMessage.Edited == nil { + return true + } + + if len(ev.Files) > 0 { + return b.filesCached(ev.Files) + } + return false +} + +func (b *Bslack) filesCached(files []slack.File) bool { + for i := range files { + if !b.fileCached(&files[i]) { + return false + } + } + return true +} + +// handleMessageEvent handles the message events. Together with any called sub-methods, +// this method implements the following event processing pipeline: +// +// 1. Check if the message should be ignored. +// NOTE: This is not actually part of the method below but is done just before it +// is called via the 'skipMessageEvent()' method. +// 2. Populate the Matterbridge message that will be sent to the router based on the +// received event and logic that is common to all events that are not skipped. +// 3. Detect and handle any message that is "status" related (think join channel, etc.). +// This might result in an early exit from the pipeline and passing of the +// pre-populated message to the Matterbridge router. +// 4. Handle the specific case of messages that edit existing messages depending on +// configuration. +// 5. Handle any attachments of the received event. +// 6. Check that the Matterbridge message that we end up with after at the end of the +// pipeline is valid before sending it to the Matterbridge router. +func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, error) { + rmsg, err := b.populateReceivedMessage(ev) + if err != nil { + return nil, err + } + + // Handle some message types early. + if b.handleStatusEvent(ev, rmsg) { + return rmsg, nil + } + + b.handleAttachments(ev, rmsg) + + // Verify that we have the right information and the message + // is well-formed before sending it out to the router. + if len(ev.Files) == 0 && (rmsg.Text == "" || rmsg.Username == "") { + if ev.BotID != "" { + // This is probably a webhook we couldn't resolve. + return nil, fmt.Errorf("message handling resulted in an empty bot message (probably an incoming webhook we couldn't resolve): %#v", ev) + } + return nil, fmt.Errorf("message handling resulted in an empty message: %#v", ev) + } + return rmsg, nil +} + +func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) bool { + switch ev.SubType { + case sChannelJoined, sMemberJoined: + b.populateUsers() + // There's no further processing needed on channel events + // so we return 'true'. + return true + case sChannelJoin, sChannelLeave: + rmsg.Username = sSystemUser + rmsg.Event = config.EventJoinLeave + case sChannelTopic, sChannelPurpose: + b.populateChannels() + rmsg.Event = config.EventTopicChange + case sMessageChanged: + rmsg.Text = ev.SubMessage.Text + // handle deleted thread starting messages + if ev.SubMessage.Text == "This message was deleted." { + rmsg.Event = config.EventMsgDelete + return true + } + case sMessageDeleted: + rmsg.Text = config.EventMsgDelete + rmsg.Event = config.EventMsgDelete + rmsg.ID = ev.DeletedTimestamp + // If a message is being deleted we do not need to process + // the event any further so we return 'true'. + return true + case sMeMessage: + rmsg.Event = config.EventUserAction + } + return false +} + +func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) { + // File comments are set by the system (because there is no username given). + if ev.SubType == sFileComment { + rmsg.Username = sSystemUser + } + + // See if we have some text in the attachments. + if rmsg.Text == "" { + for _, attach := range ev.Attachments { + if attach.Text != "" { + if attach.Title != "" { + rmsg.Text = attach.Title + "\n" + } + rmsg.Text += attach.Text + } else { + rmsg.Text = attach.Fallback + } + } + } + + // Save the attachments, so that we can send them to other slack (compatible) bridges. + if len(ev.Attachments) > 0 { + rmsg.Extra[sSlackAttachment] = append(rmsg.Extra[sSlackAttachment], ev.Attachments) + } + + // If we have files attached, download them (in memory) and put a pointer to it in msg.Extra. + for i := range ev.Files { + if err := b.handleDownloadFile(rmsg, &ev.Files[i]); err != nil { + b.Log.Errorf("Could not download incoming file: %#v", err) + } + } +} + +func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message, error) { + channelInfo, err := b.getChannelByID(ev.Channel) + if err != nil { + return nil, err + } + return &config.Message{ + Channel: channelInfo.Name, + Account: b.Account, + Event: config.EventUserTyping, + }, nil +} + +// handleDownloadFile handles file download +func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File) error { + if b.fileCached(file) { + return nil + } + // Check that the file is neither too large nor blacklisted. + if err := helper.HandleDownloadSize(b.Log, rmsg, file.Name, int64(file.Size), b.General); err != nil { + b.Log.WithError(err).Infof("Skipping download of incoming file.") + return nil + } + + // Actually download the file. + data, err := helper.DownloadFileAuth(file.URLPrivateDownload, "Bearer "+b.GetString(tokenConfig)) + if err != nil { + return fmt.Errorf("download %s failed %#v", file.URLPrivateDownload, err) + } + + // If a comment is attached to the file(s) it is in the 'Text' field of the Slack messge event + // and should be added as comment to only one of the files. We reset the 'Text' field to ensure + // that the comment is not duplicated. + comment := rmsg.Text + rmsg.Text = "" + helper.HandleDownloadData(b.Log, rmsg, file.Name, comment, file.URLPrivateDownload, data, b.General) + return nil +} + +// fileCached implements Matterbridge's caching logic for files +// shared via Slack. +// +// We consider that a file was cached if its ID was added in the last minute or +// it's name was registered in the last 10 seconds. This ensures that an +// identically named file but with different content will be uploaded correctly +// (the assumption is that such name collisions will not occur within the given +// timeframes). +func (b *Bslack) fileCached(file *slack.File) bool { + if ts, ok := b.cache.Get("file" + file.ID); ok && time.Since(ts.(time.Time)) < time.Minute { + return true + } else if ts, ok = b.cache.Get("filename" + file.Name); ok && time.Since(ts.(time.Time)) < 10*time.Second { + return true + } + return false +} diff --git a/bridge/slack/helpers.go b/bridge/slack/helpers.go new file mode 100644 index 00000000..ff039c58 --- /dev/null +++ b/bridge/slack/helpers.go @@ -0,0 +1,338 @@ +package bslack + +import ( + "context" + "fmt" + "regexp" + "strings" + "time" + + "github.com/42wim/matterbridge/bridge/config" + "github.com/nlopes/slack" +) + +func (b *Bslack) getUser(id string) *slack.User { + b.usersMutex.RLock() + defer b.usersMutex.RUnlock() + + return b.users[id] +} + +func (b *Bslack) getUsername(id string) string { + if user := b.getUser(id); user != nil { + if user.Profile.DisplayName != "" { + return user.Profile.DisplayName + } + return user.Name + } + b.Log.Warnf("Could not find user with ID '%s'", id) + return "" +} + +func (b *Bslack) getAvatar(id string) string { + if user := b.getUser(id); user != nil { + return user.Profile.Image48 + } + return "" +} + +func (b *Bslack) getChannel(channel string) (*slack.Channel, error) { + if strings.HasPrefix(channel, "ID:") { + return b.getChannelByID(strings.TrimPrefix(channel, "ID:")) + } + return b.getChannelByName(channel) +} + +func (b *Bslack) getChannelByName(name string) (*slack.Channel, error) { + return b.getChannelBy(name, b.channelsByName) +} + +func (b *Bslack) getChannelByID(ID string) (*slack.Channel, error) { + return b.getChannelBy(ID, b.channelsByID) +} + +func (b *Bslack) getChannelBy(lookupKey string, lookupMap map[string]*slack.Channel) (*slack.Channel, error) { + b.channelsMutex.RLock() + defer b.channelsMutex.RUnlock() + + if channel, ok := lookupMap[lookupKey]; ok { + return channel, nil + } + return nil, fmt.Errorf("%s: channel %s not found", b.Account, lookupKey) +} + +const minimumRefreshInterval = 10 * time.Second + +func (b *Bslack) populateUsers() { + b.refreshMutex.Lock() + if time.Now().Before(b.earliestUserRefresh) || b.refreshInProgress { + b.Log.Debugf("Not refreshing user list as it was done less than %v ago.", + minimumRefreshInterval) + b.refreshMutex.Unlock() + + return + } + b.refreshInProgress = true + b.refreshMutex.Unlock() + + newUsers := map[string]*slack.User{} + pagination := b.sc.GetUsersPaginated(slack.GetUsersOptionLimit(200)) + for { + var err error + pagination, err = pagination.Next(context.Background()) + if err != nil { + if pagination.Done(err) { + break + } + + if err = b.handleRateLimit(err); err != nil { + b.Log.Errorf("Could not retrieve users: %#v", err) + return + } + continue + } + + for i := range pagination.Users { + newUsers[pagination.Users[i].ID] = &pagination.Users[i] + } + } + + b.usersMutex.Lock() + defer b.usersMutex.Unlock() + b.users = newUsers + + b.refreshMutex.Lock() + defer b.refreshMutex.Unlock() + b.earliestUserRefresh = time.Now().Add(minimumRefreshInterval) + b.refreshInProgress = false +} + +func (b *Bslack) populateChannels() { + b.refreshMutex.Lock() + if time.Now().Before(b.earliestChannelRefresh) || b.refreshInProgress { + b.Log.Debugf("Not refreshing channel list as it was done less than %v seconds ago.", + minimumRefreshInterval) + b.refreshMutex.Unlock() + return + } + b.refreshInProgress = true + b.refreshMutex.Unlock() + + newChannelsByID := map[string]*slack.Channel{} + newChannelsByName := map[string]*slack.Channel{} + + // We only retrieve public and private channels, not IMs + // and MPIMs as those do not have a channel name. + queryParams := &slack.GetConversationsParameters{ + ExcludeArchived: "true", + Types: []string{"public_channel,private_channel"}, + } + for { + channels, nextCursor, err := b.sc.GetConversations(queryParams) + if err != nil { + if err = b.handleRateLimit(err); err != nil { + b.Log.Errorf("Could not retrieve channels: %#v", err) + return + } + continue + } + + for i := range channels { + newChannelsByID[channels[i].ID] = &channels[i] + newChannelsByName[channels[i].Name] = &channels[i] + } + if nextCursor == "" { + break + } + queryParams.Cursor = nextCursor + } + + b.channelsMutex.Lock() + defer b.channelsMutex.Unlock() + b.channelsByID = newChannelsByID + b.channelsByName = newChannelsByName + + b.refreshMutex.Lock() + defer b.refreshMutex.Unlock() + b.earliestChannelRefresh = time.Now().Add(minimumRefreshInterval) + b.refreshInProgress = false +} + +// populateReceivedMessage shapes the initial Matterbridge message that we will forward to the +// router before we apply message-dependent modifications. +func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Message, error) { + // Use our own func because rtm.GetChannelInfo doesn't work for private channels. + channel, err := b.getChannelByID(ev.Channel) + if err != nil { + return nil, err + } + + rmsg := &config.Message{ + Text: ev.Text, + Channel: channel.Name, + Account: b.Account, + ID: ev.Timestamp, + Extra: make(map[string][]interface{}), + ParentID: ev.ThreadTimestamp, + Protocol: b.Protocol, + } + if b.useChannelID { + rmsg.Channel = "ID:" + channel.ID + } + + // Handle 'edit' messages. + if ev.SubMessage != nil && !b.GetBool(editDisableConfig) { + rmsg.ID = ev.SubMessage.Timestamp + if ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp { + b.Log.Debugf("SubMessage %#v", ev.SubMessage) + rmsg.Text = ev.SubMessage.Text + b.GetString(editSuffixConfig) + } + } + + if err = b.populateMessageWithUserInfo(ev, rmsg); err != nil { + return nil, err + } + return rmsg, err +} + +func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *config.Message) error { + if ev.SubType == sMessageDeleted || ev.SubType == sFileComment { + return nil + } + + // First, deal with bot-originating messages but only do so when not using webhooks: we + // would not be able to distinguish which bot would be sending them. + if err := b.populateMessageWithBotInfo(ev, rmsg); err != nil { + return err + } + + // Second, deal with "real" users if we have the necessary information. + var userID string + switch { + case ev.User != "": + userID = ev.User + case ev.SubMessage != nil && ev.SubMessage.User != "": + userID = ev.SubMessage.User + default: + return nil + } + + user := b.getUser(userID) + if user == nil { + return fmt.Errorf("could not find information for user with id %s", ev.User) + } + + rmsg.UserID = user.ID + rmsg.Username = user.Name + if user.Profile.DisplayName != "" { + rmsg.Username = user.Profile.DisplayName + } + return nil +} + +func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config.Message) error { + if ev.BotID == "" || b.GetString(outgoingWebhookConfig) != "" { + return nil + } + + var err error + var bot *slack.Bot + for { + bot, err = b.rtm.GetBotInfo(ev.BotID) + if err == nil { + break + } + + if err = b.handleRateLimit(err); err != nil { + b.Log.Errorf("Could not retrieve bot information: %#v", err) + return err + } + } + b.Log.Debugf("Found bot %#v", bot) + + if bot.Name != "" { + rmsg.Username = bot.Name + if ev.Username != "" { + rmsg.Username = ev.Username + } + rmsg.UserID = bot.ID + } + return nil +} + +var ( + mentionRE = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`) + channelRE = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`) + variableRE = regexp.MustCompile(``) + urlRE = regexp.MustCompile(`<(.*?)(\|.*?)?>`) + topicOrPurposeRE = regexp.MustCompile(`(?s)(@.+) (cleared|set)(?: the)? channel (topic|purpose)(?:: (.*))?`) +) + +func (b *Bslack) extractTopicOrPurpose(text string) (string, string) { + r := topicOrPurposeRE.FindStringSubmatch(text) + if len(r) == 5 { + action, updateType, extracted := r[2], r[3], r[4] + switch action { + case "set": + return updateType, extracted + case "cleared": + return updateType, "" + } + } + b.Log.Warnf("Encountered channel topic or purpose change message with unexpected format: %s", text) + return "unknown", "" +} + +// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users +func (b *Bslack) replaceMention(text string) string { + replaceFunc := func(match string) string { + userID := strings.Trim(match, "@<>") + if username := b.getUsername(userID); userID != "" { + return "@" + username + } + return match + } + return mentionRE.ReplaceAllStringFunc(text, replaceFunc) +} + +// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users +func (b *Bslack) replaceChannel(text string) string { + for _, r := range channelRE.FindAllStringSubmatch(text, -1) { + text = strings.Replace(text, r[0], "#"+r[1], 1) + } + return text +} + +// @see https://api.slack.com/docs/message-formatting#variables +func (b *Bslack) replaceVariable(text string) string { + for _, r := range variableRE.FindAllStringSubmatch(text, -1) { + if r[2] != "" { + text = strings.Replace(text, r[0], "@"+r[2], 1) + } else { + text = strings.Replace(text, r[0], "@"+r[1], 1) + } + } + return text +} + +// @see https://api.slack.com/docs/message-formatting#linking_to_urls +func (b *Bslack) replaceURL(text string) string { + for _, r := range urlRE.FindAllStringSubmatch(text, -1) { + if len(strings.TrimSpace(r[2])) == 1 { // A display text separator was found, but the text was blank + text = strings.Replace(text, r[0], "", 1) + } else { + text = strings.Replace(text, r[0], r[1], 1) + } + } + return text +} + +func (b *Bslack) handleRateLimit(err error) error { + rateLimit, ok := err.(*slack.RateLimitedError) + if !ok { + return err + } + b.Log.Infof("Rate-limited by Slack. Sleeping for %v", rateLimit.RetryAfter) + time.Sleep(rateLimit.RetryAfter) + return nil +} diff --git a/bridge/slack/helpers_test.go b/bridge/slack/helpers_test.go new file mode 100644 index 00000000..c9ff647d --- /dev/null +++ b/bridge/slack/helpers_test.go @@ -0,0 +1,36 @@ +package bslack + +import ( + "io/ioutil" + "testing" + + "github.com/42wim/matterbridge/bridge" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestExtractTopicOrPurpose(t *testing.T) { + testcases := map[string]struct { + input string + wantChangeType string + wantOutput string + }{ + "success - topic type": {"@someone set channel topic: foo bar", "topic", "foo bar"}, + "success - purpose type": {"@someone set channel purpose: foo bar", "purpose", "foo bar"}, + "success - one line": {"@someone set channel topic: foo bar", "topic", "foo bar"}, + "success - multi-line": {"@someone set channel topic: foo\nbar", "topic", "foo\nbar"}, + "success - cleared": {"@someone cleared channel topic", "topic", ""}, + "error - unhandled": {"some unmatched message", "unknown", ""}, + } + + logger := logrus.New() + logger.SetOutput(ioutil.Discard) + cfg := &bridge.Config{Log: logger.WithFields(nil)} + b := newBridge(cfg) + for name, tc := range testcases { + gotChangeType, gotOutput := b.extractTopicOrPurpose(tc.input) + + assert.Equalf(t, tc.wantChangeType, gotChangeType, "This testcase failed: %s", name) + assert.Equalf(t, tc.wantOutput, gotOutput, "This testcase failed: %s", name) + } +} diff --git a/bridge/slack/legacy.go b/bridge/slack/legacy.go new file mode 100644 index 00000000..be372edd --- /dev/null +++ b/bridge/slack/legacy.go @@ -0,0 +1,74 @@ +package bslack + +import ( + "errors" + + "github.com/42wim/matterbridge/bridge" + "github.com/42wim/matterbridge/matterhook" + "github.com/nlopes/slack" +) + +type BLegacy struct { + *Bslack +} + +func NewLegacy(cfg *bridge.Config) bridge.Bridger { + return &BLegacy{Bslack: newBridge(cfg)} +} + +func (b *BLegacy) Connect() error { + b.RLock() + defer b.RUnlock() + if b.GetString(incomingWebhookConfig) != "" { + switch { + case b.GetString(outgoingWebhookConfig) != "": + b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)") + b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{ + InsecureSkipVerify: b.GetBool(skipTLSConfig), + BindAddress: b.GetString(incomingWebhookConfig), + }) + case b.GetString(tokenConfig) != "": + b.Log.Info("Connecting using token (sending)") + b.sc = slack.New(b.GetString(tokenConfig)) + b.rtm = b.sc.NewRTM() + go b.rtm.ManageConnection() + b.Log.Info("Connecting using webhookbindaddress (receiving)") + b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{ + InsecureSkipVerify: b.GetBool(skipTLSConfig), + BindAddress: b.GetString(incomingWebhookConfig), + }) + default: + b.Log.Info("Connecting using webhookbindaddress (receiving)") + b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{ + InsecureSkipVerify: b.GetBool(skipTLSConfig), + BindAddress: b.GetString(incomingWebhookConfig), + }) + } + go b.handleSlack() + return nil + } + if b.GetString(outgoingWebhookConfig) != "" { + b.Log.Info("Connecting using webhookurl (sending)") + b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{ + InsecureSkipVerify: b.GetBool(skipTLSConfig), + DisableServer: true, + }) + if b.GetString(tokenConfig) != "" { + b.Log.Info("Connecting using token (receiving)") + b.sc = slack.New(b.GetString(tokenConfig)) + b.rtm = b.sc.NewRTM() + go b.rtm.ManageConnection() + go b.handleSlack() + } + } else if b.GetString(tokenConfig) != "" { + b.Log.Info("Connecting using token (sending and receiving)") + b.sc = slack.New(b.GetString(tokenConfig)) + b.rtm = b.sc.NewRTM() + go b.rtm.ManageConnection() + go b.handleSlack() + } + if b.GetString(incomingWebhookConfig) == "" && b.GetString(outgoingWebhookConfig) == "" && b.GetString(tokenConfig) == "" { + return errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Token configured") + } + return nil +} diff --git a/bridge/slack/slack.go b/bridge/slack/slack.go index e6f69ed8..225e16e9 100644 --- a/bridge/slack/slack.go +++ b/bridge/slack/slack.go @@ -4,8 +4,6 @@ import ( "bytes" "errors" "fmt" - "html" - "regexp" "strings" "sync" "time" @@ -20,25 +18,88 @@ import ( ) type Bslack struct { - mh *matterhook.Client - sc *slack.Client - rtm *slack.RTM - Users []slack.User - Usergroups []slack.UserGroup - si *slack.Info - channels []slack.Channel - cache *lru.Cache - UseChannelID bool - uuid string - *bridge.Config sync.RWMutex + *bridge.Config + + mh *matterhook.Client + sc *slack.Client + rtm *slack.RTM + si *slack.Info + + cache *lru.Cache + uuid string + useChannelID bool + + users map[string]*slack.User + usersMutex sync.RWMutex + + channelsByID map[string]*slack.Channel + channelsByName map[string]*slack.Channel + channelsMutex sync.RWMutex + + refreshInProgress bool + earliestChannelRefresh time.Time + earliestUserRefresh time.Time + refreshMutex sync.Mutex } -const messageDeleted = "message_deleted" +const ( + sChannelJoin = "channel_join" + sChannelLeave = "channel_leave" + sChannelJoined = "channel_joined" + sMemberJoined = "member_joined_channel" + sMessageChanged = "message_changed" + sMessageDeleted = "message_deleted" + sSlackAttachment = "slack_attachment" + sPinnedItem = "pinned_item" + sUnpinnedItem = "unpinned_item" + sChannelTopic = "channel_topic" + sChannelPurpose = "channel_purpose" + sFileComment = "file_comment" + sMeMessage = "me_message" + sUserTyping = "user_typing" + sLatencyReport = "latency_report" + sSystemUser = "system" + sSlackBotUser = "slackbot" + + tokenConfig = "Token" + incomingWebhookConfig = "WebhookBindAddress" + outgoingWebhookConfig = "WebhookURL" + skipTLSConfig = "SkipTLSVerify" + useNickPrefixConfig = "PrefixMessagesWithNick" + editDisableConfig = "EditDisable" + editSuffixConfig = "EditSuffix" + iconURLConfig = "iconurl" + noSendJoinConfig = "nosendjoinpart" +) func New(cfg *bridge.Config) bridge.Bridger { - b := &Bslack{Config: cfg, uuid: xid.New().String()} - b.cache, _ = lru.New(5000) + // Print a deprecation warning for legacy non-bot tokens (#527). + token := cfg.GetString(tokenConfig) + if token != "" && !strings.HasPrefix(token, "xoxb") { + cfg.Log.Warn("Non-bot token detected. It is STRONGLY recommended to use a proper bot-token instead.") + cfg.Log.Warn("Legacy tokens may be deprecated by Slack at short notice. See the Matterbridge GitHub wiki for a migration guide.") + cfg.Log.Warn("See https://github.com/42wim/matterbridge/wiki/Slack-bot-setup") + return NewLegacy(cfg) + } + return newBridge(cfg) +} + +func newBridge(cfg *bridge.Config) *Bslack { + newCache, err := lru.New(5000) + if err != nil { + cfg.Log.Fatalf("Could not create LRU cache for Slack bridge: %v", err) + } + b := &Bslack{ + Config: cfg, + uuid: xid.New().String(), + cache: newCache, + users: map[string]*slack.User{}, + channelsByID: map[string]*slack.Channel{}, + channelsByName: map[string]*slack.Channel{}, + earliestChannelRefresh: time.Now(), + earliestUserRefresh: time.Now(), + } return b } @@ -49,51 +110,38 @@ func (b *Bslack) Command(cmd string) string { func (b *Bslack) Connect() error { b.RLock() defer b.RUnlock() - if b.GetString("WebhookBindAddress") != "" { - if b.GetString("WebhookURL") != "" { - b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)") - b.mh = matterhook.New(b.GetString("WebhookURL"), - matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), - BindAddress: b.GetString("WebhookBindAddress")}) - } else if b.GetString("Token") != "" { - b.Log.Info("Connecting using token (sending)") - b.sc = slack.New(b.GetString("Token")) - b.rtm = b.sc.NewRTM() - go b.rtm.ManageConnection() - b.Log.Info("Connecting using webhookbindaddress (receiving)") - b.mh = matterhook.New(b.GetString("WebhookURL"), - matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), - BindAddress: b.GetString("WebhookBindAddress")}) - } else { - b.Log.Info("Connecting using webhookbindaddress (receiving)") - b.mh = matterhook.New(b.GetString("WebhookURL"), - matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), - BindAddress: b.GetString("WebhookBindAddress")}) - } - go b.handleSlack() - return nil + + if b.GetString(incomingWebhookConfig) == "" && b.GetString(outgoingWebhookConfig) == "" && b.GetString(tokenConfig) == "" { + return errors.New("no connection method found: WebhookBindAddress, WebhookURL or Token need to be configured") } - if b.GetString("WebhookURL") != "" { - b.Log.Info("Connecting using webhookurl (sending)") - b.mh = matterhook.New(b.GetString("WebhookURL"), - matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), - DisableServer: true}) - if b.GetString("Token") != "" { - b.Log.Info("Connecting using token (receiving)") - b.sc = slack.New(b.GetString("Token")) - b.rtm = b.sc.NewRTM() - go b.rtm.ManageConnection() - go b.handleSlack() - } - } else if b.GetString("Token") != "" { - b.Log.Info("Connecting using token (sending and receiving)") - b.sc = slack.New(b.GetString("Token")) + + // If we have a token we use the Slack websocket-based RTM for both sending and receiving. + if token := b.GetString(tokenConfig); token != "" { + b.Log.Info("Connecting using token") + b.sc = slack.New(token) b.rtm = b.sc.NewRTM() go b.rtm.ManageConnection() go b.handleSlack() + return nil } - if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" && b.GetString("Token") == "" { - return errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Token configured") + + // In absence of a token we fall back to incoming and outgoing Webhooks. + b.mh = matterhook.New( + "", + matterhook.Config{ + InsecureSkipVerify: b.GetBool("SkipTLSVerify"), + DisableServer: true, + }, + ) + if b.GetString(outgoingWebhookConfig) != "" { + b.Log.Info("Using specified webhook for outgoing messages.") + b.mh.Url = b.GetString(outgoingWebhookConfig) + } + if b.GetString(incomingWebhookConfig) != "" { + b.Log.Info("Setting up local webhook for incoming messages.") + b.mh.BindAddress = b.GetString(incomingWebhookConfig) + b.mh.DisableServer = false + go b.handleSlack() } return nil } @@ -102,642 +150,375 @@ func (b *Bslack) Disconnect() error { return b.rtm.Disconnect() } +// JoinChannel only acts as a verification method that checks whether Matterbridge's +// Slack integration is already member of the channel. This is because Slack does not +// allow apps or bots to join channels themselves and they need to be invited +// manually by a user. func (b *Bslack) JoinChannel(channel config.ChannelInfo) error { - // use ID:channelid and resolve it to the actual name - idcheck := strings.Split(channel.Name, "ID:") - if len(idcheck) > 1 { - b.UseChannelID = true - ch, err := b.sc.GetChannelInfo(idcheck[1]) - if err != nil { - return err - } - channel.Name = ch.Name - if err != nil { - return err - } + // We can only join a channel through the Slack API. + if b.sc == nil { + return nil } - // we can only join channels using the API - if b.sc != nil { - if strings.HasPrefix(b.GetString("Token"), "xoxb") { - // TODO check if bot has already joined channel - return nil - } - _, err := b.sc.JoinChannel(channel.Name) - if err != nil { - switch err.Error() { - case "name_taken", "restricted_action": - case "default": - { - return err - } - } - } + b.populateChannels() + + channelInfo, err := b.getChannel(channel.Name) + if err != nil { + return fmt.Errorf("could not join channel: %#v", err) + } + + if strings.HasPrefix(channel.Name, "ID:") { + b.useChannelID = true + channel.Name = channelInfo.Name + } + + if !channelInfo.IsMember { + return fmt.Errorf("slack integration that matterbridge is using is not member of channel '%s', please add it manually", channelInfo.Name) } return nil } -func (b *Bslack) Send(msg config.Message) (string, error) { - b.Log.Debugf("=> Receiving %#v", msg) - - // Make a action /me of the message - if msg.Event == config.EVENT_USER_ACTION { - msg.Text = "_" + msg.Text + "_" - } - - // Use webhook to send the message - if b.GetString("WebhookURL") != "" { - return b.sendWebhook(msg) - } - - channelID := b.getChannelID(msg.Channel) - - // Delete message - if msg.Event == config.EVENT_MSG_DELETE { - // some protocols echo deletes, but with empty ID - if msg.ID == "" { - return "", nil - } - // we get a "slack ", split it - ts := strings.Fields(msg.ID) - _, _, err := b.sc.DeleteMessage(channelID, ts[1]) - if err != nil { - return msg.ID, err - } - return msg.ID, nil - } - - // Prepend nick if configured - if b.GetBool("PrefixMessagesWithNick") { - msg.Text = msg.Username + msg.Text - } - - // Edit message if we have an ID - if msg.ID != "" { - ts := strings.Fields(msg.ID) - _, _, _, err := b.sc.UpdateMessage(channelID, ts[1], msg.Text) - if err != nil { - return msg.ID, err - } - return msg.ID, nil - } - - // create slack new post parameters - np := slack.NewPostMessageParameters() - if b.GetBool("PrefixMessagesWithNick") { - np.AsUser = true - } - np.Username = msg.Username - np.LinkNames = 1 // replace mentions - np.IconURL = config.GetIconURL(&msg, b.GetString("iconurl")) - if msg.Avatar != "" { - np.IconURL = msg.Avatar - } - // add a callback ID so we can see we created it - np.Attachments = append(np.Attachments, slack.Attachment{CallbackID: "matterbridge_" + b.uuid}) - // add file attachments - np.Attachments = append(np.Attachments, b.createAttach(msg.Extra)...) - // add slack attachments (from another slack bridge) - if len(msg.Extra["slack_attachment"]) > 0 { - for _, attach := range msg.Extra["slack_attachment"] { - np.Attachments = append(np.Attachments, attach.([]slack.Attachment)...) - } - } - - // Upload a file if it exists - if msg.Extra != nil { - for _, rmsg := range helper.HandleExtra(&msg, b.General) { - b.sc.PostMessage(channelID, rmsg.Username+rmsg.Text, np) - } - // check if we have files to upload (from slack, telegram or mattermost) - if len(msg.Extra["file"]) > 0 { - b.handleUploadFile(&msg, channelID) - } - } - - // Post normal message - _, id, err := b.sc.PostMessage(channelID, msg.Text, np) - if err != nil { - return "", err - } - return "slack " + id, nil -} - func (b *Bslack) Reload(cfg *bridge.Config) (string, error) { return "", nil } -func (b *Bslack) getAvatar(userid string) string { - var avatar string - if b.Users != nil { - for _, u := range b.Users { - if userid == u.ID { - return u.Profile.Image48 - } - } - } - return avatar -} - -/* -func (b *Bslack) getChannelByName(name string) (*slack.Channel, error) { - if b.channels == nil { - return nil, fmt.Errorf("%s: channel %s not found (no channels found)", b.Account, name) - } - for _, channel := range b.channels { - if channel.Name == name { - return &channel, nil - } - } - return nil, fmt.Errorf("%s: channel %s not found", b.Account, name) -} -*/ - -func (b *Bslack) getChannelByID(ID string) (*slack.Channel, error) { - if b.channels == nil { - return nil, fmt.Errorf("%s: channel %s not found (no channels found)", b.Account, ID) - } - for _, channel := range b.channels { - if channel.ID == ID { - return &channel, nil - } - } - return nil, fmt.Errorf("%s: channel %s not found", b.Account, ID) -} - -func (b *Bslack) handleSlack() { - messages := make(chan *config.Message) - if b.GetString("WebhookBindAddress") != "" { - b.Log.Debugf("Choosing webhooks based receiving") - go b.handleMatterHook(messages) - } else { - b.Log.Debugf("Choosing token based receiving") - go b.handleSlackClient(messages) - } - time.Sleep(time.Second) - b.Log.Debug("Start listening for Slack messages") - for message := range messages { - b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account) - - // cleanup the message - message.Text = b.replaceMention(message.Text) - message.Text = b.replaceVariable(message.Text) - message.Text = b.replaceChannel(message.Text) - message.Text = b.replaceURL(message.Text) - message.Text = html.UnescapeString(message.Text) - - // Add the avatar - message.Avatar = b.getAvatar(message.UserID) - - b.Log.Debugf("<= Message is %#v", message) - b.Remote <- *message - } -} - -func (b *Bslack) handleSlackClient(messages chan *config.Message) { - for msg := range b.rtm.IncomingEvents { - if msg.Type != "user_typing" && msg.Type != "latency_report" { - b.Log.Debugf("== Receiving event %#v", msg.Data) - } - switch ev := msg.Data.(type) { - case *slack.MessageEvent: - if b.skipMessageEvent(ev) { - b.Log.Debugf("Skipped message: %#v", ev) - continue - } - rmsg, err := b.handleMessageEvent(ev) - if err != nil { - b.Log.Errorf("%#v", err) - continue - } - messages <- rmsg - case *slack.OutgoingErrorEvent: - b.Log.Debugf("%#v", ev.Error()) - case *slack.ChannelJoinedEvent: - b.Users, _ = b.sc.GetUsers() - b.Usergroups, _ = b.sc.GetUserGroups() - case *slack.ConnectedEvent: - var err error - b.channels, _, err = b.sc.GetConversations(&slack.GetConversationsParameters{Limit: 1000, Types: []string{"public_channel,private_channel,mpim,im"}}) - if err != nil { - b.Log.Errorf("Channel list failed: %#v", err) - } - b.si = ev.Info - b.Users, _ = b.sc.GetUsers() - b.Usergroups, _ = b.sc.GetUserGroups() - case *slack.InvalidAuthEvent: - b.Log.Fatalf("Invalid Token %#v", ev) - case *slack.ConnectionErrorEvent: - b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj) - default: - } - } -} - -func (b *Bslack) handleMatterHook(messages chan *config.Message) { - for { - message := b.mh.Receive() - b.Log.Debugf("receiving from matterhook (slack) %#v", message) - if message.UserName == "slackbot" { - continue - } - messages <- &config.Message{Username: message.UserName, Text: message.Text, Channel: message.ChannelName} - } -} - -func (b *Bslack) userName(id string) string { - for _, u := range b.Users { - if u.ID == id { - if u.Profile.DisplayName != "" { - return u.Profile.DisplayName - } - return u.Name - } - } - return "" -} - -/* -func (b *Bslack) userGroupName(id string) string { - for _, u := range b.Usergroups { - if u.ID == id { - return u.Name - } - } - return "" -} -*/ - -// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users -func (b *Bslack) replaceMention(text string) string { - results := regexp.MustCompile(`<@([a-zA-Z0-9]+)>`).FindAllStringSubmatch(text, -1) - for _, r := range results { - text = strings.Replace(text, "<@"+r[1]+">", "@"+b.userName(r[1]), -1) - } - return text -} - -// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users -func (b *Bslack) replaceChannel(text string) string { - results := regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`).FindAllStringSubmatch(text, -1) - for _, r := range results { - text = strings.Replace(text, r[0], "#"+r[1], -1) - } - return text -} - -// @see https://api.slack.com/docs/message-formatting#variables -func (b *Bslack) replaceVariable(text string) string { - results := regexp.MustCompile(``).FindAllStringSubmatch(text, -1) - for _, r := range results { - if r[2] != "" { - text = strings.Replace(text, r[0], "@"+r[2], -1) - } else { - text = strings.Replace(text, r[0], "@"+r[1], -1) - } - } - return text -} - -// @see https://api.slack.com/docs/message-formatting#linking_to_urls -func (b *Bslack) replaceURL(text string) string { - results := regexp.MustCompile(`<(.*?)(\|.*?)?>`).FindAllStringSubmatch(text, -1) - for _, r := range results { - if len(strings.TrimSpace(r[2])) == 1 { // A display text separator was found, but the text was blank - text = strings.Replace(text, r[0], "", -1) - } else { - text = strings.Replace(text, r[0], r[1], -1) - } - } - return text -} - -func (b *Bslack) createAttach(extra map[string][]interface{}) []slack.Attachment { - var attachs []slack.Attachment - for _, v := range extra["attachments"] { - entry := v.(map[string]interface{}) - s := slack.Attachment{} - s.Fallback = entry["fallback"].(string) - s.Color = entry["color"].(string) - s.Pretext = entry["pretext"].(string) - s.AuthorName = entry["author_name"].(string) - s.AuthorLink = entry["author_link"].(string) - s.AuthorIcon = entry["author_icon"].(string) - s.Title = entry["title"].(string) - s.TitleLink = entry["title_link"].(string) - s.Text = entry["text"].(string) - s.ImageURL = entry["image_url"].(string) - s.ThumbURL = entry["thumb_url"].(string) - s.Footer = entry["footer"].(string) - s.FooterIcon = entry["footer_icon"].(string) - attachs = append(attachs, s) - } - return attachs -} - -// handleDownloadFile handles file download -func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File) error { - // if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra - // limit to 1MB for now - comment := "" - results := regexp.MustCompile(`.*?commented: (.*)`).FindAllStringSubmatch(rmsg.Text, -1) - if len(results) > 0 { - comment = results[0][1] - } - err := helper.HandleDownloadSize(b.Log, rmsg, file.Name, int64(file.Size), b.General) - if err != nil { - return err - } - // actually download the file - data, err := helper.DownloadFileAuth(file.URLPrivateDownload, "Bearer "+b.GetString("Token")) - if err != nil { - return fmt.Errorf("download %s failed %#v", file.URLPrivateDownload, err) - } - // add the downloaded data to the message - helper.HandleDownloadData(b.Log, rmsg, file.Name, comment, file.URLPrivateDownload, data, b.General) - return nil -} - -// handleUploadFile handles native upload of files -func (b *Bslack) handleUploadFile(msg *config.Message, channelID string) (string, error) { - for _, f := range msg.Extra["file"] { - fi := f.(config.FileInfo) - if msg.Text == fi.Comment { - msg.Text = "" - } - /* because the result of the UploadFile is slower than the MessageEvent from slack - we can't match on the file ID yet, so we have to match on the filename too - */ - b.Log.Debugf("Adding file %s to cache %s", fi.Name, time.Now().String()) - b.cache.Add("filename"+fi.Name, time.Now()) - res, err := b.sc.UploadFile(slack.FileUploadParameters{ - Reader: bytes.NewReader(*fi.Data), - Filename: fi.Name, - Channels: []string{channelID}, - InitialComment: fi.Comment, - }) - if res.ID != "" { - b.Log.Debugf("Adding fileid %s to cache %s", res.ID, time.Now().String()) - b.cache.Add("file"+res.ID, time.Now()) - } - if err != nil { - b.Log.Errorf("uploadfile %#v", err) - } - } - return "", nil -} - -// handleMessageEvent handles the message events -func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, error) { - // update the userlist on a channel_join - if ev.SubType == "channel_join" { - b.Users, _ = b.sc.GetUsers() +func (b *Bslack) Send(msg config.Message) (string, error) { + // Too noisy to log like other events + if msg.Event != config.EventUserTyping { + b.Log.Debugf("=> Receiving %#v", msg) } - // Edit message - if !b.GetBool("EditDisable") && ev.SubMessage != nil && ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp { - b.Log.Debugf("SubMessage %#v", ev.SubMessage) - ev.User = ev.SubMessage.User - ev.Text = ev.SubMessage.Text + b.GetString("EditSuffix") + // Make a action /me of the message + if msg.Event == config.EventUserAction { + msg.Text = "_" + msg.Text + "_" } - // use our own func because rtm.GetChannelInfo doesn't work for private channels - channel, err := b.getChannelByID(ev.Channel) - if err != nil { - return nil, err + // Use webhook to send the message + if b.GetString(outgoingWebhookConfig) != "" { + return "", b.sendWebhook(msg) } - - rmsg := config.Message{Text: ev.Text, Channel: channel.Name, Account: b.Account, ID: "slack " + ev.Timestamp, Extra: make(map[string][]interface{})} - - if b.UseChannelID { - rmsg.Channel = "ID:" + channel.ID - } - - // find the user id and name - if ev.User != "" && ev.SubType != messageDeleted && ev.SubType != "file_comment" { - user, err := b.rtm.GetUserInfo(ev.User) - if err != nil { - return nil, err - } - rmsg.UserID = user.ID - rmsg.Username = user.Name - if user.Profile.DisplayName != "" { - rmsg.Username = user.Profile.DisplayName - } - } - - // See if we have some text in the attachments - if rmsg.Text == "" { - for _, attach := range ev.Attachments { - if attach.Text != "" { - if attach.Title != "" { - rmsg.Text = attach.Title + "\n" - } - rmsg.Text += attach.Text - } else { - rmsg.Text = attach.Fallback - } - } - } - - // when using webhookURL we can't check if it's our webhook or not for now - if rmsg.Username == "" && ev.BotID != "" && b.GetString("WebhookURL") == "" { - bot, err := b.rtm.GetBotInfo(ev.BotID) - if err != nil { - return nil, err - } - if bot.Name != "" { - rmsg.Username = bot.Name - if ev.Username != "" { - rmsg.Username = ev.Username - } - rmsg.UserID = bot.ID - } - - // fixes issues with matterircd users - if bot.Name == "Slack API Tester" { - user, err := b.rtm.GetUserInfo(ev.User) - if err != nil { - return nil, err - } - rmsg.UserID = user.ID - rmsg.Username = user.Name - if user.Profile.DisplayName != "" { - rmsg.Username = user.Profile.DisplayName - } - } - } - - // file comments are set by the system (because there is no username given) - if ev.SubType == "file_comment" { - rmsg.Username = "system" - } - - // do we have a /me action - if ev.SubType == "me_message" { - rmsg.Event = config.EVENT_USER_ACTION - } - - // Handle join/leave - if ev.SubType == "channel_leave" || ev.SubType == "channel_join" { - rmsg.Username = "system" - rmsg.Event = config.EVENT_JOIN_LEAVE - } - - // edited messages have a submessage, use this timestamp - if ev.SubMessage != nil { - rmsg.ID = "slack " + ev.SubMessage.Timestamp - } - - // deleted message event - if ev.SubType == messageDeleted { - rmsg.Text = config.EVENT_MSG_DELETE - rmsg.Event = config.EVENT_MSG_DELETE - rmsg.ID = "slack " + ev.DeletedTimestamp - } - - // topic change event - if ev.SubType == "channel_topic" || ev.SubType == "channel_purpose" { - rmsg.Event = config.EVENT_TOPIC_CHANGE - } - - // Only deleted messages can have a empty username and text - if (rmsg.Text == "" || rmsg.Username == "") && ev.SubType != messageDeleted && len(ev.Files) == 0 { - // this is probably a webhook we couldn't resolve - if ev.BotID != "" { - return nil, fmt.Errorf("probably an incoming webhook we couldn't resolve (maybe ourselves)") - } - return nil, fmt.Errorf("empty message and not a deleted message") - } - - // save the attachments, so that we can send them to other slack (compatible) bridges - if len(ev.Attachments) > 0 { - rmsg.Extra["slack_attachment"] = append(rmsg.Extra["slack_attachment"], ev.Attachments) - } - - // if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra - if len(ev.Files) > 0 { - for _, f := range ev.Files { - err := b.handleDownloadFile(&rmsg, &f) - if err != nil { - b.Log.Errorf("download failed: %s", err) - } - } - } - - return &rmsg, nil + return b.sendRTM(msg) } // sendWebhook uses the configured WebhookURL to send the message -func (b *Bslack) sendWebhook(msg config.Message) (string, error) { - // skip events +func (b *Bslack) sendWebhook(msg config.Message) error { + // Skip events. if msg.Event != "" { - return "", nil + return nil } - if b.GetBool("PrefixMessagesWithNick") { + if b.GetBool(useNickPrefixConfig) { msg.Text = msg.Username + msg.Text } if msg.Extra != nil { - // this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE + // This sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE. for _, rmsg := range helper.HandleExtra(&msg, b.General) { - iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl")) - matterMessage := matterhook.OMessage{IconURL: iconURL, Channel: msg.Channel, UserName: rmsg.Username, Text: rmsg.Text} - b.mh.Send(matterMessage) + rmsg := rmsg // scopelint + iconURL := config.GetIconURL(&rmsg, b.GetString(iconURLConfig)) + matterMessage := matterhook.OMessage{ + IconURL: iconURL, + Channel: msg.Channel, + UserName: rmsg.Username, + Text: rmsg.Text, + } + if err := b.mh.Send(matterMessage); err != nil { + b.Log.Errorf("Failed to send message: %v", err) + } } - // webhook doesn't support file uploads, so we add the url manually - if len(msg.Extra["file"]) > 0 { - for _, f := range msg.Extra["file"] { - fi := f.(config.FileInfo) - if fi.URL != "" { - msg.Text += " " + fi.URL - } + // Webhook doesn't support file uploads, so we add the URL manually. + for _, f := range msg.Extra["file"] { + fi, ok := f.(config.FileInfo) + if !ok { + b.Log.Errorf("Received a file with unexpected content: %#v", f) + continue + } + if fi.URL != "" { + msg.Text += " " + fi.URL } } } - // if we have native slack_attachments add them + // If we have native slack_attachments add them. var attachs []slack.Attachment - if len(msg.Extra["slack_attachment"]) > 0 { - for _, attach := range msg.Extra["slack_attachment"] { - attachs = append(attachs, attach.([]slack.Attachment)...) - } + for _, attach := range msg.Extra[sSlackAttachment] { + attachs = append(attachs, attach.([]slack.Attachment)...) } - iconURL := config.GetIconURL(&msg, b.GetString("iconurl")) - matterMessage := matterhook.OMessage{IconURL: iconURL, Attachments: attachs, Channel: msg.Channel, UserName: msg.Username, Text: msg.Text} + iconURL := config.GetIconURL(&msg, b.GetString(iconURLConfig)) + matterMessage := matterhook.OMessage{ + IconURL: iconURL, + Attachments: attachs, + Channel: msg.Channel, + UserName: msg.Username, + Text: msg.Text, + } if msg.Avatar != "" { matterMessage.IconURL = msg.Avatar } - err := b.mh.Send(matterMessage) + if err := b.mh.Send(matterMessage); err != nil { + b.Log.Errorf("Failed to send message via webhook: %#v", err) + return err + } + return nil +} + +func (b *Bslack) sendRTM(msg config.Message) (string, error) { + channelInfo, err := b.getChannel(msg.Channel) if err != nil { - b.Log.Error(err) + return "", fmt.Errorf("could not send message: %v", err) + } + if msg.Event == config.EventUserTyping { + if b.GetBool("ShowUserTyping") { + b.rtm.SendMessage(b.rtm.NewTypingMessage(channelInfo.ID)) + } + return "", nil + } + + var handled bool + + // Handle topic/purpose updates. + if handled, err = b.handleTopicOrPurpose(&msg, channelInfo); handled { return "", err } - return "", nil -} -// skipMessageEvent skips event that need to be skipped :-) -func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool { - if ev.SubType == "channel_leave" || ev.SubType == "channel_join" { - return b.GetBool("nosendjoinpart") + // Handle message deletions. + if handled, err = b.deleteMessage(&msg, channelInfo); handled { + return msg.ID, err } - // ignore pinned items - if ev.SubType == "pinned_item" || ev.SubType == "unpinned_item" { - return true + // Prepend nickname if configured. + if b.GetBool(useNickPrefixConfig) { + msg.Text = msg.Username + msg.Text } - // do not send messages from ourself - if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" && ev.Username == b.si.User.Name { - return true + // Handle message edits. + if handled, err = b.editMessage(&msg, channelInfo); handled { + return msg.ID, err } - // skip messages we made ourselves - if len(ev.Attachments) > 0 { - if ev.Attachments[0].CallbackID == "matterbridge_"+b.uuid { - return true - } - } - - if !b.GetBool("EditDisable") && ev.SubMessage != nil && ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp { - // it seems ev.SubMessage.Edited == nil when slack unfurls - // do not forward these messages #266 - if ev.SubMessage.Edited == nil { - return true - } - } - - if len(ev.Files) > 0 { - for _, f := range ev.Files { - // if the file is in the cache and isn't older then a minute, skip it - if ts, ok := b.cache.Get("file" + f.ID); ok && time.Since(ts.(time.Time)) < time.Minute { - b.Log.Debugf("Not downloading file id %s which we uploaded", f.ID) - return true - } else { - if ts, ok := b.cache.Get("filename" + f.Name); ok && time.Since(ts.(time.Time)) < time.Second*10 { - b.Log.Debugf("Not downloading file name %s which we uploaded", f.Name) - return true - } else { - b.Log.Debugf("Not skipping %s %s", f.Name, time.Now().String()) - } + // Upload a file if it exists. + if msg.Extra != nil { + extraMsgs := helper.HandleExtra(&msg, b.General) + for i := range extraMsgs { + rmsg := &extraMsgs[i] + rmsg.Text = rmsg.Username + rmsg.Text + _, err = b.postMessage(rmsg, channelInfo) + if err != nil { + b.Log.Error(err) } } + // Upload files if necessary (from Slack, Telegram or Mattermost). + b.uploadFile(&msg, channelInfo.ID) } - return false + // Post message. + return b.postMessage(&msg, channelInfo) } -func (b *Bslack) getChannelID(name string) string { - idcheck := strings.Split(name, "ID:") - if len(idcheck) > 1 { - return idcheck[1] +func (b *Bslack) updateTopicOrPurpose(msg *config.Message, channelInfo *slack.Channel) error { + var updateFunc func(channelID string, value string) (*slack.Channel, error) + + incomingChangeType, text := b.extractTopicOrPurpose(msg.Text) + switch incomingChangeType { + case "topic": + updateFunc = b.rtm.SetTopicOfConversation + case "purpose": + updateFunc = b.rtm.SetPurposeOfConversation + default: + b.Log.Errorf("Unhandled type received from extractTopicOrPurpose: %s", incomingChangeType) + return nil } - for _, channel := range b.channels { - if channel.Name == name { - return channel.ID + for { + _, err := updateFunc(channelInfo.ID, text) + if err == nil { + return nil + } + if err = b.handleRateLimit(err); err != nil { + return err + } + } +} + +// handles updating topic/purpose and determining whether to further propagate update messages. +func (b *Bslack) handleTopicOrPurpose(msg *config.Message, channelInfo *slack.Channel) (bool, error) { + if msg.Event != config.EventTopicChange { + return false, nil + } + + if b.GetBool("SyncTopic") { + return true, b.updateTopicOrPurpose(msg, channelInfo) + } + + // Pass along to normal message handlers. + if b.GetBool("ShowTopicChange") { + return false, nil + } + + // Swallow message as handled no-op. + return true, nil +} + +func (b *Bslack) deleteMessage(msg *config.Message, channelInfo *slack.Channel) (bool, error) { + if msg.Event != config.EventMsgDelete { + return false, nil + } + + // Some protocols echo deletes, but with an empty ID. + if msg.ID == "" { + return true, nil + } + + for { + _, _, err := b.rtm.DeleteMessage(channelInfo.ID, msg.ID) + if err == nil { + return true, nil + } + + if err = b.handleRateLimit(err); err != nil { + b.Log.Errorf("Failed to delete user message from Slack: %#v", err) + return true, err + } + } +} + +func (b *Bslack) editMessage(msg *config.Message, channelInfo *slack.Channel) (bool, error) { + if msg.ID == "" { + return false, nil + } + messageOptions := b.prepareMessageOptions(msg) + for { + messageOptions = append(messageOptions, slack.MsgOptionText(msg.Text, false)) + _, _, _, err := b.rtm.UpdateMessage(channelInfo.ID, msg.ID, messageOptions...) + if err == nil { + return true, nil + } + + if err = b.handleRateLimit(err); err != nil { + b.Log.Errorf("Failed to edit user message on Slack: %#v", err) + return true, err + } + } +} + +func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (string, error) { + // don't post empty messages + if msg.Text == "" { + return "", nil + } + messageOptions := b.prepareMessageOptions(msg) + messageOptions = append(messageOptions, slack.MsgOptionText(msg.Text, false)) + for { + _, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...) + if err == nil { + return id, nil + } + + if err = b.handleRateLimit(err); err != nil { + b.Log.Errorf("Failed to sent user message to Slack: %#v", err) + return "", err + } + } +} + +// uploadFile handles native upload of files +func (b *Bslack) uploadFile(msg *config.Message, channelID string) { + for _, f := range msg.Extra["file"] { + fi, ok := f.(config.FileInfo) + if !ok { + b.Log.Errorf("Received a file with unexpected content: %#v", f) + continue + } + if msg.Text == fi.Comment { + msg.Text = "" + } + // Because the result of the UploadFile is slower than the MessageEvent from slack + // we can't match on the file ID yet, so we have to match on the filename too. + ts := time.Now() + b.Log.Debugf("Adding file %s to cache at %s with timestamp", fi.Name, ts.String()) + b.cache.Add("filename"+fi.Name, ts) + initialComment := fmt.Sprintf("File from %s", msg.Username) + if fi.Comment != "" { + initialComment += fmt.Sprintf("with comment: %s", fi.Comment) + } + res, err := b.sc.UploadFile(slack.FileUploadParameters{ + Reader: bytes.NewReader(*fi.Data), + Filename: fi.Name, + Channels: []string{channelID}, + InitialComment: initialComment, + ThreadTimestamp: msg.ParentID, + }) + if err != nil { + b.Log.Errorf("uploadfile %#v", err) + return + } + if res.ID != "" { + b.Log.Debugf("Adding file ID %s to cache with timestamp %s", res.ID, ts.String()) + b.cache.Add("file"+res.ID, ts) + } + } +} + +func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption { + params := slack.NewPostMessageParameters() + if b.GetBool(useNickPrefixConfig) { + params.AsUser = true + } + params.Username = msg.Username + params.LinkNames = 1 // replace mentions + params.IconURL = config.GetIconURL(msg, b.GetString(iconURLConfig)) + params.ThreadTimestamp = msg.ParentID + if msg.Avatar != "" { + params.IconURL = msg.Avatar + } + + var attachments []slack.Attachment + // add a callback ID so we can see we created it + attachments = append(attachments, slack.Attachment{CallbackID: "matterbridge_" + b.uuid}) + // add file attachments + attachments = append(attachments, b.createAttach(msg.Extra)...) + // add slack attachments (from another slack bridge) + if msg.Extra != nil { + for _, attach := range msg.Extra[sSlackAttachment] { + attachments = append(attachments, attach.([]slack.Attachment)...) + } + } + + var opts []slack.MsgOption + opts = append(opts, slack.MsgOptionAttachments(attachments...)) + opts = append(opts, slack.MsgOptionPostMessageParameters(params)) + return opts +} + +func (b *Bslack) createAttach(extra map[string][]interface{}) []slack.Attachment { + var attachements []slack.Attachment + for _, v := range extra["attachments"] { + entry := v.(map[string]interface{}) + s := slack.Attachment{ + Fallback: extractStringField(entry, "fallback"), + Color: extractStringField(entry, "color"), + Pretext: extractStringField(entry, "pretext"), + AuthorName: extractStringField(entry, "author_name"), + AuthorLink: extractStringField(entry, "author_link"), + AuthorIcon: extractStringField(entry, "author_icon"), + Title: extractStringField(entry, "title"), + TitleLink: extractStringField(entry, "title_link"), + Text: extractStringField(entry, "text"), + ImageURL: extractStringField(entry, "image_url"), + ThumbURL: extractStringField(entry, "thumb_url"), + Footer: extractStringField(entry, "footer"), + FooterIcon: extractStringField(entry, "footer_icon"), + } + attachements = append(attachements, s) + } + return attachements +} + +func extractStringField(data map[string]interface{}, field string) string { + if rawValue, found := data[field]; found { + if value, ok := rawValue.(string); ok { + return value } } return "" diff --git a/bridge/sshchat/sshchat.go b/bridge/sshchat/sshchat.go index 846a9081..79ba9fb5 100644 --- a/bridge/sshchat/sshchat.go +++ b/bridge/sshchat/sshchat.go @@ -30,9 +30,10 @@ func (b *Bsshchat) Connect() error { b.r = bufio.NewScanner(r) b.w = w b.r.Scan() - w.Write([]byte("/theme mono\r\n")) - b.handleSshChat() - return nil + if _, handleErr := w.Write([]byte("/theme mono\r\n")); handleErr != nil { + return handleErr + } + return b.handleSSHChat() }) }() if err != nil { @@ -53,33 +54,20 @@ func (b *Bsshchat) JoinChannel(channel config.ChannelInfo) error { func (b *Bsshchat) Send(msg config.Message) (string, error) { // ignore delete messages - if msg.Event == config.EVENT_MSG_DELETE { + if msg.Event == config.EventMsgDelete { return "", nil } b.Log.Debugf("=> Receiving %#v", msg) if msg.Extra != nil { for _, rmsg := range helper.HandleExtra(&msg, b.General) { - b.w.Write([]byte(rmsg.Username + rmsg.Text + "\r\n")) - } - if len(msg.Extra["file"]) > 0 { - for _, f := range msg.Extra["file"] { - fi := f.(config.FileInfo) - if fi.Comment != "" { - msg.Text += fi.Comment + ": " - } - if fi.URL != "" { - msg.Text = fi.URL - if fi.Comment != "" { - msg.Text = fi.Comment + ": " + fi.URL - } - } - b.w.Write([]byte(msg.Username + msg.Text)) + if _, err := b.w.Write([]byte(rmsg.Username + rmsg.Text + "\r\n")); err != nil { + b.Log.Errorf("Could not send extra message: %#v", err) } - return "", nil } + return b.handleUploadFile(&msg) } - b.w.Write([]byte(msg.Username + msg.Text + "\r\n")) - return "", nil + _, err := b.w.Write([]byte(msg.Username + msg.Text + "\r\n")) + return "", err } /* @@ -113,7 +101,7 @@ func stripPrompt(s string) string { return s[pos+3:] } -func (b *Bsshchat) handleSshChat() error { +func (b *Bsshchat) handleSSHChat() error { /* done := b.sshchatKeepAlive() defer close(done) @@ -139,3 +127,22 @@ func (b *Bsshchat) handleSshChat() error { } } } + +func (b *Bsshchat) handleUploadFile(msg *config.Message) (string, error) { + for _, f := range msg.Extra["file"] { + fi := f.(config.FileInfo) + if fi.Comment != "" { + msg.Text += fi.Comment + ": " + } + if fi.URL != "" { + msg.Text = fi.URL + if fi.Comment != "" { + msg.Text = fi.Comment + ": " + fi.URL + } + } + if _, err := b.w.Write([]byte(msg.Username + msg.Text)); err != nil { + b.Log.Errorf("Could not send file message: %#v", err) + } + } + return "", nil +} diff --git a/bridge/steam/handlers.go b/bridge/steam/handlers.go new file mode 100644 index 00000000..a591281e --- /dev/null +++ b/bridge/steam/handlers.go @@ -0,0 +1,126 @@ +package bsteam + +import ( + "fmt" + "strconv" + + "github.com/42wim/matterbridge/bridge/config" + "github.com/Philipp15b/go-steam" + "github.com/Philipp15b/go-steam/protocol/steamlang" +) + +func (b *Bsteam) handleChatMsg(e *steam.ChatMsgEvent) { + b.Log.Debugf("Receiving ChatMsgEvent: %#v", e) + b.Log.Debugf("<= Sending message from %s on %s to gateway", b.getNick(e.ChatterId), b.Account) + var channel int64 + if e.ChatRoomId == 0 { + channel = int64(e.ChatterId) + } else { + // for some reason we have to remove 0x18000000000000 + // TODO + // https://github.com/42wim/matterbridge/pull/630#discussion_r238102751 + // channel = int64(e.ChatRoomId) & 0xfffffffffffff + channel = int64(e.ChatRoomId) - 0x18000000000000 + } + msg := config.Message{ + Username: b.getNick(e.ChatterId), + Text: e.Message, + Channel: strconv.FormatInt(channel, 10), + Account: b.Account, + UserID: strconv.FormatInt(int64(e.ChatterId), 10), + } + b.Remote <- msg +} + +func (b *Bsteam) handleEvents() { + myLoginInfo := &steam.LogOnDetails{ + Username: b.GetString("Login"), + Password: b.GetString("Password"), + AuthCode: b.GetString("AuthCode"), + } + // TODO Attempt to read existing auth hash to avoid steam guard. + // Maybe works + //myLoginInfo.SentryFileHash, _ = ioutil.ReadFile("sentry") + for event := range b.c.Events() { + switch e := event.(type) { + case *steam.ChatMsgEvent: + b.handleChatMsg(e) + case *steam.PersonaStateEvent: + b.Log.Debugf("PersonaStateEvent: %#v\n", e) + b.Lock() + b.userMap[e.FriendId] = e.Name + b.Unlock() + case *steam.ConnectedEvent: + b.c.Auth.LogOn(myLoginInfo) + case *steam.MachineAuthUpdateEvent: + // TODO sentry files for 2 auth + /* + b.Log.Info("authupdate", e) + b.Log.Info("hash", e.Hash) + ioutil.WriteFile("sentry", e.Hash, 0666) + */ + case *steam.LogOnFailedEvent: + b.Log.Info("Logon failed", e) + err := b.handleLogOnFailed(e, myLoginInfo) + if err != nil { + b.Log.Error(err) + return + } + case *steam.LoggedOnEvent: + b.Log.Debugf("LoggedOnEvent: %#v", e) + b.connected <- struct{}{} + b.Log.Debugf("setting online") + b.c.Social.SetPersonaState(steamlang.EPersonaState_Online) + case *steam.DisconnectedEvent: + b.Log.Info("Disconnected") + b.Log.Info("Attempting to reconnect...") + b.c.Connect() + case steam.FatalErrorEvent: + b.Log.Errorf("steam FatalErrorEvent: %#v", e) + default: + b.Log.Debugf("unknown event %#v", e) + } + } +} + +func (b *Bsteam) handleLogOnFailed(e *steam.LogOnFailedEvent, myLoginInfo *steam.LogOnDetails) error { + switch e.Result { + case steamlang.EResult_AccountLogonDeniedNeedTwoFactorCode: + b.Log.Info("Steam guard isn't letting me in! Enter 2FA code:") + var code string + fmt.Scanf("%s", &code) + // TODO https://github.com/42wim/matterbridge/pull/630#discussion_r238103978 + myLoginInfo.TwoFactorCode = code + case steamlang.EResult_AccountLogonDenied: + b.Log.Info("Steam guard isn't letting me in! Enter auth code:") + var code string + fmt.Scanf("%s", &code) + // TODO https://github.com/42wim/matterbridge/pull/630#discussion_r238103978 + myLoginInfo.AuthCode = code + case steamlang.EResult_InvalidLoginAuthCode: + return fmt.Errorf("Steam guard: invalid login auth code: %#v ", e.Result) + default: + return fmt.Errorf("LogOnFailedEvent: %#v ", e.Result) + // TODO: Handle EResult_InvalidLoginAuthCode + } + return nil +} + +// handleFileInfo handles config.FileInfo and adds correct file comment or URL to msg.Text. +// Returns error if cast fails. +func (b *Bsteam) handleFileInfo(msg *config.Message, f interface{}) error { + if _, ok := f.(config.FileInfo); !ok { + return fmt.Errorf("handleFileInfo cast failed %#v", f) + } + fi := f.(config.FileInfo) + if fi.Comment != "" { + msg.Text += fi.Comment + ": " + } + if fi.URL != "" { + msg.Text = fi.URL + if fi.Comment != "" { + msg.Text = fi.Comment + ": " + fi.URL + } + } + return nil +} diff --git a/bridge/steam/steam.go b/bridge/steam/steam.go index 27bf284a..5a577a28 100644 --- a/bridge/steam/steam.go +++ b/bridge/steam/steam.go @@ -2,6 +2,8 @@ package bsteam import ( "fmt" + "sync" + "time" "github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge/config" @@ -9,10 +11,6 @@ import ( "github.com/Philipp15b/go-steam" "github.com/Philipp15b/go-steam/protocol/steamlang" "github.com/Philipp15b/go-steam/steamid" - //"io/ioutil" - "strconv" - "sync" - "time" ) type Bsteam struct { @@ -61,7 +59,7 @@ func (b *Bsteam) JoinChannel(channel config.ChannelInfo) error { func (b *Bsteam) Send(msg config.Message) (string, error) { // ignore delete messages - if msg.Event == config.EVENT_MSG_DELETE { + if msg.Event == config.EventMsgDelete { return "", nil } id, err := steamid.NewId(msg.Channel) @@ -74,22 +72,13 @@ func (b *Bsteam) Send(msg config.Message) (string, error) { for _, rmsg := range helper.HandleExtra(&msg, b.General) { b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, rmsg.Username+rmsg.Text) } - if len(msg.Extra["file"]) > 0 { - for _, f := range msg.Extra["file"] { - fi := f.(config.FileInfo) - if fi.Comment != "" { - msg.Text += fi.Comment + ": " - } - if fi.URL != "" { - msg.Text = fi.URL - if fi.Comment != "" { - msg.Text = fi.Comment + ": " + fi.URL - } - } - b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text) + for i := range msg.Extra["file"] { + if err := b.handleFileInfo(&msg, msg.Extra["file"][i]); err != nil { + b.Log.Error(err) } - return "", nil + b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text) } + return "", nil } b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text) @@ -104,80 +93,3 @@ func (b *Bsteam) getNick(id steamid.SteamId) string { } return "unknown" } - -func (b *Bsteam) handleEvents() { - myLoginInfo := new(steam.LogOnDetails) - myLoginInfo.Username = b.GetString("Login") - myLoginInfo.Password = b.GetString("Password") - myLoginInfo.AuthCode = b.GetString("AuthCode") - // Attempt to read existing auth hash to avoid steam guard. - // Maybe works - //myLoginInfo.SentryFileHash, _ = ioutil.ReadFile("sentry") - for event := range b.c.Events() { - //b.Log.Info(event) - switch e := event.(type) { - case *steam.ChatMsgEvent: - b.Log.Debugf("Receiving ChatMsgEvent: %#v", e) - b.Log.Debugf("<= Sending message from %s on %s to gateway", b.getNick(e.ChatterId), b.Account) - var channel int64 - if e.ChatRoomId == 0 { - channel = int64(e.ChatterId) - } else { - // for some reason we have to remove 0x18000000000000 - channel = int64(e.ChatRoomId) - 0x18000000000000 - } - msg := config.Message{Username: b.getNick(e.ChatterId), Text: e.Message, Channel: strconv.FormatInt(channel, 10), Account: b.Account, UserID: strconv.FormatInt(int64(e.ChatterId), 10)} - b.Remote <- msg - case *steam.PersonaStateEvent: - b.Log.Debugf("PersonaStateEvent: %#v\n", e) - b.Lock() - b.userMap[e.FriendId] = e.Name - b.Unlock() - case *steam.ConnectedEvent: - b.c.Auth.LogOn(myLoginInfo) - case *steam.MachineAuthUpdateEvent: - /* - b.Log.Info("authupdate", e) - b.Log.Info("hash", e.Hash) - ioutil.WriteFile("sentry", e.Hash, 0666) - */ - case *steam.LogOnFailedEvent: - b.Log.Info("Logon failed", e) - switch e.Result { - case steamlang.EResult_AccountLogonDeniedNeedTwoFactorCode: - { - b.Log.Info("Steam guard isn't letting me in! Enter 2FA code:") - var code string - fmt.Scanf("%s", &code) - myLoginInfo.TwoFactorCode = code - } - case steamlang.EResult_AccountLogonDenied: - { - b.Log.Info("Steam guard isn't letting me in! Enter auth code:") - var code string - fmt.Scanf("%s", &code) - myLoginInfo.AuthCode = code - } - default: - b.Log.Errorf("LogOnFailedEvent: %#v ", e.Result) - // TODO: Handle EResult_InvalidLoginAuthCode - return - } - case *steam.LoggedOnEvent: - b.Log.Debugf("LoggedOnEvent: %#v", e) - b.connected <- struct{}{} - b.Log.Debugf("setting online") - b.c.Social.SetPersonaState(steamlang.EPersonaState_Online) - case *steam.DisconnectedEvent: - b.Log.Info("Disconnected") - b.Log.Info("Attempting to reconnect...") - b.c.Connect() - case steam.FatalErrorEvent: - b.Log.Error(e) - case error: - b.Log.Error(e) - default: - b.Log.Debugf("unknown event %#v", e) - } - } -} diff --git a/bridge/telegram/handlers.go b/bridge/telegram/handlers.go new file mode 100644 index 00000000..e87466ca --- /dev/null +++ b/bridge/telegram/handlers.go @@ -0,0 +1,346 @@ +package btelegram + +import ( + "html" + "regexp" + "strconv" + "strings" + + "github.com/42wim/matterbridge/bridge/config" + "github.com/42wim/matterbridge/bridge/helper" + "github.com/go-telegram-bot-api/telegram-bot-api" +) + +func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message { + // handle channels + if posted != nil { + message = posted + rmsg.Text = message.Text + } + + // edited channel message + if edited != nil && !b.GetBool("EditDisable") { + message = edited + rmsg.Text = rmsg.Text + message.Text + b.GetString("EditSuffix") + } + return message +} + +// handleChannels checks if it's a channel message and if the message is a new or edited messages +func (b *Btelegram) handleChannels(rmsg *config.Message, message *tgbotapi.Message, update tgbotapi.Update) *tgbotapi.Message { + return b.handleUpdate(rmsg, message, update.ChannelPost, update.EditedChannelPost) +} + +// handleGroups checks if it's a group message and if the message is a new or edited messages +func (b *Btelegram) handleGroups(rmsg *config.Message, message *tgbotapi.Message, update tgbotapi.Update) *tgbotapi.Message { + return b.handleUpdate(rmsg, message, update.Message, update.EditedMessage) +} + +// handleForwarded handles forwarded messages +func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Message) { + if message.ForwardFrom != nil { + usernameForward := "" + if b.GetBool("UseFirstName") { + usernameForward = message.ForwardFrom.FirstName + } + if usernameForward == "" { + usernameForward = message.ForwardFrom.UserName + if usernameForward == "" { + usernameForward = message.ForwardFrom.FirstName + } + } + if usernameForward == "" { + usernameForward = unknownUser + } + rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text + } +} + +// handleQuoting handles quoting of previous messages +func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Message) { + if message.ReplyToMessage != nil { + usernameReply := "" + if message.ReplyToMessage.From != nil { + if b.GetBool("UseFirstName") { + usernameReply = message.ReplyToMessage.From.FirstName + } + if usernameReply == "" { + usernameReply = message.ReplyToMessage.From.UserName + if usernameReply == "" { + usernameReply = message.ReplyToMessage.From.FirstName + } + } + } + if usernameReply == "" { + usernameReply = unknownUser + } + if !b.GetBool("QuoteDisable") { + rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, message.ReplyToMessage.Text) + } + } +} + +// handleUsername handles the correct setting of the username +func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) { + if message.From != nil { + rmsg.UserID = strconv.Itoa(message.From.ID) + if b.GetBool("UseFirstName") { + rmsg.Username = message.From.FirstName + } + if rmsg.Username == "" { + rmsg.Username = message.From.UserName + if rmsg.Username == "" { + rmsg.Username = message.From.FirstName + } + } + // only download avatars if we have a place to upload them (configured mediaserver) + if b.General.MediaServerUpload != "" { + b.handleDownloadAvatar(message.From.ID, rmsg.Channel) + } + } + + // if we really didn't find a username, set it to unknown + if rmsg.Username == "" { + rmsg.Username = unknownUser + } +} + +func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { + for update := range updates { + b.Log.Debugf("== Receiving event: %#v", update.Message) + + if update.Message == nil && update.ChannelPost == nil && + update.EditedMessage == nil && update.EditedChannelPost == nil { + b.Log.Error("Getting nil messages, this shouldn't happen.") + continue + } + + var message *tgbotapi.Message + + rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})} + + // handle channels + message = b.handleChannels(&rmsg, message, update) + + // handle groups + message = b.handleGroups(&rmsg, message, update) + + // set the ID's from the channel or group message + rmsg.ID = strconv.Itoa(message.MessageID) + rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10) + + // handle username + b.handleUsername(&rmsg, message) + + // handle any downloads + err := b.handleDownload(&rmsg, message) + if err != nil { + b.Log.Errorf("download failed: %s", err) + } + + // handle forwarded messages + b.handleForwarded(&rmsg, message) + + // quote the previous message + b.handleQuoting(&rmsg, message) + + if rmsg.Text != "" || len(rmsg.Extra) > 0 { + rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text) + // channels don't have (always?) user information. see #410 + if message.From != nil { + rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General) + } + + b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account) + b.Log.Debugf("<= Message is %#v", rmsg) + b.Remote <- rmsg + } + } +} + +// handleDownloadAvatar downloads the avatar of userid from channel +// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful. +// logs an error message if it fails +func (b *Btelegram) handleDownloadAvatar(userid int, channel string) { + rmsg := config.Message{Username: "system", + Text: "avatar", + Channel: channel, + Account: b.Account, + UserID: strconv.Itoa(userid), + Event: config.EventAvatarDownload, + Extra: make(map[string][]interface{})} + + if _, ok := b.avatarMap[strconv.Itoa(userid)]; !ok { + photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1}) + if err != nil { + b.Log.Errorf("Userprofile download failed for %#v %s", userid, err) + } + + if len(photos.Photos) > 0 { + photo := photos.Photos[0][0] + url := b.getFileDirectURL(photo.FileID) + name := strconv.Itoa(userid) + ".png" + b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize) + + err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General) + if err != nil { + b.Log.Error(err) + return + } + data, err := helper.DownloadFile(url) + if err != nil { + b.Log.Errorf("download %s failed %#v", url, err) + return + } + helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General) + b.Remote <- rmsg + } + } +} + +// handleDownloadFile handles file download +func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error { + size := 0 + var url, name, text string + switch { + case message.Sticker != nil: + text, name, url = b.getDownloadInfo(message.Sticker.FileID, ".webp", true) + size = message.Sticker.FileSize + case message.Voice != nil: + text, name, url = b.getDownloadInfo(message.Voice.FileID, ".ogg", true) + size = message.Voice.FileSize + case message.Video != nil: + text, name, url = b.getDownloadInfo(message.Video.FileID, "", true) + size = message.Video.FileSize + case message.Audio != nil: + text, name, url = b.getDownloadInfo(message.Audio.FileID, "", true) + size = message.Audio.FileSize + case message.Document != nil: + _, _, url = b.getDownloadInfo(message.Document.FileID, "", false) + size = message.Document.FileSize + name = message.Document.FileName + text = " " + message.Document.FileName + " : " + url + case message.Photo != nil: + photos := *message.Photo + size = photos[len(photos)-1].FileSize + text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true) + } + + // if name is empty we didn't match a thing to download + if name == "" { + return nil + } + // use the URL instead of native upload + if b.GetBool("UseInsecureURL") { + b.Log.Debugf("Setting message text to :%s", text) + rmsg.Text += text + return nil + } + // if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra + err := helper.HandleDownloadSize(b.Log, rmsg, name, int64(size), b.General) + if err != nil { + return err + } + data, err := helper.DownloadFile(url) + if err != nil { + return err + } + helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General) + return nil +} + +func (b *Btelegram) getDownloadInfo(id string, suffix string, urlpart bool) (string, string, string) { + url := b.getFileDirectURL(id) + name := "" + if urlpart { + urlPart := strings.Split(url, "/") + name = urlPart[len(urlPart)-1] + } + if suffix != "" && !strings.HasSuffix(name, suffix) { + name += suffix + } + text := " " + url + return text, name, url +} + +// handleDelete handles message deleting +func (b *Btelegram) handleDelete(msg *config.Message, chatid int64) (string, error) { + if msg.ID == "" { + return "", nil + } + msgid, err := strconv.Atoi(msg.ID) + if err != nil { + return "", err + } + _, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid}) + return "", err +} + +// handleEdit handles message editing. +func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error) { + msgid, err := strconv.Atoi(msg.ID) + if err != nil { + return "", err + } + if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick { + b.Log.Debug("Using mode HTML - nick only") + msg.Text = html.EscapeString(msg.Text) + } + m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text) + switch b.GetString("MessageFormat") { + case HTMLFormat: + b.Log.Debug("Using mode HTML") + m.ParseMode = tgbotapi.ModeHTML + case "Markdown": + b.Log.Debug("Using mode markdown") + m.ParseMode = tgbotapi.ModeMarkdown + } + if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick { + b.Log.Debug("Using mode HTML - nick only") + m.ParseMode = tgbotapi.ModeHTML + } + _, err = b.c.Send(m) + if err != nil { + return "", err + } + return "", nil +} + +// handleUploadFile handles native upload of files +func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) string { + var c tgbotapi.Chattable + for _, f := range msg.Extra["file"] { + fi := f.(config.FileInfo) + file := tgbotapi.FileBytes{ + Name: fi.Name, + Bytes: *fi.Data, + } + re := regexp.MustCompile(".(jpg|png)$") + if re.MatchString(fi.Name) { + c = tgbotapi.NewPhotoUpload(chatid, file) + } else { + c = tgbotapi.NewDocumentUpload(chatid, file) + } + _, err := b.c.Send(c) + if err != nil { + b.Log.Errorf("file upload failed: %#v", err) + } + if fi.Comment != "" { + if _, err := b.sendMessage(chatid, msg.Username, fi.Comment); err != nil { + b.Log.Errorf("posting file comment %s failed: %s", fi.Comment, err) + } + } + } + return "" +} + +func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string { + format := b.GetString("quoteformat") + if format == "" { + format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})" + } + format = strings.Replace(format, "{MESSAGE}", message, -1) + format = strings.Replace(format, "{QUOTENICK}", quoteNick, -1) + format = strings.Replace(format, "{QUOTEMESSAGE}", quoteMessage, -1) + return format +} diff --git a/bridge/telegram/html.go b/bridge/telegram/html.go index 85547d7b..148f42e6 100644 --- a/bridge/telegram/html.go +++ b/bridge/telegram/html.go @@ -3,6 +3,7 @@ package btelegram import ( "bytes" "html" + "io" "github.com/russross/blackfriday" ) @@ -32,8 +33,8 @@ func (options *customHTML) Header(out *bytes.Buffer, text func() bool, level int options.Paragraph(out, text) } -func (options *customHTML) HRule(out *bytes.Buffer) { - out.WriteByte('\n') +func (options *customHTML) HRule(out io.ByteWriter) { + out.WriteByte('\n') //nolint:errcheck } func (options *customHTML) BlockQuote(out *bytes.Buffer, text []byte) { @@ -53,13 +54,16 @@ func (options *customHTML) ListItem(out *bytes.Buffer, text []byte, flags int) { } func makeHTML(input string) string { - return string(blackfriday.Markdown([]byte(input), - &customHTML{blackfriday.HtmlRenderer(blackfriday.HTML_USE_XHTML|blackfriday.HTML_SKIP_IMAGES, "", "")}, - blackfriday.EXTENSION_NO_INTRA_EMPHASIS| - blackfriday.EXTENSION_FENCED_CODE| - blackfriday.EXTENSION_AUTOLINK| - blackfriday.EXTENSION_SPACE_HEADERS| - blackfriday.EXTENSION_HEADER_IDS| - blackfriday.EXTENSION_BACKSLASH_LINE_BREAK| - blackfriday.EXTENSION_DEFINITION_LISTS)) + extensions := blackfriday.NoIntraEmphasis | + blackfriday.FencedCode | + blackfriday.Autolink | + blackfriday.SpaceHeadings | + blackfriday.HeadingIDs | + blackfriday.BackslashLineBreak | + blackfriday.DefinitionLists + + renderer := &customHTML{blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{ + Flags: blackfriday.UseXHTML | blackfriday.SkipImages, + })} + return string(blackfriday.Run([]byte(input), blackfriday.WithExtensions(extensions), blackfriday.WithRenderer(renderer))) } diff --git a/bridge/telegram/telegram.go b/bridge/telegram/telegram.go index 2fe70c01..c37ef23b 100644 --- a/bridge/telegram/telegram.go +++ b/bridge/telegram/telegram.go @@ -2,7 +2,6 @@ package btelegram import ( "html" - "regexp" "strconv" "strings" @@ -12,6 +11,12 @@ import ( "github.com/go-telegram-bot-api/telegram-bot-api" ) +const ( + unknownUser = "unknown" + HTMLFormat = "HTML" + HTMLNick = "htmlnick" +) + type Btelegram struct { c *tgbotapi.BotAPI *bridge.Config @@ -60,31 +65,25 @@ func (b *Btelegram) Send(msg config.Message) (string, error) { } // map the file SHA to our user (caches the avatar) - if msg.Event == config.EVENT_AVATAR_DOWNLOAD { + if msg.Event == config.EventAvatarDownload { return b.cacheAvatar(&msg) } - if b.GetString("MessageFormat") == "HTML" { + if b.GetString("MessageFormat") == HTMLFormat { msg.Text = makeHTML(msg.Text) } // Delete message - if msg.Event == config.EVENT_MSG_DELETE { - if msg.ID == "" { - return "", nil - } - msgid, err := strconv.Atoi(msg.ID) - if err != nil { - return "", err - } - _, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid}) - return "", err + if msg.Event == config.EventMsgDelete { + return b.handleDelete(&msg, chatid) } // Upload a file if it exists if msg.Extra != nil { for _, rmsg := range helper.HandleExtra(&msg, b.General) { - b.sendMessage(chatid, rmsg.Username, rmsg.Text) + if _, err := b.sendMessage(chatid, rmsg.Username, rmsg.Text); err != nil { + b.Log.Errorf("sendMessage failed: %s", err) + } } // check if we have files to upload (from slack, telegram or mattermost) if len(msg.Extra["file"]) > 0 { @@ -94,38 +93,14 @@ func (b *Btelegram) Send(msg config.Message) (string, error) { // edit the message if we have a msg ID if msg.ID != "" { - msgid, err := strconv.Atoi(msg.ID) - if err != nil { - return "", err - } - if strings.ToLower(b.GetString("MessageFormat")) == "htmlnick" { - b.Log.Debug("Using mode HTML - nick only") - msg.Text = html.EscapeString(msg.Text) - } - m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text) - if b.GetString("MessageFormat") == "HTML" { - b.Log.Debug("Using mode HTML") - m.ParseMode = tgbotapi.ModeHTML - } - if b.GetString("MessageFormat") == "Markdown" { - b.Log.Debug("Using mode markdown") - m.ParseMode = tgbotapi.ModeMarkdown - } - if strings.ToLower(b.GetString("MessageFormat")) == "htmlnick" { - b.Log.Debug("Using mode HTML - nick only") - m.ParseMode = tgbotapi.ModeHTML - } - _, err = b.c.Send(m) - if err != nil { - return "", err - } - return "", nil + return b.handleEdit(&msg, chatid) } // Post normal message return b.sendMessage(chatid, msg.Username, msg.Text) } +<<<<<<< HEAD func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { for update := range updates { b.Log.Debugf("== Receiving event: %#v", update.Message) @@ -256,6 +231,8 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { } } +======= +>>>>>>> upstream/master func (b *Btelegram) getFileDirectURL(id string) string { res, err := b.c.GetFileDirectURL(id) if err != nil { @@ -264,147 +241,10 @@ func (b *Btelegram) getFileDirectURL(id string) string { return res } -// handleDownloadAvatar downloads the avatar of userid from channel -// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful. -// logs an error message if it fails -func (b *Btelegram) handleDownloadAvatar(userid int, channel string) { - rmsg := config.Message{Username: "system", Text: "avatar", Channel: channel, Account: b.Account, UserID: strconv.Itoa(userid), Event: config.EVENT_AVATAR_DOWNLOAD, Extra: make(map[string][]interface{})} - if _, ok := b.avatarMap[strconv.Itoa(userid)]; !ok { - photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1}) - if err != nil { - b.Log.Errorf("Userprofile download failed for %#v %s", userid, err) - } - - if len(photos.Photos) > 0 { - photo := photos.Photos[0][0] - url := b.getFileDirectURL(photo.FileID) - name := strconv.Itoa(userid) + ".png" - b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize) - - err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General) - if err != nil { - b.Log.Error(err) - return - } - data, err := helper.DownloadFile(url) - if err != nil { - b.Log.Errorf("download %s failed %#v", url, err) - return - } - helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General) - b.Remote <- rmsg - } - } -} - -// handleDownloadFile handles file download -func (b *Btelegram) handleDownload(message *tgbotapi.Message, rmsg *config.Message) error { - size := 0 - var url, name, text string - - if message.Sticker != nil { - v := message.Sticker - size = v.FileSize - url = b.getFileDirectURL(v.FileID) - urlPart := strings.Split(url, "/") - name = urlPart[len(urlPart)-1] - if !strings.HasSuffix(name, ".webp") { - name = name + ".webp" - } - text = " " + url - } - if message.Video != nil { - v := message.Video - size = v.FileSize - url = b.getFileDirectURL(v.FileID) - urlPart := strings.Split(url, "/") - name = urlPart[len(urlPart)-1] - text = " " + url - } - if message.Photo != nil { - photos := *message.Photo - size = photos[len(photos)-1].FileSize - url = b.getFileDirectURL(photos[len(photos)-1].FileID) - urlPart := strings.Split(url, "/") - name = urlPart[len(urlPart)-1] - text = " " + url - } - if message.Document != nil { - v := message.Document - size = v.FileSize - url = b.getFileDirectURL(v.FileID) - name = v.FileName - text = " " + v.FileName + " : " + url - } - if message.Voice != nil { - v := message.Voice - size = v.FileSize - url = b.getFileDirectURL(v.FileID) - urlPart := strings.Split(url, "/") - name = urlPart[len(urlPart)-1] - text = " " + url - if !strings.HasSuffix(name, ".ogg") { - name = name + ".ogg" - } - } - if message.Audio != nil { - v := message.Audio - size = v.FileSize - url = b.getFileDirectURL(v.FileID) - urlPart := strings.Split(url, "/") - name = urlPart[len(urlPart)-1] - text = " " + url - } - // if name is empty we didn't match a thing to download - if name == "" { - return nil - } - // use the URL instead of native upload - if b.GetBool("UseInsecureURL") { - b.Log.Debugf("Setting message text to :%s", text) - rmsg.Text = rmsg.Text + text - return nil - } - // if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra - err := helper.HandleDownloadSize(b.Log, rmsg, name, int64(size), b.General) - if err != nil { - return err - } - data, err := helper.DownloadFile(url) - if err != nil { - return err - } - helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General) - return nil -} - -// handleUploadFile handles native upload of files -func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) (string, error) { - var c tgbotapi.Chattable - for _, f := range msg.Extra["file"] { - fi := f.(config.FileInfo) - file := tgbotapi.FileBytes{fi.Name, *fi.Data} - re := regexp.MustCompile(".(jpg|png)$") - if re.MatchString(fi.Name) { - c = tgbotapi.NewPhotoUpload(chatid, file) - } else { - c = tgbotapi.NewDocumentUpload(chatid, file) - } - _, err := b.c.Send(c) - if err != nil { - b.Log.Errorf("file upload failed: %#v", err) - } - if fi.Comment != "" { - b.sendMessage(chatid, msg.Username, fi.Comment) - } - } - return "", nil -} - func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, error) { m := tgbotapi.NewMessage(chatid, "") m.Text = username + text - if b.GetString("MessageFormat") == "HTML" { + if b.GetString("MessageFormat") == HTMLFormat { b.Log.Debug("Using mode HTML") m.ParseMode = tgbotapi.ModeHTML } @@ -412,7 +252,7 @@ func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, er b.Log.Debug("Using mode markdown") m.ParseMode = tgbotapi.ModeMarkdown } - if strings.ToLower(b.GetString("MessageFormat")) == "htmlnick" { + if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick { b.Log.Debug("Using mode HTML - nick only") m.Text = username + html.EscapeString(text) m.ParseMode = tgbotapi.ModeHTML @@ -434,14 +274,3 @@ func (b *Btelegram) cacheAvatar(msg *config.Message) (string, error) { } return "", nil } - -func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string { - format := b.GetString("quoteformat") - if format == "" { - format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})" - } - format = strings.Replace(format, "{MESSAGE}", message, -1) - format = strings.Replace(format, "{QUOTENICK}", quoteNick, -1) - format = strings.Replace(format, "{QUOTEMESSAGE}", quoteMessage, -1) - return format -} diff --git a/bridge/xmpp/xmpp.go b/bridge/xmpp/xmpp.go index 4ace2abd..75fc83a4 100644 --- a/bridge/xmpp/xmpp.go +++ b/bridge/xmpp/xmpp.go @@ -51,7 +51,7 @@ func (b *Bxmpp) Connect() error { time.Sleep(d) b.xc, err = b.createXMPP() if err == nil { - b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS} + b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EventRejoinChannels} b.handleXMPP() bf.Reset() } @@ -75,10 +75,8 @@ func (b *Bxmpp) JoinChannel(channel config.ChannelInfo) error { } func (b *Bxmpp) Send(msg config.Message) (string, error) { - var msgid = "" - var msgreplaceid = "" // ignore delete messages - if msg.Event == config.EVENT_MSG_DELETE { + if msg.Event == config.EventMsgDelete { return "", nil } b.Log.Debugf("=> Receiving %#v", msg) @@ -93,7 +91,8 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) { } } - msgid = xid.New().String() + var msgreplaceid string + msgid := xid.New().String() if msg.ID != "" { msgid = msg.ID msgreplaceid = msg.ID @@ -178,7 +177,7 @@ func (b *Bxmpp) handleXMPP() error { // check if we have an action event rmsg.Text, ok = b.replaceAction(rmsg.Text) if ok { - rmsg.Event = config.EVENT_USER_ACTION + rmsg.Event = config.EventUserAction } b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account) b.Log.Debugf("<= Message is %#v", rmsg) diff --git a/bridge/zulip/zulip.go b/bridge/zulip/zulip.go index ebeabc1c..88832d36 100644 --- a/bridge/zulip/zulip.go +++ b/bridge/zulip/zulip.go @@ -52,7 +52,7 @@ func (b *Bzulip) Send(msg config.Message) (string, error) { b.Log.Debugf("=> Receiving %#v", msg) // Delete message - if msg.Event == config.EVENT_MSG_DELETE { + if msg.Event == config.EventMsgDelete { if msg.ID == "" { return "", nil } diff --git a/changelog.md b/changelog.md index db02a3fc..71f7f3f1 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,56 @@ +# v1.12.2 + +## Bugfix +* irc: Fix multiple channel join regression. Closes #639 +* slack: Make slack-legacy change less restrictive (#626) + +# v1.12.1 + +## Bugfix +* discord: fix regression on server ID connection #619 #617 +* discord: Limit discord username via webhook to 32 chars +* slack: Make sure threaded files stay in thread (slack). Fixes #590 +* slack: Do not post empty messages (slack). Fixes #574 +* slack: Handle deleted/edited thread starting messages (slack). Fixes #600 (#605) +* irc: Rework connection logic (irc) +* irc: Fix Nickserv logic (irc) #602 + +# v1.12.0 + +## Breaking changes +The slack bridge has been split in a `slack-legacy` and `slack` bridge. +If you're still using `legacy tokens` and want to keep using them you'll have to rename `slack` to `slack-legacy` in your configuration. See [wiki](https://github.com/42wim/matterbridge/wiki/Section-Slack-(basic)#legacy-configuration) for more information. + +To migrate to the new bot-token based setup you can follow the instructions [here](https://github.com/42wim/matterbridge/wiki/Slack-bot-setup). + +Slack legacy tokens may be deprecated by Slack at short notice, so it is STRONGLY recommended to use a proper bot-token instead. + +## New features +* general: New {GATEWAY} variable for `RemoteNickFormat` #501. See `RemoteNickFormat` in matterbridge.toml.sample. +* general: New {CHANNEL} variable for `RemoteNickFormat` #515. See `RemoteNickFormat` in matterbridge.toml.sample. +* general: Remove hyphens when auto-loading envvars from viper config #545 +* discord: You can mention discord-users from other bridges. +* slack: Preserve threading between Slack instances #529. See `PreserveThreading` in matterbridge.toml.sample. +* slack: Add ability to show when user is typing across Slack bridges #559 +* slack: Add rate-limiting +* mattermost: Add support for mattermost [matterbridge plugin](https://github.com/matterbridge/mattermost-plugin) +* api: Respond with message on connect. #550 +* api: Add a health endpoint to API #554 + +## Bugfix +* slack: Refactoring and making it better. +* slack: Restore file comments coming from Slack. #583 +* irc: Fix IRC line splitting. #587 +* mattermost: Fix cookie and personal token behaviour. #530 +* mattermost: Check for expiring sessions and reconnect. + + +## Contributors +This release couldn't exist without the following contributors: +@jheiselman, @NikkyAI, @dajohi, @NetwideRogue, @patcon and @Helcaraxan + +Special thanks to @Helcaraxan and @patcon for their work on improving/refactoring slack. + # v1.11.3 ## Bugfix diff --git a/ci/bintray.sh b/ci/bintray.sh index 1ca8ba25..b0fb7d6a 100755 --- a/ci/bintray.sh +++ b/ci/bintray.sh @@ -1,5 +1,5 @@ #!/bin/bash -go version |grep go1.10 || exit +go version | grep go1.11 || exit VERSION=$(git describe --tags) mkdir ci/binaries GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-windows-amd64.exe diff --git a/gateway/bridgemap/bridgemap.go b/gateway/bridgemap/bridgemap.go new file mode 100644 index 00000000..20577dc1 --- /dev/null +++ b/gateway/bridgemap/bridgemap.go @@ -0,0 +1,35 @@ +package bridgemap + +import ( + "github.com/42wim/matterbridge/bridge" + "github.com/42wim/matterbridge/bridge/api" + "github.com/42wim/matterbridge/bridge/discord" + "github.com/42wim/matterbridge/bridge/gitter" + "github.com/42wim/matterbridge/bridge/irc" + "github.com/42wim/matterbridge/bridge/matrix" + "github.com/42wim/matterbridge/bridge/mattermost" + "github.com/42wim/matterbridge/bridge/rocketchat" + "github.com/42wim/matterbridge/bridge/slack" + "github.com/42wim/matterbridge/bridge/sshchat" + "github.com/42wim/matterbridge/bridge/steam" + "github.com/42wim/matterbridge/bridge/telegram" + "github.com/42wim/matterbridge/bridge/xmpp" + "github.com/42wim/matterbridge/bridge/zulip" +) + +var FullMap = map[string]bridge.Factory{ + "api": api.New, + "discord": bdiscord.New, + "gitter": bgitter.New, + "irc": birc.New, + "mattermost": bmattermost.New, + "matrix": bmatrix.New, + "rocketchat": brocketchat.New, + "slack-legacy": bslack.NewLegacy, + "slack": bslack.New, + "sshchat": bsshchat.New, + "steam": bsteam.New, + "telegram": btelegram.New, + "xmpp": bxmpp.New, + "zulip": bzulip.New, +} diff --git a/gateway/gateway.go b/gateway/gateway.go index 6529393f..d6c6fcea 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -1,41 +1,20 @@ package gateway import ( - "bytes" - "fmt" - "io/ioutil" - "net/http" - "os" - - "github.com/42wim/matterbridge/bridge" - "github.com/42wim/matterbridge/bridge/api" - "github.com/42wim/matterbridge/bridge/config" - bdiscord "github.com/42wim/matterbridge/bridge/discord" - bgitter "github.com/42wim/matterbridge/bridge/gitter" - birc "github.com/42wim/matterbridge/bridge/irc" - bmatrix "github.com/42wim/matterbridge/bridge/matrix" - bmattermost "github.com/42wim/matterbridge/bridge/mattermost" - brocketchat "github.com/42wim/matterbridge/bridge/rocketchat" - bslack "github.com/42wim/matterbridge/bridge/slack" - bsshchat "github.com/42wim/matterbridge/bridge/sshchat" - bsteam "github.com/42wim/matterbridge/bridge/steam" - btelegram "github.com/42wim/matterbridge/bridge/telegram" - bxmpp "github.com/42wim/matterbridge/bridge/xmpp" - bzulip "github.com/42wim/matterbridge/bridge/zulip" - "github.com/hashicorp/golang-lru" - log "github.com/sirupsen/logrus" - // "github.com/davecgh/go-spew/spew" - "crypto/sha1" - "path/filepath" "regexp" "strings" "time" + "github.com/42wim/matterbridge/bridge" + "github.com/42wim/matterbridge/bridge/config" + "github.com/hashicorp/golang-lru" "github.com/peterhellberg/emojilib" + log "github.com/sirupsen/logrus" ) type Gateway struct { - *config.Config + config.Config + Router *Router MyConfig *config.Gateway Bridges map[string]*bridge.Bridge @@ -54,46 +33,53 @@ type BrMsgID struct { var flog *log.Entry -var bridgeMap = map[string]bridge.Factory{ - "api": api.New, - "discord": bdiscord.New, - "gitter": bgitter.New, - "irc": birc.New, - "mattermost": bmattermost.New, - "matrix": bmatrix.New, - "rocketchat": brocketchat.New, - "slack": bslack.New, - "sshchat": bsshchat.New, - "steam": bsteam.New, - "telegram": btelegram.New, - "xmpp": bxmpp.New, - "zulip": bzulip.New, -} - -func init() { - flog = log.WithFields(log.Fields{"prefix": "gateway"}) -} +const ( + apiProtocol = "api" +) func New(cfg config.Gateway, r *Router) *Gateway { + flog = log.WithFields(log.Fields{"prefix": "gateway"}) gw := &Gateway{Channels: make(map[string]*config.ChannelInfo), Message: r.Message, Router: r, Bridges: make(map[string]*bridge.Bridge), Config: r.Config} cache, _ := lru.New(5000) gw.Messages = cache - gw.AddConfig(&cfg) + if err := gw.AddConfig(&cfg); err != nil { + flog.Errorf("AddConfig failed: %s", err) + } return gw } +// Find the canonical ID that the message is keyed under in cache +func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string { + ID := protocol + " " + mID + if gw.Messages.Contains(ID) { + return mID + } + + // If not keyed, iterate through cache for downstream, and infer upstream. + for _, mid := range gw.Messages.Keys() { + v, _ := gw.Messages.Peek(mid) + ids := v.([]*BrMsgID) + for _, downstreamMsgObj := range ids { + if ID == downstreamMsgObj.ID { + return strings.Replace(mid.(string), protocol+" ", "", 1) + } + } + } + return "" +} + func (gw *Gateway) AddBridge(cfg *config.Bridge) error { br := gw.Router.getBridge(cfg.Account) if br == nil { br = bridge.New(cfg) br.Config = gw.Router.Config - br.General = &gw.General + br.General = &gw.BridgeValues().General // set logging br.Log = log.WithFields(log.Fields{"prefix": "bridge"}) brconfig := &bridge.Config{Remote: gw.Message, Log: log.WithFields(log.Fields{"prefix": br.Protocol}), Bridge: br} // add the actual bridger for this protocol to this bridge using the bridgeMap - br.Bridger = bridgeMap[br.Protocol](brconfig) + br.Bridger = gw.Router.BridgeMap[br.Protocol](brconfig) } gw.mapChannelsToBridge(br) gw.Bridges[cfg.Account] = br @@ -103,8 +89,11 @@ func (gw *Gateway) AddBridge(cfg *config.Bridge) error { func (gw *Gateway) AddConfig(cfg *config.Gateway) error { gw.Name = cfg.Name gw.MyConfig = cfg - gw.mapChannels() + if err := gw.mapChannels(); err != nil { + flog.Errorf("mapChannels() failed: %s", err) + } for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) { + br := br //scopelint err := gw.AddBridge(&br) if err != nil { return err @@ -122,7 +111,9 @@ func (gw *Gateway) mapChannelsToBridge(br *bridge.Bridge) { } func (gw *Gateway) reconnectBridge(br *bridge.Bridge) { - br.Disconnect() + if err := br.Disconnect(); err != nil { + flog.Errorf("Disconnect() %s failed: %s", br.Account, err) + } time.Sleep(time.Second * 5) RECONNECT: flog.Infof("Reconnecting %s", br.Account) @@ -133,13 +124,15 @@ RECONNECT: goto RECONNECT } br.Joined = make(map[string]bool) - br.JoinChannels() + if err := br.JoinChannels(); err != nil { + flog.Errorf("JoinChannels() %s failed: %s", br.Account, err) + } } func (gw *Gateway) mapChannelConfig(cfg []config.Bridge, direction string) { for _, br := range cfg { - if isApi(br.Account) { - br.Channel = "api" + if isAPI(br.Account) { + br.Channel = apiProtocol } // make sure to lowercase irc channels in config #348 if strings.HasPrefix(br.Account, "irc.") { @@ -172,7 +165,7 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con var channels []config.ChannelInfo // for messages received from the api check that the gateway is the specified one - if msg.Protocol == "api" && gw.Name != msg.Gateway { + if msg.Protocol == apiProtocol && gw.Name != msg.Gateway { return channels } @@ -199,94 +192,76 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con } continue } - if strings.Contains(channel.Direction, "out") && channel.Account == dest.Account && gw.validGatewayDest(msg, channel) { + if strings.Contains(channel.Direction, "out") && channel.Account == dest.Account && gw.validGatewayDest(msg) { channels = append(channels, *channel) } } return channels } -func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrMsgID { - var brMsgIDs []*BrMsgID - - // if we have an attached file, or other info - if msg.Extra != nil { - if len(msg.Extra[config.EVENT_FILE_FAILURE_SIZE]) != 0 { - if msg.Text == "" { - return brMsgIDs +func (gw *Gateway) getDestMsgID(msgID string, dest *bridge.Bridge, channel config.ChannelInfo) string { + if res, ok := gw.Messages.Get(msgID); ok { + IDs := res.([]*BrMsgID) + for _, id := range IDs { + // check protocol, bridge name and channelname + // for people that reuse the same bridge multiple times. see #342 + if dest.Protocol == id.br.Protocol && dest.Name == id.br.Name && channel.ID == id.ChannelID { + return strings.Replace(id.ID, dest.Protocol+" ", "", 1) } } } + return "" +} - // Avatar downloads are only relevant for telegram and mattermost for now - if msg.Event == config.EVENT_AVATAR_DOWNLOAD { - if dest.Protocol != "mattermost" && - dest.Protocol != "telegram" { - return brMsgIDs - } +// ignoreTextEmpty returns true if we need to ignore a message with an empty text. +func (gw *Gateway) ignoreTextEmpty(msg *config.Message) bool { + if msg.Text != "" { + return false } - - // only relay join/part when configured - if msg.Event == config.EVENT_JOIN_LEAVE && !gw.Bridges[dest.Account].GetBool("ShowJoinPart") { - return brMsgIDs + if msg.Event == config.EventUserTyping { + return false } - - // only relay topic change when configured - if msg.Event == config.EVENT_TOPIC_CHANGE && !gw.Bridges[dest.Account].GetBool("ShowTopicChange") { - return brMsgIDs + // we have an attachment or actual bytes, do not ignore + if msg.Extra != nil && + (msg.Extra["attachments"] != nil || + len(msg.Extra["file"]) > 0 || + len(msg.Extra[config.EventFileFailureSize]) > 0) { + return false } + flog.Debugf("ignoring empty message %#v from %s", msg, msg.Account) + return true +} - // broadcast to every out channel (irc QUIT) - if msg.Channel == "" && msg.Event != config.EVENT_JOIN_LEAVE { - flog.Debug("empty channel") - return brMsgIDs - } - - originchannel := msg.Channel - origmsg := msg - channels := gw.getDestChannel(&msg, *dest) - for _, channel := range channels { - // Only send the avatar download event to ourselves. - if msg.Event == config.EVENT_AVATAR_DOWNLOAD { - if channel.ID != getChannelID(origmsg) { - continue - } - } else { - // do not send to ourself for any other event - if channel.ID == getChannelID(origmsg) { - continue - } +// ignoreTexts returns true if msg.Text matches any of the input regexes. +func (gw *Gateway) ignoreTexts(msg *config.Message, input []string) bool { + for _, entry := range input { + if entry == "" { + continue } - flog.Debugf("=> Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, originchannel, dest.Account, channel.Name) - msg.Channel = channel.Name - msg.Avatar = gw.modifyAvatar(origmsg, dest) - msg.Username = gw.modifyUsername(origmsg, dest) - msg.ID = "" - if res, ok := gw.Messages.Get(origmsg.ID); ok { - IDs := res.([]*BrMsgID) - for _, id := range IDs { - // check protocol, bridge name and channelname - // for people that reuse the same bridge multiple times. see #342 - if dest.Protocol == id.br.Protocol && dest.Name == id.br.Name && channel.ID == id.ChannelID { - msg.ID = id.ID - } - } - } - // for api we need originchannel as channel - if dest.Protocol == "api" { - msg.Channel = originchannel - } - mID, err := dest.Send(msg) + // TODO do not compile regexps everytime + re, err := regexp.Compile(entry) if err != nil { - flog.Error(err) + flog.Errorf("incorrect regexp %s for %s", entry, msg.Account) + continue } - // append the message ID (mID) from this bridge (dest) to our brMsgIDs slice - if mID != "" { - flog.Debugf("mID %s: %s", dest.Account, mID) - brMsgIDs = append(brMsgIDs, &BrMsgID{dest, mID, channel.ID}) + if re.MatchString(msg.Text) { + flog.Debugf("matching %s. ignoring %s from %s", entry, msg.Text, msg.Account) + return true } } - return brMsgIDs + return false +} + +// ignoreNicks returns true if msg.Username matches any of the input regexes. +func (gw *Gateway) ignoreNicks(msg *config.Message, input []string) bool { + // is the username in IgnoreNicks field + for _, entry := range input { + if msg.Username == entry { + flog.Debugf("ignoring %s from %s", msg.Username, msg.Account) + return true + } + } + return false } func (gw *Gateway) ignoreMessage(msg *config.Message) bool { @@ -295,56 +270,23 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool { return true } - // check if we need to ignore a empty message - if msg.Text == "" { - // we have an attachment or actual bytes, do not ignore - if msg.Extra != nil && - (msg.Extra["attachments"] != nil || - len(msg.Extra["file"]) > 0 || - len(msg.Extra[config.EVENT_FILE_FAILURE_SIZE]) > 0) { - return false - } - flog.Debugf("ignoring empty message %#v from %s", msg, msg.Account) + igNicks := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreNicks")) + igMessages := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreMessages")) + if gw.ignoreTextEmpty(msg) || gw.ignoreNicks(msg, igNicks) || gw.ignoreTexts(msg, igMessages) { return true } - // is the username in IgnoreNicks field - for _, entry := range strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreNicks")) { - if msg.Username == entry { - flog.Debugf("ignoring %s from %s", msg.Username, msg.Account) - return true - } - } - - // does the message match regex in IgnoreMessages field - // TODO do not compile regexps everytime - for _, entry := range strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreMessages")) { - if entry != "" { - re, err := regexp.Compile(entry) - if err != nil { - flog.Errorf("incorrect regexp %s for %s", entry, msg.Account) - continue - } - if re.MatchString(msg.Text) { - flog.Debugf("matching %s. ignoring %s from %s", entry, msg.Text, msg.Account) - return true - } - } - } return false } func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) string { br := gw.Bridges[msg.Account] msg.Protocol = br.Protocol - if gw.Config.General.StripNick || dest.GetBool("StripNick") { + if dest.GetBool("StripNick") { re := regexp.MustCompile("[^a-zA-Z0-9]+") msg.Username = re.ReplaceAllString(msg.Username, "") } nick := dest.GetString("RemoteNickFormat") - if nick == "" { - nick = gw.Config.General.RemoteNickFormat - } // loop to replace nicks for _, outer := range br.GetStringSlice2D("ReplaceNicks") { @@ -374,16 +316,15 @@ func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) strin nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1) nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1) + nick = strings.Replace(nick, "{GATEWAY}", gw.Name, -1) nick = strings.Replace(nick, "{LABEL}", br.GetString("Label"), -1) nick = strings.Replace(nick, "{NICK}", msg.Username, -1) + nick = strings.Replace(nick, "{CHANNEL}", msg.Channel, -1) return nick } func (gw *Gateway) modifyAvatar(msg config.Message, dest *bridge.Bridge) string { - iconurl := gw.Config.General.IconURL - if iconurl == "" { - iconurl = dest.GetString("IconURL") - } + iconurl := dest.GetString("IconURL") iconurl = strings.Replace(iconurl, "{NICK}", msg.Username, -1) if msg.Avatar == "" { msg.Avatar = iconurl @@ -410,92 +351,69 @@ func (gw *Gateway) modifyMessage(msg *config.Message) { } // messages from api have Gateway specified, don't overwrite - if msg.Protocol != "api" { + if msg.Protocol != apiProtocol { msg.Gateway = gw.Name } } -// handleFiles uploads or places all files on the given msg to the MediaServer and -// adds the new URL of the file on the MediaServer onto the given msg. -func (gw *Gateway) handleFiles(msg *config.Message) { - reg := regexp.MustCompile("[^a-zA-Z0-9]+") - - // If we don't have a attachfield or we don't have a mediaserver configured return - if msg.Extra == nil || (gw.Config.General.MediaServerUpload == "" && gw.Config.General.MediaDownloadPath == "") { - return - } - - // If we don't have files, nothing to upload. - if len(msg.Extra["file"]) == 0 { - return - } - - client := &http.Client{ - Timeout: time.Second * 5, - } - - for i, f := range msg.Extra["file"] { - fi := f.(config.FileInfo) - ext := filepath.Ext(fi.Name) - fi.Name = fi.Name[0 : len(fi.Name)-len(ext)] - fi.Name = reg.ReplaceAllString(fi.Name, "_") - fi.Name = fi.Name + ext - - sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))[:8] - - if gw.Config.General.MediaServerUpload != "" { - // Use MediaServerUpload. Upload using a PUT HTTP request and basicauth. - - url := gw.Config.General.MediaServerUpload + "/" + sha1sum + "/" + fi.Name - - req, err := http.NewRequest("PUT", url, bytes.NewReader(*fi.Data)) - if err != nil { - flog.Errorf("mediaserver upload failed, could not create request: %#v", err) - continue - } - - flog.Debugf("mediaserver upload url: %s", url) - - req.Header.Set("Content-Type", "binary/octet-stream") - _, err = client.Do(req) - if err != nil { - flog.Errorf("mediaserver upload failed, could not Do request: %#v", err) - continue - } - } else { - // Use MediaServerPath. Place the file on the current filesystem. - - dir := gw.Config.General.MediaDownloadPath + "/" + sha1sum - err := os.Mkdir(dir, os.ModePerm) - if err != nil && !os.IsExist(err) { - flog.Errorf("mediaserver path failed, could not mkdir: %s %#v", err, err) - continue - } - - path := dir + "/" + fi.Name - flog.Debugf("mediaserver path placing file: %s", path) - - err = ioutil.WriteFile(path, *fi.Data, os.ModePerm) - if err != nil { - flog.Errorf("mediaserver path failed, could not writefile: %s %#v", err, err) - continue - } +// SendMessage sends a message (with specified parentID) to the channel on the selected destination bridge. +// returns a message id and error. +func (gw *Gateway) SendMessage(origmsg config.Message, dest *bridge.Bridge, channel config.ChannelInfo, canonicalParentMsgID string) (string, error) { + msg := origmsg + // Only send the avatar download event to ourselves. + if msg.Event == config.EventAvatarDownload { + if channel.ID != getChannelID(origmsg) { + return "", nil + } + } else { + // do not send to ourself for any other event + if channel.ID == getChannelID(origmsg) { + return "", nil } - - // Download URL. - durl := gw.Config.General.MediaServerDownload + "/" + sha1sum + "/" + fi.Name - - flog.Debugf("mediaserver download URL = %s", durl) - - // We uploaded/placed the file successfully. Add the SHA and URL. - extra := msg.Extra["file"][i].(config.FileInfo) - extra.URL = durl - extra.SHA = sha1sum - msg.Extra["file"][i] = extra } + + // Too noisy to log like other events + if msg.Event != config.EventUserTyping { + flog.Debugf("=> Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, origmsg.Channel, dest.Account, channel.Name) + } + + msg.Channel = channel.Name + msg.Avatar = gw.modifyAvatar(origmsg, dest) + msg.Username = gw.modifyUsername(origmsg, dest) + + msg.ID = gw.getDestMsgID(origmsg.Protocol+" "+origmsg.ID, dest, channel) + + // for api we need originchannel as channel + if dest.Protocol == apiProtocol { + msg.Channel = origmsg.Channel + } + + msg.ParentID = gw.getDestMsgID(origmsg.Protocol+" "+canonicalParentMsgID, dest, channel) + if msg.ParentID == "" { + msg.ParentID = canonicalParentMsgID + } + + // if we are using mattermost plugin account, send messages to MattermostPlugin channel + // that can be picked up by the mattermost matterbridge plugin + if dest.Account == "mattermost.plugin" { + gw.Router.MattermostPlugin <- msg + } + + mID, err := dest.Send(msg) + if err != nil { + return mID, err + } + + // append the message ID (mID) from this bridge (dest) to our brMsgIDs slice + if mID != "" { + flog.Debugf("mID %s: %s", dest.Account, mID) + return mID, nil + //brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + mID, channel.ID}) + } + return "", nil } -func (gw *Gateway) validGatewayDest(msg *config.Message, channel *config.ChannelInfo) bool { +func (gw *Gateway) validGatewayDest(msg *config.Message) bool { return msg.Gateway == gw.Name } @@ -503,6 +421,6 @@ func getChannelID(msg config.Message) string { return msg.Channel + msg.Account } -func isApi(account string) bool { +func isAPI(account string) bool { return strings.HasPrefix(account, "api.") } diff --git a/gateway/gateway_test.go b/gateway/gateway_test.go index bf9461c7..2e6d828c 100644 --- a/gateway/gateway_test.go +++ b/gateway/gateway_test.go @@ -5,6 +5,7 @@ import ( "strconv" "github.com/42wim/matterbridge/bridge/config" + "github.com/42wim/matterbridge/gateway/bridgemap" "github.com/stretchr/testify/assert" "testing" @@ -152,9 +153,15 @@ enable=true channel="--333333333333" `) +const ( + ircTestAccount = "irc.zzz" + tgTestAccount = "telegram.zzz" + slackTestAccount = "slack.zzz" +) + func maketestRouter(input []byte) *Router { cfg := config.NewConfigFromString(input) - r, err := NewRouter(cfg) + r, err := NewRouter(cfg, bridgemap.FullMap) if err != nil { fmt.Println(err) } @@ -172,18 +179,27 @@ func TestNewRouter(t *testing.T) { assert.Equal(t, 3, len(r.Gateways["bridge2"].Bridges)) assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels)) assert.Equal(t, 3, len(r.Gateways["bridge2"].Channels)) - assert.Equal(t, &config.ChannelInfo{Name: "42wim/testroom", Direction: "out", - ID: "42wim/testroomgitter.42wim", Account: "gitter.42wim", - SameChannel: map[string]bool{"bridge2": false}}, - r.Gateways["bridge2"].Channels["42wim/testroomgitter.42wim"]) - assert.Equal(t, &config.ChannelInfo{Name: "42wim/testroom", Direction: "in", - ID: "42wim/testroomgitter.42wim", Account: "gitter.42wim", - SameChannel: map[string]bool{"bridge1": false}}, - r.Gateways["bridge1"].Channels["42wim/testroomgitter.42wim"]) - assert.Equal(t, &config.ChannelInfo{Name: "general", Direction: "inout", - ID: "generaldiscord.test", Account: "discord.test", - SameChannel: map[string]bool{"bridge1": false}}, - r.Gateways["bridge1"].Channels["generaldiscord.test"]) + assert.Equal(t, &config.ChannelInfo{ + Name: "42wim/testroom", + Direction: "out", + ID: "42wim/testroomgitter.42wim", + Account: "gitter.42wim", + SameChannel: map[string]bool{"bridge2": false}, + }, r.Gateways["bridge2"].Channels["42wim/testroomgitter.42wim"]) + assert.Equal(t, &config.ChannelInfo{ + Name: "42wim/testroom", + Direction: "in", + ID: "42wim/testroomgitter.42wim", + Account: "gitter.42wim", + SameChannel: map[string]bool{"bridge1": false}, + }, r.Gateways["bridge1"].Channels["42wim/testroomgitter.42wim"]) + assert.Equal(t, &config.ChannelInfo{ + Name: "general", + Direction: "inout", + ID: "generaldiscord.test", + Account: "discord.test", + SameChannel: map[string]bool{"bridge1": false}, + }, r.Gateways["bridge1"].Channels["generaldiscord.test"]) } func TestGetDestChannel(t *testing.T) { @@ -192,11 +208,23 @@ func TestGetDestChannel(t *testing.T) { for _, br := range r.Gateways["bridge1"].Bridges { switch br.Account { case "discord.test": - assert.Equal(t, []config.ChannelInfo{{Name: "general", Account: "discord.test", Direction: "inout", ID: "generaldiscord.test", SameChannel: map[string]bool{"bridge1": false}, Options: config.ChannelOptions{Key: ""}}}, - r.Gateways["bridge1"].getDestChannel(msg, *br)) + assert.Equal(t, []config.ChannelInfo{{ + Name: "general", + Account: "discord.test", + Direction: "inout", + ID: "generaldiscord.test", + SameChannel: map[string]bool{"bridge1": false}, + Options: config.ChannelOptions{Key: ""}, + }}, r.Gateways["bridge1"].getDestChannel(msg, *br)) case "slack.test": - assert.Equal(t, []config.ChannelInfo{{Name: "testing", Account: "slack.test", Direction: "out", ID: "testingslack.test", SameChannel: map[string]bool{"bridge1": false}, Options: config.ChannelOptions{Key: ""}}}, - r.Gateways["bridge1"].getDestChannel(msg, *br)) + assert.Equal(t, []config.ChannelInfo{{ + Name: "testing", + Account: "slack.test", + Direction: "out", + ID: "testingslack.test", + SameChannel: map[string]bool{"bridge1": false}, + Options: config.ChannelOptions{Key: ""}, + }}, r.Gateways["bridge1"].getDestChannel(msg, *br)) case "gitter.42wim": assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br)) case "irc.freenode": @@ -226,35 +254,87 @@ func TestGetDestChannelAdvanced(t *testing.T) { } switch gw.Name { case "bridge": - if (msg.Channel == "#main" || msg.Channel == "-1111111111111" || msg.Channel == "irc") && (msg.Account == "irc.zzz" || msg.Account == "telegram.zzz" || msg.Account == "slack.zzz") { + if (msg.Channel == "#main" || msg.Channel == "-1111111111111" || msg.Channel == "irc") && + (msg.Account == ircTestAccount || msg.Account == tgTestAccount || msg.Account == slackTestAccount) { hits[gw.Name]++ switch br.Account { - case "irc.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "#main", Account: "irc.zzz", Direction: "inout", ID: "#mainirc.zzz", SameChannel: map[string]bool{"bridge": false}, Options: config.ChannelOptions{Key: ""}}}, channels) - case "telegram.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "-1111111111111", Account: "telegram.zzz", Direction: "inout", ID: "-1111111111111telegram.zzz", SameChannel: map[string]bool{"bridge": false}, Options: config.ChannelOptions{Key: ""}}}, channels) - case "slack.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "irc", Account: "slack.zzz", Direction: "inout", ID: "ircslack.zzz", SameChannel: map[string]bool{"bridge": false}, Options: config.ChannelOptions{Key: ""}}}, channels) + case ircTestAccount: + assert.Equal(t, []config.ChannelInfo{{ + Name: "#main", + Account: ircTestAccount, + Direction: "inout", + ID: "#mainirc.zzz", + SameChannel: map[string]bool{"bridge": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) + case tgTestAccount: + assert.Equal(t, []config.ChannelInfo{{ + Name: "-1111111111111", + Account: tgTestAccount, + Direction: "inout", + ID: "-1111111111111telegram.zzz", + SameChannel: map[string]bool{"bridge": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) + case slackTestAccount: + assert.Equal(t, []config.ChannelInfo{{ + Name: "irc", + Account: slackTestAccount, + Direction: "inout", + ID: "ircslack.zzz", + SameChannel: map[string]bool{"bridge": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) } } case "bridge2": - if (msg.Channel == "#main-help" || msg.Channel == "--444444444444") && (msg.Account == "irc.zzz" || msg.Account == "telegram.zzz") { + if (msg.Channel == "#main-help" || msg.Channel == "--444444444444") && + (msg.Account == ircTestAccount || msg.Account == tgTestAccount) { hits[gw.Name]++ switch br.Account { - case "irc.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "#main-help", Account: "irc.zzz", Direction: "inout", ID: "#main-helpirc.zzz", SameChannel: map[string]bool{"bridge2": false}, Options: config.ChannelOptions{Key: ""}}}, channels) - case "telegram.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "--444444444444", Account: "telegram.zzz", Direction: "inout", ID: "--444444444444telegram.zzz", SameChannel: map[string]bool{"bridge2": false}, Options: config.ChannelOptions{Key: ""}}}, channels) + case ircTestAccount: + assert.Equal(t, []config.ChannelInfo{{ + Name: "#main-help", + Account: ircTestAccount, + Direction: "inout", + ID: "#main-helpirc.zzz", + SameChannel: map[string]bool{"bridge2": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) + case tgTestAccount: + assert.Equal(t, []config.ChannelInfo{{ + Name: "--444444444444", + Account: tgTestAccount, + Direction: "inout", + ID: "--444444444444telegram.zzz", + SameChannel: map[string]bool{"bridge2": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) } } case "bridge3": - if (msg.Channel == "#main-telegram" || msg.Channel == "--333333333333") && (msg.Account == "irc.zzz" || msg.Account == "telegram.zzz") { + if (msg.Channel == "#main-telegram" || msg.Channel == "--333333333333") && + (msg.Account == ircTestAccount || msg.Account == tgTestAccount) { hits[gw.Name]++ switch br.Account { - case "irc.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "#main-telegram", Account: "irc.zzz", Direction: "inout", ID: "#main-telegramirc.zzz", SameChannel: map[string]bool{"bridge3": false}, Options: config.ChannelOptions{Key: ""}}}, channels) - case "telegram.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "--333333333333", Account: "telegram.zzz", Direction: "inout", ID: "--333333333333telegram.zzz", SameChannel: map[string]bool{"bridge3": false}, Options: config.ChannelOptions{Key: ""}}}, channels) + case ircTestAccount: + assert.Equal(t, []config.ChannelInfo{{ + Name: "#main-telegram", + Account: ircTestAccount, + Direction: "inout", + ID: "#main-telegramirc.zzz", + SameChannel: map[string]bool{"bridge3": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) + case tgTestAccount: + assert.Equal(t, []config.ChannelInfo{{ + Name: "--333333333333", + Account: tgTestAccount, + Direction: "inout", + ID: "--333333333333telegram.zzz", + SameChannel: map[string]bool{"bridge3": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) } } case "announcements": @@ -264,12 +344,42 @@ func TestGetDestChannelAdvanced(t *testing.T) { } hits[gw.Name]++ switch br.Account { - case "irc.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "#main", Account: "irc.zzz", Direction: "out", ID: "#mainirc.zzz", SameChannel: map[string]bool{"announcements": false}, Options: config.ChannelOptions{Key: ""}}, {Name: "#main-help", Account: "irc.zzz", Direction: "out", ID: "#main-helpirc.zzz", SameChannel: map[string]bool{"announcements": false}, Options: config.ChannelOptions{Key: ""}}}, channels) - case "slack.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "general", Account: "slack.zzz", Direction: "out", ID: "generalslack.zzz", SameChannel: map[string]bool{"announcements": false}, Options: config.ChannelOptions{Key: ""}}}, channels) - case "telegram.zzz": - assert.Equal(t, []config.ChannelInfo{{Name: "--333333333333", Account: "telegram.zzz", Direction: "out", ID: "--333333333333telegram.zzz", SameChannel: map[string]bool{"announcements": false}, Options: config.ChannelOptions{Key: ""}}}, channels) + case ircTestAccount: + assert.Len(t, channels, 2) + assert.Contains(t, channels, config.ChannelInfo{ + Name: "#main", + Account: ircTestAccount, + Direction: "out", + ID: "#mainirc.zzz", + SameChannel: map[string]bool{"announcements": false}, + Options: config.ChannelOptions{Key: ""}, + }) + assert.Contains(t, channels, config.ChannelInfo{ + Name: "#main-help", + Account: ircTestAccount, + Direction: "out", + ID: "#main-helpirc.zzz", + SameChannel: map[string]bool{"announcements": false}, + Options: config.ChannelOptions{Key: ""}, + }) + case slackTestAccount: + assert.Equal(t, []config.ChannelInfo{{ + Name: "general", + Account: slackTestAccount, + Direction: "out", + ID: "generalslack.zzz", + SameChannel: map[string]bool{"announcements": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) + case tgTestAccount: + assert.Equal(t, []config.ChannelInfo{{ + Name: "--333333333333", + Account: tgTestAccount, + Direction: "out", + ID: "--333333333333telegram.zzz", + SameChannel: map[string]bool{"announcements": false}, + Options: config.ChannelOptions{Key: ""}, + }}, channels) } } } @@ -277,3 +387,116 @@ func TestGetDestChannelAdvanced(t *testing.T) { } assert.Equal(t, map[string]int{"bridge3": 4, "bridge": 9, "announcements": 3, "bridge2": 4}, hits) } + +func TestIgnoreTextEmpty(t *testing.T) { + extraFile := make(map[string][]interface{}) + extraAttach := make(map[string][]interface{}) + extraFailure := make(map[string][]interface{}) + extraFile["file"] = append(extraFile["file"], config.FileInfo{}) + extraAttach["attachments"] = append(extraAttach["attachments"], []string{}) + extraFailure[config.EventFileFailureSize] = append(extraFailure[config.EventFileFailureSize], config.FileInfo{}) + + msgTests := map[string]struct { + input *config.Message + output bool + }{ + "usertyping": { + input: &config.Message{Event: config.EventUserTyping}, + output: false, + }, + "file attach": { + input: &config.Message{Extra: extraFile}, + output: false, + }, + "attachments": { + input: &config.Message{Extra: extraAttach}, + output: false, + }, + config.EventFileFailureSize: { + input: &config.Message{Extra: extraFailure}, + output: false, + }, + "nil extra": { + input: &config.Message{Extra: nil}, + output: true, + }, + "empty": { + input: &config.Message{}, + output: true, + }, + } + gw := &Gateway{} + for testname, testcase := range msgTests { + output := gw.ignoreTextEmpty(testcase.input) + assert.Equalf(t, testcase.output, output, "case '%s' failed", testname) + } + +} + +func TestIgnoreTexts(t *testing.T) { + msgTests := map[string]struct { + input *config.Message + re []string + output bool + }{ + "no regex": { + input: &config.Message{Text: "a text message"}, + re: []string{}, + output: false, + }, + "simple regex": { + input: &config.Message{Text: "a text message"}, + re: []string{"text"}, + output: true, + }, + "multiple regex fail": { + input: &config.Message{Text: "a text message"}, + re: []string{"abc", "123$"}, + output: false, + }, + "multiple regex pass": { + input: &config.Message{Text: "a text message"}, + re: []string{"lala", "sage$"}, + output: true, + }, + } + gw := &Gateway{} + for testname, testcase := range msgTests { + output := gw.ignoreTexts(testcase.input, testcase.re) + assert.Equalf(t, testcase.output, output, "case '%s' failed", testname) + } +} + +func TestIgnoreNicks(t *testing.T) { + msgTests := map[string]struct { + input *config.Message + re []string + output bool + }{ + "no entry": { + input: &config.Message{Username: "user", Text: "a text message"}, + re: []string{}, + output: false, + }, + "one entry": { + input: &config.Message{Username: "user", Text: "a text message"}, + re: []string{"user"}, + output: true, + }, + "multiple entries": { + input: &config.Message{Username: "user", Text: "a text message"}, + re: []string{"abc", "user"}, + output: true, + }, + "multiple entries fail": { + input: &config.Message{Username: "user", Text: "a text message"}, + re: []string{"abc", "def"}, + output: false, + }, + } + gw := &Gateway{} + for testname, testcase := range msgTests { + output := gw.ignoreNicks(testcase.input, testcase.re) + assert.Equalf(t, testcase.output, output, "case '%s' failed", testname) + } +} diff --git a/gateway/handlers.go b/gateway/handlers.go new file mode 100644 index 00000000..741c312e --- /dev/null +++ b/gateway/handlers.go @@ -0,0 +1,210 @@ +package gateway + +import ( + "bytes" + "crypto/sha1" //nolint:gosec + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "regexp" + "time" + + "github.com/42wim/matterbridge/bridge" + "github.com/42wim/matterbridge/bridge/config" +) + +// handleEventFailure handles failures and reconnects bridges. +func (r *Router) handleEventFailure(msg *config.Message) { + if msg.Event != config.EventFailure { + return + } + for _, gw := range r.Gateways { + for _, br := range gw.Bridges { + if msg.Account == br.Account { + go gw.reconnectBridge(br) + return + } + } + } +} + +// handleEventRejoinChannels handles rejoining of channels. +func (r *Router) handleEventRejoinChannels(msg *config.Message) { + if msg.Event != config.EventRejoinChannels { + return + } + for _, gw := range r.Gateways { + for _, br := range gw.Bridges { + if msg.Account == br.Account { + br.Joined = make(map[string]bool) + if err := br.JoinChannels(); err != nil { + flog.Errorf("channel join failed for %s: %s", msg.Account, err) + } + } + } + } +} + +// handleFiles uploads or places all files on the given msg to the MediaServer and +// adds the new URL of the file on the MediaServer onto the given msg. +func (gw *Gateway) handleFiles(msg *config.Message) { + reg := regexp.MustCompile("[^a-zA-Z0-9]+") + + // If we don't have a attachfield or we don't have a mediaserver configured return + if msg.Extra == nil || + (gw.BridgeValues().General.MediaServerUpload == "" && + gw.BridgeValues().General.MediaDownloadPath == "") { + return + } + + // If we don't have files, nothing to upload. + if len(msg.Extra["file"]) == 0 { + return + } + + for i, f := range msg.Extra["file"] { + fi := f.(config.FileInfo) + ext := filepath.Ext(fi.Name) + fi.Name = fi.Name[0 : len(fi.Name)-len(ext)] + fi.Name = reg.ReplaceAllString(fi.Name, "_") + fi.Name += ext + + sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))[:8] //nolint:gosec + + if gw.BridgeValues().General.MediaServerUpload != "" { + // Use MediaServerUpload. Upload using a PUT HTTP request and basicauth. + if err := gw.handleFilesUpload(&fi); err != nil { + flog.Error(err) + continue + } + } else { + // Use MediaServerPath. Place the file on the current filesystem. + if err := gw.handleFilesLocal(&fi); err != nil { + flog.Error(err) + continue + } + } + + // Download URL. + durl := gw.BridgeValues().General.MediaServerDownload + "/" + sha1sum + "/" + fi.Name + + flog.Debugf("mediaserver download URL = %s", durl) + + // We uploaded/placed the file successfully. Add the SHA and URL. + extra := msg.Extra["file"][i].(config.FileInfo) + extra.URL = durl + extra.SHA = sha1sum + msg.Extra["file"][i] = extra + } +} + +// handleFilesUpload uses MediaServerUpload configuration to upload the file. +// Returns error on failure. +func (gw *Gateway) handleFilesUpload(fi *config.FileInfo) error { + client := &http.Client{ + Timeout: time.Second * 5, + } + // Use MediaServerUpload. Upload using a PUT HTTP request and basicauth. + sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))[:8] //nolint:gosec + url := gw.BridgeValues().General.MediaServerUpload + "/" + sha1sum + "/" + fi.Name + + req, err := http.NewRequest("PUT", url, bytes.NewReader(*fi.Data)) + if err != nil { + return fmt.Errorf("mediaserver upload failed, could not create request: %#v", err) + } + + flog.Debugf("mediaserver upload url: %s", url) + + req.Header.Set("Content-Type", "binary/octet-stream") + _, err = client.Do(req) + if err != nil { + return fmt.Errorf("mediaserver upload failed, could not Do request: %#v", err) + } + return nil +} + +// handleFilesLocal use MediaServerPath configuration, places the file on the current filesystem. +// Returns error on failure. +func (gw *Gateway) handleFilesLocal(fi *config.FileInfo) error { + sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))[:8] //nolint:gosec + dir := gw.BridgeValues().General.MediaDownloadPath + "/" + sha1sum + err := os.Mkdir(dir, os.ModePerm) + if err != nil && !os.IsExist(err) { + return fmt.Errorf("mediaserver path failed, could not mkdir: %s %#v", err, err) + } + + path := dir + "/" + fi.Name + flog.Debugf("mediaserver path placing file: %s", path) + + err = ioutil.WriteFile(path, *fi.Data, os.ModePerm) + if err != nil { + return fmt.Errorf("mediaserver path failed, could not writefile: %s %#v", err, err) + } + return nil +} + +// ignoreEvent returns true if we need to ignore this event for the specified destination bridge. +func (gw *Gateway) ignoreEvent(event string, dest *bridge.Bridge) bool { + switch event { + case config.EventAvatarDownload: + // Avatar downloads are only relevant for telegram and mattermost for now + if dest.Protocol != "mattermost" && dest.Protocol != "telegram" { + return true + } + case config.EventJoinLeave: + // only relay join/part when configured + if !dest.GetBool("ShowJoinPart") { + return true + } + case config.EventTopicChange: + // only relay topic change when used in some way on other side + if dest.GetBool("ShowTopicChange") && dest.GetBool("SyncTopic") { + return true + } + } + return false +} + +// handleMessage makes sure the message get sent to the correct bridge/channels. +// Returns an array of msg ID's +func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrMsgID { + var brMsgIDs []*BrMsgID + + // if we have an attached file, or other info + if msg.Extra != nil && len(msg.Extra[config.EventFileFailureSize]) != 0 && msg.Text == "" { + return brMsgIDs + } + + if gw.ignoreEvent(msg.Event, dest) { + return brMsgIDs + } + + // broadcast to every out channel (irc QUIT) + if msg.Channel == "" && msg.Event != config.EventJoinLeave { + flog.Debug("empty channel") + return brMsgIDs + } + + // Get the ID of the parent message in thread + var canonicalParentMsgID string + if msg.ParentID != "" && dest.GetBool("PreserveThreading") { + canonicalParentMsgID = gw.FindCanonicalMsgID(msg.Protocol, msg.ParentID) + } + + origmsg := msg + channels := gw.getDestChannel(&msg, *dest) + for _, channel := range channels { + msgID, err := gw.SendMessage(origmsg, dest, channel, canonicalParentMsgID) + if err != nil { + flog.Errorf("SendMessage failed: %s", err) + continue + } + if msgID == "" { + continue + } + brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + msgID, channel.ID}) + } + return brMsgIDs +} diff --git a/gateway/router.go b/gateway/router.go index 3a45de36..a7181b96 100644 --- a/gateway/router.go +++ b/gateway/router.go @@ -2,26 +2,34 @@ package gateway import ( "fmt" + "time" "github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge/config" samechannelgateway "github.com/42wim/matterbridge/gateway/samechannel" - // "github.com/davecgh/go-spew/spew" - "time" ) type Router struct { - Gateways map[string]*Gateway - Message chan config.Message - *config.Config + config.Config + + BridgeMap map[string]bridge.Factory + Gateways map[string]*Gateway + Message chan config.Message + MattermostPlugin chan config.Message } -func NewRouter(cfg *config.Config) (*Router, error) { - r := &Router{Message: make(chan config.Message), Gateways: make(map[string]*Gateway), Config: cfg} +func NewRouter(cfg config.Config, bridgeMap map[string]bridge.Factory) (*Router, error) { + r := &Router{ + Config: cfg, + BridgeMap: bridgeMap, + Message: make(chan config.Message), + MattermostPlugin: make(chan config.Message), + Gateways: make(map[string]*Gateway), + } sgw := samechannelgateway.New(cfg) gwconfigs := sgw.GetConfig() - for _, entry := range append(gwconfigs, cfg.Gateway...) { + for _, entry := range append(gwconfigs, cfg.BridgeValues().Gateway...) { if !entry.Enable { continue } @@ -48,17 +56,46 @@ func (r *Router) Start() error { flog.Infof("Starting bridge: %s ", br.Account) err := br.Connect() if err != nil { - return fmt.Errorf("Bridge %s failed to start: %v", br.Account, err) + e := fmt.Errorf("Bridge %s failed to start: %v", br.Account, err) + if r.disableBridge(br, e) { + continue + } + return e } err = br.JoinChannels() if err != nil { - return fmt.Errorf("Bridge %s failed to join channel: %v", br.Account, err) + e := fmt.Errorf("Bridge %s failed to join channel: %v", br.Account, err) + if r.disableBridge(br, e) { + continue + } + return e + } + } + // remove unused bridges + for _, gw := range r.Gateways { + for i, br := range gw.Bridges { + if br.Bridger == nil { + flog.Errorf("removing failed bridge %s", i) + delete(gw.Bridges, i) + } } } go r.handleReceive() return nil } +// disableBridge returns true and empties a bridge if we have IgnoreFailureOnStart configured +// otherwise returns false +func (r *Router) disableBridge(br *bridge.Bridge, err error) bool { + if r.BridgeValues().General.IgnoreFailureOnStart { + flog.Error(err) + // setting this bridge empty + *br = bridge.Bridge{} + return true + } + return false +} + func (r *Router) getBridge(account string) *bridge.Bridge { for _, gw := range r.Gateways { if br, ok := gw.Bridges[account]; ok { @@ -70,41 +107,24 @@ func (r *Router) getBridge(account string) *bridge.Bridge { func (r *Router) handleReceive() { for msg := range r.Message { - if msg.Event == config.EVENT_FAILURE { - Loop: - for _, gw := range r.Gateways { - for _, br := range gw.Bridges { - if msg.Account == br.Account { - go gw.reconnectBridge(br) - break Loop - } - } - } - } - if msg.Event == config.EVENT_REJOIN_CHANNELS { - for _, gw := range r.Gateways { - for _, br := range gw.Bridges { - if msg.Account == br.Account { - br.Joined = make(map[string]bool) - br.JoinChannels() - } - } - } - } + msg := msg // scopelint + r.handleEventFailure(&msg) + r.handleEventRejoinChannels(&msg) for _, gw := range r.Gateways { // record all the message ID's of the different bridges var msgIDs []*BrMsgID - if !gw.ignoreMessage(&msg) { - msg.Timestamp = time.Now() - gw.modifyMessage(&msg) - gw.handleFiles(&msg) - for _, br := range gw.Bridges { - msgIDs = append(msgIDs, gw.handleMessage(msg, br)...) - } - // only add the message ID if it doesn't already exists - if _, ok := gw.Messages.Get(msg.ID); !ok && msg.ID != "" { - gw.Messages.Add(msg.ID, msgIDs) - } + if gw.ignoreMessage(&msg) { + continue + } + msg.Timestamp = time.Now() + gw.modifyMessage(&msg) + gw.handleFiles(&msg) + for _, br := range gw.Bridges { + msgIDs = append(msgIDs, gw.handleMessage(msg, br)...) + } + // only add the message ID if it doesn't already exists + if _, ok := gw.Messages.Get(msg.Protocol + " " + msg.ID); !ok && msg.ID != "" { + gw.Messages.Add(msg.Protocol+" "+msg.ID, msgIDs) } } } diff --git a/gateway/samechannel/samechannel.go b/gateway/samechannel/samechannel.go index 937d769b..1d85ea7d 100644 --- a/gateway/samechannel/samechannel.go +++ b/gateway/samechannel/samechannel.go @@ -5,17 +5,17 @@ import ( ) type SameChannelGateway struct { - *config.Config + config.Config } -func New(cfg *config.Config) *SameChannelGateway { +func New(cfg config.Config) *SameChannelGateway { return &SameChannelGateway{Config: cfg} } func (sgw *SameChannelGateway) GetConfig() []config.Gateway { var gwconfigs []config.Gateway cfg := sgw.Config - for _, gw := range cfg.SameChannelGateway { + for _, gw := range cfg.BridgeValues().SameChannelGateway { gwconfig := config.Gateway{Name: gw.Name, Enable: gw.Enable} for _, account := range gw.Accounts { for _, channel := range gw.Channels { diff --git a/gateway/samechannel/samechannel_test.go b/gateway/samechannel/samechannel_test.go index 7c75444f..c0e579ae 100644 --- a/gateway/samechannel/samechannel_test.go +++ b/gateway/samechannel/samechannel_test.go @@ -1,16 +1,13 @@ package samechannelgateway import ( - "fmt" - "github.com/42wim/matterbridge/bridge/config" - "github.com/BurntSushi/toml" "github.com/stretchr/testify/assert" "testing" ) -var testconfig = ` +const testConfig = ` [mattermost.test] [slack.test] @@ -21,12 +18,56 @@ var testconfig = ` channels = [ "testing","testing2","testing10"] ` -func TestGetConfig(t *testing.T) { - var cfg *config.Config - if _, err := toml.Decode(testconfig, &cfg); err != nil { - fmt.Println(err) +var ( + expectedConfig = config.Gateway{ + Name: "blah", + Enable: true, + In: []config.Bridge(nil), + Out: []config.Bridge(nil), + InOut: []config.Bridge{ + { + Account: "mattermost.test", + Channel: "testing", + Options: config.ChannelOptions{Key: ""}, + SameChannel: true, + }, + { + Account: "mattermost.test", + Channel: "testing2", + Options: config.ChannelOptions{Key: ""}, + SameChannel: true, + }, + { + Account: "mattermost.test", + Channel: "testing10", + Options: config.ChannelOptions{Key: ""}, + SameChannel: true, + }, + { + Account: "slack.test", + Channel: "testing", + Options: config.ChannelOptions{Key: ""}, + SameChannel: true, + }, + { + Account: "slack.test", + Channel: "testing2", + Options: config.ChannelOptions{Key: ""}, + SameChannel: true, + }, + { + Account: "slack.test", + Channel: "testing10", + Options: config.ChannelOptions{Key: ""}, + SameChannel: true, + }, + }, } +) + +func TestGetConfig(t *testing.T) { + cfg := config.NewConfigFromString([]byte(testConfig)) sgw := New(cfg) configs := sgw.GetConfig() - assert.Equal(t, []config.Gateway{{Name: "blah", Enable: true, In: []config.Bridge(nil), Out: []config.Bridge(nil), InOut: []config.Bridge{{Account: "mattermost.test", Channel: "testing", Options: config.ChannelOptions{Key: ""}, SameChannel: true}, {Account: "mattermost.test", Channel: "testing2", Options: config.ChannelOptions{Key: ""}, SameChannel: true}, {Account: "mattermost.test", Channel: "testing10", Options: config.ChannelOptions{Key: ""}, SameChannel: true}, {Account: "slack.test", Channel: "testing", Options: config.ChannelOptions{Key: ""}, SameChannel: true}, {Account: "slack.test", Channel: "testing2", Options: config.ChannelOptions{Key: ""}, SameChannel: true}, {Account: "slack.test", Channel: "testing10", Options: config.ChannelOptions{Key: ""}, SameChannel: true}}}}, configs) + assert.Equal(t, []config.Gateway{expectedConfig}, configs) } diff --git a/go.mod b/go.mod index 2d6107bf..99041ada 100644 --- a/go.mod +++ b/go.mod @@ -2,82 +2,74 @@ module github.com/42wim/matterbridge require ( github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 - github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14 - github.com/Philipp15b/go-steam v0.0.0-20161020161927-e0f3bb9566e3 - github.com/Sirupsen/logrus v1.0.6 // indirect - github.com/alecthomas/log4go v0.0.0-20160307011253-e5dc62318d9b // indirect - github.com/bwmarrin/discordgo v0.0.0-20180201002541-8d5ab59c63e5 // indirect - github.com/davecgh/go-spew v1.1.0 // indirect - github.com/dfordsoft/golib v0.0.0-20180313113957-2ea3495aee1d + github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14 // indirect + github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329 + github.com/bwmarrin/discordgo v0.19.0 + github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec github.com/dgrijalva/jwt-go v0.0.0-20170508165458-6c8dedd55f8a // indirect github.com/fsnotify/fsnotify v1.4.7 - github.com/go-telegram-bot-api/telegram-bot-api v0.0.0-20180428185002-212b1541150c + github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible github.com/golang/protobuf v0.0.0-20170613224224-e325f446bebc // indirect - github.com/google/gops v0.0.0-20170319002943-62f833fc9f6c + github.com/google/gops v0.3.5 github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f // indirect - github.com/gorilla/schema v0.0.0-20170317173100-f3c80893412c - github.com/gorilla/websocket v0.0.0-20170319172727-a91eba7f9777 - github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad - github.com/hashicorp/hcl v0.0.0-20171017181929-23c074d0eceb // indirect + github.com/gorilla/schema v1.0.2 + github.com/gorilla/websocket v1.4.0 + github.com/hashicorp/golang-lru v0.5.0 github.com/hpcloud/tail v1.0.0 // indirect - github.com/jpillora/backoff v0.0.0-20170222002228-06c7a16c845d + github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/kardianos/osext v0.0.0-20170207191655-9b883c5eb462 // indirect github.com/kr/pretty v0.1.0 // indirect - github.com/labstack/echo v0.0.0-20180219162101-7eec915044a1 + github.com/labstack/echo v3.3.5+incompatible github.com/labstack/gommon v0.2.1 // indirect - github.com/lrstanley/girc v0.0.0-20180427160007-102f17f86306 - github.com/magiconair/properties v0.0.0-20180217134545-2c9e95027885 // indirect - github.com/matterbridge/discordgo v0.0.0-20180806170629-ef40ff5ba64f + github.com/lrstanley/girc v0.0.0-20181114171214-3aee8c249519 + github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect + github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 // indirect github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91 github.com/matterbridge/gomatrix v0.0.0-20171224233421-78ac6a1a0f5f github.com/matterbridge/gozulipbot v0.0.0-20180507190239-b6bb12d33544 github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61 - github.com/mattermost/platform v4.6.2+incompatible + github.com/mattermost/mattermost-server v5.5.0+incompatible github.com/mattn/go-colorable v0.0.0-20170210172801-5411d3eea597 // indirect github.com/mattn/go-isatty v0.0.0-20170216235908-dda3de49cbfc // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect - github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 // indirect + github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 // indirect github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect github.com/nicksnyder/go-i18n v1.4.0 // indirect - github.com/nlopes/slack v0.3.1-0.20180805133408-21749ab136a8 + github.com/nlopes/slack v0.4.1-0.20181111125009-5963eafd777b github.com/onsi/ginkgo v1.6.0 // indirect github.com/onsi/gomega v1.4.1 // indirect github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83 github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606 // indirect - github.com/pelletier/go-toml v0.0.0-20180228233631-05bcc0fb0d3e // indirect - github.com/peterhellberg/emojilib v0.0.0-20170616163716-41920917e271 + github.com/peterhellberg/emojilib v0.0.0-20180820090156-eeb3823dab9a github.com/pkg/errors v0.8.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rs/xid v0.0.0-20180525034800-088c5cf1423a - github.com/russross/blackfriday v1.5.1 + github.com/rs/xid v1.2.1 + github.com/russross/blackfriday v2.0.0+incompatible github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca - github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1 // indirect - github.com/shazow/ssh-chat v0.0.0-20171012174035-2078e1381991 - github.com/sirupsen/logrus v0.0.0-20180213143110-8c0189d9f6bb + github.com/shazow/ssh-chat v0.0.0-20181028152505-f36d7eb9ccc6 + github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect + github.com/sirupsen/logrus v1.2.0 github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9 // indirect github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect - github.com/spf13/afero v0.0.0-20180211162714-bbf41cb36dff // indirect - github.com/spf13/cast v1.2.0 // indirect - github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec // indirect - github.com/spf13/pflag v0.0.0-20180220143236-ee5fd03fd6ac // indirect - github.com/spf13/viper v0.0.0-20171227194143-aafc9e6bc7b7 - github.com/stretchr/testify v0.0.0-20170714215325-05e8a0eda380 + github.com/spf13/cast v1.3.0 // indirect + github.com/spf13/pflag v1.0.3 // indirect + github.com/spf13/viper v1.2.1 + github.com/stretchr/testify v1.2.2 github.com/technoweenie/multipartstreamer v1.0.1 // indirect github.com/valyala/bytebufferpool v0.0.0-20160817181652-e746df99fe4a // indirect github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect github.com/zfjagann/golang-ring v0.0.0-20141111230621-17637388c9f6 - golang.org/x/crypto v0.0.0-20180228161326-91a49db82a88 // indirect + go.uber.org/atomic v1.3.2 // indirect + go.uber.org/multierr v1.1.0 // indirect + go.uber.org/zap v1.9.1 // indirect + golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 // indirect golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37 // indirect golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect - golang.org/x/sys v0.0.0-20171130163741-8b4580aae2a0 // indirect - golang.org/x/text v0.0.0-20180511172408-5c1cf69b5978 // indirect - gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect + golang.org/x/sys v0.0.0-20181116161606-93218def8b18 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect - gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect - gopkg.in/yaml.v2 v2.0.0-20160301204022-a83829b6f129 // indirect ) diff --git a/go.sum b/go.sum index 99fffb38..8f341afb 100644 --- a/go.sum +++ b/go.sum @@ -2,61 +2,65 @@ github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 h1:IZtuWGfzQnKnCSu github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557/go.mod h1:jL0YSXMs/txjtGJ4PWrmETOk6KUHMDPMshgQZlTeB3Y= github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14 h1:v/zr4ns/4sSahF9KBm4Uc933bLsEEv7LuT63CJ019yo= github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Philipp15b/go-steam v0.0.0-20161020161927-e0f3bb9566e3 h1:V4+1E1SRYUySqwOoI3ZphFADtabbF568zTHa5ix/zU0= -github.com/Philipp15b/go-steam v0.0.0-20161020161927-e0f3bb9566e3/go.mod h1:HuVM+sZFzumUdKPWiz+IlCMb4RdsKdT3T+nQBKL+sYg= -github.com/Sirupsen/logrus v1.0.6 h1:HCAGQRk48dRVPA5Y+Yh0qdCSTzPOyU1tBJ7Q9YzotII= -github.com/Sirupsen/logrus v1.0.6/go.mod h1:rmk17hk6i8ZSAJkSDa7nOxamrG+SP4P0mm+DAvExv4U= -github.com/alecthomas/log4go v0.0.0-20160307011253-e5dc62318d9b h1:1OpGXps6UOY5HtQaQcLowsV1qMWCNBzhFvK7q4fgXtc= -github.com/alecthomas/log4go v0.0.0-20160307011253-e5dc62318d9b/go.mod h1:iCVmQ9g4TfaRX5m5jq5sXY7RXYWPv9/PynM/GocbG3w= -github.com/bwmarrin/discordgo v0.0.0-20180201002541-8d5ab59c63e5 h1:M7u44DKGpA5goDIBf0zRMYhT1Sp2Rd7hiTzXfeuw1UY= -github.com/bwmarrin/discordgo v0.0.0-20180201002541-8d5ab59c63e5/go.mod h1:5NIvFv5Z7HddYuXbuQegZ684DleQaCFqChP2iuBivJ8= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dfordsoft/golib v0.0.0-20180313113957-2ea3495aee1d h1:rONNnZDE5CYuaSFQk+gP4GEQTXEUcyQ5p6p/dgxIHas= -github.com/dfordsoft/golib v0.0.0-20180313113957-2ea3495aee1d/go.mod h1:UGa5M2Sz/Uh13AMse4+RELKCDw7kqgqlTjeGae+7vUY= +github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329 h1:xZBoq249G9MSt+XuY7sVQzcfONJ6IQuwpCK+KAaOpnY= +github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329/go.mod h1:HuVM+sZFzumUdKPWiz+IlCMb4RdsKdT3T+nQBKL+sYg= +github.com/alexcesaro/log v0.0.0-20150915221235-61e686294e58 h1:MkpmYfld/S8kXqTYI68DfL8/hHXjHogL120Dy00TIxc= +github.com/alexcesaro/log v0.0.0-20150915221235-61e686294e58/go.mod h1:YNfsMyWSs+h+PaYkxGeMVmVCX75Zj/pqdjbu12ciCYE= +github.com/bwmarrin/discordgo v0.19.0 h1:kMED/DB0NR1QhRcalb85w0Cu3Ep2OrGAqZH1R5awQiY= +github.com/bwmarrin/discordgo v0.19.0/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec h1:JEUiu7P9smN7zgX87a2zVnnbPPickIM9Gf9OIhsIgWQ= +github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec/go.mod h1:UGa5M2Sz/Uh13AMse4+RELKCDw7kqgqlTjeGae+7vUY= github.com/dgrijalva/jwt-go v0.0.0-20170508165458-6c8dedd55f8a h1:MuHMeSsXbNEeUyxjB7T9P8s1+5k8OLTC/M27qsVwixM= github.com/dgrijalva/jwt-go v0.0.0-20170508165458-6c8dedd55f8a/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/go-telegram-bot-api/telegram-bot-api v0.0.0-20180428185002-212b1541150c h1:3gMh737vMGqAkkkSfNbwjO8VRHOSaCjYRG4y9xVMEIQ= -github.com/go-telegram-bot-api/telegram-bot-api v0.0.0-20180428185002-212b1541150c/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= +github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU= +github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= github.com/golang/protobuf v0.0.0-20170613224224-e325f446bebc h1:wdhDSKrkYy24mcfzuA3oYm58h0QkyXjwERCkzJDP5kA= github.com/golang/protobuf v0.0.0-20170613224224-e325f446bebc/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/gops v0.0.0-20170319002943-62f833fc9f6c h1:MrMA1vhRTNidtgENqmsmLOIUS6ixMBOU/g10rm7IUe8= -github.com/google/gops v0.0.0-20170319002943-62f833fc9f6c/go.mod h1:pMQgrscwEK/aUSW1IFSaBPbJX82FPHWaSoJw1axQfD0= +github.com/google/gops v0.3.5 h1:SIWvPLiYvy5vMwjxB3rVFTE4QBhUFj2KKWr3Xm7CKhw= +github.com/google/gops v0.3.5/go.mod h1:pMQgrscwEK/aUSW1IFSaBPbJX82FPHWaSoJw1axQfD0= github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f h1:FDM3EtwZLyhW48YRiyqjivNlNZjAObv4xt4NnJaU+NQ= github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/schema v0.0.0-20170317173100-f3c80893412c h1:mORYpib1aLu3M2Oi50Z1pNTXuDJEHcoLb6oo6VdOutk= -github.com/gorilla/schema v0.0.0-20170317173100-f3c80893412c/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= -github.com/gorilla/websocket v0.0.0-20170319172727-a91eba7f9777 h1:JIM+OacoOJRU30xpjMf8sulYqjr0ViA3WDrTX6j/yDI= -github.com/gorilla/websocket v0.0.0-20170319172727-a91eba7f9777/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad h1:eMxs9EL0PvIGS9TTtxg4R+JxuPGav82J8rA+GFnY7po= -github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v0.0.0-20171017181929-23c074d0eceb h1:1OvvPvZkn/yCQ3xBcM8y4020wdkMXPHLB4+NfoGWh4U= -github.com/hashicorp/hcl v0.0.0-20171017181929-23c074d0eceb/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/gorilla/schema v1.0.2 h1:sAgNfOcNYvdDSrzGHVy9nzCQahG+qmsg+nE8dK85QRA= +github.com/gorilla/schema v1.0.2/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/jpillora/backoff v0.0.0-20170222002228-06c7a16c845d h1:ETeT81zgLgSNc4BWdDO2Fg9ekVItYErbNtE8mKD2pJA= -github.com/jpillora/backoff v0.0.0-20170222002228-06c7a16c845d/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= +github.com/jessevdk/go-flags v1.3.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 h1:K//n/AqR5HjG3qxbrBCL4vJPW0MVFSs9CPK1OOJdRME= +github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kardianos/osext v0.0.0-20170207191655-9b883c5eb462 h1:oSOOTPHkCzMeu1vJ0nHxg5+XZBdMMjNa+6NPnm8arok= github.com/kardianos/osext v0.0.0-20170207191655-9b883c5eb462/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/labstack/echo v0.0.0-20180219162101-7eec915044a1 h1:cOIt0LZKdfeirAfTP4VtIJuWbjVTGtd1suuPXp/J+dE= -github.com/labstack/echo v0.0.0-20180219162101-7eec915044a1/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= +github.com/labstack/echo v3.3.5+incompatible h1:9PfxPUmasKzeJor9uQTaXLT6WUG/r+vSTmvXxvv3JO4= +github.com/labstack/echo v3.3.5+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= github.com/labstack/gommon v0.2.1 h1:C+I4NYknueQncqKYZQ34kHsLZJVeB5KwPUhnO0nmbpU= github.com/labstack/gommon v0.2.1/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= -github.com/lrstanley/girc v0.0.0-20180427160007-102f17f86306 h1:IqN61cmi7LM/IaYaP9a/KXFtHRS2a3+WHu8GhAXJT7c= -github.com/lrstanley/girc v0.0.0-20180427160007-102f17f86306/go.mod h1:7cRs1SIBfKQ7e3Tam6GKTILSNHzR862JD0JpINaZoJk= -github.com/magiconair/properties v0.0.0-20180217134545-2c9e95027885 h1:HWxJJvF+QceKcql4r9PC93NtMEgEBfBxlQrZPvbcQvs= -github.com/magiconair/properties v0.0.0-20180217134545-2c9e95027885/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/matterbridge/discordgo v0.0.0-20180806170629-ef40ff5ba64f h1:9IIOO9Aznn8zJx3nokZ4U6nfuzWw5xAlygPvuRZMisQ= -github.com/matterbridge/discordgo v0.0.0-20180806170629-ef40ff5ba64f/go.mod h1:5QtN542bJn9FunZqYlIbleNtToxfLCVV9pW7m7Q42Fc= +github.com/lrstanley/girc v0.0.0-20181114171214-3aee8c249519 h1:o7duXxs4nxplgWrFRJoyGrPAS+U9Sk5eQyc2mflk6/Q= +github.com/lrstanley/girc v0.0.0-20181114171214-3aee8c249519/go.mod h1:7cRs1SIBfKQ7e3Tam6GKTILSNHzR862JD0JpINaZoJk= +github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 h1:AsEBgzv3DhuYHI/GiQh2HxvTP71HCCE9E/tzGUzGdtU= +github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5/go.mod h1:c2mYKRyMb1BPkO5St0c/ps62L4S0W2NAkaTXj9qEI+0= +github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 h1:iOAVXzZyXtW408TMYejlUPo6BIn92HmOacWtIfNyYns= +github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg= +github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91 h1:KzDEcy8eDbTx881giW8a6llsAck3e2bJvMyKvh1IK+k= github.com/matterbridge/go-xmpp v0.0.0-20180529212104-cd19799fba91/go.mod h1:ECDRehsR9TYTKCAsRS8/wLeOk6UUqDydw47ln7wG41Q= github.com/matterbridge/gomatrix v0.0.0-20171224233421-78ac6a1a0f5f h1:2eKh6Qi/sJ8bXvYMoyVfQxHgR8UcCDWjOmhV1oCstMU= @@ -65,26 +69,25 @@ github.com/matterbridge/gozulipbot v0.0.0-20180507190239-b6bb12d33544 h1:A8lLG3D github.com/matterbridge/gozulipbot v0.0.0-20180507190239-b6bb12d33544/go.mod h1:yAjnZ34DuDyPHMPHHjOsTk/FefW4JJjoMMCGt/8uuQA= github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61 h1:R/MgM/eUyRBQx2FiH6JVmXck8PaAuKfe2M1tWIzW7nE= github.com/matterbridge/logrus-prefixed-formatter v0.0.0-20180806162718-01618749af61/go.mod h1:iXGEotOvwI1R1SjLxRc+BF5rUORTMtE0iMZBT2lxqAU= -github.com/mattermost/platform v4.6.2+incompatible h1:9WqKNuJFIp6SDYn5wl1RF5urdhEw8d7o5tAOwT1MW0A= -github.com/mattermost/platform v4.6.2+incompatible/go.mod h1:HjGKtkQNu3HXTOykPMQckMnH11WHvNvQqDBNnVXVbfM= +github.com/mattermost/mattermost-server v5.5.0+incompatible h1:0wcLGgYtd+YImtLDPf2AOfpBHxbU4suATx+6XKw1XbU= +github.com/mattermost/mattermost-server v5.5.0+incompatible/go.mod h1:5L6MjAec+XXQwMIt791Ganu45GKsSiM+I0tLR9wUj8Y= github.com/mattn/go-colorable v0.0.0-20170210172801-5411d3eea597 h1:hGizH4aMDFFt1iOA4HNKC13lqIBoCyxIjWcAnWIy7aU= github.com/mattn/go-colorable v0.0.0-20170210172801-5411d3eea597/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.0-20170216235908-dda3de49cbfc h1:pK7tzC30erKOTfEDCYGvPZQCkmM9X5iSmmAR5m9x3Yc= github.com/mattn/go-isatty v0.0.0-20170216235908-dda3de49cbfc/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 h1:+MZW2uvHgN8kYvksEN3f7eFL2wpzk0GxmlFsMybWc7E= -github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 h1:oKIteTqeSpenyTrOVj5zkiyCaflLa8B+CD0324otT+o= github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff h1:HLGD5/9UxxfEuO9DtP8gnTmNtMxbPyhYltfxsITel8g= github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff/go.mod h1:B8jLfIIPn2sKyWr0D7cL2v7tnrDD5z291s2Zypdu89E= github.com/nicksnyder/go-i18n v1.4.0 h1:AgLl+Yq7kg5OYlzCgu9cKTZOyI4tD/NgukKqLqC8E+I= github.com/nicksnyder/go-i18n v1.4.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q= -github.com/nlopes/slack v0.0.0-20180101221843-107290b5bbaf h1:M+xGhDxie/MqC+tzs+3ZHBSY4Wsv+fEkrpIMCKy8PTg= -github.com/nlopes/slack v0.0.0-20180101221843-107290b5bbaf/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= -github.com/nlopes/slack v0.3.1-0.20180805133408-21749ab136a8 h1:PSy8NkmkyldLmPPnNNw7mwfQFOHDqOI6bINpJ+/KV7Y= -github.com/nlopes/slack v0.3.1-0.20180805133408-21749ab136a8/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= +github.com/nlopes/slack v0.4.1-0.20181111125009-5963eafd777b h1:8ncrr7Xps0GafXIxBzrq1qSjy1zhiCDp/9C4cOrE+GU= +github.com/nlopes/slack v0.4.1-0.20181111125009-5963eafd777b/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U= @@ -93,42 +96,49 @@ github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83 h1:XQonH5Iv github.com/paulrosania/go-charset v0.0.0-20151028000031-621bb39fcc83/go.mod h1:YnNlZP7l4MhyGQ4CBRwv6ohZTPrUJJZtEv4ZgADkbs4= github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606 h1:/CPgDYrfeK2LMK6xcUhvI17yO9SlpAdDIJGkhDEgO8A= github.com/pborman/uuid v0.0.0-20160216163710-c55201b03606/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34= -github.com/pelletier/go-toml v0.0.0-20180228233631-05bcc0fb0d3e h1:ZW8599OjioQsmBbkGpyruHUlRVQceYFWnJsGr4NCkiA= -github.com/pelletier/go-toml v0.0.0-20180228233631-05bcc0fb0d3e/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/peterhellberg/emojilib v0.0.0-20170616163716-41920917e271 h1:wQ9lVx75za6AT2kI0S9QID0uWuwTWnvcTfN+uw1F8vg= -github.com/peterhellberg/emojilib v0.0.0-20170616163716-41920917e271/go.mod h1:G7LufuPajuIvdt9OitkNt2qh0mmvD4bfRgRM7bhDIOA= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterhellberg/emojilib v0.0.0-20180820090156-eeb3823dab9a h1:zAss6STq7oejKWTMEUYDUKYZhqXe0xALo8pJhJ3JJAs= +github.com/peterhellberg/emojilib v0.0.0-20180820090156-eeb3823dab9a/go.mod h1:G7LufuPajuIvdt9OitkNt2qh0mmvD4bfRgRM7bhDIOA= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rs/xid v0.0.0-20180525034800-088c5cf1423a h1:UWKek6MK3K6/TpbsFcv+8rrO6rSc6KKSp2FbMOHWsq4= -github.com/rs/xid v0.0.0-20180525034800-088c5cf1423a/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/russross/blackfriday v1.5.1 h1:B8ZN6pD4PVofmlDCDUdELeYrbsVIDM/bpjW3v3zgcRc= -github.com/russross/blackfriday v1.5.1/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk= +github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI= github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1 h1:Lx3BlDGFElJt4u/zKc9A3BuGYbQAGlEFyPuUA3jeMD0= github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1/go.mod h1:vt2jWY/3Qw1bIzle5thrJWucsLuuX9iUNnp20CqCciI= -github.com/shazow/ssh-chat v0.0.0-20171012174035-2078e1381991 h1:PQiUTDzUC5EUh0vNurK7KQS22zlKqLLOFn+K9nJXDQQ= -github.com/shazow/ssh-chat v0.0.0-20171012174035-2078e1381991/go.mod h1:KwtnpMClmrXsHCKTbRui5xBUNt17n1GGrGhdiw2KcoY= -github.com/sirupsen/logrus v0.0.0-20180213143110-8c0189d9f6bb h1:eKjx20EiekBRT2tjZ0XEdKpftfPJQwiavtFshwTyqf0= -github.com/sirupsen/logrus v0.0.0-20180213143110-8c0189d9f6bb/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/shazow/ssh-chat v0.0.0-20181028152505-f36d7eb9ccc6 h1:qNoZx1RWPGKiqfs8ZZAYsYtw3ejo3HIF7iECaeaJhFk= +github.com/shazow/ssh-chat v0.0.0-20181028152505-f36d7eb9ccc6/go.mod h1:SA/9+Wy3zV0UvPjttpGgs90FS9ZZ5D/LTffnVqdIBE8= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9 h1:lXQ+j+KwZcbwrbgU0Rp4Eglg3EJLHbuZU3BbOqAGBmg= github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo= github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= -github.com/spf13/afero v0.0.0-20180211162714-bbf41cb36dff h1:HLvGWId7M56TfuxTeZ6aoiTAcrWO5Mnq/ArwVRgV62I= -github.com/spf13/afero v0.0.0-20180211162714-bbf41cb36dff/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg= github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= -github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec h1:2ZXvIUGghLpdTVHR1UfvfrzoVlZaE/yOWC5LueIHZig= -github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v0.0.0-20180220143236-ee5fd03fd6ac h1:+uzyQ0TQ3aKorQxsOjcDDgE7CuUXwpkKnK19LULQALQ= -github.com/spf13/pflag v0.0.0-20180220143236-ee5fd03fd6ac/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v0.0.0-20171227194143-aafc9e6bc7b7 h1:Wj4cg2M6Um7j1N7yD/mxsdy1/wrsdjzVha2eWdOhti8= -github.com/spf13/viper v0.0.0-20171227194143-aafc9e6bc7b7/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= -github.com/stretchr/testify v0.0.0-20170714215325-05e8a0eda380 h1:MsolbevHkd4SpbeG4dHLHj6I9jzoohyNI6EK6JvR5hE= -github.com/stretchr/testify v0.0.0-20170714215325-05e8a0eda380/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.2.1 h1:bIcUwXqLseLF3BDAZduuNfekWG87ibtFxi59Bq+oI9M= +github.com/spf13/viper v1.2.1/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= github.com/valyala/bytebufferpool v0.0.0-20160817181652-e746df99fe4a h1:AOcehBWpFhYPYw0ioDTppQzgI8pAAahVCiMSKTp9rbo= @@ -139,25 +149,37 @@ github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJ github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= github.com/zfjagann/golang-ring v0.0.0-20141111230621-17637388c9f6 h1:/WULP+6asFz569UbOwg87f3iDT7T+GF5/vjLmL51Pdk= github.com/zfjagann/golang-ring v0.0.0-20141111230621-17637388c9f6/go.mod h1:0MsIttMJIF/8Y7x0XjonJP7K99t3sR6bjj4m5S4JmqU= -golang.org/x/crypto v0.0.0-20180228161326-91a49db82a88 h1:jLkAo/qlT9whgCLYC5GAJ9kcKrv3Wj8VCc4N+KJ4wpw= -golang.org/x/crypto v0.0.0-20180228161326-91a49db82a88/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180119074636-ee41a25c63fb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 h1:kkXA53yGe04D0adEYJwEVQjeBppL01Exg+fnMjfUraU= +golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37 h1:BkNcmLtAVeWe9h5k0jt24CQgaG5vb4x/doFbAiEC/Ho= golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20171130163741-8b4580aae2a0 h1:x4M4WCms+ErQg/4VyECbP2kSNcDJ6nLwqEGov1QPtqk= -golang.org/x/sys v0.0.0-20171130163741-8b4580aae2a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.0.0-20180511172408-5c1cf69b5978 h1:WNm0tmiuBMW4FJRuXKWOqaQfmKptHs0n8nTCyG0ayjc= -golang.org/x/text v0.0.0-20180511172408-5c1cf69b5978/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +golang.org/x/sys v0.0.0-20180117170059-2c42eef0765b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116161606-93218def8b18 h1:Wh+XCfg3kNpjhdq2LXrsiOProjtQZKme5XUx7VcxwAw= +golang.org/x/sys v0.0.0-20181116161606-93218def8b18/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.1 h1:4buh9nXkpqc7+GLzDFHei0jwoU9wCQYfVB5Kfo58Yz0= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.1/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20160301204022-a83829b6f129 h1:RBgb9aPUbZ9nu66ecQNIBNsA7j3mB5h8PNDIfhPjaJg= -gopkg.in/yaml.v2 v2.0.0-20160301204022-a83829b6f129/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/hook/rockethook/rockethook.go b/hook/rockethook/rockethook.go index 063be742..1ac8cb02 100644 --- a/hook/rockethook/rockethook.go +++ b/hook/rockethook/rockethook.go @@ -38,7 +38,7 @@ type Config struct { func New(url string, config Config) *Client { c := &Client{In: make(chan Message), Config: config} tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}, + TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}, //nolint:gosec } c.httpclient = &http.Client{Transport: tr} _, _, err := net.SplitHostPort(c.BindAddress) diff --git a/img/matterbridge-notext.gif b/img/matterbridge-notext.gif new file mode 100644 index 00000000..009b05c2 Binary files /dev/null and b/img/matterbridge-notext.gif differ diff --git a/img/slack-setup-add-scopes.png b/img/slack-setup-add-scopes.png new file mode 100644 index 00000000..03189ff9 Binary files /dev/null and b/img/slack-setup-add-scopes.png differ diff --git a/img/slack-setup-app-page.png b/img/slack-setup-app-page.png new file mode 100644 index 00000000..04898373 Binary files /dev/null and b/img/slack-setup-app-page.png differ diff --git a/img/slack-setup-create-app.png b/img/slack-setup-create-app.png new file mode 100644 index 00000000..534a7e8c Binary files /dev/null and b/img/slack-setup-create-app.png differ diff --git a/img/slack-setup-create-bot.png b/img/slack-setup-create-bot.png new file mode 100644 index 00000000..8abaff1b Binary files /dev/null and b/img/slack-setup-create-bot.png differ diff --git a/img/slack-setup-finished.png b/img/slack-setup-finished.png new file mode 100644 index 00000000..da46aa83 Binary files /dev/null and b/img/slack-setup-finished.png differ diff --git a/img/slack-setup-install-app.png b/img/slack-setup-install-app.png new file mode 100644 index 00000000..a8d620f6 Binary files /dev/null and b/img/slack-setup-install-app.png differ diff --git a/img/slack-setup-invite-bot.png b/img/slack-setup-invite-bot.png new file mode 100644 index 00000000..3c3b1b9f Binary files /dev/null and b/img/slack-setup-invite-bot.png differ diff --git a/matterbridge.go b/matterbridge.go index 8e852b0f..e713fb0b 100644 --- a/matterbridge.go +++ b/matterbridge.go @@ -8,13 +8,14 @@ import ( "github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/gateway" + "github.com/42wim/matterbridge/gateway/bridgemap" "github.com/google/gops/agent" prefixed "github.com/matterbridge/logrus-prefixed-formatter" log "github.com/sirupsen/logrus" ) var ( - version = "1.11.4-dev" + version = "1.12.3-dev" githash string ) @@ -27,8 +28,11 @@ func main() { flagGops := flag.Bool("gops", false, "enable gops agent") flag.Parse() if *flagGops { - agent.Listen(&agent.Options{}) - defer agent.Close() + if err := agent.Listen(agent.Options{}); err != nil { + flog.Errorf("failed to start gops agent: %#v", err) + } else { + defer agent.Close() + } } if *flagVersion { fmt.Printf("version: %s %s\n", version, githash) @@ -44,8 +48,8 @@ func main() { flog.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.") } cfg := config.NewConfig(*flagConfig) - cfg.General.Debug = *flagDebug - r, err := gateway.NewRouter(cfg) + cfg.BridgeValues().General.Debug = *flagDebug + r, err := gateway.NewRouter(cfg, bridgemap.FullMap) if err != nil { flog.Fatalf("Starting gateway failed: %s", err) } diff --git a/matterbridge.toml.sample b/matterbridge.toml.sample index 790eaadc..42d20af7 100644 --- a/matterbridge.toml.sample +++ b/matterbridge.toml.sample @@ -96,6 +96,11 @@ RejoinDelay=0 #Only works in IRC right now. ColorNicks=false +#RunCommands allows you to send RAW irc commands after connection +#Array of strings +#OPTIONAL (default empty) +RunCommands=["PRIVMSG user hello","PRIVMSG chanserv something"] + #Nicks you want to ignore. #Messages from those users will not be sent to other bridges. #OPTIONAL @@ -129,12 +134,8 @@ ReplaceNicks=[ ["user--","user"] ] Label="" #RemoteNickFormat defines how remote users appear on this bridge -#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. -#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge -#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge -#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge +#See [general] config section for default options #The string "{NOPINGNICK}" (case sensitive) will be replaced by the actual nick / username, but with a ZWSP inside the nick, so the irc user with the same nick won't get pinged. See https://github.com/42wim/matterbridge/issues/175 for more information -#OPTIONAL (default empty) RemoteNickFormat="[{PROTOCOL}] <{NICK}> " #Enable to show users joins/parts from other bridges @@ -227,11 +228,7 @@ ReplaceNicks=[ ["user--","user"] ] Label="" #RemoteNickFormat defines how remote users appear on this bridge -#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. -#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge -#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge -#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge -#OPTIONAL (default empty) +#See [general] config section for default options RemoteNickFormat="[{PROTOCOL}] <{NICK}> " #Enable to show users joins/parts from other bridges @@ -311,11 +308,7 @@ ReplaceNicks=[ ["user--","user"] ] Label="" #RemoteNickFormat defines how remote users appear on this bridge -#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. -#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge -#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge -#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge -#OPTIONAL (default empty) +#See [general] config section for default options RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> " #Enable to show users joins/parts from other bridges @@ -455,11 +448,7 @@ ReplaceNicks=[ ["user--","user"] ] Label="" #RemoteNickFormat defines how remote users appear on this bridge -#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. -#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge -#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge -#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge -#OPTIONAL (default empty) +#See [general] config section for default options RemoteNickFormat="[{PROTOCOL}] <{NICK}> " #Enable to show users joins/parts from other bridges @@ -534,11 +523,7 @@ ReplaceNicks=[ ["user--","user"] ] Label="" #RemoteNickFormat defines how remote users appear on this bridge -#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. -#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge -#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge -#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge -#OPTIONAL (default empty) +#See [general] config section for default options RemoteNickFormat="[{PROTOCOL}] <{NICK}> " #Enable to show users joins/parts from other bridges @@ -656,11 +641,7 @@ ReplaceNicks=[ ["user--","user"] ] Label="" #RemoteNickFormat defines how remote users appear on this bridge -#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. -#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge -#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge -#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge -#OPTIONAL (default empty) +#See [general] config section for default options RemoteNickFormat="[{PROTOCOL}] <{NICK}> " #Enable to show users joins/parts from other bridges @@ -683,6 +664,20 @@ StripNick=false #OPTIONAL (default false) ShowTopicChange=false +#Opportunistically preserve threaded replies between Slack channels. +#This only works if the parent message is still in the cache. +#Cache is flushed between restarts. +#Note: Not currently working on gateways with mixed bridges of +# both slack and slack-legacy type. Context in issue #624. +#OPTIONAL (default false) +PreserveThreading=false + +#Enable showing "user_typing" events from across gateway when available. +#Protip: Set your bot/user's "Full Name" to be "Someone (over chat bridge)", +#and so the message will say "Someone (over chat bridge) is typing". +#OPTIONAL (default false) +ShowUserTyping=false + ################################################################### #discord section ################################################################### @@ -759,11 +754,7 @@ ReplaceNicks=[ ["user--","user"] ] Label="" #RemoteNickFormat defines how remote users appear on this bridge -#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. -#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge -#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge -#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge -#OPTIONAL (default empty) +#See [general] config section for default options RemoteNickFormat="[{PROTOCOL}] <{NICK}> " #Enable to show users joins/parts from other bridges @@ -776,11 +767,16 @@ ShowJoinPart=false #OPTIONAL (default false) StripNick=false -#Enable to show topic changes from other bridges +#Enable to show topic/purpose changes from other bridges #Only works hiding/show topic changes from slack bridge for now #OPTIONAL (default false) ShowTopicChange=false +#Enable to sync topic/purpose changes from other bridges +#Only works syncing topic changes from slack bridge for now +#OPTIONAL (default false) +SyncTopic=false + ################################################################### #telegram section ################################################################### @@ -866,16 +862,11 @@ ReplaceNicks=[ ["user--","user"] ] Label="" #RemoteNickFormat defines how remote users appear on this bridge -#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. -#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge -#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge -#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge +#See [general] config section for default options # #WARNING: if you have set MessageFormat="HTML" be sure that this format matches the guidelines #on https://core.telegram.org/bots/api#html-style otherwise the message will not go through to #telegram! eg <{NICK}> should be <{NICK}> -# -#OPTIONAL (default empty) RemoteNickFormat="[{PROTOCOL}] <{NICK}> " #Enable to show users joins/parts from other bridges @@ -969,11 +960,7 @@ ReplaceNicks=[ ["user--","user"] ] Label="" #RemoteNickFormat defines how remote users appear on this bridge -#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. -#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge -#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge -#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge -#OPTIONAL (default empty) +#See [general] config section for default options RemoteNickFormat="[{PROTOCOL}] <{NICK}> " #Enable to show users joins/parts from other bridges @@ -1059,11 +1046,7 @@ ReplaceNicks=[ ["user--","user"] ] Label="" #RemoteNickFormat defines how remote users appear on this bridge -#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. -#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge -#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge -#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge -#OPTIONAL (default empty) +#See [general] config section for default options RemoteNickFormat="[{PROTOCOL}] <{NICK}> " #Enable to show users joins/parts from other bridges @@ -1143,11 +1126,7 @@ ReplaceNicks=[ ["user--","user"] ] Label="" #RemoteNickFormat defines how remote users appear on this bridge -#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. -#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge -#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge -#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge -#OPTIONAL (default empty) +#See [general] config section for default options RemoteNickFormat="[{PROTOCOL}] <{NICK}> " #Enable to show users joins/parts from other bridges @@ -1227,11 +1206,7 @@ ReplaceNicks=[ ["user--","user"] ] Label="" #RemoteNickFormat defines how remote users appear on this bridge -#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. -#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge -#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge -#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge -#OPTIONAL (default empty) +#See [general] config section for default options RemoteNickFormat="[{PROTOCOL}] <{NICK}> " #Enable to show users joins/parts from other bridges @@ -1263,6 +1238,7 @@ ShowTopicChange=false BindAddress="127.0.0.1:4242" #Amount of messages to keep in memory +#OPTIONAL (library default 10) Buffer=1000 #Bearer token used for authentication @@ -1275,11 +1251,7 @@ Token="mytoken" Label="" #RemoteNickFormat defines how remote users appear on this bridge -#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. -#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge -#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge -#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge -#OPTIONAL (default empty) +#See [general] config section for default options RemoteNickFormat="{NICK}" @@ -1298,6 +1270,8 @@ RemoteNickFormat="{NICK}" #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge #The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge +#The string "{GATEWAY}" (case sensitive) will be replaced by the origin gateway name that is replicating the message. +#The string "{CHANNEL}" (case sensitive) will be replaced by the origin channel name used by the bridge #OPTIONAL (default empty) RemoteNickFormat="[{PROTOCOL}] <{NICK}> " @@ -1341,6 +1315,12 @@ MediaDownloadSize=1000000 #OPTIONAL (default empty) MediaDownloadBlacklist=[".html$",".htm$"] +#IgnoreFailureOnStart allows you to ignore failing bridges on startup. +#Matterbridge will disable the failed bridge and continue with the other ones. +#Context: https://github.com/42wim/matterbridge/issues/455 +#OPTIONAL (default false) +IgnoreFailureOnStart=false + ################################################################### #Gateway configuration ################################################################### @@ -1427,7 +1407,7 @@ enable=true #OPTIONAL - webhookurl only works for discord (it needs a different URL for each cahnnel) [gateway.inout.options] - webhookurl=""https://discordapp.com/api/webhooks/123456789123456789/C9WPqExYWONPDZabcdef-def1434FGFjstasJX9pYht73y" + webhookurl="https://discordapp.com/api/webhooks/123456789123456789/C9WPqExYWONPDZabcdef-def1434FGFjstasJX9pYht73y" #API example #[[gateway.inout]] diff --git a/matterclient/channels.go b/matterclient/channels.go new file mode 100644 index 00000000..84d43bae --- /dev/null +++ b/matterclient/channels.go @@ -0,0 +1,208 @@ +package matterclient + +import ( + "errors" + "strings" + + "github.com/mattermost/mattermost-server/model" + log "github.com/sirupsen/logrus" +) + +// GetChannels returns all channels we're members off +func (m *MMClient) GetChannels() []*model.Channel { + m.RLock() + defer m.RUnlock() + var channels []*model.Channel + // our primary team channels first + channels = append(channels, m.Team.Channels...) + for _, t := range m.OtherTeams { + if t.Id != m.Team.Id { + channels = append(channels, t.Channels...) + } + } + return channels +} + +func (m *MMClient) GetChannelHeader(channelId string) string { //nolint:golint + m.RLock() + defer m.RUnlock() + for _, t := range m.OtherTeams { + for _, channel := range append(t.Channels, t.MoreChannels...) { + if channel.Id == channelId { + return channel.Header + } + + } + } + return "" +} + +func (m *MMClient) GetChannelId(name string, teamId string) string { //nolint:golint + m.RLock() + defer m.RUnlock() + if teamId != "" { + return m.getChannelIdTeam(name, teamId) + } + + for _, t := range m.OtherTeams { + for _, channel := range append(t.Channels, t.MoreChannels...) { + if channel.Type == model.CHANNEL_GROUP { + res := strings.Replace(channel.DisplayName, ", ", "-", -1) + res = strings.Replace(res, " ", "_", -1) + if res == name { + return channel.Id + } + } + + } + } + return "" +} + +func (m *MMClient) getChannelIdTeam(name string, teamId string) string { //nolint:golint + for _, t := range m.OtherTeams { + if t.Id == teamId { + for _, channel := range append(t.Channels, t.MoreChannels...) { + if channel.Name == name { + return channel.Id + } + } + } + } + return "" +} + +func (m *MMClient) GetChannelName(channelId string) string { //nolint:golint + m.RLock() + defer m.RUnlock() + for _, t := range m.OtherTeams { + if t == nil { + continue + } + for _, channel := range append(t.Channels, t.MoreChannels...) { + if channel.Id == channelId { + if channel.Type == model.CHANNEL_GROUP { + res := strings.Replace(channel.DisplayName, ", ", "-", -1) + res = strings.Replace(res, " ", "_", -1) + return res + } + return channel.Name + } + } + } + return "" +} + +func (m *MMClient) GetChannelTeamId(id string) string { //nolint:golint + m.RLock() + defer m.RUnlock() + for _, t := range append(m.OtherTeams, m.Team) { + for _, channel := range append(t.Channels, t.MoreChannels...) { + if channel.Id == id { + return channel.TeamId + } + } + } + return "" +} + +func (m *MMClient) GetLastViewedAt(channelId string) int64 { //nolint:golint + m.RLock() + defer m.RUnlock() + res, resp := m.Client.GetChannelMember(channelId, m.User.Id, "") + if resp.Error != nil { + return model.GetMillis() + } + return res.LastViewedAt +} + +// GetMoreChannels returns existing channels where we're not a member off. +func (m *MMClient) GetMoreChannels() []*model.Channel { + m.RLock() + defer m.RUnlock() + var channels []*model.Channel + for _, t := range m.OtherTeams { + channels = append(channels, t.MoreChannels...) + } + return channels +} + +// GetTeamFromChannel returns teamId belonging to channel (DM channels have no teamId). +func (m *MMClient) GetTeamFromChannel(channelId string) string { //nolint:golint + m.RLock() + defer m.RUnlock() + var channels []*model.Channel + for _, t := range m.OtherTeams { + channels = append(channels, t.Channels...) + if t.MoreChannels != nil { + channels = append(channels, t.MoreChannels...) + } + for _, c := range channels { + if c.Id == channelId { + if c.Type == model.CHANNEL_GROUP { + return "G" + } + return t.Id + } + } + channels = nil + } + return "" +} + +func (m *MMClient) JoinChannel(channelId string) error { //nolint:golint + m.RLock() + defer m.RUnlock() + for _, c := range m.Team.Channels { + if c.Id == channelId { + m.log.Debug("Not joining ", channelId, " already joined.") + return nil + } + } + m.log.Debug("Joining ", channelId) + _, resp := m.Client.AddChannelMember(channelId, m.User.Id) + if resp.Error != nil { + return resp.Error + } + return nil +} + +func (m *MMClient) UpdateChannels() error { + mmchannels, resp := m.Client.GetChannelsForTeamForUser(m.Team.Id, m.User.Id, "") + if resp.Error != nil { + return errors.New(resp.Error.DetailedError) + } + m.Lock() + m.Team.Channels = mmchannels + m.Unlock() + + mmchannels, resp = m.Client.GetPublicChannelsForTeam(m.Team.Id, 0, 5000, "") + if resp.Error != nil { + return errors.New(resp.Error.DetailedError) + } + + m.Lock() + m.Team.MoreChannels = mmchannels + m.Unlock() + return nil +} + +func (m *MMClient) UpdateChannelHeader(channelId string, header string) { //nolint:golint + channel := &model.Channel{Id: channelId, Header: header} + m.log.Debugf("updating channelheader %#v, %#v", channelId, header) + _, resp := m.Client.UpdateChannel(channel) + if resp.Error != nil { + log.Error(resp.Error) + } +} + +func (m *MMClient) UpdateLastViewed(channelId string) error { //nolint:golint + m.log.Debugf("posting lastview %#v", channelId) + view := &model.ChannelView{ChannelId: channelId} + _, resp := m.Client.ViewChannel(m.User.Id, view) + if resp.Error != nil { + m.log.Errorf("ChannelView update for %s failed: %s", channelId, resp.Error) + return resp.Error + } + return nil +} diff --git a/matterclient/helpers.go b/matterclient/helpers.go new file mode 100644 index 00000000..625fffaa --- /dev/null +++ b/matterclient/helpers.go @@ -0,0 +1,282 @@ +package matterclient + +import ( + "crypto/md5" //nolint:gosec + "crypto/tls" + "errors" + "fmt" + "net/http" + "net/http/cookiejar" + "net/url" + "strings" + "time" + + "github.com/gorilla/websocket" + "github.com/jpillora/backoff" + "github.com/mattermost/mattermost-server/model" +) + +func (m *MMClient) doLogin(firstConnection bool, b *backoff.Backoff) error { + var resp *model.Response + var appErr *model.AppError + var logmsg = "trying login" + var err error + for { + m.log.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server) + if m.Credentials.Token != "" { + resp, err = m.doLoginToken() + if err != nil { + return err + } + } else { + m.User, resp = m.Client.Login(m.Credentials.Login, m.Credentials.Pass) + } + appErr = resp.Error + if appErr != nil { + d := b.Duration() + m.log.Debug(appErr.DetailedError) + if firstConnection { + if appErr.Message == "" { + return errors.New(appErr.DetailedError) + } + return errors.New(appErr.Message) + } + m.log.Debugf("LOGIN: %s, reconnecting in %s", appErr, d) + time.Sleep(d) + logmsg = "retrying login" + continue + } + break + } + // reset timer + b.Reset() + return nil +} + +func (m *MMClient) doLoginToken() (*model.Response, error) { + var resp *model.Response + var logmsg = "trying login" + m.Client.AuthType = model.HEADER_BEARER + m.Client.AuthToken = m.Credentials.Token + if m.Credentials.CookieToken { + m.log.Debugf(logmsg + " with cookie (MMAUTH) token") + m.Client.HttpClient.Jar = m.createCookieJar(m.Credentials.Token) + } else { + m.log.Debugf(logmsg + " with personal token") + } + m.User, resp = m.Client.GetMe("") + if resp.Error != nil { + return resp, resp.Error + } + if m.User == nil { + m.log.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass) + return resp, errors.New("invalid token") + } + return resp, nil +} + +func (m *MMClient) handleLoginToken() error { + switch { + case strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN): + token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=") + if len(token) != 2 { + return errors.New("incorrect MMAUTHTOKEN. valid input is MMAUTHTOKEN=yourtoken") + } + m.Credentials.Token = token[1] + m.Credentials.CookieToken = true + case strings.Contains(m.Credentials.Pass, "token="): + token := strings.Split(m.Credentials.Pass, "token=") + if len(token) != 2 { + return errors.New("incorrect personal token. valid input is token=yourtoken") + } + m.Credentials.Token = token[1] + } + return nil +} + +func (m *MMClient) initClient(firstConnection bool, b *backoff.Backoff) error { + uriScheme := "https://" + if m.NoTLS { + uriScheme = "http://" + } + // login to mattermost + m.Client = model.NewAPIv4Client(uriScheme + m.Credentials.Server) + m.Client.HttpClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec + Proxy: http.ProxyFromEnvironment, + } + m.Client.HttpClient.Timeout = time.Second * 10 + + // handle MMAUTHTOKEN and personal token + if err := m.handleLoginToken(); err != nil { + return err + } + + // check if server alive, retry until + if err := m.serverAlive(firstConnection, b); err != nil { + return err + } + + return nil +} + +// initialize user and teams +func (m *MMClient) initUser() error { + m.Lock() + defer m.Unlock() + // we only load all team data on initial login. + // all other updates are for channels from our (primary) team only. + //m.log.Debug("initUser(): loading all team data") + teams, resp := m.Client.GetTeamsForUser(m.User.Id, "") + if resp.Error != nil { + return resp.Error + } + for _, team := range teams { + mmusers, resp := m.Client.GetUsersInTeam(team.Id, 0, 50000, "") + if resp.Error != nil { + return errors.New(resp.Error.DetailedError) + } + usermap := make(map[string]*model.User) + for _, user := range mmusers { + usermap[user.Id] = user + } + + t := &Team{Team: team, Users: usermap, Id: team.Id} + + mmchannels, resp := m.Client.GetChannelsForTeamForUser(team.Id, m.User.Id, "") + if resp.Error != nil { + return resp.Error + } + t.Channels = mmchannels + mmchannels, resp = m.Client.GetPublicChannelsForTeam(team.Id, 0, 5000, "") + if resp.Error != nil { + return resp.Error + } + t.MoreChannels = mmchannels + m.OtherTeams = append(m.OtherTeams, t) + if team.Name == m.Credentials.Team { + m.Team = t + m.log.Debugf("initUser(): found our team %s (id: %s)", team.Name, team.Id) + } + // add all users + for k, v := range t.Users { + m.Users[k] = v + } + } + return nil +} + +func (m *MMClient) serverAlive(firstConnection bool, b *backoff.Backoff) error { + defer b.Reset() + for { + d := b.Duration() + // bogus call to get the serverversion + _, resp := m.Client.Logout() + if resp.Error != nil { + return fmt.Errorf("%#v", resp.Error.Error()) + } + if firstConnection && !supportedVersion(resp.ServerVersion) { + return fmt.Errorf("unsupported mattermost version: %s", resp.ServerVersion) + } + m.ServerVersion = resp.ServerVersion + if m.ServerVersion == "" { + m.log.Debugf("Server not up yet, reconnecting in %s", d) + time.Sleep(d) + } else { + m.log.Infof("Found version %s", m.ServerVersion) + return nil + } + } +} + +func (m *MMClient) wsConnect() { + b := &backoff.Backoff{ + Min: time.Second, + Max: 5 * time.Minute, + Jitter: true, + } + + m.WsConnected = false + wsScheme := "wss://" + if m.NoTLS { + wsScheme = "ws://" + } + + // setup websocket connection + wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX_V4 + "/websocket" + header := http.Header{} + header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken) + + m.log.Debugf("WsClient: making connection: %s", wsurl) + for { + wsDialer := &websocket.Dialer{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec + Proxy: http.ProxyFromEnvironment, + } + var err error + m.WsClient, _, err = wsDialer.Dial(wsurl, header) + if err != nil { + d := b.Duration() + m.log.Debugf("WSS: %s, reconnecting in %s", err, d) + time.Sleep(d) + continue + } + break + } + + m.log.Debug("WsClient: connected") + m.WsSequence = 1 + m.WsPingChan = make(chan *model.WebSocketResponse) + // only start to parse WS messages when login is completely done + m.WsConnected = true +} + +func (m *MMClient) createCookieJar(token string) *cookiejar.Jar { + var cookies []*http.Cookie + jar, _ := cookiejar.New(nil) + firstCookie := &http.Cookie{ + Name: "MMAUTHTOKEN", + Value: token, + Path: "/", + Domain: m.Credentials.Server, + } + cookies = append(cookies, firstCookie) + cookieURL, _ := url.Parse("https://" + m.Credentials.Server) + jar.SetCookies(cookieURL, cookies) + return jar +} + +func (m *MMClient) checkAlive() error { + // check if session still is valid + _, resp := m.Client.GetMe("") + if resp.Error != nil { + return resp.Error + } + m.log.Debug("WS PING") + return m.sendWSRequest("ping", nil) +} + +func (m *MMClient) sendWSRequest(action string, data map[string]interface{}) error { + req := &model.WebSocketRequest{} + req.Seq = m.WsSequence + req.Action = action + req.Data = data + m.WsSequence++ + m.log.Debugf("sendWsRequest %#v", req) + return m.WsClient.WriteJSON(req) +} + +func supportedVersion(version string) bool { + if strings.HasPrefix(version, "3.8.0") || + strings.HasPrefix(version, "3.9.0") || + strings.HasPrefix(version, "3.10.0") || + strings.HasPrefix(version, "4.") || + strings.HasPrefix(version, "5.") { + return true + } + return false +} + +func digestString(s string) string { + return fmt.Sprintf("%x", md5.Sum([]byte(s))) //nolint:gosec +} diff --git a/matterclient/matterclient.go b/matterclient/matterclient.go index b86fbe3d..f15b1d1b 100644 --- a/matterclient/matterclient.go +++ b/matterclient/matterclient.go @@ -1,31 +1,26 @@ package matterclient import ( - "crypto/md5" - "crypto/tls" "encoding/json" - "errors" "fmt" - "net/http" - "net/http/cookiejar" - "net/url" "strings" "sync" "time" - prefixed "github.com/matterbridge/logrus-prefixed-formatter" - log "github.com/sirupsen/logrus" - "github.com/gorilla/websocket" "github.com/hashicorp/golang-lru" "github.com/jpillora/backoff" - "github.com/mattermost/platform/model" + prefixed "github.com/matterbridge/logrus-prefixed-formatter" + "github.com/mattermost/mattermost-server/model" + log "github.com/sirupsen/logrus" ) type Credentials struct { Login string Team string Pass string + Token string + CookieToken bool Server string NoTLS bool SkipTLSVerify bool @@ -42,6 +37,7 @@ type Message struct { UserID string } +//nolint:golint type Team struct { Team *model.Team Id string @@ -108,84 +104,17 @@ func (m *MMClient) Login() error { Max: 5 * time.Minute, Jitter: true, } - uriScheme := "https://" - if m.NoTLS { - uriScheme = "http://" - } - // login to mattermost - m.Client = model.NewAPIv4Client(uriScheme + m.Credentials.Server) - m.Client.HttpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, Proxy: http.ProxyFromEnvironment} - m.Client.HttpClient.Timeout = time.Second * 10 - for { - d := b.Duration() - // bogus call to get the serverversion - _, resp := m.Client.Logout() - if resp.Error != nil { - return fmt.Errorf("%#v", resp.Error.Error()) - } - if firstConnection && !supportedVersion(resp.ServerVersion) { - return fmt.Errorf("unsupported mattermost version: %s", resp.ServerVersion) - } - m.ServerVersion = resp.ServerVersion - if m.ServerVersion == "" { - m.log.Debugf("Server not up yet, reconnecting in %s", d) - time.Sleep(d) - } else { - m.log.Infof("Found version %s", m.ServerVersion) - break - } + // do initialization setup + if err := m.initClient(firstConnection, b); err != nil { + return err } - b.Reset() - var resp *model.Response - //var myinfo *model.Result - var appErr *model.AppError - var logmsg = "trying login" - for { - m.log.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server) - if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) { - m.log.Debugf(logmsg + " with token") - token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=") - if len(token) != 2 { - return errors.New("incorrect MMAUTHTOKEN. valid input is MMAUTHTOKEN=yourtoken") - } - m.Client.HttpClient.Jar = m.createCookieJar(token[1]) - m.Client.AuthToken = token[1] - m.Client.AuthType = model.HEADER_BEARER - m.User, resp = m.Client.GetMe("") - if resp.Error != nil { - return resp.Error - } - if m.User == nil { - m.log.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass) - return errors.New("invalid " + model.SESSION_COOKIE_TOKEN) - } - } else { - m.User, resp = m.Client.Login(m.Credentials.Login, m.Credentials.Pass) - } - appErr = resp.Error - if appErr != nil { - d := b.Duration() - m.log.Debug(appErr.DetailedError) - if firstConnection { - if appErr.Message == "" { - return errors.New(appErr.DetailedError) - } - return errors.New(appErr.Message) - } - m.log.Debugf("LOGIN: %s, reconnecting in %s", appErr, d) - time.Sleep(d) - logmsg = "retrying login" - continue - } - break + if err := m.doLogin(firstConnection, b); err != nil { + return err } - // reset timer - b.Reset() - err := m.initUser() - if err != nil { + if err := m.initUser(); err != nil { return err } @@ -202,45 +131,6 @@ func (m *MMClient) Login() error { return nil } -func (m *MMClient) wsConnect() { - b := &backoff.Backoff{ - Min: time.Second, - Max: 5 * time.Minute, - Jitter: true, - } - - m.WsConnected = false - wsScheme := "wss://" - if m.NoTLS { - wsScheme = "ws://" - } - - // setup websocket connection - wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX_V4 + "/websocket" - header := http.Header{} - header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken) - - m.log.Debugf("WsClient: making connection: %s", wsurl) - for { - wsDialer := &websocket.Dialer{Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}} - var err error - m.WsClient, _, err = wsDialer.Dial(wsurl, header) - if err != nil { - d := b.Duration() - m.log.Debugf("WSS: %s, reconnecting in %s", err, d) - time.Sleep(d) - continue - } - break - } - - m.log.Debug("WsClient: connected") - m.WsSequence = 1 - m.WsPingChan = make(chan *model.WebSocketResponse) - // only start to parse WS messages when login is completely done - m.WsConnected = true -} - func (m *MMClient) Logout() error { m.log.Debugf("logout as %s (team: %s) on %s", m.Credentials.Login, m.Credentials.Team, m.Credentials.Server) m.WsQuit = true @@ -306,551 +196,21 @@ func (m *MMClient) WsReceiver() { } } -func (m *MMClient) parseMessage(rmsg *Message) { - switch rmsg.Raw.Event { - case model.WEBSOCKET_EVENT_POSTED, model.WEBSOCKET_EVENT_POST_EDITED, model.WEBSOCKET_EVENT_POST_DELETED: - m.parseActionPost(rmsg) - case "user_updated": - user := rmsg.Raw.Data["user"].(map[string]interface{}) - if _, ok := user["id"].(string); ok { - m.UpdateUser(user["id"].(string)) - } - /* - case model.ACTION_USER_REMOVED: - m.handleWsActionUserRemoved(&rmsg) - case model.ACTION_USER_ADDED: - m.handleWsActionUserAdded(&rmsg) - */ - } -} - -func (m *MMClient) parseResponse(rmsg model.WebSocketResponse) { - if rmsg.Data != nil { - // ping reply - if rmsg.Data["text"].(string) == "pong" { - m.WsPingChan <- &rmsg - } - } -} - -func (m *MMClient) parseActionPost(rmsg *Message) { - // add post to cache, if it already exists don't relay this again. - // this should fix reposts - if ok, _ := m.lruCache.ContainsOrAdd(digestString(rmsg.Raw.Data["post"].(string)), true); ok { - m.log.Debugf("message %#v in cache, not processing again", rmsg.Raw.Data["post"].(string)) - rmsg.Text = "" - return - } - data := model.PostFromJson(strings.NewReader(rmsg.Raw.Data["post"].(string))) - // we don't have the user, refresh the userlist - if m.GetUser(data.UserId) == nil { - m.log.Infof("User %s is not known, ignoring message %s", data) - return - } - rmsg.Username = m.GetUserName(data.UserId) - rmsg.Channel = m.GetChannelName(data.ChannelId) - rmsg.UserID = data.UserId - rmsg.Type = data.Type - teamid, _ := rmsg.Raw.Data["team_id"].(string) - // edit messsages have no team_id for some reason - if teamid == "" { - // we can find the team_id from the channelid - teamid = m.GetChannelTeamId(data.ChannelId) - rmsg.Raw.Data["team_id"] = teamid - } - if teamid != "" { - rmsg.Team = m.GetTeamName(teamid) - } - // direct message - if rmsg.Raw.Data["channel_type"] == "D" { - rmsg.Channel = m.GetUser(data.UserId).Username - } - rmsg.Text = data.Message - rmsg.Post = data -} - -func (m *MMClient) UpdateUsers() error { - mmusers, resp := m.Client.GetUsers(0, 50000, "") - if resp.Error != nil { - return errors.New(resp.Error.DetailedError) - } - m.Lock() - for _, user := range mmusers { - m.Users[user.Id] = user - } - m.Unlock() - return nil -} - -func (m *MMClient) UpdateChannels() error { - mmchannels, resp := m.Client.GetChannelsForTeamForUser(m.Team.Id, m.User.Id, "") - if resp.Error != nil { - return errors.New(resp.Error.DetailedError) - } - m.Lock() - m.Team.Channels = mmchannels - m.Unlock() - - mmchannels, resp = m.Client.GetPublicChannelsForTeam(m.Team.Id, 0, 5000, "") - if resp.Error != nil { - return errors.New(resp.Error.DetailedError) - } - - m.Lock() - m.Team.MoreChannels = mmchannels - m.Unlock() - return nil -} - -func (m *MMClient) GetChannelName(channelId string) string { - m.RLock() - defer m.RUnlock() - for _, t := range m.OtherTeams { - if t == nil { - continue - } - if t.Channels != nil { - for _, channel := range t.Channels { - if channel.Id == channelId { - return channel.Name - } - } - } - if t.MoreChannels != nil { - for _, channel := range t.MoreChannels { - if channel.Id == channelId { - return channel.Name - } - } - } - } - return "" -} - -func (m *MMClient) GetChannelId(name string, teamId string) string { - m.RLock() - defer m.RUnlock() - if teamId == "" { - teamId = m.Team.Id - } - for _, t := range m.OtherTeams { - if t.Id == teamId { - for _, channel := range append(t.Channels, t.MoreChannels...) { - if channel.Name == name { - return channel.Id - } - } - } - } - return "" -} - -func (m *MMClient) GetChannelTeamId(id string) string { - m.RLock() - defer m.RUnlock() - for _, t := range append(m.OtherTeams, m.Team) { - for _, channel := range append(t.Channels, t.MoreChannels...) { - if channel.Id == id { - return channel.TeamId - } - } - } - return "" -} - -func (m *MMClient) GetChannelHeader(channelId string) string { - m.RLock() - defer m.RUnlock() - for _, t := range m.OtherTeams { - for _, channel := range append(t.Channels, t.MoreChannels...) { - if channel.Id == channelId { - return channel.Header - } - - } - } - return "" -} - -func (m *MMClient) PostMessage(channelId string, text string) (string, error) { - post := &model.Post{ChannelId: channelId, Message: text} - res, resp := m.Client.CreatePost(post) - if resp.Error != nil { - return "", resp.Error - } - return res.Id, nil -} - -func (m *MMClient) PostMessageWithFiles(channelId string, text string, fileIds []string) (string, error) { - post := &model.Post{ChannelId: channelId, Message: text, FileIds: fileIds} - res, resp := m.Client.CreatePost(post) - if resp.Error != nil { - return "", resp.Error - } - return res.Id, nil -} - -func (m *MMClient) EditMessage(postId string, text string) (string, error) { - post := &model.Post{Message: text} - res, resp := m.Client.UpdatePost(postId, post) - if resp.Error != nil { - return "", resp.Error - } - return res.Id, nil -} - -func (m *MMClient) DeleteMessage(postId string) error { - _, resp := m.Client.DeletePost(postId) - if resp.Error != nil { - return resp.Error - } - return nil -} - -func (m *MMClient) JoinChannel(channelId string) error { - m.RLock() - defer m.RUnlock() - for _, c := range m.Team.Channels { - if c.Id == channelId { - m.log.Debug("Not joining ", channelId, " already joined.") - return nil - } - } - m.log.Debug("Joining ", channelId) - _, resp := m.Client.AddChannelMember(channelId, m.User.Id) - if resp.Error != nil { - return resp.Error - } - return nil -} - -func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList { - res, resp := m.Client.GetPostsSince(channelId, time) - if resp.Error != nil { - return nil - } - return res -} - -func (m *MMClient) SearchPosts(query string) *model.PostList { - res, resp := m.Client.SearchPosts(m.Team.Id, query, false) - if resp.Error != nil { - return nil - } - return res -} - -func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList { - res, resp := m.Client.GetPostsForChannel(channelId, 0, limit, "") - if resp.Error != nil { - return nil - } - return res -} - -func (m *MMClient) GetPublicLink(filename string) string { - res, resp := m.Client.GetFileLink(filename) - if resp.Error != nil { - return "" - } - return res -} - -func (m *MMClient) GetPublicLinks(filenames []string) []string { - var output []string - for _, f := range filenames { - res, resp := m.Client.GetFileLink(f) - if resp.Error != nil { - continue - } - output = append(output, res) - } - return output -} - -func (m *MMClient) GetFileLinks(filenames []string) []string { - uriScheme := "https://" - if m.NoTLS { - uriScheme = "http://" - } - - var output []string - for _, f := range filenames { - res, resp := m.Client.GetFileLink(f) - if resp.Error != nil { - // public links is probably disabled, create the link ourselves - output = append(output, uriScheme+m.Credentials.Server+model.API_URL_SUFFIX_V4+"/files/"+f) - continue - } - output = append(output, res) - } - return output -} - -func (m *MMClient) UpdateChannelHeader(channelId string, header string) { - channel := &model.Channel{Id: channelId, Header: header} - m.log.Debugf("updating channelheader %#v, %#v", channelId, header) - _, resp := m.Client.UpdateChannel(channel) - if resp.Error != nil { - log.Error(resp.Error) - } -} - -func (m *MMClient) UpdateLastViewed(channelId string) { - m.log.Debugf("posting lastview %#v", channelId) - view := &model.ChannelView{ChannelId: channelId} - _, resp := m.Client.ViewChannel(m.User.Id, view) - if resp.Error != nil { - m.log.Errorf("ChannelView update for %s failed: %s", channelId, resp.Error) - } -} - -func (m *MMClient) UpdateUserNick(nick string) error { - user := m.User - user.Nickname = nick - _, resp := m.Client.UpdateUser(user) - if resp.Error != nil { - return resp.Error - } - return nil -} - -func (m *MMClient) UsernamesInChannel(channelId string) []string { - res, resp := m.Client.GetChannelMembers(channelId, 0, 50000, "") - if resp.Error != nil { - m.log.Errorf("UsernamesInChannel(%s) failed: %s", channelId, resp.Error) - return []string{} - } - allusers := m.GetUsers() - result := []string{} - for _, member := range *res { - result = append(result, allusers[member.UserId].Nickname) - } - return result -} - -func (m *MMClient) createCookieJar(token string) *cookiejar.Jar { - var cookies []*http.Cookie - jar, _ := cookiejar.New(nil) - firstCookie := &http.Cookie{ - Name: "MMAUTHTOKEN", - Value: token, - Path: "/", - Domain: m.Credentials.Server, - } - cookies = append(cookies, firstCookie) - cookieURL, _ := url.Parse("https://" + m.Credentials.Server) - jar.SetCookies(cookieURL, cookies) - return jar -} - -// SendDirectMessage sends a direct message to specified user -func (m *MMClient) SendDirectMessage(toUserId string, msg string) { - m.log.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg) - // create DM channel (only happens on first message) - _, resp := m.Client.CreateDirectChannel(m.User.Id, toUserId) - if resp.Error != nil { - m.log.Debugf("SendDirectMessage to %#v failed: %s", toUserId, resp.Error) - return - } - channelName := model.GetDMNameFromIds(toUserId, m.User.Id) - - // update our channels - m.UpdateChannels() - - // build & send the message - msg = strings.Replace(msg, "\r", "", -1) - post := &model.Post{ChannelId: m.GetChannelId(channelName, ""), Message: msg} - m.Client.CreatePost(post) -} - -// GetTeamName returns the name of the specified teamId -func (m *MMClient) GetTeamName(teamId string) string { - m.RLock() - defer m.RUnlock() - for _, t := range m.OtherTeams { - if t.Id == teamId { - return t.Team.Name - } - } - return "" -} - -// GetChannels returns all channels we're members off -func (m *MMClient) GetChannels() []*model.Channel { - m.RLock() - defer m.RUnlock() - var channels []*model.Channel - // our primary team channels first - channels = append(channels, m.Team.Channels...) - for _, t := range m.OtherTeams { - if t.Id != m.Team.Id { - channels = append(channels, t.Channels...) - } - } - return channels -} - -// GetMoreChannels returns existing channels where we're not a member off. -func (m *MMClient) GetMoreChannels() []*model.Channel { - m.RLock() - defer m.RUnlock() - var channels []*model.Channel - for _, t := range m.OtherTeams { - channels = append(channels, t.MoreChannels...) - } - return channels -} - -// GetTeamFromChannel returns teamId belonging to channel (DM channels have no teamId). -func (m *MMClient) GetTeamFromChannel(channelId string) string { - m.RLock() - defer m.RUnlock() - var channels []*model.Channel - for _, t := range m.OtherTeams { - channels = append(channels, t.Channels...) - if t.MoreChannels != nil { - channels = append(channels, t.MoreChannels...) - } - for _, c := range channels { - if c.Id == channelId { - return t.Id - } - } - } - return "" -} - -func (m *MMClient) GetLastViewedAt(channelId string) int64 { - m.RLock() - defer m.RUnlock() - res, resp := m.Client.GetChannelMember(channelId, m.User.Id, "") - if resp.Error != nil { - return model.GetMillis() - } - return res.LastViewedAt -} - -func (m *MMClient) GetUsers() map[string]*model.User { - users := make(map[string]*model.User) - m.RLock() - defer m.RUnlock() - for k, v := range m.Users { - users[k] = v - } - return users -} - -func (m *MMClient) GetUser(userId string) *model.User { - m.Lock() - defer m.Unlock() - _, ok := m.Users[userId] - if !ok { - res, resp := m.Client.GetUser(userId, "") - if resp.Error != nil { - return nil - } - m.Users[userId] = res - } - return m.Users[userId] -} - -func (m *MMClient) UpdateUser(userId string) { - m.Lock() - defer m.Unlock() - res, resp := m.Client.GetUser(userId, "") - if resp.Error != nil { - return - } - m.Users[userId] = res -} - -func (m *MMClient) GetUserName(userId string) string { - user := m.GetUser(userId) - if user != nil { - return user.Username - } - return "" -} - -func (m *MMClient) GetNickName(userId string) string { - user := m.GetUser(userId) - if user != nil { - return user.Nickname - } - return "" -} - -func (m *MMClient) GetStatus(userId string) string { - res, resp := m.Client.GetUserStatus(userId, "") - if resp.Error != nil { - return "" - } - if res.Status == model.STATUS_AWAY { - return "away" - } - if res.Status == model.STATUS_ONLINE { - return "online" - } - return "offline" -} - -func (m *MMClient) UpdateStatus(userId string, status string) error { - _, resp := m.Client.UpdateUserStatus(userId, &model.Status{Status: status}) - if resp.Error != nil { - return resp.Error - } - return nil -} - -func (m *MMClient) GetStatuses() map[string]string { - var ids []string - statuses := make(map[string]string) - for id := range m.Users { - ids = append(ids, id) - } - res, resp := m.Client.GetUsersStatusesByIds(ids) - if resp.Error != nil { - return statuses - } - for _, status := range res { - statuses[status.UserId] = "offline" - if status.Status == model.STATUS_AWAY { - statuses[status.UserId] = "away" - } - if status.Status == model.STATUS_ONLINE { - statuses[status.UserId] = "online" - } - } - return statuses -} - -func (m *MMClient) GetTeamId() string { - return m.Team.Id -} - -func (m *MMClient) UploadFile(data []byte, channelId string, filename string) (string, error) { - f, resp := m.Client.UploadFile(data, channelId, filename) - if resp.Error != nil { - return "", resp.Error - } - return f.FileInfos[0].Id, nil -} - func (m *MMClient) StatusLoop() { retries := 0 backoff := time.Second * 60 if m.OnWsConnect != nil { m.OnWsConnect() } - m.log.Debug("StatusLoop:", m.OnWsConnect) + m.log.Debug("StatusLoop:", m.OnWsConnect != nil) for { if m.WsQuit { return } if m.WsConnected { - m.log.Debug("WS PING") - m.sendWSRequest("ping", nil) + if err := m.checkAlive(); err != nil { + log.Errorf("Connection is not alive: %#v", err) + } select { case <-m.WsPingChan: m.log.Debug("WS PONG received") @@ -878,75 +238,3 @@ func (m *MMClient) StatusLoop() { time.Sleep(backoff) } } - -// initialize user and teams -func (m *MMClient) initUser() error { - m.Lock() - defer m.Unlock() - // we only load all team data on initial login. - // all other updates are for channels from our (primary) team only. - //m.log.Debug("initUser(): loading all team data") - teams, resp := m.Client.GetTeamsForUser(m.User.Id, "") - if resp.Error != nil { - return resp.Error - } - for _, team := range teams { - mmusers, resp := m.Client.GetUsersInTeam(team.Id, 0, 50000, "") - if resp.Error != nil { - return errors.New(resp.Error.DetailedError) - } - usermap := make(map[string]*model.User) - for _, user := range mmusers { - usermap[user.Id] = user - } - - t := &Team{Team: team, Users: usermap, Id: team.Id} - - mmchannels, resp := m.Client.GetChannelsForTeamForUser(team.Id, m.User.Id, "") - if resp.Error != nil { - return resp.Error - } - t.Channels = mmchannels - mmchannels, resp = m.Client.GetPublicChannelsForTeam(team.Id, 0, 5000, "") - if resp.Error != nil { - return resp.Error - } - t.MoreChannels = mmchannels - m.OtherTeams = append(m.OtherTeams, t) - if team.Name == m.Credentials.Team { - m.Team = t - m.log.Debugf("initUser(): found our team %s (id: %s)", team.Name, team.Id) - } - // add all users - for k, v := range t.Users { - m.Users[k] = v - } - } - return nil -} - -func (m *MMClient) sendWSRequest(action string, data map[string]interface{}) error { - req := &model.WebSocketRequest{} - req.Seq = m.WsSequence - req.Action = action - req.Data = data - m.WsSequence++ - m.log.Debugf("sendWsRequest %#v", req) - m.WsClient.WriteJSON(req) - return nil -} - -func supportedVersion(version string) bool { - if strings.HasPrefix(version, "3.8.0") || - strings.HasPrefix(version, "3.9.0") || - strings.HasPrefix(version, "3.10.0") || - strings.HasPrefix(version, "4.") || - strings.HasPrefix(version, "5.") { - return true - } - return false -} - -func digestString(s string) string { - return fmt.Sprintf("%x", md5.Sum([]byte(s))) -} diff --git a/matterclient/messages.go b/matterclient/messages.go new file mode 100644 index 00000000..985cfe04 --- /dev/null +++ b/matterclient/messages.go @@ -0,0 +1,207 @@ +package matterclient + +import ( + "strings" + + "github.com/mattermost/mattermost-server/model" +) + +func (m *MMClient) parseActionPost(rmsg *Message) { + // add post to cache, if it already exists don't relay this again. + // this should fix reposts + if ok, _ := m.lruCache.ContainsOrAdd(digestString(rmsg.Raw.Data["post"].(string)), true); ok { + m.log.Debugf("message %#v in cache, not processing again", rmsg.Raw.Data["post"].(string)) + rmsg.Text = "" + return + } + data := model.PostFromJson(strings.NewReader(rmsg.Raw.Data["post"].(string))) + // we don't have the user, refresh the userlist + if m.GetUser(data.UserId) == nil { + m.log.Infof("User '%v' is not known, ignoring message '%#v'", + data.UserId, data) + return + } + rmsg.Username = m.GetUserName(data.UserId) + rmsg.Channel = m.GetChannelName(data.ChannelId) + rmsg.UserID = data.UserId + rmsg.Type = data.Type + teamid, _ := rmsg.Raw.Data["team_id"].(string) + // edit messsages have no team_id for some reason + if teamid == "" { + // we can find the team_id from the channelid + teamid = m.GetChannelTeamId(data.ChannelId) + rmsg.Raw.Data["team_id"] = teamid + } + if teamid != "" { + rmsg.Team = m.GetTeamName(teamid) + } + // direct message + if rmsg.Raw.Data["channel_type"] == "D" { + rmsg.Channel = m.GetUser(data.UserId).Username + } + rmsg.Text = data.Message + rmsg.Post = data +} + +func (m *MMClient) parseMessage(rmsg *Message) { + switch rmsg.Raw.Event { + case model.WEBSOCKET_EVENT_POSTED, model.WEBSOCKET_EVENT_POST_EDITED, model.WEBSOCKET_EVENT_POST_DELETED: + m.parseActionPost(rmsg) + case "user_updated": + user := rmsg.Raw.Data["user"].(map[string]interface{}) + if _, ok := user["id"].(string); ok { + m.UpdateUser(user["id"].(string)) + } + case "group_added": + if err := m.UpdateChannels(); err != nil { + m.log.Errorf("failed to update channels: %#v", err) + } + /* + case model.ACTION_USER_REMOVED: + m.handleWsActionUserRemoved(&rmsg) + case model.ACTION_USER_ADDED: + m.handleWsActionUserAdded(&rmsg) + */ + } +} + +func (m *MMClient) parseResponse(rmsg model.WebSocketResponse) { + if rmsg.Data != nil { + // ping reply + if rmsg.Data["text"].(string) == "pong" { + m.WsPingChan <- &rmsg + } + } +} + +func (m *MMClient) DeleteMessage(postId string) error { //nolint:golint + _, resp := m.Client.DeletePost(postId) + if resp.Error != nil { + return resp.Error + } + return nil +} + +func (m *MMClient) EditMessage(postId string, text string) (string, error) { //nolint:golint + post := &model.Post{Message: text} + res, resp := m.Client.UpdatePost(postId, post) + if resp.Error != nil { + return "", resp.Error + } + return res.Id, nil +} + +func (m *MMClient) GetFileLinks(filenames []string) []string { + uriScheme := "https://" + if m.NoTLS { + uriScheme = "http://" + } + + var output []string + for _, f := range filenames { + res, resp := m.Client.GetFileLink(f) + if resp.Error != nil { + // public links is probably disabled, create the link ourselves + output = append(output, uriScheme+m.Credentials.Server+model.API_URL_SUFFIX_V4+"/files/"+f) + continue + } + output = append(output, res) + } + return output +} + +func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList { //nolint:golint + res, resp := m.Client.GetPostsForChannel(channelId, 0, limit, "") + if resp.Error != nil { + return nil + } + return res +} + +func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList { //nolint:golint + res, resp := m.Client.GetPostsSince(channelId, time) + if resp.Error != nil { + return nil + } + return res +} + +func (m *MMClient) GetPublicLink(filename string) string { + res, resp := m.Client.GetFileLink(filename) + if resp.Error != nil { + return "" + } + return res +} + +func (m *MMClient) GetPublicLinks(filenames []string) []string { + var output []string + for _, f := range filenames { + res, resp := m.Client.GetFileLink(f) + if resp.Error != nil { + continue + } + output = append(output, res) + } + return output +} + +func (m *MMClient) PostMessage(channelId string, text string) (string, error) { //nolint:golint + post := &model.Post{ChannelId: channelId, Message: text} + res, resp := m.Client.CreatePost(post) + if resp.Error != nil { + return "", resp.Error + } + return res.Id, nil +} + +func (m *MMClient) PostMessageWithFiles(channelId string, text string, fileIds []string) (string, error) { //nolint:golint + post := &model.Post{ChannelId: channelId, Message: text, FileIds: fileIds} + res, resp := m.Client.CreatePost(post) + if resp.Error != nil { + return "", resp.Error + } + return res.Id, nil +} + +func (m *MMClient) SearchPosts(query string) *model.PostList { + res, resp := m.Client.SearchPosts(m.Team.Id, query, false) + if resp.Error != nil { + return nil + } + return res +} + +// SendDirectMessage sends a direct message to specified user +func (m *MMClient) SendDirectMessage(toUserId string, msg string) { //nolint:golint + m.SendDirectMessageProps(toUserId, msg, nil) +} + +func (m *MMClient) SendDirectMessageProps(toUserId string, msg string, props map[string]interface{}) { //nolint:golint + m.log.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg) + // create DM channel (only happens on first message) + _, resp := m.Client.CreateDirectChannel(m.User.Id, toUserId) + if resp.Error != nil { + m.log.Debugf("SendDirectMessage to %#v failed: %s", toUserId, resp.Error) + return + } + channelName := model.GetDMNameFromIds(toUserId, m.User.Id) + + // update our channels + if err := m.UpdateChannels(); err != nil { + m.log.Errorf("failed to update channels: %#v", err) + } + + // build & send the message + msg = strings.Replace(msg, "\r", "", -1) + post := &model.Post{ChannelId: m.GetChannelId(channelName, m.Team.Id), Message: msg, Props: props} + m.Client.CreatePost(post) +} + +func (m *MMClient) UploadFile(data []byte, channelId string, filename string) (string, error) { //nolint:golint + f, resp := m.Client.UploadFile(data, channelId, filename) + if resp.Error != nil { + return "", resp.Error + } + return f.FileInfos[0].Id, nil +} diff --git a/matterclient/users.go b/matterclient/users.go new file mode 100644 index 00000000..3dea7ce5 --- /dev/null +++ b/matterclient/users.go @@ -0,0 +1,154 @@ +package matterclient + +import ( + "errors" + + "github.com/mattermost/mattermost-server/model" +) + +func (m *MMClient) GetNickName(userId string) string { //nolint:golint + user := m.GetUser(userId) + if user != nil { + return user.Nickname + } + return "" +} + +func (m *MMClient) GetStatus(userId string) string { //nolint:golint + res, resp := m.Client.GetUserStatus(userId, "") + if resp.Error != nil { + return "" + } + if res.Status == model.STATUS_AWAY { + return "away" + } + if res.Status == model.STATUS_ONLINE { + return "online" + } + return "offline" +} + +func (m *MMClient) GetStatuses() map[string]string { + var ids []string + statuses := make(map[string]string) + for id := range m.Users { + ids = append(ids, id) + } + res, resp := m.Client.GetUsersStatusesByIds(ids) + if resp.Error != nil { + return statuses + } + for _, status := range res { + statuses[status.UserId] = "offline" + if status.Status == model.STATUS_AWAY { + statuses[status.UserId] = "away" + } + if status.Status == model.STATUS_ONLINE { + statuses[status.UserId] = "online" + } + } + return statuses +} + +func (m *MMClient) GetTeamId() string { //nolint:golint + return m.Team.Id +} + +// GetTeamName returns the name of the specified teamId +func (m *MMClient) GetTeamName(teamId string) string { //nolint:golint + m.RLock() + defer m.RUnlock() + for _, t := range m.OtherTeams { + if t.Id == teamId { + return t.Team.Name + } + } + return "" +} + +func (m *MMClient) GetUser(userId string) *model.User { //nolint:golint + m.Lock() + defer m.Unlock() + _, ok := m.Users[userId] + if !ok { + res, resp := m.Client.GetUser(userId, "") + if resp.Error != nil { + return nil + } + m.Users[userId] = res + } + return m.Users[userId] +} + +func (m *MMClient) GetUserName(userId string) string { //nolint:golint + user := m.GetUser(userId) + if user != nil { + return user.Username + } + return "" +} + +func (m *MMClient) GetUsers() map[string]*model.User { + users := make(map[string]*model.User) + m.RLock() + defer m.RUnlock() + for k, v := range m.Users { + users[k] = v + } + return users +} + +func (m *MMClient) UpdateUsers() error { + mmusers, resp := m.Client.GetUsers(0, 50000, "") + if resp.Error != nil { + return errors.New(resp.Error.DetailedError) + } + m.Lock() + for _, user := range mmusers { + m.Users[user.Id] = user + } + m.Unlock() + return nil +} + +func (m *MMClient) UpdateUserNick(nick string) error { + user := m.User + user.Nickname = nick + _, resp := m.Client.UpdateUser(user) + if resp.Error != nil { + return resp.Error + } + return nil +} + +func (m *MMClient) UsernamesInChannel(channelId string) []string { //nolint:golint + res, resp := m.Client.GetChannelMembers(channelId, 0, 50000, "") + if resp.Error != nil { + m.log.Errorf("UsernamesInChannel(%s) failed: %s", channelId, resp.Error) + return []string{} + } + allusers := m.GetUsers() + result := []string{} + for _, member := range *res { + result = append(result, allusers[member.UserId].Nickname) + } + return result +} + +func (m *MMClient) UpdateStatus(userId string, status string) error { //nolint:golint + _, resp := m.Client.UpdateUserStatus(userId, &model.Status{Status: status}) + if resp.Error != nil { + return resp.Error + } + return nil +} + +func (m *MMClient) UpdateUser(userId string) { //nolint:golint + m.Lock() + defer m.Unlock() + res, resp := m.Client.GetUser(userId, "") + if resp.Error != nil { + return + } + m.Users[userId] = res +} diff --git a/matterhook/matterhook.go b/matterhook/matterhook.go index 9287baa9..f5133112 100644 --- a/matterhook/matterhook.go +++ b/matterhook/matterhook.go @@ -41,9 +41,9 @@ type IMessage struct { Timestamp string `schema:"timestamp"` UserID string `schema:"user_id"` UserName string `schema:"user_name"` - PostId string `schema:"post_id"` + PostId string `schema:"post_id"` //nolint:golint RawText string `schema:"raw_text"` - ServiceId string `schema:"service_id"` + ServiceId string `schema:"service_id"` //nolint:golint Text string `schema:"text"` TriggerWord string `schema:"trigger_word"` FileIDs string `schema:"file_ids"` @@ -51,7 +51,8 @@ type IMessage struct { // Client for Mattermost. type Client struct { - Url string // URL for incoming webhooks on mattermost. + // URL for incoming webhooks on mattermost. + Url string // nolint:golint In chan IMessage Out chan OMessage httpclient *http.Client @@ -70,7 +71,7 @@ type Config struct { func New(url string, config Config) *Client { c := &Client{Url: url, In: make(chan IMessage), Out: make(chan OMessage), Config: config} tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}, + TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}, //nolint:gosec } c.httpclient = &http.Client{Transport: tr} if !c.DisableServer { diff --git a/vendor/github.com/BurntSushi/toml/.gitignore b/vendor/github.com/BurntSushi/toml/.gitignore deleted file mode 100644 index 0cd38003..00000000 --- a/vendor/github.com/BurntSushi/toml/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -TAGS -tags -.*.swp -tomlcheck/tomlcheck -toml.test diff --git a/vendor/github.com/BurntSushi/toml/.travis.yml b/vendor/github.com/BurntSushi/toml/.travis.yml deleted file mode 100644 index 8b8afc4f..00000000 --- a/vendor/github.com/BurntSushi/toml/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: go -go: - - 1.1 - - 1.2 - - 1.3 - - 1.4 - - 1.5 - - 1.6 - - tip -install: - - go install ./... - - go get github.com/BurntSushi/toml-test -script: - - export PATH="$PATH:$HOME/gopath/bin" - - make test diff --git a/vendor/github.com/BurntSushi/toml/COMPATIBLE b/vendor/github.com/BurntSushi/toml/COMPATIBLE deleted file mode 100644 index 6efcfd0c..00000000 --- a/vendor/github.com/BurntSushi/toml/COMPATIBLE +++ /dev/null @@ -1,3 +0,0 @@ -Compatible with TOML version -[v0.4.0](https://github.com/toml-lang/toml/blob/v0.4.0/versions/en/toml-v0.4.0.md) - diff --git a/vendor/github.com/BurntSushi/toml/COPYING b/vendor/github.com/BurntSushi/toml/COPYING deleted file mode 100644 index 5a8e3325..00000000 --- a/vendor/github.com/BurntSushi/toml/COPYING +++ /dev/null @@ -1,14 +0,0 @@ - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - Version 2, December 2004 - - Copyright (C) 2004 Sam Hocevar - - Everyone is permitted to copy and distribute verbatim or modified - copies of this license document, and changing it is allowed as long - as the name is changed. - - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. You just DO WHAT THE FUCK YOU WANT TO. - diff --git a/vendor/github.com/BurntSushi/toml/Makefile b/vendor/github.com/BurntSushi/toml/Makefile deleted file mode 100644 index 3600848d..00000000 --- a/vendor/github.com/BurntSushi/toml/Makefile +++ /dev/null @@ -1,19 +0,0 @@ -install: - go install ./... - -test: install - go test -v - toml-test toml-test-decoder - toml-test -encoder toml-test-encoder - -fmt: - gofmt -w *.go */*.go - colcheck *.go */*.go - -tags: - find ./ -name '*.go' -print0 | xargs -0 gotags > TAGS - -push: - git push origin master - git push github master - diff --git a/vendor/github.com/BurntSushi/toml/README.md b/vendor/github.com/BurntSushi/toml/README.md deleted file mode 100644 index 7c1b37ec..00000000 --- a/vendor/github.com/BurntSushi/toml/README.md +++ /dev/null @@ -1,218 +0,0 @@ -## TOML parser and encoder for Go with reflection - -TOML stands for Tom's Obvious, Minimal Language. This Go package provides a -reflection interface similar to Go's standard library `json` and `xml` -packages. This package also supports the `encoding.TextUnmarshaler` and -`encoding.TextMarshaler` interfaces so that you can define custom data -representations. (There is an example of this below.) - -Spec: https://github.com/toml-lang/toml - -Compatible with TOML version -[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md) - -Documentation: https://godoc.org/github.com/BurntSushi/toml - -Installation: - -```bash -go get github.com/BurntSushi/toml -``` - -Try the toml validator: - -```bash -go get github.com/BurntSushi/toml/cmd/tomlv -tomlv some-toml-file.toml -``` - -[![Build Status](https://travis-ci.org/BurntSushi/toml.svg?branch=master)](https://travis-ci.org/BurntSushi/toml) [![GoDoc](https://godoc.org/github.com/BurntSushi/toml?status.svg)](https://godoc.org/github.com/BurntSushi/toml) - -### Testing - -This package passes all tests in -[toml-test](https://github.com/BurntSushi/toml-test) for both the decoder -and the encoder. - -### Examples - -This package works similarly to how the Go standard library handles `XML` -and `JSON`. Namely, data is loaded into Go values via reflection. - -For the simplest example, consider some TOML file as just a list of keys -and values: - -```toml -Age = 25 -Cats = [ "Cauchy", "Plato" ] -Pi = 3.14 -Perfection = [ 6, 28, 496, 8128 ] -DOB = 1987-07-05T05:45:00Z -``` - -Which could be defined in Go as: - -```go -type Config struct { - Age int - Cats []string - Pi float64 - Perfection []int - DOB time.Time // requires `import time` -} -``` - -And then decoded with: - -```go -var conf Config -if _, err := toml.Decode(tomlData, &conf); err != nil { - // handle error -} -``` - -You can also use struct tags if your struct field name doesn't map to a TOML -key value directly: - -```toml -some_key_NAME = "wat" -``` - -```go -type TOML struct { - ObscureKey string `toml:"some_key_NAME"` -} -``` - -### Using the `encoding.TextUnmarshaler` interface - -Here's an example that automatically parses duration strings into -`time.Duration` values: - -```toml -[[song]] -name = "Thunder Road" -duration = "4m49s" - -[[song]] -name = "Stairway to Heaven" -duration = "8m03s" -``` - -Which can be decoded with: - -```go -type song struct { - Name string - Duration duration -} -type songs struct { - Song []song -} -var favorites songs -if _, err := toml.Decode(blob, &favorites); err != nil { - log.Fatal(err) -} - -for _, s := range favorites.Song { - fmt.Printf("%s (%s)\n", s.Name, s.Duration) -} -``` - -And you'll also need a `duration` type that satisfies the -`encoding.TextUnmarshaler` interface: - -```go -type duration struct { - time.Duration -} - -func (d *duration) UnmarshalText(text []byte) error { - var err error - d.Duration, err = time.ParseDuration(string(text)) - return err -} -``` - -### More complex usage - -Here's an example of how to load the example from the official spec page: - -```toml -# This is a TOML document. Boom. - -title = "TOML Example" - -[owner] -name = "Tom Preston-Werner" -organization = "GitHub" -bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." -dob = 1979-05-27T07:32:00Z # First class dates? Why not? - -[database] -server = "192.168.1.1" -ports = [ 8001, 8001, 8002 ] -connection_max = 5000 -enabled = true - -[servers] - - # You can indent as you please. Tabs or spaces. TOML don't care. - [servers.alpha] - ip = "10.0.0.1" - dc = "eqdc10" - - [servers.beta] - ip = "10.0.0.2" - dc = "eqdc10" - -[clients] -data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it - -# Line breaks are OK when inside arrays -hosts = [ - "alpha", - "omega" -] -``` - -And the corresponding Go types are: - -```go -type tomlConfig struct { - Title string - Owner ownerInfo - DB database `toml:"database"` - Servers map[string]server - Clients clients -} - -type ownerInfo struct { - Name string - Org string `toml:"organization"` - Bio string - DOB time.Time -} - -type database struct { - Server string - Ports []int - ConnMax int `toml:"connection_max"` - Enabled bool -} - -type server struct { - IP string - DC string -} - -type clients struct { - Data [][]interface{} - Hosts []string -} -``` - -Note that a case insensitive match will be tried if an exact match can't be -found. - -A working example of the above can be found in `_examples/example.{go,toml}`. diff --git a/vendor/github.com/BurntSushi/toml/decode.go b/vendor/github.com/BurntSushi/toml/decode.go deleted file mode 100644 index b0fd51d5..00000000 --- a/vendor/github.com/BurntSushi/toml/decode.go +++ /dev/null @@ -1,509 +0,0 @@ -package toml - -import ( - "fmt" - "io" - "io/ioutil" - "math" - "reflect" - "strings" - "time" -) - -func e(format string, args ...interface{}) error { - return fmt.Errorf("toml: "+format, args...) -} - -// Unmarshaler is the interface implemented by objects that can unmarshal a -// TOML description of themselves. -type Unmarshaler interface { - UnmarshalTOML(interface{}) error -} - -// Unmarshal decodes the contents of `p` in TOML format into a pointer `v`. -func Unmarshal(p []byte, v interface{}) error { - _, err := Decode(string(p), v) - return err -} - -// Primitive is a TOML value that hasn't been decoded into a Go value. -// When using the various `Decode*` functions, the type `Primitive` may -// be given to any value, and its decoding will be delayed. -// -// A `Primitive` value can be decoded using the `PrimitiveDecode` function. -// -// The underlying representation of a `Primitive` value is subject to change. -// Do not rely on it. -// -// N.B. Primitive values are still parsed, so using them will only avoid -// the overhead of reflection. They can be useful when you don't know the -// exact type of TOML data until run time. -type Primitive struct { - undecoded interface{} - context Key -} - -// DEPRECATED! -// -// Use MetaData.PrimitiveDecode instead. -func PrimitiveDecode(primValue Primitive, v interface{}) error { - md := MetaData{decoded: make(map[string]bool)} - return md.unify(primValue.undecoded, rvalue(v)) -} - -// PrimitiveDecode is just like the other `Decode*` functions, except it -// decodes a TOML value that has already been parsed. Valid primitive values -// can *only* be obtained from values filled by the decoder functions, -// including this method. (i.e., `v` may contain more `Primitive` -// values.) -// -// Meta data for primitive values is included in the meta data returned by -// the `Decode*` functions with one exception: keys returned by the Undecoded -// method will only reflect keys that were decoded. Namely, any keys hidden -// behind a Primitive will be considered undecoded. Executing this method will -// update the undecoded keys in the meta data. (See the example.) -func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error { - md.context = primValue.context - defer func() { md.context = nil }() - return md.unify(primValue.undecoded, rvalue(v)) -} - -// Decode will decode the contents of `data` in TOML format into a pointer -// `v`. -// -// TOML hashes correspond to Go structs or maps. (Dealer's choice. They can be -// used interchangeably.) -// -// TOML arrays of tables correspond to either a slice of structs or a slice -// of maps. -// -// TOML datetimes correspond to Go `time.Time` values. -// -// All other TOML types (float, string, int, bool and array) correspond -// to the obvious Go types. -// -// An exception to the above rules is if a type implements the -// encoding.TextUnmarshaler interface. In this case, any primitive TOML value -// (floats, strings, integers, booleans and datetimes) will be converted to -// a byte string and given to the value's UnmarshalText method. See the -// Unmarshaler example for a demonstration with time duration strings. -// -// Key mapping -// -// TOML keys can map to either keys in a Go map or field names in a Go -// struct. The special `toml` struct tag may be used to map TOML keys to -// struct fields that don't match the key name exactly. (See the example.) -// A case insensitive match to struct names will be tried if an exact match -// can't be found. -// -// The mapping between TOML values and Go values is loose. That is, there -// may exist TOML values that cannot be placed into your representation, and -// there may be parts of your representation that do not correspond to -// TOML values. This loose mapping can be made stricter by using the IsDefined -// and/or Undecoded methods on the MetaData returned. -// -// This decoder will not handle cyclic types. If a cyclic type is passed, -// `Decode` will not terminate. -func Decode(data string, v interface{}) (MetaData, error) { - rv := reflect.ValueOf(v) - if rv.Kind() != reflect.Ptr { - return MetaData{}, e("Decode of non-pointer %s", reflect.TypeOf(v)) - } - if rv.IsNil() { - return MetaData{}, e("Decode of nil %s", reflect.TypeOf(v)) - } - p, err := parse(data) - if err != nil { - return MetaData{}, err - } - md := MetaData{ - p.mapping, p.types, p.ordered, - make(map[string]bool, len(p.ordered)), nil, - } - return md, md.unify(p.mapping, indirect(rv)) -} - -// DecodeFile is just like Decode, except it will automatically read the -// contents of the file at `fpath` and decode it for you. -func DecodeFile(fpath string, v interface{}) (MetaData, error) { - bs, err := ioutil.ReadFile(fpath) - if err != nil { - return MetaData{}, err - } - return Decode(string(bs), v) -} - -// DecodeReader is just like Decode, except it will consume all bytes -// from the reader and decode it for you. -func DecodeReader(r io.Reader, v interface{}) (MetaData, error) { - bs, err := ioutil.ReadAll(r) - if err != nil { - return MetaData{}, err - } - return Decode(string(bs), v) -} - -// unify performs a sort of type unification based on the structure of `rv`, -// which is the client representation. -// -// Any type mismatch produces an error. Finding a type that we don't know -// how to handle produces an unsupported type error. -func (md *MetaData) unify(data interface{}, rv reflect.Value) error { - - // Special case. Look for a `Primitive` value. - if rv.Type() == reflect.TypeOf((*Primitive)(nil)).Elem() { - // Save the undecoded data and the key context into the primitive - // value. - context := make(Key, len(md.context)) - copy(context, md.context) - rv.Set(reflect.ValueOf(Primitive{ - undecoded: data, - context: context, - })) - return nil - } - - // Special case. Unmarshaler Interface support. - if rv.CanAddr() { - if v, ok := rv.Addr().Interface().(Unmarshaler); ok { - return v.UnmarshalTOML(data) - } - } - - // Special case. Handle time.Time values specifically. - // TODO: Remove this code when we decide to drop support for Go 1.1. - // This isn't necessary in Go 1.2 because time.Time satisfies the encoding - // interfaces. - if rv.Type().AssignableTo(rvalue(time.Time{}).Type()) { - return md.unifyDatetime(data, rv) - } - - // Special case. Look for a value satisfying the TextUnmarshaler interface. - if v, ok := rv.Interface().(TextUnmarshaler); ok { - return md.unifyText(data, v) - } - // BUG(burntsushi) - // The behavior here is incorrect whenever a Go type satisfies the - // encoding.TextUnmarshaler interface but also corresponds to a TOML - // hash or array. In particular, the unmarshaler should only be applied - // to primitive TOML values. But at this point, it will be applied to - // all kinds of values and produce an incorrect error whenever those values - // are hashes or arrays (including arrays of tables). - - k := rv.Kind() - - // laziness - if k >= reflect.Int && k <= reflect.Uint64 { - return md.unifyInt(data, rv) - } - switch k { - case reflect.Ptr: - elem := reflect.New(rv.Type().Elem()) - err := md.unify(data, reflect.Indirect(elem)) - if err != nil { - return err - } - rv.Set(elem) - return nil - case reflect.Struct: - return md.unifyStruct(data, rv) - case reflect.Map: - return md.unifyMap(data, rv) - case reflect.Array: - return md.unifyArray(data, rv) - case reflect.Slice: - return md.unifySlice(data, rv) - case reflect.String: - return md.unifyString(data, rv) - case reflect.Bool: - return md.unifyBool(data, rv) - case reflect.Interface: - // we only support empty interfaces. - if rv.NumMethod() > 0 { - return e("unsupported type %s", rv.Type()) - } - return md.unifyAnything(data, rv) - case reflect.Float32: - fallthrough - case reflect.Float64: - return md.unifyFloat64(data, rv) - } - return e("unsupported type %s", rv.Kind()) -} - -func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error { - tmap, ok := mapping.(map[string]interface{}) - if !ok { - if mapping == nil { - return nil - } - return e("type mismatch for %s: expected table but found %T", - rv.Type().String(), mapping) - } - - for key, datum := range tmap { - var f *field - fields := cachedTypeFields(rv.Type()) - for i := range fields { - ff := &fields[i] - if ff.name == key { - f = ff - break - } - if f == nil && strings.EqualFold(ff.name, key) { - f = ff - } - } - if f != nil { - subv := rv - for _, i := range f.index { - subv = indirect(subv.Field(i)) - } - if isUnifiable(subv) { - md.decoded[md.context.add(key).String()] = true - md.context = append(md.context, key) - if err := md.unify(datum, subv); err != nil { - return err - } - md.context = md.context[0 : len(md.context)-1] - } else if f.name != "" { - // Bad user! No soup for you! - return e("cannot write unexported field %s.%s", - rv.Type().String(), f.name) - } - } - } - return nil -} - -func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error { - tmap, ok := mapping.(map[string]interface{}) - if !ok { - if tmap == nil { - return nil - } - return badtype("map", mapping) - } - if rv.IsNil() { - rv.Set(reflect.MakeMap(rv.Type())) - } - for k, v := range tmap { - md.decoded[md.context.add(k).String()] = true - md.context = append(md.context, k) - - rvkey := indirect(reflect.New(rv.Type().Key())) - rvval := reflect.Indirect(reflect.New(rv.Type().Elem())) - if err := md.unify(v, rvval); err != nil { - return err - } - md.context = md.context[0 : len(md.context)-1] - - rvkey.SetString(k) - rv.SetMapIndex(rvkey, rvval) - } - return nil -} - -func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error { - datav := reflect.ValueOf(data) - if datav.Kind() != reflect.Slice { - if !datav.IsValid() { - return nil - } - return badtype("slice", data) - } - sliceLen := datav.Len() - if sliceLen != rv.Len() { - return e("expected array length %d; got TOML array of length %d", - rv.Len(), sliceLen) - } - return md.unifySliceArray(datav, rv) -} - -func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error { - datav := reflect.ValueOf(data) - if datav.Kind() != reflect.Slice { - if !datav.IsValid() { - return nil - } - return badtype("slice", data) - } - n := datav.Len() - if rv.IsNil() || rv.Cap() < n { - rv.Set(reflect.MakeSlice(rv.Type(), n, n)) - } - rv.SetLen(n) - return md.unifySliceArray(datav, rv) -} - -func (md *MetaData) unifySliceArray(data, rv reflect.Value) error { - sliceLen := data.Len() - for i := 0; i < sliceLen; i++ { - v := data.Index(i).Interface() - sliceval := indirect(rv.Index(i)) - if err := md.unify(v, sliceval); err != nil { - return err - } - } - return nil -} - -func (md *MetaData) unifyDatetime(data interface{}, rv reflect.Value) error { - if _, ok := data.(time.Time); ok { - rv.Set(reflect.ValueOf(data)) - return nil - } - return badtype("time.Time", data) -} - -func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error { - if s, ok := data.(string); ok { - rv.SetString(s) - return nil - } - return badtype("string", data) -} - -func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error { - if num, ok := data.(float64); ok { - switch rv.Kind() { - case reflect.Float32: - fallthrough - case reflect.Float64: - rv.SetFloat(num) - default: - panic("bug") - } - return nil - } - return badtype("float", data) -} - -func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error { - if num, ok := data.(int64); ok { - if rv.Kind() >= reflect.Int && rv.Kind() <= reflect.Int64 { - switch rv.Kind() { - case reflect.Int, reflect.Int64: - // No bounds checking necessary. - case reflect.Int8: - if num < math.MinInt8 || num > math.MaxInt8 { - return e("value %d is out of range for int8", num) - } - case reflect.Int16: - if num < math.MinInt16 || num > math.MaxInt16 { - return e("value %d is out of range for int16", num) - } - case reflect.Int32: - if num < math.MinInt32 || num > math.MaxInt32 { - return e("value %d is out of range for int32", num) - } - } - rv.SetInt(num) - } else if rv.Kind() >= reflect.Uint && rv.Kind() <= reflect.Uint64 { - unum := uint64(num) - switch rv.Kind() { - case reflect.Uint, reflect.Uint64: - // No bounds checking necessary. - case reflect.Uint8: - if num < 0 || unum > math.MaxUint8 { - return e("value %d is out of range for uint8", num) - } - case reflect.Uint16: - if num < 0 || unum > math.MaxUint16 { - return e("value %d is out of range for uint16", num) - } - case reflect.Uint32: - if num < 0 || unum > math.MaxUint32 { - return e("value %d is out of range for uint32", num) - } - } - rv.SetUint(unum) - } else { - panic("unreachable") - } - return nil - } - return badtype("integer", data) -} - -func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error { - if b, ok := data.(bool); ok { - rv.SetBool(b) - return nil - } - return badtype("boolean", data) -} - -func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error { - rv.Set(reflect.ValueOf(data)) - return nil -} - -func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error { - var s string - switch sdata := data.(type) { - case TextMarshaler: - text, err := sdata.MarshalText() - if err != nil { - return err - } - s = string(text) - case fmt.Stringer: - s = sdata.String() - case string: - s = sdata - case bool: - s = fmt.Sprintf("%v", sdata) - case int64: - s = fmt.Sprintf("%d", sdata) - case float64: - s = fmt.Sprintf("%f", sdata) - default: - return badtype("primitive (string-like)", data) - } - if err := v.UnmarshalText([]byte(s)); err != nil { - return err - } - return nil -} - -// rvalue returns a reflect.Value of `v`. All pointers are resolved. -func rvalue(v interface{}) reflect.Value { - return indirect(reflect.ValueOf(v)) -} - -// indirect returns the value pointed to by a pointer. -// Pointers are followed until the value is not a pointer. -// New values are allocated for each nil pointer. -// -// An exception to this rule is if the value satisfies an interface of -// interest to us (like encoding.TextUnmarshaler). -func indirect(v reflect.Value) reflect.Value { - if v.Kind() != reflect.Ptr { - if v.CanSet() { - pv := v.Addr() - if _, ok := pv.Interface().(TextUnmarshaler); ok { - return pv - } - } - return v - } - if v.IsNil() { - v.Set(reflect.New(v.Type().Elem())) - } - return indirect(reflect.Indirect(v)) -} - -func isUnifiable(rv reflect.Value) bool { - if rv.CanSet() { - return true - } - if _, ok := rv.Interface().(TextUnmarshaler); ok { - return true - } - return false -} - -func badtype(expected string, data interface{}) error { - return e("cannot load TOML value of type %T into a Go %s", data, expected) -} diff --git a/vendor/github.com/BurntSushi/toml/decode_meta.go b/vendor/github.com/BurntSushi/toml/decode_meta.go deleted file mode 100644 index b9914a67..00000000 --- a/vendor/github.com/BurntSushi/toml/decode_meta.go +++ /dev/null @@ -1,121 +0,0 @@ -package toml - -import "strings" - -// MetaData allows access to meta information about TOML data that may not -// be inferrable via reflection. In particular, whether a key has been defined -// and the TOML type of a key. -type MetaData struct { - mapping map[string]interface{} - types map[string]tomlType - keys []Key - decoded map[string]bool - context Key // Used only during decoding. -} - -// IsDefined returns true if the key given exists in the TOML data. The key -// should be specified hierarchially. e.g., -// -// // access the TOML key 'a.b.c' -// IsDefined("a", "b", "c") -// -// IsDefined will return false if an empty key given. Keys are case sensitive. -func (md *MetaData) IsDefined(key ...string) bool { - if len(key) == 0 { - return false - } - - var hash map[string]interface{} - var ok bool - var hashOrVal interface{} = md.mapping - for _, k := range key { - if hash, ok = hashOrVal.(map[string]interface{}); !ok { - return false - } - if hashOrVal, ok = hash[k]; !ok { - return false - } - } - return true -} - -// Type returns a string representation of the type of the key specified. -// -// Type will return the empty string if given an empty key or a key that -// does not exist. Keys are case sensitive. -func (md *MetaData) Type(key ...string) string { - fullkey := strings.Join(key, ".") - if typ, ok := md.types[fullkey]; ok { - return typ.typeString() - } - return "" -} - -// Key is the type of any TOML key, including key groups. Use (MetaData).Keys -// to get values of this type. -type Key []string - -func (k Key) String() string { - return strings.Join(k, ".") -} - -func (k Key) maybeQuotedAll() string { - var ss []string - for i := range k { - ss = append(ss, k.maybeQuoted(i)) - } - return strings.Join(ss, ".") -} - -func (k Key) maybeQuoted(i int) string { - quote := false - for _, c := range k[i] { - if !isBareKeyChar(c) { - quote = true - break - } - } - if quote { - return "\"" + strings.Replace(k[i], "\"", "\\\"", -1) + "\"" - } - return k[i] -} - -func (k Key) add(piece string) Key { - newKey := make(Key, len(k)+1) - copy(newKey, k) - newKey[len(k)] = piece - return newKey -} - -// Keys returns a slice of every key in the TOML data, including key groups. -// Each key is itself a slice, where the first element is the top of the -// hierarchy and the last is the most specific. -// -// The list will have the same order as the keys appeared in the TOML data. -// -// All keys returned are non-empty. -func (md *MetaData) Keys() []Key { - return md.keys -} - -// Undecoded returns all keys that have not been decoded in the order in which -// they appear in the original TOML document. -// -// This includes keys that haven't been decoded because of a Primitive value. -// Once the Primitive value is decoded, the keys will be considered decoded. -// -// Also note that decoding into an empty interface will result in no decoding, -// and so no keys will be considered decoded. -// -// In this sense, the Undecoded keys correspond to keys in the TOML document -// that do not have a concrete type in your representation. -func (md *MetaData) Undecoded() []Key { - undecoded := make([]Key, 0, len(md.keys)) - for _, key := range md.keys { - if !md.decoded[key.String()] { - undecoded = append(undecoded, key) - } - } - return undecoded -} diff --git a/vendor/github.com/BurntSushi/toml/doc.go b/vendor/github.com/BurntSushi/toml/doc.go deleted file mode 100644 index b371f396..00000000 --- a/vendor/github.com/BurntSushi/toml/doc.go +++ /dev/null @@ -1,27 +0,0 @@ -/* -Package toml provides facilities for decoding and encoding TOML configuration -files via reflection. There is also support for delaying decoding with -the Primitive type, and querying the set of keys in a TOML document with the -MetaData type. - -The specification implemented: https://github.com/toml-lang/toml - -The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify -whether a file is a valid TOML document. It can also be used to print the -type of each key in a TOML document. - -Testing - -There are two important types of tests used for this package. The first is -contained inside '*_test.go' files and uses the standard Go unit testing -framework. These tests are primarily devoted to holistically testing the -decoder and encoder. - -The second type of testing is used to verify the implementation's adherence -to the TOML specification. These tests have been factored into their own -project: https://github.com/BurntSushi/toml-test - -The reason the tests are in a separate project is so that they can be used by -any implementation of TOML. Namely, it is language agnostic. -*/ -package toml diff --git a/vendor/github.com/BurntSushi/toml/encode.go b/vendor/github.com/BurntSushi/toml/encode.go deleted file mode 100644 index d905c21a..00000000 --- a/vendor/github.com/BurntSushi/toml/encode.go +++ /dev/null @@ -1,568 +0,0 @@ -package toml - -import ( - "bufio" - "errors" - "fmt" - "io" - "reflect" - "sort" - "strconv" - "strings" - "time" -) - -type tomlEncodeError struct{ error } - -var ( - errArrayMixedElementTypes = errors.New( - "toml: cannot encode array with mixed element types") - errArrayNilElement = errors.New( - "toml: cannot encode array with nil element") - errNonString = errors.New( - "toml: cannot encode a map with non-string key type") - errAnonNonStruct = errors.New( - "toml: cannot encode an anonymous field that is not a struct") - errArrayNoTable = errors.New( - "toml: TOML array element cannot contain a table") - errNoKey = errors.New( - "toml: top-level values must be Go maps or structs") - errAnything = errors.New("") // used in testing -) - -var quotedReplacer = strings.NewReplacer( - "\t", "\\t", - "\n", "\\n", - "\r", "\\r", - "\"", "\\\"", - "\\", "\\\\", -) - -// Encoder controls the encoding of Go values to a TOML document to some -// io.Writer. -// -// The indentation level can be controlled with the Indent field. -type Encoder struct { - // A single indentation level. By default it is two spaces. - Indent string - - // hasWritten is whether we have written any output to w yet. - hasWritten bool - w *bufio.Writer -} - -// NewEncoder returns a TOML encoder that encodes Go values to the io.Writer -// given. By default, a single indentation level is 2 spaces. -func NewEncoder(w io.Writer) *Encoder { - return &Encoder{ - w: bufio.NewWriter(w), - Indent: " ", - } -} - -// Encode writes a TOML representation of the Go value to the underlying -// io.Writer. If the value given cannot be encoded to a valid TOML document, -// then an error is returned. -// -// The mapping between Go values and TOML values should be precisely the same -// as for the Decode* functions. Similarly, the TextMarshaler interface is -// supported by encoding the resulting bytes as strings. (If you want to write -// arbitrary binary data then you will need to use something like base64 since -// TOML does not have any binary types.) -// -// When encoding TOML hashes (i.e., Go maps or structs), keys without any -// sub-hashes are encoded first. -// -// If a Go map is encoded, then its keys are sorted alphabetically for -// deterministic output. More control over this behavior may be provided if -// there is demand for it. -// -// Encoding Go values without a corresponding TOML representation---like map -// types with non-string keys---will cause an error to be returned. Similarly -// for mixed arrays/slices, arrays/slices with nil elements, embedded -// non-struct types and nested slices containing maps or structs. -// (e.g., [][]map[string]string is not allowed but []map[string]string is OK -// and so is []map[string][]string.) -func (enc *Encoder) Encode(v interface{}) error { - rv := eindirect(reflect.ValueOf(v)) - if err := enc.safeEncode(Key([]string{}), rv); err != nil { - return err - } - return enc.w.Flush() -} - -func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) { - defer func() { - if r := recover(); r != nil { - if terr, ok := r.(tomlEncodeError); ok { - err = terr.error - return - } - panic(r) - } - }() - enc.encode(key, rv) - return nil -} - -func (enc *Encoder) encode(key Key, rv reflect.Value) { - // Special case. Time needs to be in ISO8601 format. - // Special case. If we can marshal the type to text, then we used that. - // Basically, this prevents the encoder for handling these types as - // generic structs (or whatever the underlying type of a TextMarshaler is). - switch rv.Interface().(type) { - case time.Time, TextMarshaler: - enc.keyEqElement(key, rv) - return - } - - k := rv.Kind() - switch k { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, - reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, - reflect.Uint64, - reflect.Float32, reflect.Float64, reflect.String, reflect.Bool: - enc.keyEqElement(key, rv) - case reflect.Array, reflect.Slice: - if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) { - enc.eArrayOfTables(key, rv) - } else { - enc.keyEqElement(key, rv) - } - case reflect.Interface: - if rv.IsNil() { - return - } - enc.encode(key, rv.Elem()) - case reflect.Map: - if rv.IsNil() { - return - } - enc.eTable(key, rv) - case reflect.Ptr: - if rv.IsNil() { - return - } - enc.encode(key, rv.Elem()) - case reflect.Struct: - enc.eTable(key, rv) - default: - panic(e("unsupported type for key '%s': %s", key, k)) - } -} - -// eElement encodes any value that can be an array element (primitives and -// arrays). -func (enc *Encoder) eElement(rv reflect.Value) { - switch v := rv.Interface().(type) { - case time.Time: - // Special case time.Time as a primitive. Has to come before - // TextMarshaler below because time.Time implements - // encoding.TextMarshaler, but we need to always use UTC. - enc.wf(v.UTC().Format("2006-01-02T15:04:05Z")) - return - case TextMarshaler: - // Special case. Use text marshaler if it's available for this value. - if s, err := v.MarshalText(); err != nil { - encPanic(err) - } else { - enc.writeQuoted(string(s)) - } - return - } - switch rv.Kind() { - case reflect.Bool: - enc.wf(strconv.FormatBool(rv.Bool())) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, - reflect.Int64: - enc.wf(strconv.FormatInt(rv.Int(), 10)) - case reflect.Uint, reflect.Uint8, reflect.Uint16, - reflect.Uint32, reflect.Uint64: - enc.wf(strconv.FormatUint(rv.Uint(), 10)) - case reflect.Float32: - enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 32))) - case reflect.Float64: - enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 64))) - case reflect.Array, reflect.Slice: - enc.eArrayOrSliceElement(rv) - case reflect.Interface: - enc.eElement(rv.Elem()) - case reflect.String: - enc.writeQuoted(rv.String()) - default: - panic(e("unexpected primitive type: %s", rv.Kind())) - } -} - -// By the TOML spec, all floats must have a decimal with at least one -// number on either side. -func floatAddDecimal(fstr string) string { - if !strings.Contains(fstr, ".") { - return fstr + ".0" - } - return fstr -} - -func (enc *Encoder) writeQuoted(s string) { - enc.wf("\"%s\"", quotedReplacer.Replace(s)) -} - -func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) { - length := rv.Len() - enc.wf("[") - for i := 0; i < length; i++ { - elem := rv.Index(i) - enc.eElement(elem) - if i != length-1 { - enc.wf(", ") - } - } - enc.wf("]") -} - -func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) { - if len(key) == 0 { - encPanic(errNoKey) - } - for i := 0; i < rv.Len(); i++ { - trv := rv.Index(i) - if isNil(trv) { - continue - } - panicIfInvalidKey(key) - enc.newline() - enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll()) - enc.newline() - enc.eMapOrStruct(key, trv) - } -} - -func (enc *Encoder) eTable(key Key, rv reflect.Value) { - panicIfInvalidKey(key) - if len(key) == 1 { - // Output an extra newline between top-level tables. - // (The newline isn't written if nothing else has been written though.) - enc.newline() - } - if len(key) > 0 { - enc.wf("%s[%s]", enc.indentStr(key), key.maybeQuotedAll()) - enc.newline() - } - enc.eMapOrStruct(key, rv) -} - -func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value) { - switch rv := eindirect(rv); rv.Kind() { - case reflect.Map: - enc.eMap(key, rv) - case reflect.Struct: - enc.eStruct(key, rv) - default: - panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String()) - } -} - -func (enc *Encoder) eMap(key Key, rv reflect.Value) { - rt := rv.Type() - if rt.Key().Kind() != reflect.String { - encPanic(errNonString) - } - - // Sort keys so that we have deterministic output. And write keys directly - // underneath this key first, before writing sub-structs or sub-maps. - var mapKeysDirect, mapKeysSub []string - for _, mapKey := range rv.MapKeys() { - k := mapKey.String() - if typeIsHash(tomlTypeOfGo(rv.MapIndex(mapKey))) { - mapKeysSub = append(mapKeysSub, k) - } else { - mapKeysDirect = append(mapKeysDirect, k) - } - } - - var writeMapKeys = func(mapKeys []string) { - sort.Strings(mapKeys) - for _, mapKey := range mapKeys { - mrv := rv.MapIndex(reflect.ValueOf(mapKey)) - if isNil(mrv) { - // Don't write anything for nil fields. - continue - } - enc.encode(key.add(mapKey), mrv) - } - } - writeMapKeys(mapKeysDirect) - writeMapKeys(mapKeysSub) -} - -func (enc *Encoder) eStruct(key Key, rv reflect.Value) { - // Write keys for fields directly under this key first, because if we write - // a field that creates a new table, then all keys under it will be in that - // table (not the one we're writing here). - rt := rv.Type() - var fieldsDirect, fieldsSub [][]int - var addFields func(rt reflect.Type, rv reflect.Value, start []int) - addFields = func(rt reflect.Type, rv reflect.Value, start []int) { - for i := 0; i < rt.NumField(); i++ { - f := rt.Field(i) - // skip unexported fields - if f.PkgPath != "" && !f.Anonymous { - continue - } - frv := rv.Field(i) - if f.Anonymous { - t := f.Type - switch t.Kind() { - case reflect.Struct: - // Treat anonymous struct fields with - // tag names as though they are not - // anonymous, like encoding/json does. - if getOptions(f.Tag).name == "" { - addFields(t, frv, f.Index) - continue - } - case reflect.Ptr: - if t.Elem().Kind() == reflect.Struct && - getOptions(f.Tag).name == "" { - if !frv.IsNil() { - addFields(t.Elem(), frv.Elem(), f.Index) - } - continue - } - // Fall through to the normal field encoding logic below - // for non-struct anonymous fields. - } - } - - if typeIsHash(tomlTypeOfGo(frv)) { - fieldsSub = append(fieldsSub, append(start, f.Index...)) - } else { - fieldsDirect = append(fieldsDirect, append(start, f.Index...)) - } - } - } - addFields(rt, rv, nil) - - var writeFields = func(fields [][]int) { - for _, fieldIndex := range fields { - sft := rt.FieldByIndex(fieldIndex) - sf := rv.FieldByIndex(fieldIndex) - if isNil(sf) { - // Don't write anything for nil fields. - continue - } - - opts := getOptions(sft.Tag) - if opts.skip { - continue - } - keyName := sft.Name - if opts.name != "" { - keyName = opts.name - } - if opts.omitempty && isEmpty(sf) { - continue - } - if opts.omitzero && isZero(sf) { - continue - } - - enc.encode(key.add(keyName), sf) - } - } - writeFields(fieldsDirect) - writeFields(fieldsSub) -} - -// tomlTypeName returns the TOML type name of the Go value's type. It is -// used to determine whether the types of array elements are mixed (which is -// forbidden). If the Go value is nil, then it is illegal for it to be an array -// element, and valueIsNil is returned as true. - -// Returns the TOML type of a Go value. The type may be `nil`, which means -// no concrete TOML type could be found. -func tomlTypeOfGo(rv reflect.Value) tomlType { - if isNil(rv) || !rv.IsValid() { - return nil - } - switch rv.Kind() { - case reflect.Bool: - return tomlBool - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, - reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, - reflect.Uint64: - return tomlInteger - case reflect.Float32, reflect.Float64: - return tomlFloat - case reflect.Array, reflect.Slice: - if typeEqual(tomlHash, tomlArrayType(rv)) { - return tomlArrayHash - } - return tomlArray - case reflect.Ptr, reflect.Interface: - return tomlTypeOfGo(rv.Elem()) - case reflect.String: - return tomlString - case reflect.Map: - return tomlHash - case reflect.Struct: - switch rv.Interface().(type) { - case time.Time: - return tomlDatetime - case TextMarshaler: - return tomlString - default: - return tomlHash - } - default: - panic("unexpected reflect.Kind: " + rv.Kind().String()) - } -} - -// tomlArrayType returns the element type of a TOML array. The type returned -// may be nil if it cannot be determined (e.g., a nil slice or a zero length -// slize). This function may also panic if it finds a type that cannot be -// expressed in TOML (such as nil elements, heterogeneous arrays or directly -// nested arrays of tables). -func tomlArrayType(rv reflect.Value) tomlType { - if isNil(rv) || !rv.IsValid() || rv.Len() == 0 { - return nil - } - firstType := tomlTypeOfGo(rv.Index(0)) - if firstType == nil { - encPanic(errArrayNilElement) - } - - rvlen := rv.Len() - for i := 1; i < rvlen; i++ { - elem := rv.Index(i) - switch elemType := tomlTypeOfGo(elem); { - case elemType == nil: - encPanic(errArrayNilElement) - case !typeEqual(firstType, elemType): - encPanic(errArrayMixedElementTypes) - } - } - // If we have a nested array, then we must make sure that the nested - // array contains ONLY primitives. - // This checks arbitrarily nested arrays. - if typeEqual(firstType, tomlArray) || typeEqual(firstType, tomlArrayHash) { - nest := tomlArrayType(eindirect(rv.Index(0))) - if typeEqual(nest, tomlHash) || typeEqual(nest, tomlArrayHash) { - encPanic(errArrayNoTable) - } - } - return firstType -} - -type tagOptions struct { - skip bool // "-" - name string - omitempty bool - omitzero bool -} - -func getOptions(tag reflect.StructTag) tagOptions { - t := tag.Get("toml") - if t == "-" { - return tagOptions{skip: true} - } - var opts tagOptions - parts := strings.Split(t, ",") - opts.name = parts[0] - for _, s := range parts[1:] { - switch s { - case "omitempty": - opts.omitempty = true - case "omitzero": - opts.omitzero = true - } - } - return opts -} - -func isZero(rv reflect.Value) bool { - switch rv.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return rv.Int() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return rv.Uint() == 0 - case reflect.Float32, reflect.Float64: - return rv.Float() == 0.0 - } - return false -} - -func isEmpty(rv reflect.Value) bool { - switch rv.Kind() { - case reflect.Array, reflect.Slice, reflect.Map, reflect.String: - return rv.Len() == 0 - case reflect.Bool: - return !rv.Bool() - } - return false -} - -func (enc *Encoder) newline() { - if enc.hasWritten { - enc.wf("\n") - } -} - -func (enc *Encoder) keyEqElement(key Key, val reflect.Value) { - if len(key) == 0 { - encPanic(errNoKey) - } - panicIfInvalidKey(key) - enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1)) - enc.eElement(val) - enc.newline() -} - -func (enc *Encoder) wf(format string, v ...interface{}) { - if _, err := fmt.Fprintf(enc.w, format, v...); err != nil { - encPanic(err) - } - enc.hasWritten = true -} - -func (enc *Encoder) indentStr(key Key) string { - return strings.Repeat(enc.Indent, len(key)-1) -} - -func encPanic(err error) { - panic(tomlEncodeError{err}) -} - -func eindirect(v reflect.Value) reflect.Value { - switch v.Kind() { - case reflect.Ptr, reflect.Interface: - return eindirect(v.Elem()) - default: - return v - } -} - -func isNil(rv reflect.Value) bool { - switch rv.Kind() { - case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: - return rv.IsNil() - default: - return false - } -} - -func panicIfInvalidKey(key Key) { - for _, k := range key { - if len(k) == 0 { - encPanic(e("Key '%s' is not a valid table name. Key names "+ - "cannot be empty.", key.maybeQuotedAll())) - } - } -} - -func isValidKeyName(s string) bool { - return len(s) != 0 -} diff --git a/vendor/github.com/BurntSushi/toml/encoding_types.go b/vendor/github.com/BurntSushi/toml/encoding_types.go deleted file mode 100644 index d36e1dd6..00000000 --- a/vendor/github.com/BurntSushi/toml/encoding_types.go +++ /dev/null @@ -1,19 +0,0 @@ -// +build go1.2 - -package toml - -// In order to support Go 1.1, we define our own TextMarshaler and -// TextUnmarshaler types. For Go 1.2+, we just alias them with the -// standard library interfaces. - -import ( - "encoding" -) - -// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here -// so that Go 1.1 can be supported. -type TextMarshaler encoding.TextMarshaler - -// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined -// here so that Go 1.1 can be supported. -type TextUnmarshaler encoding.TextUnmarshaler diff --git a/vendor/github.com/BurntSushi/toml/encoding_types_1.1.go b/vendor/github.com/BurntSushi/toml/encoding_types_1.1.go deleted file mode 100644 index e8d503d0..00000000 --- a/vendor/github.com/BurntSushi/toml/encoding_types_1.1.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build !go1.2 - -package toml - -// These interfaces were introduced in Go 1.2, so we add them manually when -// compiling for Go 1.1. - -// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here -// so that Go 1.1 can be supported. -type TextMarshaler interface { - MarshalText() (text []byte, err error) -} - -// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined -// here so that Go 1.1 can be supported. -type TextUnmarshaler interface { - UnmarshalText(text []byte) error -} diff --git a/vendor/github.com/BurntSushi/toml/lex.go b/vendor/github.com/BurntSushi/toml/lex.go deleted file mode 100644 index f1f4b2de..00000000 --- a/vendor/github.com/BurntSushi/toml/lex.go +++ /dev/null @@ -1,953 +0,0 @@ -package toml - -import ( - "fmt" - "strings" - "unicode" - "unicode/utf8" -) - -type itemType int - -const ( - itemError itemType = iota - itemNIL // used in the parser to indicate no type - itemEOF - itemText - itemString - itemRawString - itemMultilineString - itemRawMultilineString - itemBool - itemInteger - itemFloat - itemDatetime - itemArray // the start of an array - itemArrayEnd - itemTableStart - itemTableEnd - itemArrayTableStart - itemArrayTableEnd - itemKeyStart - itemCommentStart - itemInlineTableStart - itemInlineTableEnd -) - -const ( - eof = 0 - comma = ',' - tableStart = '[' - tableEnd = ']' - arrayTableStart = '[' - arrayTableEnd = ']' - tableSep = '.' - keySep = '=' - arrayStart = '[' - arrayEnd = ']' - commentStart = '#' - stringStart = '"' - stringEnd = '"' - rawStringStart = '\'' - rawStringEnd = '\'' - inlineTableStart = '{' - inlineTableEnd = '}' -) - -type stateFn func(lx *lexer) stateFn - -type lexer struct { - input string - start int - pos int - line int - state stateFn - items chan item - - // Allow for backing up up to three runes. - // This is necessary because TOML contains 3-rune tokens (""" and '''). - prevWidths [3]int - nprev int // how many of prevWidths are in use - // If we emit an eof, we can still back up, but it is not OK to call - // next again. - atEOF bool - - // A stack of state functions used to maintain context. - // The idea is to reuse parts of the state machine in various places. - // For example, values can appear at the top level or within arbitrarily - // nested arrays. The last state on the stack is used after a value has - // been lexed. Similarly for comments. - stack []stateFn -} - -type item struct { - typ itemType - val string - line int -} - -func (lx *lexer) nextItem() item { - for { - select { - case item := <-lx.items: - return item - default: - lx.state = lx.state(lx) - } - } -} - -func lex(input string) *lexer { - lx := &lexer{ - input: input, - state: lexTop, - line: 1, - items: make(chan item, 10), - stack: make([]stateFn, 0, 10), - } - return lx -} - -func (lx *lexer) push(state stateFn) { - lx.stack = append(lx.stack, state) -} - -func (lx *lexer) pop() stateFn { - if len(lx.stack) == 0 { - return lx.errorf("BUG in lexer: no states to pop") - } - last := lx.stack[len(lx.stack)-1] - lx.stack = lx.stack[0 : len(lx.stack)-1] - return last -} - -func (lx *lexer) current() string { - return lx.input[lx.start:lx.pos] -} - -func (lx *lexer) emit(typ itemType) { - lx.items <- item{typ, lx.current(), lx.line} - lx.start = lx.pos -} - -func (lx *lexer) emitTrim(typ itemType) { - lx.items <- item{typ, strings.TrimSpace(lx.current()), lx.line} - lx.start = lx.pos -} - -func (lx *lexer) next() (r rune) { - if lx.atEOF { - panic("next called after EOF") - } - if lx.pos >= len(lx.input) { - lx.atEOF = true - return eof - } - - if lx.input[lx.pos] == '\n' { - lx.line++ - } - lx.prevWidths[2] = lx.prevWidths[1] - lx.prevWidths[1] = lx.prevWidths[0] - if lx.nprev < 3 { - lx.nprev++ - } - r, w := utf8.DecodeRuneInString(lx.input[lx.pos:]) - lx.prevWidths[0] = w - lx.pos += w - return r -} - -// ignore skips over the pending input before this point. -func (lx *lexer) ignore() { - lx.start = lx.pos -} - -// backup steps back one rune. Can be called only twice between calls to next. -func (lx *lexer) backup() { - if lx.atEOF { - lx.atEOF = false - return - } - if lx.nprev < 1 { - panic("backed up too far") - } - w := lx.prevWidths[0] - lx.prevWidths[0] = lx.prevWidths[1] - lx.prevWidths[1] = lx.prevWidths[2] - lx.nprev-- - lx.pos -= w - if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' { - lx.line-- - } -} - -// accept consumes the next rune if it's equal to `valid`. -func (lx *lexer) accept(valid rune) bool { - if lx.next() == valid { - return true - } - lx.backup() - return false -} - -// peek returns but does not consume the next rune in the input. -func (lx *lexer) peek() rune { - r := lx.next() - lx.backup() - return r -} - -// skip ignores all input that matches the given predicate. -func (lx *lexer) skip(pred func(rune) bool) { - for { - r := lx.next() - if pred(r) { - continue - } - lx.backup() - lx.ignore() - return - } -} - -// errorf stops all lexing by emitting an error and returning `nil`. -// Note that any value that is a character is escaped if it's a special -// character (newlines, tabs, etc.). -func (lx *lexer) errorf(format string, values ...interface{}) stateFn { - lx.items <- item{ - itemError, - fmt.Sprintf(format, values...), - lx.line, - } - return nil -} - -// lexTop consumes elements at the top level of TOML data. -func lexTop(lx *lexer) stateFn { - r := lx.next() - if isWhitespace(r) || isNL(r) { - return lexSkip(lx, lexTop) - } - switch r { - case commentStart: - lx.push(lexTop) - return lexCommentStart - case tableStart: - return lexTableStart - case eof: - if lx.pos > lx.start { - return lx.errorf("unexpected EOF") - } - lx.emit(itemEOF) - return nil - } - - // At this point, the only valid item can be a key, so we back up - // and let the key lexer do the rest. - lx.backup() - lx.push(lexTopEnd) - return lexKeyStart -} - -// lexTopEnd is entered whenever a top-level item has been consumed. (A value -// or a table.) It must see only whitespace, and will turn back to lexTop -// upon a newline. If it sees EOF, it will quit the lexer successfully. -func lexTopEnd(lx *lexer) stateFn { - r := lx.next() - switch { - case r == commentStart: - // a comment will read to a newline for us. - lx.push(lexTop) - return lexCommentStart - case isWhitespace(r): - return lexTopEnd - case isNL(r): - lx.ignore() - return lexTop - case r == eof: - lx.emit(itemEOF) - return nil - } - return lx.errorf("expected a top-level item to end with a newline, "+ - "comment, or EOF, but got %q instead", r) -} - -// lexTable lexes the beginning of a table. Namely, it makes sure that -// it starts with a character other than '.' and ']'. -// It assumes that '[' has already been consumed. -// It also handles the case that this is an item in an array of tables. -// e.g., '[[name]]'. -func lexTableStart(lx *lexer) stateFn { - if lx.peek() == arrayTableStart { - lx.next() - lx.emit(itemArrayTableStart) - lx.push(lexArrayTableEnd) - } else { - lx.emit(itemTableStart) - lx.push(lexTableEnd) - } - return lexTableNameStart -} - -func lexTableEnd(lx *lexer) stateFn { - lx.emit(itemTableEnd) - return lexTopEnd -} - -func lexArrayTableEnd(lx *lexer) stateFn { - if r := lx.next(); r != arrayTableEnd { - return lx.errorf("expected end of table array name delimiter %q, "+ - "but got %q instead", arrayTableEnd, r) - } - lx.emit(itemArrayTableEnd) - return lexTopEnd -} - -func lexTableNameStart(lx *lexer) stateFn { - lx.skip(isWhitespace) - switch r := lx.peek(); { - case r == tableEnd || r == eof: - return lx.errorf("unexpected end of table name " + - "(table names cannot be empty)") - case r == tableSep: - return lx.errorf("unexpected table separator " + - "(table names cannot be empty)") - case r == stringStart || r == rawStringStart: - lx.ignore() - lx.push(lexTableNameEnd) - return lexValue // reuse string lexing - default: - return lexBareTableName - } -} - -// lexBareTableName lexes the name of a table. It assumes that at least one -// valid character for the table has already been read. -func lexBareTableName(lx *lexer) stateFn { - r := lx.next() - if isBareKeyChar(r) { - return lexBareTableName - } - lx.backup() - lx.emit(itemText) - return lexTableNameEnd -} - -// lexTableNameEnd reads the end of a piece of a table name, optionally -// consuming whitespace. -func lexTableNameEnd(lx *lexer) stateFn { - lx.skip(isWhitespace) - switch r := lx.next(); { - case isWhitespace(r): - return lexTableNameEnd - case r == tableSep: - lx.ignore() - return lexTableNameStart - case r == tableEnd: - return lx.pop() - default: - return lx.errorf("expected '.' or ']' to end table name, "+ - "but got %q instead", r) - } -} - -// lexKeyStart consumes a key name up until the first non-whitespace character. -// lexKeyStart will ignore whitespace. -func lexKeyStart(lx *lexer) stateFn { - r := lx.peek() - switch { - case r == keySep: - return lx.errorf("unexpected key separator %q", keySep) - case isWhitespace(r) || isNL(r): - lx.next() - return lexSkip(lx, lexKeyStart) - case r == stringStart || r == rawStringStart: - lx.ignore() - lx.emit(itemKeyStart) - lx.push(lexKeyEnd) - return lexValue // reuse string lexing - default: - lx.ignore() - lx.emit(itemKeyStart) - return lexBareKey - } -} - -// lexBareKey consumes the text of a bare key. Assumes that the first character -// (which is not whitespace) has not yet been consumed. -func lexBareKey(lx *lexer) stateFn { - switch r := lx.next(); { - case isBareKeyChar(r): - return lexBareKey - case isWhitespace(r): - lx.backup() - lx.emit(itemText) - return lexKeyEnd - case r == keySep: - lx.backup() - lx.emit(itemText) - return lexKeyEnd - default: - return lx.errorf("bare keys cannot contain %q", r) - } -} - -// lexKeyEnd consumes the end of a key and trims whitespace (up to the key -// separator). -func lexKeyEnd(lx *lexer) stateFn { - switch r := lx.next(); { - case r == keySep: - return lexSkip(lx, lexValue) - case isWhitespace(r): - return lexSkip(lx, lexKeyEnd) - default: - return lx.errorf("expected key separator %q, but got %q instead", - keySep, r) - } -} - -// lexValue starts the consumption of a value anywhere a value is expected. -// lexValue will ignore whitespace. -// After a value is lexed, the last state on the next is popped and returned. -func lexValue(lx *lexer) stateFn { - // We allow whitespace to precede a value, but NOT newlines. - // In array syntax, the array states are responsible for ignoring newlines. - r := lx.next() - switch { - case isWhitespace(r): - return lexSkip(lx, lexValue) - case isDigit(r): - lx.backup() // avoid an extra state and use the same as above - return lexNumberOrDateStart - } - switch r { - case arrayStart: - lx.ignore() - lx.emit(itemArray) - return lexArrayValue - case inlineTableStart: - lx.ignore() - lx.emit(itemInlineTableStart) - return lexInlineTableValue - case stringStart: - if lx.accept(stringStart) { - if lx.accept(stringStart) { - lx.ignore() // Ignore """ - return lexMultilineString - } - lx.backup() - } - lx.ignore() // ignore the '"' - return lexString - case rawStringStart: - if lx.accept(rawStringStart) { - if lx.accept(rawStringStart) { - lx.ignore() // Ignore """ - return lexMultilineRawString - } - lx.backup() - } - lx.ignore() // ignore the "'" - return lexRawString - case '+', '-': - return lexNumberStart - case '.': // special error case, be kind to users - return lx.errorf("floats must start with a digit, not '.'") - } - if unicode.IsLetter(r) { - // Be permissive here; lexBool will give a nice error if the - // user wrote something like - // x = foo - // (i.e. not 'true' or 'false' but is something else word-like.) - lx.backup() - return lexBool - } - return lx.errorf("expected value but found %q instead", r) -} - -// lexArrayValue consumes one value in an array. It assumes that '[' or ',' -// have already been consumed. All whitespace and newlines are ignored. -func lexArrayValue(lx *lexer) stateFn { - r := lx.next() - switch { - case isWhitespace(r) || isNL(r): - return lexSkip(lx, lexArrayValue) - case r == commentStart: - lx.push(lexArrayValue) - return lexCommentStart - case r == comma: - return lx.errorf("unexpected comma") - case r == arrayEnd: - // NOTE(caleb): The spec isn't clear about whether you can have - // a trailing comma or not, so we'll allow it. - return lexArrayEnd - } - - lx.backup() - lx.push(lexArrayValueEnd) - return lexValue -} - -// lexArrayValueEnd consumes everything between the end of an array value and -// the next value (or the end of the array): it ignores whitespace and newlines -// and expects either a ',' or a ']'. -func lexArrayValueEnd(lx *lexer) stateFn { - r := lx.next() - switch { - case isWhitespace(r) || isNL(r): - return lexSkip(lx, lexArrayValueEnd) - case r == commentStart: - lx.push(lexArrayValueEnd) - return lexCommentStart - case r == comma: - lx.ignore() - return lexArrayValue // move on to the next value - case r == arrayEnd: - return lexArrayEnd - } - return lx.errorf( - "expected a comma or array terminator %q, but got %q instead", - arrayEnd, r, - ) -} - -// lexArrayEnd finishes the lexing of an array. -// It assumes that a ']' has just been consumed. -func lexArrayEnd(lx *lexer) stateFn { - lx.ignore() - lx.emit(itemArrayEnd) - return lx.pop() -} - -// lexInlineTableValue consumes one key/value pair in an inline table. -// It assumes that '{' or ',' have already been consumed. Whitespace is ignored. -func lexInlineTableValue(lx *lexer) stateFn { - r := lx.next() - switch { - case isWhitespace(r): - return lexSkip(lx, lexInlineTableValue) - case isNL(r): - return lx.errorf("newlines not allowed within inline tables") - case r == commentStart: - lx.push(lexInlineTableValue) - return lexCommentStart - case r == comma: - return lx.errorf("unexpected comma") - case r == inlineTableEnd: - return lexInlineTableEnd - } - lx.backup() - lx.push(lexInlineTableValueEnd) - return lexKeyStart -} - -// lexInlineTableValueEnd consumes everything between the end of an inline table -// key/value pair and the next pair (or the end of the table): -// it ignores whitespace and expects either a ',' or a '}'. -func lexInlineTableValueEnd(lx *lexer) stateFn { - r := lx.next() - switch { - case isWhitespace(r): - return lexSkip(lx, lexInlineTableValueEnd) - case isNL(r): - return lx.errorf("newlines not allowed within inline tables") - case r == commentStart: - lx.push(lexInlineTableValueEnd) - return lexCommentStart - case r == comma: - lx.ignore() - return lexInlineTableValue - case r == inlineTableEnd: - return lexInlineTableEnd - } - return lx.errorf("expected a comma or an inline table terminator %q, "+ - "but got %q instead", inlineTableEnd, r) -} - -// lexInlineTableEnd finishes the lexing of an inline table. -// It assumes that a '}' has just been consumed. -func lexInlineTableEnd(lx *lexer) stateFn { - lx.ignore() - lx.emit(itemInlineTableEnd) - return lx.pop() -} - -// lexString consumes the inner contents of a string. It assumes that the -// beginning '"' has already been consumed and ignored. -func lexString(lx *lexer) stateFn { - r := lx.next() - switch { - case r == eof: - return lx.errorf("unexpected EOF") - case isNL(r): - return lx.errorf("strings cannot contain newlines") - case r == '\\': - lx.push(lexString) - return lexStringEscape - case r == stringEnd: - lx.backup() - lx.emit(itemString) - lx.next() - lx.ignore() - return lx.pop() - } - return lexString -} - -// lexMultilineString consumes the inner contents of a string. It assumes that -// the beginning '"""' has already been consumed and ignored. -func lexMultilineString(lx *lexer) stateFn { - switch lx.next() { - case eof: - return lx.errorf("unexpected EOF") - case '\\': - return lexMultilineStringEscape - case stringEnd: - if lx.accept(stringEnd) { - if lx.accept(stringEnd) { - lx.backup() - lx.backup() - lx.backup() - lx.emit(itemMultilineString) - lx.next() - lx.next() - lx.next() - lx.ignore() - return lx.pop() - } - lx.backup() - } - } - return lexMultilineString -} - -// lexRawString consumes a raw string. Nothing can be escaped in such a string. -// It assumes that the beginning "'" has already been consumed and ignored. -func lexRawString(lx *lexer) stateFn { - r := lx.next() - switch { - case r == eof: - return lx.errorf("unexpected EOF") - case isNL(r): - return lx.errorf("strings cannot contain newlines") - case r == rawStringEnd: - lx.backup() - lx.emit(itemRawString) - lx.next() - lx.ignore() - return lx.pop() - } - return lexRawString -} - -// lexMultilineRawString consumes a raw string. Nothing can be escaped in such -// a string. It assumes that the beginning "'''" has already been consumed and -// ignored. -func lexMultilineRawString(lx *lexer) stateFn { - switch lx.next() { - case eof: - return lx.errorf("unexpected EOF") - case rawStringEnd: - if lx.accept(rawStringEnd) { - if lx.accept(rawStringEnd) { - lx.backup() - lx.backup() - lx.backup() - lx.emit(itemRawMultilineString) - lx.next() - lx.next() - lx.next() - lx.ignore() - return lx.pop() - } - lx.backup() - } - } - return lexMultilineRawString -} - -// lexMultilineStringEscape consumes an escaped character. It assumes that the -// preceding '\\' has already been consumed. -func lexMultilineStringEscape(lx *lexer) stateFn { - // Handle the special case first: - if isNL(lx.next()) { - return lexMultilineString - } - lx.backup() - lx.push(lexMultilineString) - return lexStringEscape(lx) -} - -func lexStringEscape(lx *lexer) stateFn { - r := lx.next() - switch r { - case 'b': - fallthrough - case 't': - fallthrough - case 'n': - fallthrough - case 'f': - fallthrough - case 'r': - fallthrough - case '"': - fallthrough - case '\\': - return lx.pop() - case 'u': - return lexShortUnicodeEscape - case 'U': - return lexLongUnicodeEscape - } - return lx.errorf("invalid escape character %q; only the following "+ - "escape characters are allowed: "+ - `\b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX`, r) -} - -func lexShortUnicodeEscape(lx *lexer) stateFn { - var r rune - for i := 0; i < 4; i++ { - r = lx.next() - if !isHexadecimal(r) { - return lx.errorf(`expected four hexadecimal digits after '\u', `+ - "but got %q instead", lx.current()) - } - } - return lx.pop() -} - -func lexLongUnicodeEscape(lx *lexer) stateFn { - var r rune - for i := 0; i < 8; i++ { - r = lx.next() - if !isHexadecimal(r) { - return lx.errorf(`expected eight hexadecimal digits after '\U', `+ - "but got %q instead", lx.current()) - } - } - return lx.pop() -} - -// lexNumberOrDateStart consumes either an integer, a float, or datetime. -func lexNumberOrDateStart(lx *lexer) stateFn { - r := lx.next() - if isDigit(r) { - return lexNumberOrDate - } - switch r { - case '_': - return lexNumber - case 'e', 'E': - return lexFloat - case '.': - return lx.errorf("floats must start with a digit, not '.'") - } - return lx.errorf("expected a digit but got %q", r) -} - -// lexNumberOrDate consumes either an integer, float or datetime. -func lexNumberOrDate(lx *lexer) stateFn { - r := lx.next() - if isDigit(r) { - return lexNumberOrDate - } - switch r { - case '-': - return lexDatetime - case '_': - return lexNumber - case '.', 'e', 'E': - return lexFloat - } - - lx.backup() - lx.emit(itemInteger) - return lx.pop() -} - -// lexDatetime consumes a Datetime, to a first approximation. -// The parser validates that it matches one of the accepted formats. -func lexDatetime(lx *lexer) stateFn { - r := lx.next() - if isDigit(r) { - return lexDatetime - } - switch r { - case '-', 'T', ':', '.', 'Z': - return lexDatetime - } - - lx.backup() - lx.emit(itemDatetime) - return lx.pop() -} - -// lexNumberStart consumes either an integer or a float. It assumes that a sign -// has already been read, but that *no* digits have been consumed. -// lexNumberStart will move to the appropriate integer or float states. -func lexNumberStart(lx *lexer) stateFn { - // We MUST see a digit. Even floats have to start with a digit. - r := lx.next() - if !isDigit(r) { - if r == '.' { - return lx.errorf("floats must start with a digit, not '.'") - } - return lx.errorf("expected a digit but got %q", r) - } - return lexNumber -} - -// lexNumber consumes an integer or a float after seeing the first digit. -func lexNumber(lx *lexer) stateFn { - r := lx.next() - if isDigit(r) { - return lexNumber - } - switch r { - case '_': - return lexNumber - case '.', 'e', 'E': - return lexFloat - } - - lx.backup() - lx.emit(itemInteger) - return lx.pop() -} - -// lexFloat consumes the elements of a float. It allows any sequence of -// float-like characters, so floats emitted by the lexer are only a first -// approximation and must be validated by the parser. -func lexFloat(lx *lexer) stateFn { - r := lx.next() - if isDigit(r) { - return lexFloat - } - switch r { - case '_', '.', '-', '+', 'e', 'E': - return lexFloat - } - - lx.backup() - lx.emit(itemFloat) - return lx.pop() -} - -// lexBool consumes a bool string: 'true' or 'false. -func lexBool(lx *lexer) stateFn { - var rs []rune - for { - r := lx.next() - if r == eof || isWhitespace(r) || isNL(r) { - lx.backup() - break - } - rs = append(rs, r) - } - s := string(rs) - switch s { - case "true", "false": - lx.emit(itemBool) - return lx.pop() - } - return lx.errorf("expected value but found %q instead", s) -} - -// lexCommentStart begins the lexing of a comment. It will emit -// itemCommentStart and consume no characters, passing control to lexComment. -func lexCommentStart(lx *lexer) stateFn { - lx.ignore() - lx.emit(itemCommentStart) - return lexComment -} - -// lexComment lexes an entire comment. It assumes that '#' has been consumed. -// It will consume *up to* the first newline character, and pass control -// back to the last state on the stack. -func lexComment(lx *lexer) stateFn { - r := lx.peek() - if isNL(r) || r == eof { - lx.emit(itemText) - return lx.pop() - } - lx.next() - return lexComment -} - -// lexSkip ignores all slurped input and moves on to the next state. -func lexSkip(lx *lexer, nextState stateFn) stateFn { - return func(lx *lexer) stateFn { - lx.ignore() - return nextState - } -} - -// isWhitespace returns true if `r` is a whitespace character according -// to the spec. -func isWhitespace(r rune) bool { - return r == '\t' || r == ' ' -} - -func isNL(r rune) bool { - return r == '\n' || r == '\r' -} - -func isDigit(r rune) bool { - return r >= '0' && r <= '9' -} - -func isHexadecimal(r rune) bool { - return (r >= '0' && r <= '9') || - (r >= 'a' && r <= 'f') || - (r >= 'A' && r <= 'F') -} - -func isBareKeyChar(r rune) bool { - return (r >= 'A' && r <= 'Z') || - (r >= 'a' && r <= 'z') || - (r >= '0' && r <= '9') || - r == '_' || - r == '-' -} - -func (itype itemType) String() string { - switch itype { - case itemError: - return "Error" - case itemNIL: - return "NIL" - case itemEOF: - return "EOF" - case itemText: - return "Text" - case itemString, itemRawString, itemMultilineString, itemRawMultilineString: - return "String" - case itemBool: - return "Bool" - case itemInteger: - return "Integer" - case itemFloat: - return "Float" - case itemDatetime: - return "DateTime" - case itemTableStart: - return "TableStart" - case itemTableEnd: - return "TableEnd" - case itemKeyStart: - return "KeyStart" - case itemArray: - return "Array" - case itemArrayEnd: - return "ArrayEnd" - case itemCommentStart: - return "CommentStart" - } - panic(fmt.Sprintf("BUG: Unknown type '%d'.", int(itype))) -} - -func (item item) String() string { - return fmt.Sprintf("(%s, %s)", item.typ.String(), item.val) -} diff --git a/vendor/github.com/BurntSushi/toml/parse.go b/vendor/github.com/BurntSushi/toml/parse.go deleted file mode 100644 index 50869ef9..00000000 --- a/vendor/github.com/BurntSushi/toml/parse.go +++ /dev/null @@ -1,592 +0,0 @@ -package toml - -import ( - "fmt" - "strconv" - "strings" - "time" - "unicode" - "unicode/utf8" -) - -type parser struct { - mapping map[string]interface{} - types map[string]tomlType - lx *lexer - - // A list of keys in the order that they appear in the TOML data. - ordered []Key - - // the full key for the current hash in scope - context Key - - // the base key name for everything except hashes - currentKey string - - // rough approximation of line number - approxLine int - - // A map of 'key.group.names' to whether they were created implicitly. - implicits map[string]bool -} - -type parseError string - -func (pe parseError) Error() string { - return string(pe) -} - -func parse(data string) (p *parser, err error) { - defer func() { - if r := recover(); r != nil { - var ok bool - if err, ok = r.(parseError); ok { - return - } - panic(r) - } - }() - - p = &parser{ - mapping: make(map[string]interface{}), - types: make(map[string]tomlType), - lx: lex(data), - ordered: make([]Key, 0), - implicits: make(map[string]bool), - } - for { - item := p.next() - if item.typ == itemEOF { - break - } - p.topLevel(item) - } - - return p, nil -} - -func (p *parser) panicf(format string, v ...interface{}) { - msg := fmt.Sprintf("Near line %d (last key parsed '%s'): %s", - p.approxLine, p.current(), fmt.Sprintf(format, v...)) - panic(parseError(msg)) -} - -func (p *parser) next() item { - it := p.lx.nextItem() - if it.typ == itemError { - p.panicf("%s", it.val) - } - return it -} - -func (p *parser) bug(format string, v ...interface{}) { - panic(fmt.Sprintf("BUG: "+format+"\n\n", v...)) -} - -func (p *parser) expect(typ itemType) item { - it := p.next() - p.assertEqual(typ, it.typ) - return it -} - -func (p *parser) assertEqual(expected, got itemType) { - if expected != got { - p.bug("Expected '%s' but got '%s'.", expected, got) - } -} - -func (p *parser) topLevel(item item) { - switch item.typ { - case itemCommentStart: - p.approxLine = item.line - p.expect(itemText) - case itemTableStart: - kg := p.next() - p.approxLine = kg.line - - var key Key - for ; kg.typ != itemTableEnd && kg.typ != itemEOF; kg = p.next() { - key = append(key, p.keyString(kg)) - } - p.assertEqual(itemTableEnd, kg.typ) - - p.establishContext(key, false) - p.setType("", tomlHash) - p.ordered = append(p.ordered, key) - case itemArrayTableStart: - kg := p.next() - p.approxLine = kg.line - - var key Key - for ; kg.typ != itemArrayTableEnd && kg.typ != itemEOF; kg = p.next() { - key = append(key, p.keyString(kg)) - } - p.assertEqual(itemArrayTableEnd, kg.typ) - - p.establishContext(key, true) - p.setType("", tomlArrayHash) - p.ordered = append(p.ordered, key) - case itemKeyStart: - kname := p.next() - p.approxLine = kname.line - p.currentKey = p.keyString(kname) - - val, typ := p.value(p.next()) - p.setValue(p.currentKey, val) - p.setType(p.currentKey, typ) - p.ordered = append(p.ordered, p.context.add(p.currentKey)) - p.currentKey = "" - default: - p.bug("Unexpected type at top level: %s", item.typ) - } -} - -// Gets a string for a key (or part of a key in a table name). -func (p *parser) keyString(it item) string { - switch it.typ { - case itemText: - return it.val - case itemString, itemMultilineString, - itemRawString, itemRawMultilineString: - s, _ := p.value(it) - return s.(string) - default: - p.bug("Unexpected key type: %s", it.typ) - panic("unreachable") - } -} - -// value translates an expected value from the lexer into a Go value wrapped -// as an empty interface. -func (p *parser) value(it item) (interface{}, tomlType) { - switch it.typ { - case itemString: - return p.replaceEscapes(it.val), p.typeOfPrimitive(it) - case itemMultilineString: - trimmed := stripFirstNewline(stripEscapedWhitespace(it.val)) - return p.replaceEscapes(trimmed), p.typeOfPrimitive(it) - case itemRawString: - return it.val, p.typeOfPrimitive(it) - case itemRawMultilineString: - return stripFirstNewline(it.val), p.typeOfPrimitive(it) - case itemBool: - switch it.val { - case "true": - return true, p.typeOfPrimitive(it) - case "false": - return false, p.typeOfPrimitive(it) - } - p.bug("Expected boolean value, but got '%s'.", it.val) - case itemInteger: - if !numUnderscoresOK(it.val) { - p.panicf("Invalid integer %q: underscores must be surrounded by digits", - it.val) - } - val := strings.Replace(it.val, "_", "", -1) - num, err := strconv.ParseInt(val, 10, 64) - if err != nil { - // Distinguish integer values. Normally, it'd be a bug if the lexer - // provides an invalid integer, but it's possible that the number is - // out of range of valid values (which the lexer cannot determine). - // So mark the former as a bug but the latter as a legitimate user - // error. - if e, ok := err.(*strconv.NumError); ok && - e.Err == strconv.ErrRange { - - p.panicf("Integer '%s' is out of the range of 64-bit "+ - "signed integers.", it.val) - } else { - p.bug("Expected integer value, but got '%s'.", it.val) - } - } - return num, p.typeOfPrimitive(it) - case itemFloat: - parts := strings.FieldsFunc(it.val, func(r rune) bool { - switch r { - case '.', 'e', 'E': - return true - } - return false - }) - for _, part := range parts { - if !numUnderscoresOK(part) { - p.panicf("Invalid float %q: underscores must be "+ - "surrounded by digits", it.val) - } - } - if !numPeriodsOK(it.val) { - // As a special case, numbers like '123.' or '1.e2', - // which are valid as far as Go/strconv are concerned, - // must be rejected because TOML says that a fractional - // part consists of '.' followed by 1+ digits. - p.panicf("Invalid float %q: '.' must be followed "+ - "by one or more digits", it.val) - } - val := strings.Replace(it.val, "_", "", -1) - num, err := strconv.ParseFloat(val, 64) - if err != nil { - if e, ok := err.(*strconv.NumError); ok && - e.Err == strconv.ErrRange { - - p.panicf("Float '%s' is out of the range of 64-bit "+ - "IEEE-754 floating-point numbers.", it.val) - } else { - p.panicf("Invalid float value: %q", it.val) - } - } - return num, p.typeOfPrimitive(it) - case itemDatetime: - var t time.Time - var ok bool - var err error - for _, format := range []string{ - "2006-01-02T15:04:05Z07:00", - "2006-01-02T15:04:05", - "2006-01-02", - } { - t, err = time.ParseInLocation(format, it.val, time.Local) - if err == nil { - ok = true - break - } - } - if !ok { - p.panicf("Invalid TOML Datetime: %q.", it.val) - } - return t, p.typeOfPrimitive(it) - case itemArray: - array := make([]interface{}, 0) - types := make([]tomlType, 0) - - for it = p.next(); it.typ != itemArrayEnd; it = p.next() { - if it.typ == itemCommentStart { - p.expect(itemText) - continue - } - - val, typ := p.value(it) - array = append(array, val) - types = append(types, typ) - } - return array, p.typeOfArray(types) - case itemInlineTableStart: - var ( - hash = make(map[string]interface{}) - outerContext = p.context - outerKey = p.currentKey - ) - - p.context = append(p.context, p.currentKey) - p.currentKey = "" - for it := p.next(); it.typ != itemInlineTableEnd; it = p.next() { - if it.typ != itemKeyStart { - p.bug("Expected key start but instead found %q, around line %d", - it.val, p.approxLine) - } - if it.typ == itemCommentStart { - p.expect(itemText) - continue - } - - // retrieve key - k := p.next() - p.approxLine = k.line - kname := p.keyString(k) - - // retrieve value - p.currentKey = kname - val, typ := p.value(p.next()) - // make sure we keep metadata up to date - p.setType(kname, typ) - p.ordered = append(p.ordered, p.context.add(p.currentKey)) - hash[kname] = val - } - p.context = outerContext - p.currentKey = outerKey - return hash, tomlHash - } - p.bug("Unexpected value type: %s", it.typ) - panic("unreachable") -} - -// numUnderscoresOK checks whether each underscore in s is surrounded by -// characters that are not underscores. -func numUnderscoresOK(s string) bool { - accept := false - for _, r := range s { - if r == '_' { - if !accept { - return false - } - accept = false - continue - } - accept = true - } - return accept -} - -// numPeriodsOK checks whether every period in s is followed by a digit. -func numPeriodsOK(s string) bool { - period := false - for _, r := range s { - if period && !isDigit(r) { - return false - } - period = r == '.' - } - return !period -} - -// establishContext sets the current context of the parser, -// where the context is either a hash or an array of hashes. Which one is -// set depends on the value of the `array` parameter. -// -// Establishing the context also makes sure that the key isn't a duplicate, and -// will create implicit hashes automatically. -func (p *parser) establishContext(key Key, array bool) { - var ok bool - - // Always start at the top level and drill down for our context. - hashContext := p.mapping - keyContext := make(Key, 0) - - // We only need implicit hashes for key[0:-1] - for _, k := range key[0 : len(key)-1] { - _, ok = hashContext[k] - keyContext = append(keyContext, k) - - // No key? Make an implicit hash and move on. - if !ok { - p.addImplicit(keyContext) - hashContext[k] = make(map[string]interface{}) - } - - // If the hash context is actually an array of tables, then set - // the hash context to the last element in that array. - // - // Otherwise, it better be a table, since this MUST be a key group (by - // virtue of it not being the last element in a key). - switch t := hashContext[k].(type) { - case []map[string]interface{}: - hashContext = t[len(t)-1] - case map[string]interface{}: - hashContext = t - default: - p.panicf("Key '%s' was already created as a hash.", keyContext) - } - } - - p.context = keyContext - if array { - // If this is the first element for this array, then allocate a new - // list of tables for it. - k := key[len(key)-1] - if _, ok := hashContext[k]; !ok { - hashContext[k] = make([]map[string]interface{}, 0, 5) - } - - // Add a new table. But make sure the key hasn't already been used - // for something else. - if hash, ok := hashContext[k].([]map[string]interface{}); ok { - hashContext[k] = append(hash, make(map[string]interface{})) - } else { - p.panicf("Key '%s' was already created and cannot be used as "+ - "an array.", keyContext) - } - } else { - p.setValue(key[len(key)-1], make(map[string]interface{})) - } - p.context = append(p.context, key[len(key)-1]) -} - -// setValue sets the given key to the given value in the current context. -// It will make sure that the key hasn't already been defined, account for -// implicit key groups. -func (p *parser) setValue(key string, value interface{}) { - var tmpHash interface{} - var ok bool - - hash := p.mapping - keyContext := make(Key, 0) - for _, k := range p.context { - keyContext = append(keyContext, k) - if tmpHash, ok = hash[k]; !ok { - p.bug("Context for key '%s' has not been established.", keyContext) - } - switch t := tmpHash.(type) { - case []map[string]interface{}: - // The context is a table of hashes. Pick the most recent table - // defined as the current hash. - hash = t[len(t)-1] - case map[string]interface{}: - hash = t - default: - p.bug("Expected hash to have type 'map[string]interface{}', but "+ - "it has '%T' instead.", tmpHash) - } - } - keyContext = append(keyContext, key) - - if _, ok := hash[key]; ok { - // Typically, if the given key has already been set, then we have - // to raise an error since duplicate keys are disallowed. However, - // it's possible that a key was previously defined implicitly. In this - // case, it is allowed to be redefined concretely. (See the - // `tests/valid/implicit-and-explicit-after.toml` test in `toml-test`.) - // - // But we have to make sure to stop marking it as an implicit. (So that - // another redefinition provokes an error.) - // - // Note that since it has already been defined (as a hash), we don't - // want to overwrite it. So our business is done. - if p.isImplicit(keyContext) { - p.removeImplicit(keyContext) - return - } - - // Otherwise, we have a concrete key trying to override a previous - // key, which is *always* wrong. - p.panicf("Key '%s' has already been defined.", keyContext) - } - hash[key] = value -} - -// setType sets the type of a particular value at a given key. -// It should be called immediately AFTER setValue. -// -// Note that if `key` is empty, then the type given will be applied to the -// current context (which is either a table or an array of tables). -func (p *parser) setType(key string, typ tomlType) { - keyContext := make(Key, 0, len(p.context)+1) - for _, k := range p.context { - keyContext = append(keyContext, k) - } - if len(key) > 0 { // allow type setting for hashes - keyContext = append(keyContext, key) - } - p.types[keyContext.String()] = typ -} - -// addImplicit sets the given Key as having been created implicitly. -func (p *parser) addImplicit(key Key) { - p.implicits[key.String()] = true -} - -// removeImplicit stops tagging the given key as having been implicitly -// created. -func (p *parser) removeImplicit(key Key) { - p.implicits[key.String()] = false -} - -// isImplicit returns true if the key group pointed to by the key was created -// implicitly. -func (p *parser) isImplicit(key Key) bool { - return p.implicits[key.String()] -} - -// current returns the full key name of the current context. -func (p *parser) current() string { - if len(p.currentKey) == 0 { - return p.context.String() - } - if len(p.context) == 0 { - return p.currentKey - } - return fmt.Sprintf("%s.%s", p.context, p.currentKey) -} - -func stripFirstNewline(s string) string { - if len(s) == 0 || s[0] != '\n' { - return s - } - return s[1:] -} - -func stripEscapedWhitespace(s string) string { - esc := strings.Split(s, "\\\n") - if len(esc) > 1 { - for i := 1; i < len(esc); i++ { - esc[i] = strings.TrimLeftFunc(esc[i], unicode.IsSpace) - } - } - return strings.Join(esc, "") -} - -func (p *parser) replaceEscapes(str string) string { - var replaced []rune - s := []byte(str) - r := 0 - for r < len(s) { - if s[r] != '\\' { - c, size := utf8.DecodeRune(s[r:]) - r += size - replaced = append(replaced, c) - continue - } - r += 1 - if r >= len(s) { - p.bug("Escape sequence at end of string.") - return "" - } - switch s[r] { - default: - p.bug("Expected valid escape code after \\, but got %q.", s[r]) - return "" - case 'b': - replaced = append(replaced, rune(0x0008)) - r += 1 - case 't': - replaced = append(replaced, rune(0x0009)) - r += 1 - case 'n': - replaced = append(replaced, rune(0x000A)) - r += 1 - case 'f': - replaced = append(replaced, rune(0x000C)) - r += 1 - case 'r': - replaced = append(replaced, rune(0x000D)) - r += 1 - case '"': - replaced = append(replaced, rune(0x0022)) - r += 1 - case '\\': - replaced = append(replaced, rune(0x005C)) - r += 1 - case 'u': - // At this point, we know we have a Unicode escape of the form - // `uXXXX` at [r, r+5). (Because the lexer guarantees this - // for us.) - escaped := p.asciiEscapeToUnicode(s[r+1 : r+5]) - replaced = append(replaced, escaped) - r += 5 - case 'U': - // At this point, we know we have a Unicode escape of the form - // `uXXXX` at [r, r+9). (Because the lexer guarantees this - // for us.) - escaped := p.asciiEscapeToUnicode(s[r+1 : r+9]) - replaced = append(replaced, escaped) - r += 9 - } - } - return string(replaced) -} - -func (p *parser) asciiEscapeToUnicode(bs []byte) rune { - s := string(bs) - hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32) - if err != nil { - p.bug("Could not parse '%s' as a hexadecimal number, but the "+ - "lexer claims it's OK: %s", s, err) - } - if !utf8.ValidRune(rune(hex)) { - p.panicf("Escaped character '\\u%s' is not valid UTF-8.", s) - } - return rune(hex) -} - -func isStringType(ty itemType) bool { - return ty == itemString || ty == itemMultilineString || - ty == itemRawString || ty == itemRawMultilineString -} diff --git a/vendor/github.com/BurntSushi/toml/session.vim b/vendor/github.com/BurntSushi/toml/session.vim deleted file mode 100644 index 562164be..00000000 --- a/vendor/github.com/BurntSushi/toml/session.vim +++ /dev/null @@ -1 +0,0 @@ -au BufWritePost *.go silent!make tags > /dev/null 2>&1 diff --git a/vendor/github.com/BurntSushi/toml/type_check.go b/vendor/github.com/BurntSushi/toml/type_check.go deleted file mode 100644 index c73f8afc..00000000 --- a/vendor/github.com/BurntSushi/toml/type_check.go +++ /dev/null @@ -1,91 +0,0 @@ -package toml - -// tomlType represents any Go type that corresponds to a TOML type. -// While the first draft of the TOML spec has a simplistic type system that -// probably doesn't need this level of sophistication, we seem to be militating -// toward adding real composite types. -type tomlType interface { - typeString() string -} - -// typeEqual accepts any two types and returns true if they are equal. -func typeEqual(t1, t2 tomlType) bool { - if t1 == nil || t2 == nil { - return false - } - return t1.typeString() == t2.typeString() -} - -func typeIsHash(t tomlType) bool { - return typeEqual(t, tomlHash) || typeEqual(t, tomlArrayHash) -} - -type tomlBaseType string - -func (btype tomlBaseType) typeString() string { - return string(btype) -} - -func (btype tomlBaseType) String() string { - return btype.typeString() -} - -var ( - tomlInteger tomlBaseType = "Integer" - tomlFloat tomlBaseType = "Float" - tomlDatetime tomlBaseType = "Datetime" - tomlString tomlBaseType = "String" - tomlBool tomlBaseType = "Bool" - tomlArray tomlBaseType = "Array" - tomlHash tomlBaseType = "Hash" - tomlArrayHash tomlBaseType = "ArrayHash" -) - -// typeOfPrimitive returns a tomlType of any primitive value in TOML. -// Primitive values are: Integer, Float, Datetime, String and Bool. -// -// Passing a lexer item other than the following will cause a BUG message -// to occur: itemString, itemBool, itemInteger, itemFloat, itemDatetime. -func (p *parser) typeOfPrimitive(lexItem item) tomlType { - switch lexItem.typ { - case itemInteger: - return tomlInteger - case itemFloat: - return tomlFloat - case itemDatetime: - return tomlDatetime - case itemString: - return tomlString - case itemMultilineString: - return tomlString - case itemRawString: - return tomlString - case itemRawMultilineString: - return tomlString - case itemBool: - return tomlBool - } - p.bug("Cannot infer primitive type of lex item '%s'.", lexItem) - panic("unreachable") -} - -// typeOfArray returns a tomlType for an array given a list of types of its -// values. -// -// In the current spec, if an array is homogeneous, then its type is always -// "Array". If the array is not homogeneous, an error is generated. -func (p *parser) typeOfArray(types []tomlType) tomlType { - // Empty arrays are cool. - if len(types) == 0 { - return tomlArray - } - - theType := types[0] - for _, t := range types[1:] { - if !typeEqual(theType, t) { - p.panicf("Array contains values of type '%s' and '%s', but "+ - "arrays must be homogeneous.", theType, t) - } - } - return tomlArray -} diff --git a/vendor/github.com/BurntSushi/toml/type_fields.go b/vendor/github.com/BurntSushi/toml/type_fields.go deleted file mode 100644 index 608997c2..00000000 --- a/vendor/github.com/BurntSushi/toml/type_fields.go +++ /dev/null @@ -1,242 +0,0 @@ -package toml - -// Struct field handling is adapted from code in encoding/json: -// -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the Go distribution. - -import ( - "reflect" - "sort" - "sync" -) - -// A field represents a single field found in a struct. -type field struct { - name string // the name of the field (`toml` tag included) - tag bool // whether field has a `toml` tag - index []int // represents the depth of an anonymous field - typ reflect.Type // the type of the field -} - -// byName sorts field by name, breaking ties with depth, -// then breaking ties with "name came from toml tag", then -// breaking ties with index sequence. -type byName []field - -func (x byName) Len() int { return len(x) } - -func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] } - -func (x byName) Less(i, j int) bool { - if x[i].name != x[j].name { - return x[i].name < x[j].name - } - if len(x[i].index) != len(x[j].index) { - return len(x[i].index) < len(x[j].index) - } - if x[i].tag != x[j].tag { - return x[i].tag - } - return byIndex(x).Less(i, j) -} - -// byIndex sorts field by index sequence. -type byIndex []field - -func (x byIndex) Len() int { return len(x) } - -func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } - -func (x byIndex) Less(i, j int) bool { - for k, xik := range x[i].index { - if k >= len(x[j].index) { - return false - } - if xik != x[j].index[k] { - return xik < x[j].index[k] - } - } - return len(x[i].index) < len(x[j].index) -} - -// typeFields returns a list of fields that TOML should recognize for the given -// type. The algorithm is breadth-first search over the set of structs to -// include - the top struct and then any reachable anonymous structs. -func typeFields(t reflect.Type) []field { - // Anonymous fields to explore at the current level and the next. - current := []field{} - next := []field{{typ: t}} - - // Count of queued names for current level and the next. - count := map[reflect.Type]int{} - nextCount := map[reflect.Type]int{} - - // Types already visited at an earlier level. - visited := map[reflect.Type]bool{} - - // Fields found. - var fields []field - - for len(next) > 0 { - current, next = next, current[:0] - count, nextCount = nextCount, map[reflect.Type]int{} - - for _, f := range current { - if visited[f.typ] { - continue - } - visited[f.typ] = true - - // Scan f.typ for fields to include. - for i := 0; i < f.typ.NumField(); i++ { - sf := f.typ.Field(i) - if sf.PkgPath != "" && !sf.Anonymous { // unexported - continue - } - opts := getOptions(sf.Tag) - if opts.skip { - continue - } - index := make([]int, len(f.index)+1) - copy(index, f.index) - index[len(f.index)] = i - - ft := sf.Type - if ft.Name() == "" && ft.Kind() == reflect.Ptr { - // Follow pointer. - ft = ft.Elem() - } - - // Record found field and index sequence. - if opts.name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct { - tagged := opts.name != "" - name := opts.name - if name == "" { - name = sf.Name - } - fields = append(fields, field{name, tagged, index, ft}) - if count[f.typ] > 1 { - // If there were multiple instances, add a second, - // so that the annihilation code will see a duplicate. - // It only cares about the distinction between 1 or 2, - // so don't bother generating any more copies. - fields = append(fields, fields[len(fields)-1]) - } - continue - } - - // Record new anonymous struct to explore in next round. - nextCount[ft]++ - if nextCount[ft] == 1 { - f := field{name: ft.Name(), index: index, typ: ft} - next = append(next, f) - } - } - } - } - - sort.Sort(byName(fields)) - - // Delete all fields that are hidden by the Go rules for embedded fields, - // except that fields with TOML tags are promoted. - - // The fields are sorted in primary order of name, secondary order - // of field index length. Loop over names; for each name, delete - // hidden fields by choosing the one dominant field that survives. - out := fields[:0] - for advance, i := 0, 0; i < len(fields); i += advance { - // One iteration per name. - // Find the sequence of fields with the name of this first field. - fi := fields[i] - name := fi.name - for advance = 1; i+advance < len(fields); advance++ { - fj := fields[i+advance] - if fj.name != name { - break - } - } - if advance == 1 { // Only one field with this name - out = append(out, fi) - continue - } - dominant, ok := dominantField(fields[i : i+advance]) - if ok { - out = append(out, dominant) - } - } - - fields = out - sort.Sort(byIndex(fields)) - - return fields -} - -// dominantField looks through the fields, all of which are known to -// have the same name, to find the single field that dominates the -// others using Go's embedding rules, modified by the presence of -// TOML tags. If there are multiple top-level fields, the boolean -// will be false: This condition is an error in Go and we skip all -// the fields. -func dominantField(fields []field) (field, bool) { - // The fields are sorted in increasing index-length order. The winner - // must therefore be one with the shortest index length. Drop all - // longer entries, which is easy: just truncate the slice. - length := len(fields[0].index) - tagged := -1 // Index of first tagged field. - for i, f := range fields { - if len(f.index) > length { - fields = fields[:i] - break - } - if f.tag { - if tagged >= 0 { - // Multiple tagged fields at the same level: conflict. - // Return no field. - return field{}, false - } - tagged = i - } - } - if tagged >= 0 { - return fields[tagged], true - } - // All remaining fields have the same length. If there's more than one, - // we have a conflict (two fields named "X" at the same level) and we - // return no field. - if len(fields) > 1 { - return field{}, false - } - return fields[0], true -} - -var fieldCache struct { - sync.RWMutex - m map[reflect.Type][]field -} - -// cachedTypeFields is like typeFields but uses a cache to avoid repeated work. -func cachedTypeFields(t reflect.Type) []field { - fieldCache.RLock() - f := fieldCache.m[t] - fieldCache.RUnlock() - if f != nil { - return f - } - - // Compute fields without lock. - // Might duplicate effort but won't hold other computations back. - f = typeFields(t) - if f == nil { - f = []field{} - } - - fieldCache.Lock() - if fieldCache.m == nil { - fieldCache.m = map[reflect.Type][]field{} - } - fieldCache.m[t] = f - fieldCache.Unlock() - return f -} diff --git a/vendor/github.com/Philipp15b/go-steam/auth.go b/vendor/github.com/Philipp15b/go-steam/auth.go index b67de335..16e8c19b 100644 --- a/vendor/github.com/Philipp15b/go-steam/auth.go +++ b/vendor/github.com/Philipp15b/go-steam/auth.go @@ -2,13 +2,14 @@ package steam import ( "crypto/sha1" + "sync/atomic" + "time" + . "github.com/Philipp15b/go-steam/protocol" . "github.com/Philipp15b/go-steam/protocol/protobuf" . "github.com/Philipp15b/go-steam/protocol/steamlang" . "github.com/Philipp15b/go-steam/steamid" "github.com/golang/protobuf/proto" - "sync/atomic" - "time" ) type Auth struct { @@ -19,23 +20,41 @@ type Auth struct { type SentryHash []byte type LogOnDetails struct { - Username string - Password string - AuthCode string + Username string + + // If logging into an account without a login key, the account's password. + Password string + + // If you have a Steam Guard email code, you can provide it here. + AuthCode string + + // If you have a Steam Guard mobile two-factor authentication code, you can provide it here. TwoFactorCode string SentryFileHash SentryHash + LoginKey string + + // true if you want to get a login key which can be used in lieu of + // a password for subsequent logins. false or omitted otherwise. + ShouldRememberPassword bool } // Log on with the given details. You must always specify username and -// password. For the first login, don't set an authcode or a hash and you'll receive an error +// password OR username and loginkey. For the first login, don't set an authcode or a hash and you'll +// receive an error (EResult_AccountLogonDenied) // and Steam will send you an authcode. Then you have to login again, this time with the authcode. // Shortly after logging in, you'll receive a MachineAuthUpdateEvent with a hash which allows // you to login without using an authcode in the future. // // If you don't use Steam Guard, username and password are enough. +// +// After the event EMsg_ClientNewLoginKey is received you can use the LoginKey +// to login instead of using the password. func (a *Auth) LogOn(details *LogOnDetails) { - if len(details.Username) == 0 || len(details.Password) == 0 { - panic("Username and password must be set!") + if details.Username == "" { + panic("Username must be set!") + } + if details.Password == "" && details.LoginKey == "" { + panic("Password or LoginKey must be set!") } logon := new(CMsgClientLogon) @@ -50,6 +69,12 @@ func (a *Auth) LogOn(details *LogOnDetails) { logon.ClientLanguage = proto.String("english") logon.ProtocolVersion = proto.Uint32(MsgClientLogon_CurrentProtocol) logon.ShaSentryfile = details.SentryFileHash + if details.LoginKey != "" { + logon.LoginKey = proto.String(details.LoginKey) + } + if details.ShouldRememberPassword { + logon.ShouldRememberPassword = proto.Bool(details.ShouldRememberPassword) + } atomic.StoreUint64(&a.client.steamId, uint64(NewIdAdv(0, 1, int32(EUniverse_Public), int32(EAccountType_Individual)))) diff --git a/vendor/github.com/alecthomas/log4go/.gitignore b/vendor/github.com/alecthomas/log4go/.gitignore deleted file mode 100644 index f6207cd8..00000000 --- a/vendor/github.com/alecthomas/log4go/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.sw[op] -.DS_Store diff --git a/vendor/github.com/alecthomas/log4go/LICENSE b/vendor/github.com/alecthomas/log4go/LICENSE deleted file mode 100644 index 7093402b..00000000 --- a/vendor/github.com/alecthomas/log4go/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -Copyright (c) 2010, Kyle Lemons . All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/alecthomas/log4go/README b/vendor/github.com/alecthomas/log4go/README deleted file mode 100644 index 3361567f..00000000 --- a/vendor/github.com/alecthomas/log4go/README +++ /dev/null @@ -1,14 +0,0 @@ -# This is an unmaintained fork, left only so it doesn't break imports. - -Please see http://log4go.googlecode.com/ - -Installation: -- Run `goinstall log4go.googlecode.com/hg` - -Usage: -- Add the following import: -import l4g "log4go.googlecode.com/hg" - -Acknowledgements: -- pomack - For providing awesome patches to bring log4go up to the latest Go spec diff --git a/vendor/github.com/alecthomas/log4go/config.go b/vendor/github.com/alecthomas/log4go/config.go deleted file mode 100644 index 577c3eb2..00000000 --- a/vendor/github.com/alecthomas/log4go/config.go +++ /dev/null @@ -1,288 +0,0 @@ -// Copyright (C) 2010, Kyle Lemons . All rights reserved. - -package log4go - -import ( - "encoding/xml" - "fmt" - "io/ioutil" - "os" - "strconv" - "strings" -) - -type xmlProperty struct { - Name string `xml:"name,attr"` - Value string `xml:",chardata"` -} - -type xmlFilter struct { - Enabled string `xml:"enabled,attr"` - Tag string `xml:"tag"` - Level string `xml:"level"` - Type string `xml:"type"` - Property []xmlProperty `xml:"property"` -} - -type xmlLoggerConfig struct { - Filter []xmlFilter `xml:"filter"` -} - -// Load XML configuration; see examples/example.xml for documentation -func (log Logger) LoadConfiguration(filename string) { - log.Close() - - // Open the configuration file - fd, err := os.Open(filename) - if err != nil { - fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Could not open %q for reading: %s\n", filename, err) - os.Exit(1) - } - - contents, err := ioutil.ReadAll(fd) - if err != nil { - fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Could not read %q: %s\n", filename, err) - os.Exit(1) - } - - xc := new(xmlLoggerConfig) - if err := xml.Unmarshal(contents, xc); err != nil { - fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Could not parse XML configuration in %q: %s\n", filename, err) - os.Exit(1) - } - - for _, xmlfilt := range xc.Filter { - var filt LogWriter - var lvl Level - bad, good, enabled := false, true, false - - // Check required children - if len(xmlfilt.Enabled) == 0 { - fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required attribute %s for filter missing in %s\n", "enabled", filename) - bad = true - } else { - enabled = xmlfilt.Enabled != "false" - } - if len(xmlfilt.Tag) == 0 { - fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required child <%s> for filter missing in %s\n", "tag", filename) - bad = true - } - if len(xmlfilt.Type) == 0 { - fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required child <%s> for filter missing in %s\n", "type", filename) - bad = true - } - if len(xmlfilt.Level) == 0 { - fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required child <%s> for filter missing in %s\n", "level", filename) - bad = true - } - - switch xmlfilt.Level { - case "FINEST": - lvl = FINEST - case "FINE": - lvl = FINE - case "DEBUG": - lvl = DEBUG - case "TRACE": - lvl = TRACE - case "INFO": - lvl = INFO - case "WARNING": - lvl = WARNING - case "ERROR": - lvl = ERROR - case "CRITICAL": - lvl = CRITICAL - default: - fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required child <%s> for filter has unknown value in %s: %s\n", "level", filename, xmlfilt.Level) - bad = true - } - - // Just so all of the required attributes are errored at the same time if missing - if bad { - os.Exit(1) - } - - switch xmlfilt.Type { - case "console": - filt, good = xmlToConsoleLogWriter(filename, xmlfilt.Property, enabled) - case "file": - filt, good = xmlToFileLogWriter(filename, xmlfilt.Property, enabled) - case "xml": - filt, good = xmlToXMLLogWriter(filename, xmlfilt.Property, enabled) - case "socket": - filt, good = xmlToSocketLogWriter(filename, xmlfilt.Property, enabled) - default: - fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Could not load XML configuration in %s: unknown filter type \"%s\"\n", filename, xmlfilt.Type) - os.Exit(1) - } - - // Just so all of the required params are errored at the same time if wrong - if !good { - os.Exit(1) - } - - // If we're disabled (syntax and correctness checks only), don't add to logger - if !enabled { - continue - } - - log[xmlfilt.Tag] = &Filter{lvl, filt} - } -} - -func xmlToConsoleLogWriter(filename string, props []xmlProperty, enabled bool) (*ConsoleLogWriter, bool) { - // Parse properties - for _, prop := range props { - switch prop.Name { - default: - fmt.Fprintf(os.Stderr, "LoadConfiguration: Warning: Unknown property \"%s\" for console filter in %s\n", prop.Name, filename) - } - } - - // If it's disabled, we're just checking syntax - if !enabled { - return nil, true - } - - return NewConsoleLogWriter(), true -} - -// Parse a number with K/M/G suffixes based on thousands (1000) or 2^10 (1024) -func strToNumSuffix(str string, mult int) int { - num := 1 - if len(str) > 1 { - switch str[len(str)-1] { - case 'G', 'g': - num *= mult - fallthrough - case 'M', 'm': - num *= mult - fallthrough - case 'K', 'k': - num *= mult - str = str[0 : len(str)-1] - } - } - parsed, _ := strconv.Atoi(str) - return parsed * num -} -func xmlToFileLogWriter(filename string, props []xmlProperty, enabled bool) (*FileLogWriter, bool) { - file := "" - format := "[%D %T] [%L] (%S) %M" - maxlines := 0 - maxsize := 0 - daily := false - rotate := false - - // Parse properties - for _, prop := range props { - switch prop.Name { - case "filename": - file = strings.Trim(prop.Value, " \r\n") - case "format": - format = strings.Trim(prop.Value, " \r\n") - case "maxlines": - maxlines = strToNumSuffix(strings.Trim(prop.Value, " \r\n"), 1000) - case "maxsize": - maxsize = strToNumSuffix(strings.Trim(prop.Value, " \r\n"), 1024) - case "daily": - daily = strings.Trim(prop.Value, " \r\n") != "false" - case "rotate": - rotate = strings.Trim(prop.Value, " \r\n") != "false" - default: - fmt.Fprintf(os.Stderr, "LoadConfiguration: Warning: Unknown property \"%s\" for file filter in %s\n", prop.Name, filename) - } - } - - // Check properties - if len(file) == 0 { - fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required property \"%s\" for file filter missing in %s\n", "filename", filename) - return nil, false - } - - // If it's disabled, we're just checking syntax - if !enabled { - return nil, true - } - - flw := NewFileLogWriter(file, rotate) - flw.SetFormat(format) - flw.SetRotateLines(maxlines) - flw.SetRotateSize(maxsize) - flw.SetRotateDaily(daily) - return flw, true -} - -func xmlToXMLLogWriter(filename string, props []xmlProperty, enabled bool) (*FileLogWriter, bool) { - file := "" - maxrecords := 0 - maxsize := 0 - daily := false - rotate := false - - // Parse properties - for _, prop := range props { - switch prop.Name { - case "filename": - file = strings.Trim(prop.Value, " \r\n") - case "maxrecords": - maxrecords = strToNumSuffix(strings.Trim(prop.Value, " \r\n"), 1000) - case "maxsize": - maxsize = strToNumSuffix(strings.Trim(prop.Value, " \r\n"), 1024) - case "daily": - daily = strings.Trim(prop.Value, " \r\n") != "false" - case "rotate": - rotate = strings.Trim(prop.Value, " \r\n") != "false" - default: - fmt.Fprintf(os.Stderr, "LoadConfiguration: Warning: Unknown property \"%s\" for xml filter in %s\n", prop.Name, filename) - } - } - - // Check properties - if len(file) == 0 { - fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required property \"%s\" for xml filter missing in %s\n", "filename", filename) - return nil, false - } - - // If it's disabled, we're just checking syntax - if !enabled { - return nil, true - } - - xlw := NewXMLLogWriter(file, rotate) - xlw.SetRotateLines(maxrecords) - xlw.SetRotateSize(maxsize) - xlw.SetRotateDaily(daily) - return xlw, true -} - -func xmlToSocketLogWriter(filename string, props []xmlProperty, enabled bool) (SocketLogWriter, bool) { - endpoint := "" - protocol := "udp" - - // Parse properties - for _, prop := range props { - switch prop.Name { - case "endpoint": - endpoint = strings.Trim(prop.Value, " \r\n") - case "protocol": - protocol = strings.Trim(prop.Value, " \r\n") - default: - fmt.Fprintf(os.Stderr, "LoadConfiguration: Warning: Unknown property \"%s\" for file filter in %s\n", prop.Name, filename) - } - } - - // Check properties - if len(endpoint) == 0 { - fmt.Fprintf(os.Stderr, "LoadConfiguration: Error: Required property \"%s\" for file filter missing in %s\n", "endpoint", filename) - return nil, false - } - - // If it's disabled, we're just checking syntax - if !enabled { - return nil, true - } - - return NewSocketLogWriter(protocol, endpoint), true -} diff --git a/vendor/github.com/alecthomas/log4go/filelog.go b/vendor/github.com/alecthomas/log4go/filelog.go deleted file mode 100644 index ee0ab0c0..00000000 --- a/vendor/github.com/alecthomas/log4go/filelog.go +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright (C) 2010, Kyle Lemons . All rights reserved. - -package log4go - -import ( - "fmt" - "os" - "time" -) - -// This log writer sends output to a file -type FileLogWriter struct { - rec chan *LogRecord - rot chan bool - - // The opened file - filename string - file *os.File - - // The logging format - format string - - // File header/trailer - header, trailer string - - // Rotate at linecount - maxlines int - maxlines_curlines int - - // Rotate at size - maxsize int - maxsize_cursize int - - // Rotate daily - daily bool - daily_opendate int - - // Keep old logfiles (.001, .002, etc) - rotate bool - maxbackup int -} - -// This is the FileLogWriter's output method -func (w *FileLogWriter) LogWrite(rec *LogRecord) { - w.rec <- rec -} - -func (w *FileLogWriter) Close() { - close(w.rec) - w.file.Sync() -} - -// NewFileLogWriter creates a new LogWriter which writes to the given file and -// has rotation enabled if rotate is true. -// -// If rotate is true, any time a new log file is opened, the old one is renamed -// with a .### extension to preserve it. The various Set* methods can be used -// to configure log rotation based on lines, size, and daily. -// -// The standard log-line format is: -// [%D %T] [%L] (%S) %M -func NewFileLogWriter(fname string, rotate bool) *FileLogWriter { - w := &FileLogWriter{ - rec: make(chan *LogRecord, LogBufferLength), - rot: make(chan bool), - filename: fname, - format: "[%D %T] [%L] (%S) %M", - rotate: rotate, - maxbackup: 999, - } - - // open the file for the first time - if err := w.intRotate(); err != nil { - fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.filename, err) - return nil - } - - go func() { - defer func() { - if w.file != nil { - fmt.Fprint(w.file, FormatLogRecord(w.trailer, &LogRecord{Created: time.Now()})) - w.file.Close() - } - }() - - for { - select { - case <-w.rot: - if err := w.intRotate(); err != nil { - fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.filename, err) - return - } - case rec, ok := <-w.rec: - if !ok { - return - } - now := time.Now() - if (w.maxlines > 0 && w.maxlines_curlines >= w.maxlines) || - (w.maxsize > 0 && w.maxsize_cursize >= w.maxsize) || - (w.daily && now.Day() != w.daily_opendate) { - if err := w.intRotate(); err != nil { - fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.filename, err) - return - } - } - - // Perform the write - n, err := fmt.Fprint(w.file, FormatLogRecord(w.format, rec)) - if err != nil { - fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.filename, err) - return - } - - // Update the counts - w.maxlines_curlines++ - w.maxsize_cursize += n - } - } - }() - - return w -} - -// Request that the logs rotate -func (w *FileLogWriter) Rotate() { - w.rot <- true -} - -// If this is called in a threaded context, it MUST be synchronized -func (w *FileLogWriter) intRotate() error { - // Close any log file that may be open - if w.file != nil { - fmt.Fprint(w.file, FormatLogRecord(w.trailer, &LogRecord{Created: time.Now()})) - w.file.Close() - } - - // If we are keeping log files, move it to the next available number - if w.rotate { - _, err := os.Lstat(w.filename) - if err == nil { // file exists - // Find the next available number - num := 1 - fname := "" - if w.daily && time.Now().Day() != w.daily_opendate { - yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02") - - for ; err == nil && num <= 999; num++ { - fname = w.filename + fmt.Sprintf(".%s.%03d", yesterday, num) - _, err = os.Lstat(fname) - } - // return error if the last file checked still existed - if err == nil { - return fmt.Errorf("Rotate: Cannot find free log number to rename %s\n", w.filename) - } - } else { - num = w.maxbackup - 1 - for ; num >= 1; num-- { - fname = w.filename + fmt.Sprintf(".%d", num) - nfname := w.filename + fmt.Sprintf(".%d", num+1) - _, err = os.Lstat(fname) - if err == nil { - os.Rename(fname, nfname) - } - } - } - - w.file.Close() - // Rename the file to its newfound home - err = os.Rename(w.filename, fname) - if err != nil { - return fmt.Errorf("Rotate: %s\n", err) - } - } - } - - // Open the log file - fd, err := os.OpenFile(w.filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0660) - if err != nil { - return err - } - w.file = fd - - now := time.Now() - fmt.Fprint(w.file, FormatLogRecord(w.header, &LogRecord{Created: now})) - - // Set the daily open date to the current date - w.daily_opendate = now.Day() - - // initialize rotation values - w.maxlines_curlines = 0 - w.maxsize_cursize = 0 - - return nil -} - -// Set the logging format (chainable). Must be called before the first log -// message is written. -func (w *FileLogWriter) SetFormat(format string) *FileLogWriter { - w.format = format - return w -} - -// Set the logfile header and footer (chainable). Must be called before the first log -// message is written. These are formatted similar to the FormatLogRecord (e.g. -// you can use %D and %T in your header/footer for date and time). -func (w *FileLogWriter) SetHeadFoot(head, foot string) *FileLogWriter { - w.header, w.trailer = head, foot - if w.maxlines_curlines == 0 { - fmt.Fprint(w.file, FormatLogRecord(w.header, &LogRecord{Created: time.Now()})) - } - return w -} - -// Set rotate at linecount (chainable). Must be called before the first log -// message is written. -func (w *FileLogWriter) SetRotateLines(maxlines int) *FileLogWriter { - //fmt.Fprintf(os.Stderr, "FileLogWriter.SetRotateLines: %v\n", maxlines) - w.maxlines = maxlines - return w -} - -// Set rotate at size (chainable). Must be called before the first log message -// is written. -func (w *FileLogWriter) SetRotateSize(maxsize int) *FileLogWriter { - //fmt.Fprintf(os.Stderr, "FileLogWriter.SetRotateSize: %v\n", maxsize) - w.maxsize = maxsize - return w -} - -// Set rotate daily (chainable). Must be called before the first log message is -// written. -func (w *FileLogWriter) SetRotateDaily(daily bool) *FileLogWriter { - //fmt.Fprintf(os.Stderr, "FileLogWriter.SetRotateDaily: %v\n", daily) - w.daily = daily - return w -} - -// Set max backup files. Must be called before the first log message -// is written. -func (w *FileLogWriter) SetRotateMaxBackup(maxbackup int) *FileLogWriter { - w.maxbackup = maxbackup - return w -} - -// SetRotate changes whether or not the old logs are kept. (chainable) Must be -// called before the first log message is written. If rotate is false, the -// files are overwritten; otherwise, they are rotated to another file before the -// new log is opened. -func (w *FileLogWriter) SetRotate(rotate bool) *FileLogWriter { - //fmt.Fprintf(os.Stderr, "FileLogWriter.SetRotate: %v\n", rotate) - w.rotate = rotate - return w -} - -// NewXMLLogWriter is a utility method for creating a FileLogWriter set up to -// output XML record log messages instead of line-based ones. -func NewXMLLogWriter(fname string, rotate bool) *FileLogWriter { - return NewFileLogWriter(fname, rotate).SetFormat( - ` - %D %T - %S - %M - `).SetHeadFoot("", "") -} diff --git a/vendor/github.com/alecthomas/log4go/log4go.go b/vendor/github.com/alecthomas/log4go/log4go.go deleted file mode 100644 index 822e890c..00000000 --- a/vendor/github.com/alecthomas/log4go/log4go.go +++ /dev/null @@ -1,484 +0,0 @@ -// Copyright (C) 2010, Kyle Lemons . All rights reserved. - -// Package log4go provides level-based and highly configurable logging. -// -// Enhanced Logging -// -// This is inspired by the logging functionality in Java. Essentially, you create a Logger -// object and create output filters for it. You can send whatever you want to the Logger, -// and it will filter that based on your settings and send it to the outputs. This way, you -// can put as much debug code in your program as you want, and when you're done you can filter -// out the mundane messages so only the important ones show up. -// -// Utility functions are provided to make life easier. Here is some example code to get started: -// -// log := log4go.NewLogger() -// log.AddFilter("stdout", log4go.DEBUG, log4go.NewConsoleLogWriter()) -// log.AddFilter("log", log4go.FINE, log4go.NewFileLogWriter("example.log", true)) -// log.Info("The time is now: %s", time.LocalTime().Format("15:04:05 MST 2006/01/02")) -// -// The first two lines can be combined with the utility NewDefaultLogger: -// -// log := log4go.NewDefaultLogger(log4go.DEBUG) -// log.AddFilter("log", log4go.FINE, log4go.NewFileLogWriter("example.log", true)) -// log.Info("The time is now: %s", time.LocalTime().Format("15:04:05 MST 2006/01/02")) -// -// Usage notes: -// - The ConsoleLogWriter does not display the source of the message to standard -// output, but the FileLogWriter does. -// - The utility functions (Info, Debug, Warn, etc) derive their source from the -// calling function, and this incurs extra overhead. -// -// Changes from 2.0: -// - The external interface has remained mostly stable, but a lot of the -// internals have been changed, so if you depended on any of this or created -// your own LogWriter, then you will probably have to update your code. In -// particular, Logger is now a map and ConsoleLogWriter is now a channel -// behind-the-scenes, and the LogWrite method no longer has return values. -// -// Future work: (please let me know if you think I should work on any of these particularly) -// - Log file rotation -// - Logging configuration files ala log4j -// - Have the ability to remove filters? -// - Have GetInfoChannel, GetDebugChannel, etc return a chan string that allows -// for another method of logging -// - Add an XML filter type -package log4go - -import ( - "errors" - "fmt" - "os" - "runtime" - "strings" - "time" -) - -// Version information -const ( - L4G_VERSION = "log4go-v3.0.1" - L4G_MAJOR = 3 - L4G_MINOR = 0 - L4G_BUILD = 1 -) - -/****** Constants ******/ - -// These are the integer logging levels used by the logger -type Level int - -const ( - FINEST Level = iota - FINE - DEBUG - TRACE - INFO - WARNING - ERROR - CRITICAL -) - -// Logging level strings -var ( - levelStrings = [...]string{"FNST", "FINE", "DEBG", "TRAC", "INFO", "WARN", "EROR", "CRIT"} -) - -func (l Level) String() string { - if l < 0 || int(l) > len(levelStrings) { - return "UNKNOWN" - } - return levelStrings[int(l)] -} - -/****** Variables ******/ -var ( - // LogBufferLength specifies how many log messages a particular log4go - // logger can buffer at a time before writing them. - LogBufferLength = 32 -) - -/****** LogRecord ******/ - -// A LogRecord contains all of the pertinent information for each message -type LogRecord struct { - Level Level // The log level - Created time.Time // The time at which the log message was created (nanoseconds) - Source string // The message source - Message string // The log message -} - -/****** LogWriter ******/ - -// This is an interface for anything that should be able to write logs -type LogWriter interface { - // This will be called to log a LogRecord message. - LogWrite(rec *LogRecord) - - // This should clean up anything lingering about the LogWriter, as it is called before - // the LogWriter is removed. LogWrite should not be called after Close. - Close() -} - -/****** Logger ******/ - -// A Filter represents the log level below which no log records are written to -// the associated LogWriter. -type Filter struct { - Level Level - LogWriter -} - -// A Logger represents a collection of Filters through which log messages are -// written. -type Logger map[string]*Filter - -// Create a new logger. -// -// DEPRECATED: Use make(Logger) instead. -func NewLogger() Logger { - os.Stderr.WriteString("warning: use of deprecated NewLogger\n") - return make(Logger) -} - -// Create a new logger with a "stdout" filter configured to send log messages at -// or above lvl to standard output. -// -// DEPRECATED: use NewDefaultLogger instead. -func NewConsoleLogger(lvl Level) Logger { - os.Stderr.WriteString("warning: use of deprecated NewConsoleLogger\n") - return Logger{ - "stdout": &Filter{lvl, NewConsoleLogWriter()}, - } -} - -// Create a new logger with a "stdout" filter configured to send log messages at -// or above lvl to standard output. -func NewDefaultLogger(lvl Level) Logger { - return Logger{ - "stdout": &Filter{lvl, NewConsoleLogWriter()}, - } -} - -// Closes all log writers in preparation for exiting the program or a -// reconfiguration of logging. Calling this is not really imperative, unless -// you want to guarantee that all log messages are written. Close removes -// all filters (and thus all LogWriters) from the logger. -func (log Logger) Close() { - // Close all open loggers - for name, filt := range log { - filt.Close() - delete(log, name) - } -} - -// Add a new LogWriter to the Logger which will only log messages at lvl or -// higher. This function should not be called from multiple goroutines. -// Returns the logger for chaining. -func (log Logger) AddFilter(name string, lvl Level, writer LogWriter) Logger { - log[name] = &Filter{lvl, writer} - return log -} - -/******* Logging *******/ -// Send a formatted log message internally -func (log Logger) intLogf(lvl Level, format string, args ...interface{}) { - skip := true - - // Determine if any logging will be done - for _, filt := range log { - if lvl >= filt.Level { - skip = false - break - } - } - if skip { - return - } - - // Determine caller func - pc, _, lineno, ok := runtime.Caller(2) - src := "" - if ok { - src = fmt.Sprintf("%s:%d", runtime.FuncForPC(pc).Name(), lineno) - } - - msg := format - if len(args) > 0 { - msg = fmt.Sprintf(format, args...) - } - - // Make the log record - rec := &LogRecord{ - Level: lvl, - Created: time.Now(), - Source: src, - Message: msg, - } - - // Dispatch the logs - for _, filt := range log { - if lvl < filt.Level { - continue - } - filt.LogWrite(rec) - } -} - -// Send a closure log message internally -func (log Logger) intLogc(lvl Level, closure func() string) { - skip := true - - // Determine if any logging will be done - for _, filt := range log { - if lvl >= filt.Level { - skip = false - break - } - } - if skip { - return - } - - // Determine caller func - pc, _, lineno, ok := runtime.Caller(2) - src := "" - if ok { - src = fmt.Sprintf("%s:%d", runtime.FuncForPC(pc).Name(), lineno) - } - - // Make the log record - rec := &LogRecord{ - Level: lvl, - Created: time.Now(), - Source: src, - Message: closure(), - } - - // Dispatch the logs - for _, filt := range log { - if lvl < filt.Level { - continue - } - filt.LogWrite(rec) - } -} - -// Send a log message with manual level, source, and message. -func (log Logger) Log(lvl Level, source, message string) { - skip := true - - // Determine if any logging will be done - for _, filt := range log { - if lvl >= filt.Level { - skip = false - break - } - } - if skip { - return - } - - // Make the log record - rec := &LogRecord{ - Level: lvl, - Created: time.Now(), - Source: source, - Message: message, - } - - // Dispatch the logs - for _, filt := range log { - if lvl < filt.Level { - continue - } - filt.LogWrite(rec) - } -} - -// Logf logs a formatted log message at the given log level, using the caller as -// its source. -func (log Logger) Logf(lvl Level, format string, args ...interface{}) { - log.intLogf(lvl, format, args...) -} - -// Logc logs a string returned by the closure at the given log level, using the caller as -// its source. If no log message would be written, the closure is never called. -func (log Logger) Logc(lvl Level, closure func() string) { - log.intLogc(lvl, closure) -} - -// Finest logs a message at the finest log level. -// See Debug for an explanation of the arguments. -func (log Logger) Finest(arg0 interface{}, args ...interface{}) { - const ( - lvl = FINEST - ) - switch first := arg0.(type) { - case string: - // Use the string as a format string - log.intLogf(lvl, first, args...) - case func() string: - // Log the closure (no other arguments used) - log.intLogc(lvl, first) - default: - // Build a format string so that it will be similar to Sprint - log.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...) - } -} - -// Fine logs a message at the fine log level. -// See Debug for an explanation of the arguments. -func (log Logger) Fine(arg0 interface{}, args ...interface{}) { - const ( - lvl = FINE - ) - switch first := arg0.(type) { - case string: - // Use the string as a format string - log.intLogf(lvl, first, args...) - case func() string: - // Log the closure (no other arguments used) - log.intLogc(lvl, first) - default: - // Build a format string so that it will be similar to Sprint - log.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...) - } -} - -// Debug is a utility method for debug log messages. -// The behavior of Debug depends on the first argument: -// - arg0 is a string -// When given a string as the first argument, this behaves like Logf but with -// the DEBUG log level: the first argument is interpreted as a format for the -// latter arguments. -// - arg0 is a func()string -// When given a closure of type func()string, this logs the string returned by -// the closure iff it will be logged. The closure runs at most one time. -// - arg0 is interface{} -// When given anything else, the log message will be each of the arguments -// formatted with %v and separated by spaces (ala Sprint). -func (log Logger) Debug(arg0 interface{}, args ...interface{}) { - const ( - lvl = DEBUG - ) - switch first := arg0.(type) { - case string: - // Use the string as a format string - log.intLogf(lvl, first, args...) - case func() string: - // Log the closure (no other arguments used) - log.intLogc(lvl, first) - default: - // Build a format string so that it will be similar to Sprint - log.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...) - } -} - -// Trace logs a message at the trace log level. -// See Debug for an explanation of the arguments. -func (log Logger) Trace(arg0 interface{}, args ...interface{}) { - const ( - lvl = TRACE - ) - switch first := arg0.(type) { - case string: - // Use the string as a format string - log.intLogf(lvl, first, args...) - case func() string: - // Log the closure (no other arguments used) - log.intLogc(lvl, first) - default: - // Build a format string so that it will be similar to Sprint - log.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...) - } -} - -// Info logs a message at the info log level. -// See Debug for an explanation of the arguments. -func (log Logger) Info(arg0 interface{}, args ...interface{}) { - const ( - lvl = INFO - ) - switch first := arg0.(type) { - case string: - // Use the string as a format string - log.intLogf(lvl, first, args...) - case func() string: - // Log the closure (no other arguments used) - log.intLogc(lvl, first) - default: - // Build a format string so that it will be similar to Sprint - log.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...) - } -} - -// Warn logs a message at the warning log level and returns the formatted error. -// At the warning level and higher, there is no performance benefit if the -// message is not actually logged, because all formats are processed and all -// closures are executed to format the error message. -// See Debug for further explanation of the arguments. -func (log Logger) Warn(arg0 interface{}, args ...interface{}) error { - const ( - lvl = WARNING - ) - var msg string - switch first := arg0.(type) { - case string: - // Use the string as a format string - msg = fmt.Sprintf(first, args...) - case func() string: - // Log the closure (no other arguments used) - msg = first() - default: - // Build a format string so that it will be similar to Sprint - msg = fmt.Sprintf(fmt.Sprint(first)+strings.Repeat(" %v", len(args)), args...) - } - log.intLogf(lvl, msg) - return errors.New(msg) -} - -// Error logs a message at the error log level and returns the formatted error, -// See Warn for an explanation of the performance and Debug for an explanation -// of the parameters. -func (log Logger) Error(arg0 interface{}, args ...interface{}) error { - const ( - lvl = ERROR - ) - var msg string - switch first := arg0.(type) { - case string: - // Use the string as a format string - msg = fmt.Sprintf(first, args...) - case func() string: - // Log the closure (no other arguments used) - msg = first() - default: - // Build a format string so that it will be similar to Sprint - msg = fmt.Sprintf(fmt.Sprint(first)+strings.Repeat(" %v", len(args)), args...) - } - log.intLogf(lvl, msg) - return errors.New(msg) -} - -// Critical logs a message at the critical log level and returns the formatted error, -// See Warn for an explanation of the performance and Debug for an explanation -// of the parameters. -func (log Logger) Critical(arg0 interface{}, args ...interface{}) error { - const ( - lvl = CRITICAL - ) - var msg string - switch first := arg0.(type) { - case string: - // Use the string as a format string - msg = fmt.Sprintf(first, args...) - case func() string: - // Log the closure (no other arguments used) - msg = first() - default: - // Build a format string so that it will be similar to Sprint - msg = fmt.Sprintf(fmt.Sprint(first)+strings.Repeat(" %v", len(args)), args...) - } - log.intLogf(lvl, msg) - return errors.New(msg) -} diff --git a/vendor/github.com/alecthomas/log4go/pattlog.go b/vendor/github.com/alecthomas/log4go/pattlog.go deleted file mode 100644 index 82b4e36b..00000000 --- a/vendor/github.com/alecthomas/log4go/pattlog.go +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (C) 2010, Kyle Lemons . All rights reserved. - -package log4go - -import ( - "bytes" - "fmt" - "io" - "strings" -) - -const ( - FORMAT_DEFAULT = "[%D %T] [%L] (%S) %M" - FORMAT_SHORT = "[%t %d] [%L] %M" - FORMAT_ABBREV = "[%L] %M" -) - -type formatCacheType struct { - LastUpdateSeconds int64 - shortTime, shortDate string - longTime, longDate string -} - -var formatCache = &formatCacheType{} - -// Known format codes: -// %T - Time (15:04:05 MST) -// %t - Time (15:04) -// %D - Date (2006/01/02) -// %d - Date (01/02/06) -// %L - Level (FNST, FINE, DEBG, TRAC, WARN, EROR, CRIT) -// %S - Source -// %M - Message -// Ignores unknown formats -// Recommended: "[%D %T] [%L] (%S) %M" -func FormatLogRecord(format string, rec *LogRecord) string { - if rec == nil { - return "" - } - if len(format) == 0 { - return "" - } - - out := bytes.NewBuffer(make([]byte, 0, 64)) - secs := rec.Created.UnixNano() / 1e9 - - cache := *formatCache - if cache.LastUpdateSeconds != secs { - month, day, year := rec.Created.Month(), rec.Created.Day(), rec.Created.Year() - hour, minute, second := rec.Created.Hour(), rec.Created.Minute(), rec.Created.Second() - zone, _ := rec.Created.Zone() - updated := &formatCacheType{ - LastUpdateSeconds: secs, - shortTime: fmt.Sprintf("%02d:%02d", hour, minute), - shortDate: fmt.Sprintf("%02d/%02d/%02d", day, month, year%100), - longTime: fmt.Sprintf("%02d:%02d:%02d %s", hour, minute, second, zone), - longDate: fmt.Sprintf("%04d/%02d/%02d", year, month, day), - } - cache = *updated - formatCache = updated - } - - // Split the string into pieces by % signs - pieces := bytes.Split([]byte(format), []byte{'%'}) - - // Iterate over the pieces, replacing known formats - for i, piece := range pieces { - if i > 0 && len(piece) > 0 { - switch piece[0] { - case 'T': - out.WriteString(cache.longTime) - case 't': - out.WriteString(cache.shortTime) - case 'D': - out.WriteString(cache.longDate) - case 'd': - out.WriteString(cache.shortDate) - case 'L': - out.WriteString(levelStrings[rec.Level]) - case 'S': - out.WriteString(rec.Source) - case 's': - slice := strings.Split(rec.Source, "/") - out.WriteString(slice[len(slice)-1]) - case 'M': - out.WriteString(rec.Message) - } - if len(piece) > 1 { - out.Write(piece[1:]) - } - } else if len(piece) > 0 { - out.Write(piece) - } - } - out.WriteByte('\n') - - return out.String() -} - -// This is the standard writer that prints to standard output. -type FormatLogWriter chan *LogRecord - -// This creates a new FormatLogWriter -func NewFormatLogWriter(out io.Writer, format string) FormatLogWriter { - records := make(FormatLogWriter, LogBufferLength) - go records.run(out, format) - return records -} - -func (w FormatLogWriter) run(out io.Writer, format string) { - for rec := range w { - fmt.Fprint(out, FormatLogRecord(format, rec)) - } -} - -// This is the FormatLogWriter's output method. This will block if the output -// buffer is full. -func (w FormatLogWriter) LogWrite(rec *LogRecord) { - w <- rec -} - -// Close stops the logger from sending messages to standard output. Attempts to -// send log messages to this logger after a Close have undefined behavior. -func (w FormatLogWriter) Close() { - close(w) -} diff --git a/vendor/github.com/alecthomas/log4go/socklog.go b/vendor/github.com/alecthomas/log4go/socklog.go deleted file mode 100644 index 1d224a99..00000000 --- a/vendor/github.com/alecthomas/log4go/socklog.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (C) 2010, Kyle Lemons . All rights reserved. - -package log4go - -import ( - "encoding/json" - "fmt" - "net" - "os" -) - -// This log writer sends output to a socket -type SocketLogWriter chan *LogRecord - -// This is the SocketLogWriter's output method -func (w SocketLogWriter) LogWrite(rec *LogRecord) { - w <- rec -} - -func (w SocketLogWriter) Close() { - close(w) -} - -func NewSocketLogWriter(proto, hostport string) SocketLogWriter { - sock, err := net.Dial(proto, hostport) - if err != nil { - fmt.Fprintf(os.Stderr, "NewSocketLogWriter(%q): %s\n", hostport, err) - return nil - } - - w := SocketLogWriter(make(chan *LogRecord, LogBufferLength)) - - go func() { - defer func() { - if sock != nil && proto == "tcp" { - sock.Close() - } - }() - - for rec := range w { - // Marshall into JSON - js, err := json.Marshal(rec) - if err != nil { - fmt.Fprint(os.Stderr, "SocketLogWriter(%q): %s", hostport, err) - return - } - - _, err = sock.Write(js) - if err != nil { - fmt.Fprint(os.Stderr, "SocketLogWriter(%q): %s", hostport, err) - return - } - } - }() - - return w -} diff --git a/vendor/github.com/alecthomas/log4go/termlog.go b/vendor/github.com/alecthomas/log4go/termlog.go deleted file mode 100644 index 8a941e26..00000000 --- a/vendor/github.com/alecthomas/log4go/termlog.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (C) 2010, Kyle Lemons . All rights reserved. - -package log4go - -import ( - "fmt" - "io" - "os" - "time" -) - -var stdout io.Writer = os.Stdout - -// This is the standard writer that prints to standard output. -type ConsoleLogWriter struct { - format string - w chan *LogRecord -} - -// This creates a new ConsoleLogWriter -func NewConsoleLogWriter() *ConsoleLogWriter { - consoleWriter := &ConsoleLogWriter{ - format: "[%T %D] [%L] (%S) %M", - w: make(chan *LogRecord, LogBufferLength), - } - go consoleWriter.run(stdout) - return consoleWriter -} -func (c *ConsoleLogWriter) SetFormat(format string) { - c.format = format -} -func (c *ConsoleLogWriter) run(out io.Writer) { - for rec := range c.w { - fmt.Fprint(out, FormatLogRecord(c.format, rec)) - } -} - -// This is the ConsoleLogWriter's output method. This will block if the output -// buffer is full. -func (c *ConsoleLogWriter) LogWrite(rec *LogRecord) { - c.w <- rec -} - -// Close stops the logger from sending messages to standard output. Attempts to -// send log messages to this logger after a Close have undefined behavior. -func (c *ConsoleLogWriter) Close() { - close(c.w) - time.Sleep(50 * time.Millisecond) // Try to give console I/O time to complete -} diff --git a/vendor/github.com/alecthomas/log4go/wrapper.go b/vendor/github.com/alecthomas/log4go/wrapper.go deleted file mode 100644 index 2ae222b0..00000000 --- a/vendor/github.com/alecthomas/log4go/wrapper.go +++ /dev/null @@ -1,278 +0,0 @@ -// Copyright (C) 2010, Kyle Lemons . All rights reserved. - -package log4go - -import ( - "errors" - "fmt" - "os" - "strings" -) - -var ( - Global Logger -) - -func init() { - Global = NewDefaultLogger(DEBUG) -} - -// Wrapper for (*Logger).LoadConfiguration -func LoadConfiguration(filename string) { - Global.LoadConfiguration(filename) -} - -// Wrapper for (*Logger).AddFilter -func AddFilter(name string, lvl Level, writer LogWriter) { - Global.AddFilter(name, lvl, writer) -} - -// Wrapper for (*Logger).Close (closes and removes all logwriters) -func Close() { - Global.Close() -} - -func Crash(args ...interface{}) { - if len(args) > 0 { - Global.intLogf(CRITICAL, strings.Repeat(" %v", len(args))[1:], args...) - } - panic(args) -} - -// Logs the given message and crashes the program -func Crashf(format string, args ...interface{}) { - Global.intLogf(CRITICAL, format, args...) - Global.Close() // so that hopefully the messages get logged - panic(fmt.Sprintf(format, args...)) -} - -// Compatibility with `log` -func Exit(args ...interface{}) { - if len(args) > 0 { - Global.intLogf(ERROR, strings.Repeat(" %v", len(args))[1:], args...) - } - Global.Close() // so that hopefully the messages get logged - os.Exit(0) -} - -// Compatibility with `log` -func Exitf(format string, args ...interface{}) { - Global.intLogf(ERROR, format, args...) - Global.Close() // so that hopefully the messages get logged - os.Exit(0) -} - -// Compatibility with `log` -func Stderr(args ...interface{}) { - if len(args) > 0 { - Global.intLogf(ERROR, strings.Repeat(" %v", len(args))[1:], args...) - } -} - -// Compatibility with `log` -func Stderrf(format string, args ...interface{}) { - Global.intLogf(ERROR, format, args...) -} - -// Compatibility with `log` -func Stdout(args ...interface{}) { - if len(args) > 0 { - Global.intLogf(INFO, strings.Repeat(" %v", len(args))[1:], args...) - } -} - -// Compatibility with `log` -func Stdoutf(format string, args ...interface{}) { - Global.intLogf(INFO, format, args...) -} - -// Send a log message manually -// Wrapper for (*Logger).Log -func Log(lvl Level, source, message string) { - Global.Log(lvl, source, message) -} - -// Send a formatted log message easily -// Wrapper for (*Logger).Logf -func Logf(lvl Level, format string, args ...interface{}) { - Global.intLogf(lvl, format, args...) -} - -// Send a closure log message -// Wrapper for (*Logger).Logc -func Logc(lvl Level, closure func() string) { - Global.intLogc(lvl, closure) -} - -// Utility for finest log messages (see Debug() for parameter explanation) -// Wrapper for (*Logger).Finest -func Finest(arg0 interface{}, args ...interface{}) { - const ( - lvl = FINEST - ) - switch first := arg0.(type) { - case string: - // Use the string as a format string - Global.intLogf(lvl, first, args...) - case func() string: - // Log the closure (no other arguments used) - Global.intLogc(lvl, first) - default: - // Build a format string so that it will be similar to Sprint - Global.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...) - } -} - -// Utility for fine log messages (see Debug() for parameter explanation) -// Wrapper for (*Logger).Fine -func Fine(arg0 interface{}, args ...interface{}) { - const ( - lvl = FINE - ) - switch first := arg0.(type) { - case string: - // Use the string as a format string - Global.intLogf(lvl, first, args...) - case func() string: - // Log the closure (no other arguments used) - Global.intLogc(lvl, first) - default: - // Build a format string so that it will be similar to Sprint - Global.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...) - } -} - -// Utility for debug log messages -// When given a string as the first argument, this behaves like Logf but with the DEBUG log level (e.g. the first argument is interpreted as a format for the latter arguments) -// When given a closure of type func()string, this logs the string returned by the closure iff it will be logged. The closure runs at most one time. -// When given anything else, the log message will be each of the arguments formatted with %v and separated by spaces (ala Sprint). -// Wrapper for (*Logger).Debug -func Debug(arg0 interface{}, args ...interface{}) { - const ( - lvl = DEBUG - ) - switch first := arg0.(type) { - case string: - // Use the string as a format string - Global.intLogf(lvl, first, args...) - case func() string: - // Log the closure (no other arguments used) - Global.intLogc(lvl, first) - default: - // Build a format string so that it will be similar to Sprint - Global.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...) - } -} - -// Utility for trace log messages (see Debug() for parameter explanation) -// Wrapper for (*Logger).Trace -func Trace(arg0 interface{}, args ...interface{}) { - const ( - lvl = TRACE - ) - switch first := arg0.(type) { - case string: - // Use the string as a format string - Global.intLogf(lvl, first, args...) - case func() string: - // Log the closure (no other arguments used) - Global.intLogc(lvl, first) - default: - // Build a format string so that it will be similar to Sprint - Global.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...) - } -} - -// Utility for info log messages (see Debug() for parameter explanation) -// Wrapper for (*Logger).Info -func Info(arg0 interface{}, args ...interface{}) { - const ( - lvl = INFO - ) - switch first := arg0.(type) { - case string: - // Use the string as a format string - Global.intLogf(lvl, first, args...) - case func() string: - // Log the closure (no other arguments used) - Global.intLogc(lvl, first) - default: - // Build a format string so that it will be similar to Sprint - Global.intLogf(lvl, fmt.Sprint(arg0)+strings.Repeat(" %v", len(args)), args...) - } -} - -// Utility for warn log messages (returns an error for easy function returns) (see Debug() for parameter explanation) -// These functions will execute a closure exactly once, to build the error message for the return -// Wrapper for (*Logger).Warn -func Warn(arg0 interface{}, args ...interface{}) error { - const ( - lvl = WARNING - ) - switch first := arg0.(type) { - case string: - // Use the string as a format string - Global.intLogf(lvl, first, args...) - return errors.New(fmt.Sprintf(first, args...)) - case func() string: - // Log the closure (no other arguments used) - str := first() - Global.intLogf(lvl, "%s", str) - return errors.New(str) - default: - // Build a format string so that it will be similar to Sprint - Global.intLogf(lvl, fmt.Sprint(first)+strings.Repeat(" %v", len(args)), args...) - return errors.New(fmt.Sprint(first) + fmt.Sprintf(strings.Repeat(" %v", len(args)), args...)) - } - return nil -} - -// Utility for error log messages (returns an error for easy function returns) (see Debug() for parameter explanation) -// These functions will execute a closure exactly once, to build the error message for the return -// Wrapper for (*Logger).Error -func Error(arg0 interface{}, args ...interface{}) error { - const ( - lvl = ERROR - ) - switch first := arg0.(type) { - case string: - // Use the string as a format string - Global.intLogf(lvl, first, args...) - return errors.New(fmt.Sprintf(first, args...)) - case func() string: - // Log the closure (no other arguments used) - str := first() - Global.intLogf(lvl, "%s", str) - return errors.New(str) - default: - // Build a format string so that it will be similar to Sprint - Global.intLogf(lvl, fmt.Sprint(first)+strings.Repeat(" %v", len(args)), args...) - return errors.New(fmt.Sprint(first) + fmt.Sprintf(strings.Repeat(" %v", len(args)), args...)) - } - return nil -} - -// Utility for critical log messages (returns an error for easy function returns) (see Debug() for parameter explanation) -// These functions will execute a closure exactly once, to build the error message for the return -// Wrapper for (*Logger).Critical -func Critical(arg0 interface{}, args ...interface{}) error { - const ( - lvl = CRITICAL - ) - switch first := arg0.(type) { - case string: - // Use the string as a format string - Global.intLogf(lvl, first, args...) - return errors.New(fmt.Sprintf(first, args...)) - case func() string: - // Log the closure (no other arguments used) - str := first() - Global.intLogf(lvl, "%s", str) - return errors.New(str) - default: - // Build a format string so that it will be similar to Sprint - Global.intLogf(lvl, fmt.Sprint(first)+strings.Repeat(" %v", len(args)), args...) - return errors.New(fmt.Sprint(first) + fmt.Sprintf(strings.Repeat(" %v", len(args)), args...)) - } - return nil -} diff --git a/vendor/github.com/matterbridge/discordgo/.travis.yml b/vendor/github.com/bwmarrin/discordgo/.travis.yml similarity index 76% rename from vendor/github.com/matterbridge/discordgo/.travis.yml rename to vendor/github.com/bwmarrin/discordgo/.travis.yml index fe626fcf..2656ae53 100644 --- a/vendor/github.com/matterbridge/discordgo/.travis.yml +++ b/vendor/github.com/bwmarrin/discordgo/.travis.yml @@ -1,12 +1,12 @@ language: go go: - - 1.7.x - - 1.8.x - 1.9.x + - 1.10.x + - 1.11.x install: - go get github.com/bwmarrin/discordgo - go get -v . - - go get -v github.com/golang/lint/golint + - go get -v golang.org/x/lint/golint script: - diff <(gofmt -d .) <(echo -n) - go vet -x ./... diff --git a/vendor/github.com/matterbridge/discordgo/LICENSE b/vendor/github.com/bwmarrin/discordgo/LICENSE similarity index 100% rename from vendor/github.com/matterbridge/discordgo/LICENSE rename to vendor/github.com/bwmarrin/discordgo/LICENSE diff --git a/vendor/github.com/matterbridge/discordgo/README.md b/vendor/github.com/bwmarrin/discordgo/README.md similarity index 92% rename from vendor/github.com/matterbridge/discordgo/README.md rename to vendor/github.com/bwmarrin/discordgo/README.md index acc72bf1..cb5a6659 100644 --- a/vendor/github.com/matterbridge/discordgo/README.md +++ b/vendor/github.com/bwmarrin/discordgo/README.md @@ -1,4 +1,4 @@ -# DiscordGo +# DiscordGo [![GoDoc](https://godoc.org/github.com/bwmarrin/discordgo?status.svg)](https://godoc.org/github.com/bwmarrin/discordgo) [![Go report](http://goreportcard.com/badge/bwmarrin/discordgo)](http://goreportcard.com/report/bwmarrin/discordgo) [![Build Status](https://travis-ci.org/bwmarrin/discordgo.svg?branch=master)](https://travis-ci.org/bwmarrin/discordgo) [![Discord Gophers](https://img.shields.io/badge/Discord%20Gophers-%23discordgo-blue.svg)](https://discord.gg/0f1SbxBZjYoCtNPP) [![Discord API](https://img.shields.io/badge/Discord%20API-%23go_discordgo-blue.svg)](https://discord.gg/0SBTUU1wZTWT6sqd) @@ -15,11 +15,11 @@ to add the official DiscordGo test bot **dgo** to your server. This provides indispensable help to this project. * See [dgVoice](https://github.com/bwmarrin/dgvoice) package for an example of -additional voice helper functions and features for DiscordGo +additional voice helper functions and features for DiscordGo. * See [dca](https://github.com/bwmarrin/dca) for an **experimental** stand alone tool that wraps `ffmpeg` to create opus encoded audio appropriate for use with -Discord (and DiscordGo) +Discord (and DiscordGo). **For help with this package or general Go discussion, please join the [Discord Gophers](https://discord.gg/0f1SbxBZjYq9jLBk) chat server.** @@ -39,9 +39,9 @@ the breaking changes get documented before pushing to master. *So, what should you use?* -If you can accept the constant changing nature of *develop* then it is the +If you can accept the constant changing nature of *develop*, it is the recommended branch to use. Otherwise, if you want to tail behind development -slightly and have a more stable package with documented releases then use *master* +slightly and have a more stable package with documented releases, use *master*. ### Installing @@ -96,10 +96,10 @@ that information in a nice format. ## Examples Below is a list of examples and other projects using DiscordGo. Please submit -an issue if you would like your project added or removed from this list +an issue if you would like your project added or removed from this list. -- [DiscordGo Examples](https://github.com/bwmarrin/discordgo/tree/master/examples) A collection of example programs written with DiscordGo -- [Awesome DiscordGo](https://github.com/bwmarrin/discordgo/wiki/Awesome-DiscordGo) A curated list of high quality projects using DiscordGo +- [DiscordGo Examples](https://github.com/bwmarrin/discordgo/tree/master/examples) - A collection of example programs written with DiscordGo +- [Awesome DiscordGo](https://github.com/bwmarrin/discordgo/wiki/Awesome-DiscordGo) - A curated list of high quality projects using DiscordGo ## Troubleshooting For help with common problems please reference the @@ -114,7 +114,7 @@ Contributions are very welcomed, however please follow the below guidelines. discussed. - Fork the develop branch and make your changes. - Try to match current naming conventions as closely as possible. -- This package is intended to be a low level direct mapping of the Discord API +- This package is intended to be a low level direct mapping of the Discord API, so please avoid adding enhancements outside of that scope without first discussing it. - Create a Pull Request with your changes against the develop branch. @@ -127,4 +127,4 @@ comparison and list of other Discord API libraries. ## Special Thanks -[Chris Rhodes](https://github.com/iopred) - For the DiscordGo logo and tons of PRs +[Chris Rhodes](https://github.com/iopred) - For the DiscordGo logo and tons of PRs. diff --git a/vendor/github.com/matterbridge/discordgo/discord.go b/vendor/github.com/bwmarrin/discordgo/discord.go similarity index 96% rename from vendor/github.com/matterbridge/discordgo/discord.go rename to vendor/github.com/bwmarrin/discordgo/discord.go index 99fda30b..cdac67fe 100644 --- a/vendor/github.com/matterbridge/discordgo/discord.go +++ b/vendor/github.com/bwmarrin/discordgo/discord.go @@ -6,8 +6,8 @@ // license that can be found in the LICENSE file. // This file contains high level helper functions and easy entry points for the -// entire discordgo package. These functions are beling developed and are very -// experimental at this point. They will most likley change so please use the +// entire discordgo package. These functions are being developed and are very +// experimental at this point. They will most likely change so please use the // low level functions if that's a problem. // Package discordgo provides Discord binding for Go @@ -21,7 +21,7 @@ import ( ) // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/) -const VERSION = "0.18.0" +const VERSION = "0.19.0" // ErrMFA will be risen by New when the user has 2FA. var ErrMFA = errors.New("account has 2FA enabled") diff --git a/vendor/github.com/matterbridge/discordgo/endpoints.go b/vendor/github.com/bwmarrin/discordgo/endpoints.go similarity index 83% rename from vendor/github.com/matterbridge/discordgo/endpoints.go rename to vendor/github.com/bwmarrin/discordgo/endpoints.go index 335e224d..b9619089 100644 --- a/vendor/github.com/matterbridge/discordgo/endpoints.go +++ b/vendor/github.com/bwmarrin/discordgo/endpoints.go @@ -11,6 +11,8 @@ package discordgo +import "strconv" + // APIVersion is the Discord API version used for the REST and Websocket API. var APIVersion = "6" @@ -61,14 +63,18 @@ var ( EndpointUser = func(uID string) string { return EndpointUsers + uID } EndpointUserAvatar = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".png" } EndpointUserAvatarAnimated = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".gif" } - EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" } - EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" } - EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID } - EndpointUserGuildSettings = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID + "/settings" } - EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" } - EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" } - EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" } - EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID } + EndpointDefaultUserAvatar = func(uDiscriminator string) string { + uDiscriminatorInt, _ := strconv.Atoi(uDiscriminator) + return EndpointCDN + "embed/avatars/" + strconv.Itoa(uDiscriminatorInt%5) + ".png" + } + EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" } + EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" } + EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID } + EndpointUserGuildSettings = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID + "/settings" } + EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" } + EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" } + EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" } + EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID } EndpointGuild = func(gID string) string { return EndpointGuilds + gID } EndpointGuildChannels = func(gID string) string { return EndpointGuilds + gID + "/channels" } @@ -88,6 +94,9 @@ var ( EndpointGuildIcon = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".png" } EndpointGuildSplash = func(gID, hash string) string { return EndpointCDNSplashes + gID + "/" + hash + ".png" } EndpointGuildWebhooks = func(gID string) string { return EndpointGuilds + gID + "/webhooks" } + EndpointGuildAuditLogs = func(gID string) string { return EndpointGuilds + gID + "/audit-logs" } + EndpointGuildEmojis = func(gID string) string { return EndpointGuilds + gID + "/emojis" } + EndpointGuildEmoji = func(gID, eID string) string { return EndpointGuilds + gID + "/emojis/" + eID } EndpointChannel = func(cID string) string { return EndpointChannels + cID } EndpointChannelPermissions = func(cID string) string { return EndpointChannels + cID + "/permissions" } @@ -127,7 +136,8 @@ var ( EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" } - EndpointEmoji = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".png" } + EndpointEmoji = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".png" } + EndpointEmojiAnimated = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".gif" } EndpointOauth2 = EndpointAPI + "oauth2/" EndpointApplications = EndpointOauth2 + "applications" diff --git a/vendor/github.com/matterbridge/discordgo/event.go b/vendor/github.com/bwmarrin/discordgo/event.go similarity index 92% rename from vendor/github.com/matterbridge/discordgo/event.go rename to vendor/github.com/bwmarrin/discordgo/event.go index bba396cb..97cc00a2 100644 --- a/vendor/github.com/matterbridge/discordgo/event.go +++ b/vendor/github.com/bwmarrin/discordgo/event.go @@ -98,7 +98,9 @@ func (s *Session) addEventHandlerOnce(eventHandler EventHandler) func() { // AddHandler allows you to add an event handler that will be fired anytime // the Discord WSAPI event that matches the function fires. -// events.go contains all the Discord WSAPI events that can be fired. +// The first parameter is a *Session, and the second parameter is a pointer +// to a struct corresponding to the event for which you want to listen. +// // eg: // Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { // }) @@ -106,6 +108,13 @@ func (s *Session) addEventHandlerOnce(eventHandler EventHandler) func() { // or: // Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) { // }) +// +// List of events can be found at this page, with corresponding names in the +// library for each event: https://discordapp.com/developers/docs/topics/gateway#event-names +// There are also synthetic events fired by the library internally which are +// available for handling, like Connect, Disconnect, and RateLimit. +// events.go contains all of the Discord WSAPI and synthetic events that can be handled. +// // The return value of this method is a function, that when called will remove the // event handler. func (s *Session) AddHandler(handler interface{}) func() { diff --git a/vendor/github.com/matterbridge/discordgo/eventhandlers.go b/vendor/github.com/bwmarrin/discordgo/eventhandlers.go similarity index 97% rename from vendor/github.com/matterbridge/discordgo/eventhandlers.go rename to vendor/github.com/bwmarrin/discordgo/eventhandlers.go index 5cc157de..d2b9a98b 100644 --- a/vendor/github.com/matterbridge/discordgo/eventhandlers.go +++ b/vendor/github.com/bwmarrin/discordgo/eventhandlers.go @@ -50,6 +50,7 @@ const ( userUpdateEventType = "USER_UPDATE" voiceServerUpdateEventType = "VOICE_SERVER_UPDATE" voiceStateUpdateEventType = "VOICE_STATE_UPDATE" + webhooksUpdateEventType = "WEBHOOKS_UPDATE" ) // channelCreateEventHandler is an event handler for ChannelCreate events. @@ -892,6 +893,26 @@ func (eh voiceStateUpdateEventHandler) Handle(s *Session, i interface{}) { } } +// webhooksUpdateEventHandler is an event handler for WebhooksUpdate events. +type webhooksUpdateEventHandler func(*Session, *WebhooksUpdate) + +// Type returns the event type for WebhooksUpdate events. +func (eh webhooksUpdateEventHandler) Type() string { + return webhooksUpdateEventType +} + +// New returns a new instance of WebhooksUpdate. +func (eh webhooksUpdateEventHandler) New() interface{} { + return &WebhooksUpdate{} +} + +// Handle is the handler for WebhooksUpdate events. +func (eh webhooksUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*WebhooksUpdate); ok { + eh(s, t) + } +} + func handlerForInterface(handler interface{}) EventHandler { switch v := handler.(type) { case func(*Session, interface{}): @@ -982,6 +1003,8 @@ func handlerForInterface(handler interface{}) EventHandler { return voiceServerUpdateEventHandler(v) case func(*Session, *VoiceStateUpdate): return voiceStateUpdateEventHandler(v) + case func(*Session, *WebhooksUpdate): + return webhooksUpdateEventHandler(v) } return nil @@ -1027,4 +1050,5 @@ func init() { registerInterfaceProvider(userUpdateEventHandler(nil)) registerInterfaceProvider(voiceServerUpdateEventHandler(nil)) registerInterfaceProvider(voiceStateUpdateEventHandler(nil)) + registerInterfaceProvider(webhooksUpdateEventHandler(nil)) } diff --git a/vendor/github.com/matterbridge/discordgo/events.go b/vendor/github.com/bwmarrin/discordgo/events.go similarity index 95% rename from vendor/github.com/matterbridge/discordgo/events.go rename to vendor/github.com/bwmarrin/discordgo/events.go index c78fbdd2..c4fb5205 100644 --- a/vendor/github.com/matterbridge/discordgo/events.go +++ b/vendor/github.com/bwmarrin/discordgo/events.go @@ -70,6 +70,7 @@ type ChannelDelete struct { type ChannelPinsUpdate struct { LastPinTimestamp string `json:"last_pin_timestamp"` ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id,omitempty"` } // GuildCreate is the data for a GuildCreate event. @@ -212,6 +213,7 @@ type RelationshipRemove struct { type TypingStart struct { UserID string `json:"user_id"` ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id,omitempty"` Timestamp int `json:"timestamp"` } @@ -250,4 +252,11 @@ type VoiceStateUpdate struct { type MessageDeleteBulk struct { Messages []string `json:"ids"` ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id"` +} + +// WebhooksUpdate is the data for a WebhooksUpdate event +type WebhooksUpdate struct { + GuildID string `json:"guild_id"` + ChannelID string `json:"channel_id"` } diff --git a/vendor/github.com/bwmarrin/discordgo/go.mod b/vendor/github.com/bwmarrin/discordgo/go.mod new file mode 100644 index 00000000..2ff88680 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/go.mod @@ -0,0 +1,6 @@ +module github.com/bwmarrin/discordgo + +require ( + github.com/gorilla/websocket v1.4.0 + golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 +) diff --git a/vendor/github.com/bwmarrin/discordgo/go.sum b/vendor/github.com/bwmarrin/discordgo/go.sum new file mode 100644 index 00000000..a86b0501 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/go.sum @@ -0,0 +1,4 @@ +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/vendor/github.com/matterbridge/discordgo/logging.go b/vendor/github.com/bwmarrin/discordgo/logging.go similarity index 100% rename from vendor/github.com/matterbridge/discordgo/logging.go rename to vendor/github.com/bwmarrin/discordgo/logging.go diff --git a/vendor/github.com/matterbridge/discordgo/message.go b/vendor/github.com/bwmarrin/discordgo/message.go similarity index 80% rename from vendor/github.com/matterbridge/discordgo/message.go rename to vendor/github.com/bwmarrin/discordgo/message.go index 4fd468fd..2b609920 100644 --- a/vendor/github.com/matterbridge/discordgo/message.go +++ b/vendor/github.com/bwmarrin/discordgo/message.go @@ -32,20 +32,59 @@ const ( // A Message stores all data related to a specific Discord message. type Message struct { - ID string `json:"id"` - ChannelID string `json:"channel_id"` - Content string `json:"content"` - Timestamp Timestamp `json:"timestamp"` - EditedTimestamp Timestamp `json:"edited_timestamp"` - MentionRoles []string `json:"mention_roles"` - Tts bool `json:"tts"` - MentionEveryone bool `json:"mention_everyone"` - Author *User `json:"author"` - Attachments []*MessageAttachment `json:"attachments"` - Embeds []*MessageEmbed `json:"embeds"` - Mentions []*User `json:"mentions"` - Reactions []*MessageReactions `json:"reactions"` - Type MessageType `json:"type"` + // The ID of the message. + ID string `json:"id"` + + // The ID of the channel in which the message was sent. + ChannelID string `json:"channel_id"` + + // The ID of the guild in which the message was sent. + GuildID string `json:"guild_id,omitempty"` + + // The content of the message. + Content string `json:"content"` + + // The time at which the messsage was sent. + // CAUTION: this field may be removed in a + // future API version; it is safer to calculate + // the creation time via the ID. + Timestamp Timestamp `json:"timestamp"` + + // The time at which the last edit of the message + // occurred, if it has been edited. + EditedTimestamp Timestamp `json:"edited_timestamp"` + + // The roles mentioned in the message. + MentionRoles []string `json:"mention_roles"` + + // Whether the message is text-to-speech. + Tts bool `json:"tts"` + + // Whether the message mentions everyone. + MentionEveryone bool `json:"mention_everyone"` + + // The author of the message. This is not guaranteed to be a + // valid user (webhook-sent messages do not possess a full author). + Author *User `json:"author"` + + // A list of attachments present in the message. + Attachments []*MessageAttachment `json:"attachments"` + + // A list of embeds present in the message. Multiple + // embeds can currently only be sent by webhooks. + Embeds []*MessageEmbed `json:"embeds"` + + // A list of users mentioned in the message. + Mentions []*User `json:"mentions"` + + // A list of reactions to the message. + Reactions []*MessageReactions `json:"reactions"` + + // The type of the message. + Type MessageType `json:"type"` + + // The webhook ID of the message, if it was generated by a webhook + WebhookID string `json:"webhook_id"` } // File stores info about files you e.g. send in messages. diff --git a/vendor/github.com/matterbridge/discordgo/mkdocs.yml b/vendor/github.com/bwmarrin/discordgo/mkdocs.yml similarity index 100% rename from vendor/github.com/matterbridge/discordgo/mkdocs.yml rename to vendor/github.com/bwmarrin/discordgo/mkdocs.yml diff --git a/vendor/github.com/matterbridge/discordgo/oauth2.go b/vendor/github.com/bwmarrin/discordgo/oauth2.go similarity index 100% rename from vendor/github.com/matterbridge/discordgo/oauth2.go rename to vendor/github.com/bwmarrin/discordgo/oauth2.go diff --git a/vendor/github.com/matterbridge/discordgo/ratelimit.go b/vendor/github.com/bwmarrin/discordgo/ratelimit.go similarity index 100% rename from vendor/github.com/matterbridge/discordgo/ratelimit.go rename to vendor/github.com/bwmarrin/discordgo/ratelimit.go diff --git a/vendor/github.com/matterbridge/discordgo/restapi.go b/vendor/github.com/bwmarrin/discordgo/restapi.go similarity index 89% rename from vendor/github.com/matterbridge/discordgo/restapi.go rename to vendor/github.com/bwmarrin/discordgo/restapi.go index 5dc0467f..84a2a31e 100644 --- a/vendor/github.com/matterbridge/discordgo/restapi.go +++ b/vendor/github.com/bwmarrin/discordgo/restapi.go @@ -38,6 +38,7 @@ var ( ErrPruneDaysBounds = errors.New("the number of days should be more than or equal to 1") ErrGuildNoIcon = errors.New("guild does not have an icon set") ErrGuildNoSplash = errors.New("guild does not have a splash set") + ErrUnauthorized = errors.New("HTTP request was unauthorized. This could be because the provided token was not a bot token. Please add \"Bot \" to the start of your token. https://discordapp.com/developers/docs/reference#authentication-example-bot-token-authorization-header") ) // Request is the same as RequestWithBucketID but the bucket id is the same as the urlStr @@ -89,7 +90,7 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b req.Header.Set("Content-Type", contentType) // TODO: Make a configurable static variable. - req.Header.Set("User-Agent", fmt.Sprintf("DiscordBot (https://github.com/bwmarrin/discordgo, v%s)", VERSION)) + req.Header.Set("User-Agent", "DiscordBot (https://github.com/bwmarrin/discordgo, v"+VERSION+")") if s.Debug { for k, v := range req.Header { @@ -129,13 +130,9 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b } switch resp.StatusCode { - case http.StatusOK: case http.StatusCreated: case http.StatusNoContent: - - // TODO check for 401 response, invalidate token if we get one. - case http.StatusBadGateway: // Retry sending request if possible if sequence < s.MaxRestRetries { @@ -145,7 +142,6 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b } else { err = fmt.Errorf("Exceeded Max retries HTTP %s, %s", resp.Status, response) } - case 429: // TOO MANY REQUESTS - Rate limiting rl := TooManyRequests{} err = json.Unmarshal(response, &rl) @@ -161,7 +157,12 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b // this method can cause longer delays than required response, err = s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucketObject(bucket), sequence) - + case http.StatusUnauthorized: + if strings.Index(s.Token, "Bot ") != 0 { + s.log(LogInformational, ErrUnauthorized.Error()) + err = ErrUnauthorized + } + fallthrough default: // Error condition err = newRestError(req, resp, response) } @@ -249,7 +250,7 @@ func (s *Session) Register(username string) (token string, err error) { // even use. func (s *Session) Logout() (err error) { - // _, err = s.Request("POST", LOGOUT, fmt.Sprintf(`{"token": "%s"}`, s.Token)) + // _, err = s.Request("POST", LOGOUT, `{"token": "` + s.Token + `"}`) if s.Token == "" { return @@ -361,6 +362,21 @@ func (s *Session) UserUpdateStatus(status Status) (st *Settings, err error) { return } +// UserConnections returns the user's connections +func (s *Session) UserConnections() (conn []*UserConnection, err error) { + response, err := s.RequestWithBucketID("GET", EndpointUserConnections("@me"), nil, EndpointUserConnections("@me")) + if err != nil { + return nil, err + } + + err = unmarshal(response, &conn) + if err != nil { + return + } + + return +} + // UserChannels returns an array of Channel structures for all private // channels. func (s *Session) UserChannels() (st []*Channel, err error) { @@ -412,7 +428,7 @@ func (s *Session) UserGuilds(limit int, beforeID, afterID string) (st []*UserGui uri := EndpointUserGuilds("@me") if len(v) > 0 { - uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + uri += "?" + v.Encode() } body, err := s.RequestWithBucketID("GET", uri, nil, EndpointUserGuilds("")) @@ -565,7 +581,7 @@ func (s *Session) Guild(guildID string) (st *Guild, err error) { if s.StateEnabled { // Attempt to grab the guild from State first. st, err = s.State.Guild(guildID) - if err == nil { + if err == nil && !st.Unavailable { return } } @@ -735,7 +751,7 @@ func (s *Session) GuildMembers(guildID string, after string, limit int) (st []*M } if len(v) > 0 { - uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + uri += "?" + v.Encode() } body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildMembers(guildID)) @@ -761,6 +777,32 @@ func (s *Session) GuildMember(guildID, userID string) (st *Member, err error) { return } +// GuildMemberAdd force joins a user to the guild. +// accessToken : Valid access_token for the user. +// guildID : The ID of a Guild. +// userID : The ID of a User. +// nick : Value to set users nickname to +// roles : A list of role ID's to set on the member. +// mute : If the user is muted. +// deaf : If the user is deafened. +func (s *Session) GuildMemberAdd(accessToken, guildID, userID, nick string, roles []string, mute, deaf bool) (err error) { + + data := struct { + AccessToken string `json:"access_token"` + Nick string `json:"nick,omitempty"` + Roles []string `json:"roles,omitempty"` + Mute bool `json:"mute,omitempty"` + Deaf bool `json:"deaf,omitempty"` + }{accessToken, nick, roles, mute, deaf} + + _, err = s.RequestWithBucketID("PUT", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) + if err != nil { + return err + } + + return err +} + // GuildMemberDelete removes the given user from the given guild. // guildID : The ID of a Guild. // userID : The ID of a User @@ -877,17 +919,22 @@ func (s *Session) GuildChannels(guildID string) (st []*Channel, err error) { return } -// GuildChannelCreate creates a new channel in the given guild -// guildID : The ID of a Guild. -// name : Name of the channel (2-100 chars length) -// ctype : Tpye of the channel (voice or text) -func (s *Session) GuildChannelCreate(guildID, name, ctype string) (st *Channel, err error) { - - data := struct { - Name string `json:"name"` - Type string `json:"type"` - }{name, ctype} +// GuildChannelCreateData is provided to GuildChannelCreateComplex +type GuildChannelCreateData struct { + Name string `json:"name"` + Type ChannelType `json:"type"` + Topic string `json:"topic,omitempty"` + Bitrate int `json:"bitrate,omitempty"` + UserLimit int `json:"user_limit,omitempty"` + PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"` + ParentID string `json:"parent_id,omitempty"` + NSFW bool `json:"nsfw,omitempty"` +} +// GuildChannelCreateComplex creates a new channel in the given guild +// guildID : The ID of a Guild +// data : A data struct describing the new Channel, Name and Type are mandatory, other fields depending on the type +func (s *Session) GuildChannelCreateComplex(guildID string, data GuildChannelCreateData) (st *Channel, err error) { body, err := s.RequestWithBucketID("POST", EndpointGuildChannels(guildID), data, EndpointGuildChannels(guildID)) if err != nil { return @@ -897,12 +944,33 @@ func (s *Session) GuildChannelCreate(guildID, name, ctype string) (st *Channel, return } +// GuildChannelCreate creates a new channel in the given guild +// guildID : The ID of a Guild. +// name : Name of the channel (2-100 chars length) +// ctype : Type of the channel +func (s *Session) GuildChannelCreate(guildID, name string, ctype ChannelType) (st *Channel, err error) { + return s.GuildChannelCreateComplex(guildID, GuildChannelCreateData{ + Name: name, + Type: ctype, + }) +} + // GuildChannelsReorder updates the order of channels in a guild // guildID : The ID of a Guild. // channels : Updated channels. func (s *Session) GuildChannelsReorder(guildID string, channels []*Channel) (err error) { - _, err = s.RequestWithBucketID("PATCH", EndpointGuildChannels(guildID), channels, EndpointGuildChannels(guildID)) + data := make([]struct { + ID string `json:"id"` + Position int `json:"position"` + }, len(channels)) + + for i, c := range channels { + data[i].ID = c.ID + data[i].Position = c.Position + } + + _, err = s.RequestWithBucketID("PATCH", EndpointGuildChannels(guildID), data, EndpointGuildChannels(guildID)) return } @@ -1021,7 +1089,7 @@ func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, er Pruned uint32 `json:"pruned"` }{} - uri := EndpointGuildPrune(guildID) + fmt.Sprintf("?days=%d", days) + uri := EndpointGuildPrune(guildID) + "?days=" + strconv.FormatUint(uint64(days), 10) body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildPrune(guildID)) if err != nil { return @@ -1075,7 +1143,7 @@ func (s *Session) GuildPrune(guildID string, days uint32) (count uint32, err err // GuildIntegrations returns an array of Integrations for a guild. // guildID : The ID of a Guild. -func (s *Session) GuildIntegrations(guildID string) (st []*GuildIntegration, err error) { +func (s *Session) GuildIntegrations(guildID string) (st []*Integration, err error) { body, err := s.RequestWithBucketID("GET", EndpointGuildIntegrations(guildID), nil, EndpointGuildIntegrations(guildID)) if err != nil { @@ -1206,6 +1274,94 @@ func (s *Session) GuildEmbedEdit(guildID string, enabled bool, channelID string) return } +// GuildAuditLog returns the audit log for a Guild. +// guildID : The ID of a Guild. +// userID : If provided the log will be filtered for the given ID. +// beforeID : If provided all log entries returned will be before the given ID. +// actionType : If provided the log will be filtered for the given Action Type. +// limit : The number messages that can be returned. (default 50, min 1, max 100) +func (s *Session) GuildAuditLog(guildID, userID, beforeID string, actionType, limit int) (st *GuildAuditLog, err error) { + + uri := EndpointGuildAuditLogs(guildID) + + v := url.Values{} + if userID != "" { + v.Set("user_id", userID) + } + if beforeID != "" { + v.Set("before", beforeID) + } + if actionType > 0 { + v.Set("action_type", strconv.Itoa(actionType)) + } + if limit > 0 { + v.Set("limit", strconv.Itoa(limit)) + } + if len(v) > 0 { + uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + } + + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildAuditLogs(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildEmojiCreate creates a new emoji +// guildID : The ID of a Guild. +// name : The Name of the Emoji. +// image : The base64 encoded emoji image, has to be smaller than 256KB. +// roles : The roles for which this emoji will be whitelisted, can be nil. +func (s *Session) GuildEmojiCreate(guildID, name, image string, roles []string) (emoji *Emoji, err error) { + + data := struct { + Name string `json:"name"` + Image string `json:"image"` + Roles []string `json:"roles,omitempty"` + }{name, image, roles} + + body, err := s.RequestWithBucketID("POST", EndpointGuildEmojis(guildID), data, EndpointGuildEmojis(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &emoji) + return +} + +// GuildEmojiEdit modifies an emoji +// guildID : The ID of a Guild. +// emojiID : The ID of an Emoji. +// name : The Name of the Emoji. +// roles : The roles for which this emoji will be whitelisted, can be nil. +func (s *Session) GuildEmojiEdit(guildID, emojiID, name string, roles []string) (emoji *Emoji, err error) { + + data := struct { + Name string `json:"name"` + Roles []string `json:"roles,omitempty"` + }{name, roles} + + body, err := s.RequestWithBucketID("PATCH", EndpointGuildEmoji(guildID, emojiID), data, EndpointGuildEmojis(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &emoji) + return +} + +// GuildEmojiDelete deletes an Emoji. +// guildID : The ID of a Guild. +// emojiID : The ID of an Emoji. +func (s *Session) GuildEmojiDelete(guildID, emojiID string) (err error) { + + _, err = s.RequestWithBucketID("DELETE", EndpointGuildEmoji(guildID, emojiID), nil, EndpointGuildEmojis(guildID)) + return +} + // ------------------------------------------------------------------------------------------------ // Functions specific to Discord Channels // ------------------------------------------------------------------------------------------------ @@ -1291,7 +1447,7 @@ func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID v.Set("around", aroundID) } if len(v) > 0 { - uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + uri += "?" + v.Encode() } body, err := s.RequestWithBucketID("GET", uri, nil, EndpointChannelMessages(channelID)) @@ -1586,7 +1742,8 @@ func (s *Session) ChannelInviteCreate(channelID string, i Invite) (st *Invite, e MaxAge int `json:"max_age"` MaxUses int `json:"max_uses"` Temporary bool `json:"temporary"` - }{i.MaxAge, i.MaxUses, i.Temporary} + Unique bool `json:"unique"` + }{i.MaxAge, i.MaxUses, i.Temporary, i.Unique} body, err := s.RequestWithBucketID("POST", EndpointChannelInvites(channelID), data, EndpointChannelInvites(channelID)) if err != nil { @@ -1638,6 +1795,19 @@ func (s *Session) Invite(inviteID string) (st *Invite, err error) { return } +// InviteWithCounts returns an Invite structure of the given invite including approximate member counts +// inviteID : The invite code +func (s *Session) InviteWithCounts(inviteID string) (st *Invite, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointInvite(inviteID)+"?with_counts=true", nil, EndpointInvite("")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + // InviteDelete deletes an existing invite // inviteID : the code of an invite func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) { @@ -1830,12 +2000,13 @@ func (s *Session) WebhookWithToken(webhookID, token string) (st *Webhook, err er // webhookID: The ID of a webhook. // name : The name of the webhook. // avatar : The avatar of the webhook. -func (s *Session) WebhookEdit(webhookID, name, avatar string) (st *Role, err error) { +func (s *Session) WebhookEdit(webhookID, name, avatar, channelID string) (st *Role, err error) { data := struct { - Name string `json:"name,omitempty"` - Avatar string `json:"avatar,omitempty"` - }{name, avatar} + Name string `json:"name,omitempty"` + Avatar string `json:"avatar,omitempty"` + ChannelID string `json:"channel_id,omitempty"` + }{name, avatar, channelID} body, err := s.RequestWithBucketID("PATCH", EndpointWebhook(webhookID), data, EndpointWebhooks) if err != nil { @@ -1956,7 +2127,7 @@ func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit i } if len(v) > 0 { - uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + uri += "?" + v.Encode() } body, err := s.RequestWithBucketID("GET", uri, nil, EndpointMessageReaction(channelID, "", "", "")) diff --git a/vendor/github.com/matterbridge/discordgo/state.go b/vendor/github.com/bwmarrin/discordgo/state.go similarity index 96% rename from vendor/github.com/matterbridge/discordgo/state.go rename to vendor/github.com/bwmarrin/discordgo/state.go index 8158708b..e6f08c73 100644 --- a/vendor/github.com/matterbridge/discordgo/state.go +++ b/vendor/github.com/bwmarrin/discordgo/state.go @@ -32,6 +32,7 @@ type State struct { sync.RWMutex Ready + // MaxMessageCount represents how many messages per channel the state will store. MaxMessageCount int TrackChannels bool TrackEmojis bool @@ -98,6 +99,9 @@ func (s *State) GuildAdd(guild *Guild) error { if g, ok := s.guildMap[guild.ID]; ok { // We are about to replace `g` in the state with `guild`, but first we need to // make sure we preserve any fields that the `guild` doesn't contain from `g`. + if guild.MemberCount == 0 { + guild.MemberCount = g.MemberCount + } if guild.Roles == nil { guild.Roles = g.Roles } @@ -299,7 +303,12 @@ func (s *State) MemberAdd(member *Member) error { members[member.User.ID] = member guild.Members = append(guild.Members, member) } else { - *m = *member // Update the actual data, which will also update the member pointer in the slice + // We are about to replace `m` in the state with `member`, but first we need to + // make sure we preserve any fields that the `member` doesn't contain from `m`. + if member.JoinedAt == "" { + member.JoinedAt = m.JoinedAt + } + *m = *member } return nil @@ -607,7 +616,7 @@ func (s *State) EmojisAdd(guildID string, emojis []*Emoji) error { // MessageAdd adds a message to the current world state, or updates it if it exists. // If the channel cannot be found, the message is discarded. -// Messages are kept in state up to s.MaxMessageCount +// Messages are kept in state up to s.MaxMessageCount per channel. func (s *State) MessageAdd(message *Message) error { if s == nil { return ErrNilState @@ -805,6 +814,14 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) { case *GuildDelete: err = s.GuildRemove(t.Guild) case *GuildMemberAdd: + // Updates the MemberCount of the guild. + guild, err := s.Guild(t.Member.GuildID) + if err != nil { + return err + } + guild.MemberCount++ + + // Caches member if tracking is enabled. if s.TrackMembers { err = s.MemberAdd(t.Member) } @@ -813,6 +830,14 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) { err = s.MemberAdd(t.Member) } case *GuildMemberRemove: + // Updates the MemberCount of the guild. + guild, err := s.Guild(t.Member.GuildID) + if err != nil { + return err + } + guild.MemberCount-- + + // Removes member from the cache if tracking is enabled. if s.TrackMembers { err = s.MemberRemove(t.Member) } diff --git a/vendor/github.com/matterbridge/discordgo/structs.go b/vendor/github.com/bwmarrin/discordgo/structs.go similarity index 62% rename from vendor/github.com/matterbridge/discordgo/structs.go rename to vendor/github.com/bwmarrin/discordgo/structs.go index 19d2bad7..4465ec52 100644 --- a/vendor/github.com/matterbridge/discordgo/structs.go +++ b/vendor/github.com/bwmarrin/discordgo/structs.go @@ -13,6 +13,7 @@ package discordgo import ( "encoding/json" + "fmt" "net/http" "sync" "time" @@ -84,6 +85,9 @@ type Session struct { // Stores the last HeartbeatAck that was recieved (in UTC) LastHeartbeatAck time.Time + // Stores the last Heartbeat sent (in UTC) + LastHeartbeatSent time.Time + // used to deal with rate limits Ratelimiter *RateLimiter @@ -111,6 +115,37 @@ type Session struct { wsMutex sync.Mutex } +// UserConnection is a Connection returned from the UserConnections endpoint +type UserConnection struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Revoked bool `json:"revoked"` + Integrations []*Integration `json:"integrations"` +} + +// Integration stores integration information +type Integration struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Enabled bool `json:"enabled"` + Syncing bool `json:"syncing"` + RoleID string `json:"role_id"` + ExpireBehavior int `json:"expire_behavior"` + ExpireGracePeriod int `json:"expire_grace_period"` + User *User `json:"user"` + Account IntegrationAccount `json:"account"` + SyncedAt Timestamp `json:"synced_at"` +} + +// IntegrationAccount is integration account information +// sent by the UserConnections endpoint +type IntegrationAccount struct { + ID string `json:"id"` + Name string `json:"name"` +} + // A VoiceRegion stores data for a specific voice region server. type VoiceRegion struct { ID string `json:"id"` @@ -145,6 +180,10 @@ type Invite struct { Revoked bool `json:"revoked"` Temporary bool `json:"temporary"` Unique bool `json:"unique"` + + // will only be filled when using InviteWithCounts + ApproximatePresenceCount int `json:"approximate_presence_count"` + ApproximateMemberCount int `json:"approximate_member_count"` } // ChannelType is the type of a Channel @@ -161,22 +200,61 @@ const ( // A Channel holds all data related to an individual Discord channel. type Channel struct { - ID string `json:"id"` - GuildID string `json:"guild_id"` - Name string `json:"name"` - Topic string `json:"topic"` - Type ChannelType `json:"type"` - LastMessageID string `json:"last_message_id"` - NSFW bool `json:"nsfw"` - Position int `json:"position"` - Bitrate int `json:"bitrate"` - Recipients []*User `json:"recipients"` - Messages []*Message `json:"-"` + // The ID of the channel. + ID string `json:"id"` + + // The ID of the guild to which the channel belongs, if it is in a guild. + // Else, this ID is empty (e.g. DM channels). + GuildID string `json:"guild_id"` + + // The name of the channel. + Name string `json:"name"` + + // The topic of the channel. + Topic string `json:"topic"` + + // The type of the channel. + Type ChannelType `json:"type"` + + // The ID of the last message sent in the channel. This is not + // guaranteed to be an ID of a valid message. + LastMessageID string `json:"last_message_id"` + + // Whether the channel is marked as NSFW. + NSFW bool `json:"nsfw"` + + // Icon of the group DM channel. + Icon string `json:"icon"` + + // The position of the channel, used for sorting in client. + Position int `json:"position"` + + // The bitrate of the channel, if it is a voice channel. + Bitrate int `json:"bitrate"` + + // The recipients of the channel. This is only populated in DM channels. + Recipients []*User `json:"recipients"` + + // The messages in the channel. This is only present in state-cached channels, + // and State.MaxMessageCount must be non-zero. + Messages []*Message `json:"-"` + + // A list of permission overwrites present for the channel. PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"` - ParentID string `json:"parent_id"` + + // The user limit of the voice channel. + UserLimit int `json:"user_limit"` + + // The ID of the parent channel, if the channel is under a category + ParentID string `json:"parent_id"` } -// A ChannelEdit holds Channel Feild data for a channel edit. +// Mention returns a string which mentions the channel +func (c *Channel) Mention() string { + return fmt.Sprintf("<#%s>", c.ID) +} + +// A ChannelEdit holds Channel Field data for a channel edit. type ChannelEdit struct { Name string `json:"name,omitempty"` Topic string `json:"topic,omitempty"` @@ -186,6 +264,7 @@ type ChannelEdit struct { UserLimit int `json:"user_limit,omitempty"` PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"` ParentID string `json:"parent_id,omitempty"` + RateLimitPerUser int `json:"rate_limit_per_user,omitempty"` } // A PermissionOverwrite holds permission overwrite data for a Channel @@ -206,6 +285,19 @@ type Emoji struct { Animated bool `json:"animated"` } +// MessageFormat returns a correctly formatted Emoji for use in Message content and embeds +func (e *Emoji) MessageFormat() string { + if e.ID != "" && e.Name != "" { + if e.Animated { + return "" + } + + return "<:" + e.APIName() + ">" + } + + return e.APIName() +} + // APIName returns an correctly formatted API name for use in the MessageReactions endpoints. func (e *Emoji) APIName() string { if e.ID != "" && e.Name != "" { @@ -228,31 +320,129 @@ const ( VerificationLevelHigh ) +// ExplicitContentFilterLevel type definition +type ExplicitContentFilterLevel int + +// Constants for ExplicitContentFilterLevel levels from 0 to 2 inclusive +const ( + ExplicitContentFilterDisabled ExplicitContentFilterLevel = iota + ExplicitContentFilterMembersWithoutRoles + ExplicitContentFilterAllMembers +) + +// MfaLevel type definition +type MfaLevel int + +// Constants for MfaLevel levels from 0 to 1 inclusive +const ( + MfaLevelNone MfaLevel = iota + MfaLevelElevated +) + // A Guild holds all data related to a specific Discord Guild. Guilds are also // sometimes referred to as Servers in the Discord client. type Guild struct { - ID string `json:"id"` - Name string `json:"name"` - Icon string `json:"icon"` - Region string `json:"region"` - AfkChannelID string `json:"afk_channel_id"` - EmbedChannelID string `json:"embed_channel_id"` - OwnerID string `json:"owner_id"` - JoinedAt Timestamp `json:"joined_at"` - Splash string `json:"splash"` - AfkTimeout int `json:"afk_timeout"` - MemberCount int `json:"member_count"` - VerificationLevel VerificationLevel `json:"verification_level"` - EmbedEnabled bool `json:"embed_enabled"` - Large bool `json:"large"` // ?? - DefaultMessageNotifications int `json:"default_message_notifications"` - Roles []*Role `json:"roles"` - Emojis []*Emoji `json:"emojis"` - Members []*Member `json:"members"` - Presences []*Presence `json:"presences"` - Channels []*Channel `json:"channels"` - VoiceStates []*VoiceState `json:"voice_states"` - Unavailable bool `json:"unavailable"` + // The ID of the guild. + ID string `json:"id"` + + // The name of the guild. (2–100 characters) + Name string `json:"name"` + + // The hash of the guild's icon. Use Session.GuildIcon + // to retrieve the icon itself. + Icon string `json:"icon"` + + // The voice region of the guild. + Region string `json:"region"` + + // The ID of the AFK voice channel. + AfkChannelID string `json:"afk_channel_id"` + + // The ID of the embed channel ID, used for embed widgets. + EmbedChannelID string `json:"embed_channel_id"` + + // The user ID of the owner of the guild. + OwnerID string `json:"owner_id"` + + // The time at which the current user joined the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + JoinedAt Timestamp `json:"joined_at"` + + // The hash of the guild's splash. + Splash string `json:"splash"` + + // The timeout, in seconds, before a user is considered AFK in voice. + AfkTimeout int `json:"afk_timeout"` + + // The number of members in the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + MemberCount int `json:"member_count"` + + // The verification level required for the guild. + VerificationLevel VerificationLevel `json:"verification_level"` + + // Whether the guild has embedding enabled. + EmbedEnabled bool `json:"embed_enabled"` + + // Whether the guild is considered large. This is + // determined by a member threshold in the identify packet, + // and is currently hard-coded at 250 members in the library. + Large bool `json:"large"` + + // The default message notification setting for the guild. + // 0 == all messages, 1 == mentions only. + DefaultMessageNotifications int `json:"default_message_notifications"` + + // A list of roles in the guild. + Roles []*Role `json:"roles"` + + // A list of the custom emojis present in the guild. + Emojis []*Emoji `json:"emojis"` + + // A list of the members in the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + Members []*Member `json:"members"` + + // A list of partial presence objects for members in the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + Presences []*Presence `json:"presences"` + + // A list of channels in the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + Channels []*Channel `json:"channels"` + + // A list of voice states for the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + VoiceStates []*VoiceState `json:"voice_states"` + + // Whether this guild is currently unavailable (most likely due to outage). + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + Unavailable bool `json:"unavailable"` + + // The explicit content filter level + ExplicitContentFilter ExplicitContentFilterLevel `json:"explicit_content_filter"` + + // The list of enabled guild features + Features []string `json:"features"` + + // Required MFA level for the guild + MfaLevel MfaLevel `json:"mfa_level"` + + // Whether or not the Server Widget is enabled + WidgetEnabled bool `json:"widget_enabled"` + + // The Channel ID for the Server Widget + WidgetChannelID string `json:"widget_channel_id"` + + // The Channel ID to which system messages are sent (eg join and leave messages) + SystemChannelID string `json:"system_channel_id"` } // A UserGuild holds a brief version of a Guild @@ -279,14 +469,37 @@ type GuildParams struct { // A Role stores information about Discord guild member roles. type Role struct { - ID string `json:"id"` - Name string `json:"name"` - Managed bool `json:"managed"` - Mentionable bool `json:"mentionable"` - Hoist bool `json:"hoist"` - Color int `json:"color"` - Position int `json:"position"` - Permissions int `json:"permissions"` + // The ID of the role. + ID string `json:"id"` + + // The name of the role. + Name string `json:"name"` + + // Whether this role is managed by an integration, and + // thus cannot be manually added to, or taken from, members. + Managed bool `json:"managed"` + + // Whether this role is mentionable. + Mentionable bool `json:"mentionable"` + + // Whether this role is hoisted (shows up separately in member list). + Hoist bool `json:"hoist"` + + // The hex color of this role. + Color int `json:"color"` + + // The position of this role in the guild's role hierarchy. + Position int `json:"position"` + + // The permissions of the role on the guild (doesn't include channel overrides). + // This is a combination of bit masks; the presence of a certain permission can + // be checked by performing a bitwise AND between this int and the permission. + Permissions int `json:"permissions"` +} + +// Mention returns a string which mentions the role +func (r *Role) Mention() string { + return fmt.Sprintf("<@&%s>", r.ID) } // Roles are a collection of Role @@ -334,6 +547,8 @@ type GameType int const ( GameTypeGame GameType = iota GameTypeStreaming + GameTypeListening + GameTypeWatching ) // A Game struct holds the name of the "playing .." game for a user @@ -379,15 +594,34 @@ type Assets struct { SmallText string `json:"small_text,omitempty"` } -// A Member stores user information for Guild members. +// A Member stores user information for Guild members. A guild +// member represents a certain user's presence in a guild. type Member struct { - GuildID string `json:"guild_id"` - JoinedAt string `json:"joined_at"` - Nick string `json:"nick"` - Deaf bool `json:"deaf"` - Mute bool `json:"mute"` - User *User `json:"user"` - Roles []string `json:"roles"` + // The guild ID on which the member exists. + GuildID string `json:"guild_id"` + + // The time at which the member joined the guild, in ISO8601. + JoinedAt Timestamp `json:"joined_at"` + + // The nickname of the member, if they have one. + Nick string `json:"nick"` + + // Whether the member is deafened at a guild level. + Deaf bool `json:"deaf"` + + // Whether the member is muted at a guild level. + Mute bool `json:"mute"` + + // The underlying user on which the member is based. + User *User `json:"user"` + + // A list of IDs of the roles which are possessed by the member. + Roles []string `json:"roles"` +} + +// Mention creates a member mention +func (m *Member) Mention() string { + return "<@!" + m.User.ID + ">" } // A Settings stores data for a specific users Discord client settings. @@ -467,33 +701,88 @@ type GuildBan struct { User *User `json:"user"` } -// A GuildIntegration stores data for a guild integration. -type GuildIntegration struct { - ID string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Enabled bool `json:"enabled"` - Syncing bool `json:"syncing"` - RoleID string `json:"role_id"` - ExpireBehavior int `json:"expire_behavior"` - ExpireGracePeriod int `json:"expire_grace_period"` - User *User `json:"user"` - Account *GuildIntegrationAccount `json:"account"` - SyncedAt int `json:"synced_at"` -} - -// A GuildIntegrationAccount stores data for a guild integration account. -type GuildIntegrationAccount struct { - ID string `json:"id"` - Name string `json:"name"` -} - // A GuildEmbed stores data for a guild embed. type GuildEmbed struct { Enabled bool `json:"enabled"` ChannelID string `json:"channel_id"` } +// A GuildAuditLog stores data for a guild audit log. +type GuildAuditLog struct { + Webhooks []struct { + ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id"` + ID string `json:"id"` + Avatar string `json:"avatar"` + Name string `json:"name"` + } `json:"webhooks,omitempty"` + Users []struct { + Username string `json:"username"` + Discriminator string `json:"discriminator"` + Bot bool `json:"bot"` + ID string `json:"id"` + Avatar string `json:"avatar"` + } `json:"users,omitempty"` + AuditLogEntries []struct { + TargetID string `json:"target_id"` + Changes []struct { + NewValue interface{} `json:"new_value"` + OldValue interface{} `json:"old_value"` + Key string `json:"key"` + } `json:"changes,omitempty"` + UserID string `json:"user_id"` + ID string `json:"id"` + ActionType int `json:"action_type"` + Options struct { + DeleteMembersDay string `json:"delete_member_days"` + MembersRemoved string `json:"members_removed"` + ChannelID string `json:"channel_id"` + Count string `json:"count"` + ID string `json:"id"` + Type string `json:"type"` + RoleName string `json:"role_name"` + } `json:"options,omitempty"` + Reason string `json:"reason"` + } `json:"audit_log_entries"` +} + +// Block contains Discord Audit Log Action Types +const ( + AuditLogActionGuildUpdate = 1 + + AuditLogActionChannelCreate = 10 + AuditLogActionChannelUpdate = 11 + AuditLogActionChannelDelete = 12 + AuditLogActionChannelOverwriteCreate = 13 + AuditLogActionChannelOverwriteUpdate = 14 + AuditLogActionChannelOverwriteDelete = 15 + + AuditLogActionMemberKick = 20 + AuditLogActionMemberPrune = 21 + AuditLogActionMemberBanAdd = 22 + AuditLogActionMemberBanRemove = 23 + AuditLogActionMemberUpdate = 24 + AuditLogActionMemberRoleUpdate = 25 + + AuditLogActionRoleCreate = 30 + AuditLogActionRoleUpdate = 31 + AuditLogActionRoleDelete = 32 + + AuditLogActionInviteCreate = 40 + AuditLogActionInviteUpdate = 41 + AuditLogActionInviteDelete = 42 + + AuditLogActionWebhookCreate = 50 + AuditLogActionWebhookUpdate = 51 + AuditLogActionWebhookDelete = 52 + + AuditLogActionEmojiCreate = 60 + AuditLogActionEmojiUpdate = 61 + AuditLogActionEmojiDelete = 62 + + AuditLogActionMessageDelete = 72 +) + // A UserGuildSettingsChannelOverride stores data for a channel override for a users guild settings. type UserGuildSettingsChannelOverride struct { Muted bool `json:"muted"` @@ -553,6 +842,7 @@ type MessageReaction struct { MessageID string `json:"message_id"` Emoji Emoji `json:"emoji"` ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id,omitempty"` } // GatewayBotResponse stores the data for the gateway/bot response @@ -629,7 +919,9 @@ const ( PermissionKickMembers | PermissionBanMembers | PermissionManageServer | - PermissionAdministrator + PermissionAdministrator | + PermissionManageWebhooks | + PermissionManageEmojis ) // Block contains Discord JSON Error Response codes @@ -648,6 +940,7 @@ const ( ErrCodeUnknownToken = 10012 ErrCodeUnknownUser = 10013 ErrCodeUnknownEmoji = 10014 + ErrCodeUnknownWebhook = 10015 ErrCodeBotsCannotUseEndpoint = 20001 ErrCodeOnlyBotsCanUseEndpoint = 20002 diff --git a/vendor/github.com/matterbridge/discordgo/types.go b/vendor/github.com/bwmarrin/discordgo/types.go similarity index 95% rename from vendor/github.com/matterbridge/discordgo/types.go rename to vendor/github.com/bwmarrin/discordgo/types.go index 780b6bb9..c0ce0131 100644 --- a/vendor/github.com/matterbridge/discordgo/types.go +++ b/vendor/github.com/bwmarrin/discordgo/types.go @@ -11,7 +11,6 @@ package discordgo import ( "encoding/json" - "fmt" "net/http" "time" ) @@ -54,5 +53,5 @@ func newRestError(req *http.Request, resp *http.Response, body []byte) *RESTErro } func (r RESTError) Error() string { - return fmt.Sprintf("HTTP %s, %s", r.Response.Status, r.ResponseBody) + return "HTTP " + r.Response.Status + ", " + string(r.ResponseBody) } diff --git a/vendor/github.com/bwmarrin/discordgo/user.go b/vendor/github.com/bwmarrin/discordgo/user.go new file mode 100644 index 00000000..a9af31a9 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/user.go @@ -0,0 +1,69 @@ +package discordgo + +import "strings" + +// A User stores all data for an individual Discord user. +type User struct { + // The ID of the user. + ID string `json:"id"` + + // The email of the user. This is only present when + // the application possesses the email scope for the user. + Email string `json:"email"` + + // The user's username. + Username string `json:"username"` + + // The hash of the user's avatar. Use Session.UserAvatar + // to retrieve the avatar itself. + Avatar string `json:"avatar"` + + // The user's chosen language option. + Locale string `json:"locale"` + + // The discriminator of the user (4 numbers after name). + Discriminator string `json:"discriminator"` + + // The token of the user. This is only present for + // the user represented by the current session. + Token string `json:"token"` + + // Whether the user's email is verified. + Verified bool `json:"verified"` + + // Whether the user has multi-factor authentication enabled. + MFAEnabled bool `json:"mfa_enabled"` + + // Whether the user is a bot. + Bot bool `json:"bot"` +} + +// String returns a unique identifier of the form username#discriminator +func (u *User) String() string { + return u.Username + "#" + u.Discriminator +} + +// Mention return a string which mentions the user +func (u *User) Mention() string { + return "<@" + u.ID + ">" +} + +// AvatarURL returns a URL to the user's avatar. +// size: The size of the user's avatar as a power of two +// if size is an empty string, no size parameter will +// be added to the URL. +func (u *User) AvatarURL(size string) string { + var URL string + if u.Avatar == "" { + URL = EndpointDefaultUserAvatar(u.Discriminator) + } else if strings.HasPrefix(u.Avatar, "a_") { + URL = EndpointUserAvatarAnimated(u.ID, u.Avatar) + } else { + URL = EndpointUserAvatar(u.ID, u.Avatar) + } + + if size != "" { + return URL + "?size=" + size + } + return URL +} diff --git a/vendor/github.com/matterbridge/discordgo/voice.go b/vendor/github.com/bwmarrin/discordgo/voice.go similarity index 98% rename from vendor/github.com/matterbridge/discordgo/voice.go rename to vendor/github.com/bwmarrin/discordgo/voice.go index 3bbf6212..aa630b12 100644 --- a/vendor/github.com/matterbridge/discordgo/voice.go +++ b/vendor/github.com/bwmarrin/discordgo/voice.go @@ -14,6 +14,7 @@ import ( "encoding/json" "fmt" "net" + "strconv" "strings" "sync" "time" @@ -103,7 +104,7 @@ func (v *VoiceConnection) Speaking(b bool) (err error) { defer v.Unlock() if err != nil { v.speaking = false - v.log(LogError, "Speaking() write json error:", err) + v.log(LogError, "Speaking() write json error, %s", err) return } @@ -135,7 +136,6 @@ func (v *VoiceConnection) ChangeChannel(channelID string, mute, deaf bool) (err // Disconnect disconnects from this voice channel and closes the websocket // and udp connections to Discord. -// !!! NOTE !!! this function may be removed in favour of ChannelVoiceLeave func (v *VoiceConnection) Disconnect() (err error) { // Send a OP4 with a nil channel to disconnect @@ -180,7 +180,7 @@ func (v *VoiceConnection) Close() { v.log(LogInformational, "closing udp") err := v.udpConn.Close() if err != nil { - v.log(LogError, "error closing udp connection: ", err) + v.log(LogError, "error closing udp connection, %s", err) } v.udpConn = nil } @@ -299,7 +299,7 @@ func (v *VoiceConnection) open() (err error) { } // Connect to VoiceConnection Websocket - vg := fmt.Sprintf("wss://%s", strings.TrimSuffix(v.endpoint, ":80")) + vg := "wss://" + strings.TrimSuffix(v.endpoint, ":80") v.log(LogInformational, "connecting to voice endpoint %s", vg) v.wsConn, _, err = websocket.DefaultDialer.Dial(vg, nil) if err != nil { @@ -542,7 +542,7 @@ func (v *VoiceConnection) udpOpen() (err error) { return fmt.Errorf("empty endpoint") } - host := fmt.Sprintf("%s:%d", strings.TrimSuffix(v.endpoint, ":80"), v.op2.Port) + host := strings.TrimSuffix(v.endpoint, ":80") + ":" + strconv.Itoa(v.op2.Port) addr, err := net.ResolveUDPAddr("udp", host) if err != nil { v.log(LogWarning, "error resolving udp host %s, %s", host, err) diff --git a/vendor/github.com/matterbridge/discordgo/wsapi.go b/vendor/github.com/bwmarrin/discordgo/wsapi.go similarity index 92% rename from vendor/github.com/matterbridge/discordgo/wsapi.go rename to vendor/github.com/bwmarrin/discordgo/wsapi.go index de66f693..8ecaaa77 100644 --- a/vendor/github.com/matterbridge/discordgo/wsapi.go +++ b/vendor/github.com/bwmarrin/discordgo/wsapi.go @@ -86,6 +86,10 @@ func (s *Session) Open() error { return err } + s.wsConn.SetCloseHandler(func(code int, text string) error { + return nil + }) + defer func() { // because of this, all code below must set err to the error // when exiting with an error :) Maybe someone has a better @@ -263,6 +267,13 @@ type helloOp struct { // FailedHeartbeatAcks is the Number of heartbeat intervals to wait until forcing a connection restart. const FailedHeartbeatAcks time.Duration = 5 * time.Millisecond +// HeartbeatLatency returns the latency between heartbeat acknowledgement and heartbeat send. +func (s *Session) HeartbeatLatency() time.Duration { + + return s.LastHeartbeatAck.Sub(s.LastHeartbeatSent) + +} + // heartbeat sends regular heartbeats to Discord so it knows the client // is still connected. If you do not send these heartbeats Discord will // disconnect the websocket connection after a few seconds. @@ -283,8 +294,9 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{} last := s.LastHeartbeatAck s.RUnlock() sequence := atomic.LoadInt64(s.sequence) - s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence) + s.log(LogDebug, "sending gateway websocket heartbeat seq %d", sequence) s.wsMutex.Lock() + s.LastHeartbeatSent = time.Now().UTC() err = wsConn.WriteJSON(heartbeatOp{1, sequence}) s.wsMutex.Unlock() if err != nil || time.Now().UTC().Sub(last) > (heartbeatIntervalMsec*FailedHeartbeatAcks) { @@ -323,16 +335,8 @@ type updateStatusOp struct { Data UpdateStatusData `json:"d"` } -// UpdateStreamingStatus is used to update the user's streaming status. -// If idle>0 then set status to idle. -// If game!="" then set game. -// If game!="" and url!="" then set the status type to streaming with the URL set. -// if otherwise, set status to active, and no game. -func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err error) { - - s.log(LogInformational, "called") - - usd := UpdateStatusData{ +func newUpdateStatusData(idle int, gameType GameType, game, url string) *UpdateStatusData { + usd := &UpdateStatusData{ Status: "online", } @@ -341,10 +345,6 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err } if game != "" { - gameType := GameTypeGame - if url != "" { - gameType = GameTypeStreaming - } usd.Game = &Game{ Name: game, Type: gameType, @@ -352,7 +352,35 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err } } - return s.UpdateStatusComplex(usd) + return usd +} + +// UpdateStatus is used to update the user's status. +// If idle>0 then set status to idle. +// If game!="" then set game. +// if otherwise, set status to active, and no game. +func (s *Session) UpdateStatus(idle int, game string) (err error) { + return s.UpdateStatusComplex(*newUpdateStatusData(idle, GameTypeGame, game, "")) +} + +// UpdateStreamingStatus is used to update the user's streaming status. +// If idle>0 then set status to idle. +// If game!="" then set game. +// If game!="" and url!="" then set the status type to streaming with the URL set. +// if otherwise, set status to active, and no game. +func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err error) { + gameType := GameTypeGame + if url != "" { + gameType = GameTypeStreaming + } + return s.UpdateStatusComplex(*newUpdateStatusData(idle, gameType, game, url)) +} + +// UpdateListeningStatus is used to set the user to "Listening to..." +// If game!="" then set to what user is listening to +// Else, set user to active and no game. +func (s *Session) UpdateListeningStatus(game string) (err error) { + return s.UpdateStatusComplex(*newUpdateStatusData(0, GameTypeListening, game, "")) } // UpdateStatusComplex allows for sending the raw status update data untouched by discordgo. @@ -371,14 +399,6 @@ func (s *Session) UpdateStatusComplex(usd UpdateStatusData) (err error) { return } -// UpdateStatus is used to update the user's status. -// If idle>0 then set status to idle. -// If game!="" then set game. -// if otherwise, set status to active, and no game. -func (s *Session) UpdateStatus(idle int, game string) (err error) { - return s.UpdateStreamingStatus(idle, game, "") -} - type requestGuildMembersData struct { GuildID string `json:"guild_id"` Query string `json:"query"` @@ -508,7 +528,7 @@ func (s *Session) onEvent(messageType int, message []byte) (*Event, error) { s.Lock() s.LastHeartbeatAck = time.Now().UTC() s.Unlock() - s.log(LogInformational, "got heartbeat ACK") + s.log(LogDebug, "got heartbeat ACK") return e, nil } @@ -615,6 +635,30 @@ func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *Voi return } +// ChannelVoiceJoinManual initiates a voice session to a voice channel, but does not complete it. +// +// This should only be used when the VoiceServerUpdate will be intercepted and used elsewhere. +// +// gID : Guild ID of the channel to join. +// cID : Channel ID of the channel to join. +// mute : If true, you will be set to muted upon joining. +// deaf : If true, you will be set to deafened upon joining. +func (s *Session) ChannelVoiceJoinManual(gID, cID string, mute, deaf bool) (err error) { + + s.log(LogInformational, "called") + + // Send the request to Discord that we want to join the voice channel + data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}} + s.wsMutex.Lock() + err = s.wsConn.WriteJSON(data) + s.wsMutex.Unlock() + if err != nil { + return + } + + return +} + // onVoiceStateUpdate handles Voice State Update events on the data websocket. func (s *Session) onVoiceStateUpdate(st *VoiceStateUpdate) { @@ -732,11 +776,8 @@ func (s *Session) identify() error { s.wsMutex.Lock() err := s.wsConn.WriteJSON(op) s.wsMutex.Unlock() - if err != nil { - return err - } - return nil + return err } func (s *Session) reconnect() { diff --git a/vendor/github.com/davecgh/go-spew/LICENSE b/vendor/github.com/davecgh/go-spew/LICENSE index c8364161..bc52e96f 100644 --- a/vendor/github.com/davecgh/go-spew/LICENSE +++ b/vendor/github.com/davecgh/go-spew/LICENSE @@ -2,7 +2,7 @@ ISC License Copyright (c) 2012-2016 Dave Collins -Permission to use, copy, modify, and distribute this software for any +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. diff --git a/vendor/github.com/davecgh/go-spew/spew/bypass.go b/vendor/github.com/davecgh/go-spew/spew/bypass.go index 8a4a6589..79299478 100644 --- a/vendor/github.com/davecgh/go-spew/spew/bypass.go +++ b/vendor/github.com/davecgh/go-spew/spew/bypass.go @@ -16,7 +16,9 @@ // when the code is not running on Google App Engine, compiled by GopherJS, and // "-tags safe" is not added to the go build command line. The "disableunsafe" // tag is deprecated and thus should not be used. -// +build !js,!appengine,!safe,!disableunsafe +// Go versions prior to 1.4 are disabled because they use a different layout +// for interfaces which make the implementation of unsafeReflectValue more complex. +// +build !js,!appengine,!safe,!disableunsafe,go1.4 package spew @@ -34,80 +36,49 @@ const ( ptrSize = unsafe.Sizeof((*byte)(nil)) ) -var ( - // offsetPtr, offsetScalar, and offsetFlag are the offsets for the - // internal reflect.Value fields. These values are valid before golang - // commit ecccf07e7f9d which changed the format. The are also valid - // after commit 82f48826c6c7 which changed the format again to mirror - // the original format. Code in the init function updates these offsets - // as necessary. - offsetPtr = uintptr(ptrSize) - offsetScalar = uintptr(0) - offsetFlag = uintptr(ptrSize * 2) +type flag uintptr - // flagKindWidth and flagKindShift indicate various bits that the - // reflect package uses internally to track kind information. - // - // flagRO indicates whether or not the value field of a reflect.Value is - // read-only. - // - // flagIndir indicates whether the value field of a reflect.Value is - // the actual data or a pointer to the data. - // - // These values are valid before golang commit 90a7c3c86944 which - // changed their positions. Code in the init function updates these - // flags as necessary. - flagKindWidth = uintptr(5) - flagKindShift = uintptr(flagKindWidth - 1) - flagRO = uintptr(1 << 0) - flagIndir = uintptr(1 << 1) +var ( + // flagRO indicates whether the value field of a reflect.Value + // is read-only. + flagRO flag + + // flagAddr indicates whether the address of the reflect.Value's + // value may be taken. + flagAddr flag ) -func init() { - // Older versions of reflect.Value stored small integers directly in the - // ptr field (which is named val in the older versions). Versions - // between commits ecccf07e7f9d and 82f48826c6c7 added a new field named - // scalar for this purpose which unfortunately came before the flag - // field, so the offset of the flag field is different for those - // versions. - // - // This code constructs a new reflect.Value from a known small integer - // and checks if the size of the reflect.Value struct indicates it has - // the scalar field. When it does, the offsets are updated accordingly. - vv := reflect.ValueOf(0xf00) - if unsafe.Sizeof(vv) == (ptrSize * 4) { - offsetScalar = ptrSize * 2 - offsetFlag = ptrSize * 3 - } +// flagKindMask holds the bits that make up the kind +// part of the flags field. In all the supported versions, +// it is in the lower 5 bits. +const flagKindMask = flag(0x1f) - // Commit 90a7c3c86944 changed the flag positions such that the low - // order bits are the kind. This code extracts the kind from the flags - // field and ensures it's the correct type. When it's not, the flag - // order has been changed to the newer format, so the flags are updated - // accordingly. - upf := unsafe.Pointer(uintptr(unsafe.Pointer(&vv)) + offsetFlag) - upfv := *(*uintptr)(upf) - flagKindMask := uintptr((1<>flagKindShift != uintptr(reflect.Int) { - flagKindShift = 0 - flagRO = 1 << 5 - flagIndir = 1 << 6 +// Different versions of Go have used different +// bit layouts for the flags type. This table +// records the known combinations. +var okFlags = []struct { + ro, addr flag +}{{ + // From Go 1.4 to 1.5 + ro: 1 << 5, + addr: 1 << 7, +}, { + // Up to Go tip. + ro: 1<<5 | 1<<6, + addr: 1 << 8, +}} - // Commit adf9b30e5594 modified the flags to separate the - // flagRO flag into two bits which specifies whether or not the - // field is embedded. This causes flagIndir to move over a bit - // and means that flagRO is the combination of either of the - // original flagRO bit and the new bit. - // - // This code detects the change by extracting what used to be - // the indirect bit to ensure it's set. When it's not, the flag - // order has been changed to the newer format, so the flags are - // updated accordingly. - if upfv&flagIndir == 0 { - flagRO = 3 << 5 - flagIndir = 1 << 7 - } +var flagValOffset = func() uintptr { + field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") + if !ok { + panic("reflect.Value has no flag field") } + return field.Offset +}() + +// flagField returns a pointer to the flag field of a reflect.Value. +func flagField(v *reflect.Value) *flag { + return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset)) } // unsafeReflectValue converts the passed reflect.Value into a one that bypasses @@ -119,34 +90,56 @@ func init() { // This allows us to check for implementations of the Stringer and error // interfaces to be used for pretty printing ordinarily unaddressable and // inaccessible values such as unexported struct fields. -func unsafeReflectValue(v reflect.Value) (rv reflect.Value) { - indirects := 1 - vt := v.Type() - upv := unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetPtr) - rvf := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetFlag)) - if rvf&flagIndir != 0 { - vt = reflect.PtrTo(v.Type()) - indirects++ - } else if offsetScalar != 0 { - // The value is in the scalar field when it's not one of the - // reference types. - switch vt.Kind() { - case reflect.Uintptr: - case reflect.Chan: - case reflect.Func: - case reflect.Map: - case reflect.Ptr: - case reflect.UnsafePointer: - default: - upv = unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + - offsetScalar) +func unsafeReflectValue(v reflect.Value) reflect.Value { + if !v.IsValid() || (v.CanInterface() && v.CanAddr()) { + return v + } + flagFieldPtr := flagField(&v) + *flagFieldPtr &^= flagRO + *flagFieldPtr |= flagAddr + return v +} + +// Sanity checks against future reflect package changes +// to the type or semantics of the Value.flag field. +func init() { + field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") + if !ok { + panic("reflect.Value has no flag field") + } + if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() { + panic("reflect.Value flag field has changed kind") + } + type t0 int + var t struct { + A t0 + // t0 will have flagEmbedRO set. + t0 + // a will have flagStickyRO set + a t0 + } + vA := reflect.ValueOf(t).FieldByName("A") + va := reflect.ValueOf(t).FieldByName("a") + vt0 := reflect.ValueOf(t).FieldByName("t0") + + // Infer flagRO from the difference between the flags + // for the (otherwise identical) fields in t. + flagPublic := *flagField(&vA) + flagWithRO := *flagField(&va) | *flagField(&vt0) + flagRO = flagPublic ^ flagWithRO + + // Infer flagAddr from the difference between a value + // taken from a pointer and not. + vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A") + flagNoPtr := *flagField(&vA) + flagPtr := *flagField(&vPtrA) + flagAddr = flagNoPtr ^ flagPtr + + // Check that the inferred flags tally with one of the known versions. + for _, f := range okFlags { + if flagRO == f.ro && flagAddr == f.addr { + return } } - - pv := reflect.NewAt(vt, upv) - rv = pv - for i := 0; i < indirects; i++ { - rv = rv.Elem() - } - return rv + panic("reflect.Value read-only flag has changed semantics") } diff --git a/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go b/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go index 1fe3cf3d..205c28d6 100644 --- a/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go +++ b/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go @@ -16,7 +16,7 @@ // when the code is running on Google App Engine, compiled by GopherJS, or // "-tags safe" is added to the go build command line. The "disableunsafe" // tag is deprecated and thus should not be used. -// +build js appengine safe disableunsafe +// +build js appengine safe disableunsafe !go1.4 package spew diff --git a/vendor/github.com/davecgh/go-spew/spew/common.go b/vendor/github.com/davecgh/go-spew/spew/common.go index 7c519ff4..1be8ce94 100644 --- a/vendor/github.com/davecgh/go-spew/spew/common.go +++ b/vendor/github.com/davecgh/go-spew/spew/common.go @@ -180,7 +180,7 @@ func printComplex(w io.Writer, c complex128, floatPrecision int) { w.Write(closeParenBytes) } -// printHexPtr outputs a uintptr formatted as hexidecimal with a leading '0x' +// printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x' // prefix to Writer w. func printHexPtr(w io.Writer, p uintptr) { // Null pointer. diff --git a/vendor/github.com/davecgh/go-spew/spew/dump.go b/vendor/github.com/davecgh/go-spew/spew/dump.go index df1d582a..f78d89fc 100644 --- a/vendor/github.com/davecgh/go-spew/spew/dump.go +++ b/vendor/github.com/davecgh/go-spew/spew/dump.go @@ -35,16 +35,16 @@ var ( // cCharRE is a regular expression that matches a cgo char. // It is used to detect character arrays to hexdump them. - cCharRE = regexp.MustCompile("^.*\\._Ctype_char$") + cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`) // cUnsignedCharRE is a regular expression that matches a cgo unsigned // char. It is used to detect unsigned character arrays to hexdump // them. - cUnsignedCharRE = regexp.MustCompile("^.*\\._Ctype_unsignedchar$") + cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`) // cUint8tCharRE is a regular expression that matches a cgo uint8_t. // It is used to detect uint8_t arrays to hexdump them. - cUint8tCharRE = regexp.MustCompile("^.*\\._Ctype_uint8_t$") + cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`) ) // dumpState contains information about the state of a dump operation. @@ -143,10 +143,10 @@ func (d *dumpState) dumpPtr(v reflect.Value) { // Display dereferenced value. d.w.Write(openParenBytes) switch { - case nilFound == true: + case nilFound: d.w.Write(nilAngleBytes) - case cycleFound == true: + case cycleFound: d.w.Write(circularBytes) default: diff --git a/vendor/github.com/davecgh/go-spew/spew/format.go b/vendor/github.com/davecgh/go-spew/spew/format.go index c49875ba..b04edb7d 100644 --- a/vendor/github.com/davecgh/go-spew/spew/format.go +++ b/vendor/github.com/davecgh/go-spew/spew/format.go @@ -182,10 +182,10 @@ func (f *formatState) formatPtr(v reflect.Value) { // Display dereferenced value. switch { - case nilFound == true: + case nilFound: f.fs.Write(nilAngleBytes) - case cycleFound == true: + case cycleFound: f.fs.Write(circularShortBytes) default: diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/.gitignore b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/.gitignore index aa7ac806..fb5a5e83 100644 --- a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/.gitignore +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/.gitignore @@ -1,2 +1,3 @@ .idea/ coverage.out +tmp/ diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/.travis.yml b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/.travis.yml index 8408fb7b..5769aa14 100644 --- a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/.travis.yml +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/.travis.yml @@ -1,5 +1,6 @@ language: go go: - - 1.4 - - tip \ No newline at end of file + - '1.10' + - '1.11' + - tip diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/README.md b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/README.md index d9a6873d..93250611 100644 --- a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/README.md +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/README.md @@ -3,10 +3,6 @@ [![GoDoc](https://godoc.org/github.com/go-telegram-bot-api/telegram-bot-api?status.svg)](http://godoc.org/github.com/go-telegram-bot-api/telegram-bot-api) [![Travis](https://travis-ci.org/go-telegram-bot-api/telegram-bot-api.svg)](https://travis-ci.org/go-telegram-bot-api/telegram-bot-api) -All methods have been added, and all features should be available. -If you want a feature that hasn't been added yet or something is broken, -open an issue and I'll see what I can do. - All methods are fairly self explanatory, and reading the godoc page should explain everything. If something isn't clear, open an issue or submit a pull request. @@ -16,14 +12,14 @@ without any additional features. There are other projects for creating something with plugins and command handlers without having to design all that yourself. -Use `github.com/go-telegram-bot-api/telegram-bot-api` for the latest -version, or use `gopkg.in/telegram-bot-api.v4` for the stable build. - Join [the development group](https://telegram.me/go_telegram_bot_api) if you want to ask questions or discuss development. ## Example +First, ensure the library is installed and up to date by running +`go get -u github.com/go-telegram-bot-api/telegram-bot-api`. + This is a very simple bot that just displays any gotten updates, then replies it to that chat. @@ -32,7 +28,8 @@ package main import ( "log" - "gopkg.in/telegram-bot-api.v4" + + "github.com/go-telegram-bot-api/telegram-bot-api" ) func main() { @@ -51,7 +48,7 @@ func main() { updates, err := bot.GetUpdatesChan(u) for update := range updates { - if update.Message == nil { + if update.Message == nil { // ignore any non-Message Updates continue } @@ -65,6 +62,11 @@ func main() { } ``` +There are more examples on the [wiki](https://github.com/go-telegram-bot-api/telegram-bot-api/wiki) +with detailed information on how to do many differen kinds of things. +It's a great place to get started on using keyboards, commands, or other +kinds of reply markup. + If you need to use webhooks (if you wish to run on Google App Engine), you may use a slightly different method. @@ -72,9 +74,10 @@ you may use a slightly different method. package main import ( - "gopkg.in/telegram-bot-api.v4" "log" "net/http" + + "github.com/go-telegram-bot-api/telegram-bot-api" ) func main() { @@ -96,7 +99,7 @@ func main() { log.Fatal(err) } if info.LastErrorDate != 0 { - log.Printf("[Telegram callback failed]%s", info.LastErrorMessage) + log.Printf("Telegram callback failed: %s", info.LastErrorMessage) } updates := bot.ListenForWebhook("/" + bot.Token) go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil) @@ -114,5 +117,5 @@ properly signed. openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3560 -subj "//O=Org\CN=Test" -nodes -Now that [Let's Encrypt](https://letsencrypt.org) has entered public beta, +Now that [Let's Encrypt](https://letsencrypt.org) is available, you may wish to generate your free TLS certificate there. diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/bot.go b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/bot.go index 8fb6200e..d56aaf82 100644 --- a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/bot.go +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/bot.go @@ -9,7 +9,6 @@ import ( "fmt" "io" "io/ioutil" - "log" "net/http" "net/url" "os" @@ -28,6 +27,7 @@ type BotAPI struct { Self User `json:"-"` Client *http.Client `json:"-"` + shutdownChannel chan interface{} } // NewBotAPI creates a new BotAPI instance. @@ -46,6 +46,7 @@ func NewBotAPIWithClient(token string, client *http.Client) (*BotAPI, error) { Token: token, Client: client, Buffer: 100, + shutdownChannel: make(chan interface{}), } self, err := bot.GetMe() @@ -484,6 +485,12 @@ func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (UpdatesChannel, error) { go func() { for { + select { + case <-bot.shutdownChannel: + return + default: + } + updates, err := bot.GetUpdates(config) if err != nil { log.Println(err) @@ -505,6 +512,14 @@ func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (UpdatesChannel, error) { return ch, nil } +// StopReceivingUpdates stops the go routine which receives updates +func (bot *BotAPI) StopReceivingUpdates() { + if bot.Debug { + log.Println("Stopping the update receiver routine...") + } + close(bot.shutdownChannel) +} + // ListenForWebhook registers a http handler for a webhook. func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel { ch := make(chan Update, bot.Buffer) diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/configs.go b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/configs.go index 574b3dd9..181d4e43 100644 --- a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/configs.go +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/configs.go @@ -243,7 +243,8 @@ func (config ForwardConfig) method() string { // PhotoConfig contains information about a SendPhoto request. type PhotoConfig struct { BaseFile - Caption string + Caption string + ParseMode string } // Params returns a map[string]string representation of PhotoConfig. @@ -252,6 +253,9 @@ func (config PhotoConfig) params() (map[string]string, error) { if config.Caption != "" { params["caption"] = config.Caption + if config.ParseMode != "" { + params["parse_mode"] = config.ParseMode + } } return params, nil @@ -267,7 +271,11 @@ func (config PhotoConfig) values() (url.Values, error) { v.Add(config.name(), config.FileID) if config.Caption != "" { v.Add("caption", config.Caption) + if config.ParseMode != "" { + v.Add("parse_mode", config.ParseMode) + } } + return v, nil } @@ -285,6 +293,7 @@ func (config PhotoConfig) method() string { type AudioConfig struct { BaseFile Caption string + ParseMode string Duration int Performer string Title string @@ -310,6 +319,9 @@ func (config AudioConfig) values() (url.Values, error) { } if config.Caption != "" { v.Add("caption", config.Caption) + if config.ParseMode != "" { + v.Add("parse_mode", config.ParseMode) + } } return v, nil @@ -331,6 +343,9 @@ func (config AudioConfig) params() (map[string]string, error) { } if config.Caption != "" { params["caption"] = config.Caption + if config.ParseMode != "" { + params["parse_mode"] = config.ParseMode + } } return params, nil @@ -349,7 +364,8 @@ func (config AudioConfig) method() string { // DocumentConfig contains information about a SendDocument request. type DocumentConfig struct { BaseFile - Caption string + Caption string + ParseMode string } // values returns a url.Values representation of DocumentConfig. @@ -362,6 +378,9 @@ func (config DocumentConfig) values() (url.Values, error) { v.Add(config.name(), config.FileID) if config.Caption != "" { v.Add("caption", config.Caption) + if config.ParseMode != "" { + v.Add("parse_mode", config.ParseMode) + } } return v, nil @@ -373,6 +392,9 @@ func (config DocumentConfig) params() (map[string]string, error) { if config.Caption != "" { params["caption"] = config.Caption + if config.ParseMode != "" { + params["parse_mode"] = config.ParseMode + } } return params, nil @@ -425,8 +447,9 @@ func (config StickerConfig) method() string { // VideoConfig contains information about a SendVideo request. type VideoConfig struct { BaseFile - Duration int - Caption string + Duration int + Caption string + ParseMode string } // values returns a url.Values representation of VideoConfig. @@ -442,6 +465,9 @@ func (config VideoConfig) values() (url.Values, error) { } if config.Caption != "" { v.Add("caption", config.Caption) + if config.ParseMode != "" { + v.Add("parse_mode", config.ParseMode) + } } return v, nil @@ -453,6 +479,9 @@ func (config VideoConfig) params() (map[string]string, error) { if config.Caption != "" { params["caption"] = config.Caption + if config.ParseMode != "" { + params["parse_mode"] = config.ParseMode + } } return params, nil @@ -468,6 +497,59 @@ func (config VideoConfig) method() string { return "sendVideo" } +// AnimationConfig contains information about a SendAnimation request. +type AnimationConfig struct { + BaseFile + Duration int + Caption string + ParseMode string +} + +// values returns a url.Values representation of AnimationConfig. +func (config AnimationConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + + v.Add(config.name(), config.FileID) + if config.Duration != 0 { + v.Add("duration", strconv.Itoa(config.Duration)) + } + if config.Caption != "" { + v.Add("caption", config.Caption) + if config.ParseMode != "" { + v.Add("parse_mode", config.ParseMode) + } + } + + return v, nil +} + +// params returns a map[string]string representation of AnimationConfig. +func (config AnimationConfig) params() (map[string]string, error) { + params, _ := config.BaseFile.params() + + if config.Caption != "" { + params["caption"] = config.Caption + if config.ParseMode != "" { + params["parse_mode"] = config.ParseMode + } + } + + return params, nil +} + +// name returns the field name for the Animation. +func (config AnimationConfig) name() string { + return "animation" +} + +// method returns Telegram API method name for sending Animation. +func (config AnimationConfig) method() string { + return "sendAnimation" +} + // VideoNoteConfig contains information about a SendVideoNote request. type VideoNoteConfig struct { BaseFile @@ -522,8 +604,9 @@ func (config VideoNoteConfig) method() string { // VoiceConfig contains information about a SendVoice request. type VoiceConfig struct { BaseFile - Caption string - Duration int + Caption string + ParseMode string + Duration int } // values returns a url.Values representation of VoiceConfig. @@ -539,6 +622,9 @@ func (config VoiceConfig) values() (url.Values, error) { } if config.Caption != "" { v.Add("caption", config.Caption) + if config.ParseMode != "" { + v.Add("parse_mode", config.ParseMode) + } } return v, nil @@ -553,6 +639,9 @@ func (config VoiceConfig) params() (map[string]string, error) { } if config.Caption != "" { params["caption"] = config.Caption + if config.ParseMode != "" { + params["parse_mode"] = config.ParseMode + } } return params, nil @@ -568,6 +657,32 @@ func (config VoiceConfig) method() string { return "sendVoice" } +// MediaGroupConfig contains information about a sendMediaGroup request. +type MediaGroupConfig struct { + BaseChat + InputMedia []interface{} +} + +func (config MediaGroupConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + + data, err := json.Marshal(config.InputMedia) + if err != nil { + return v, err + } + + v.Add("media", string(data)) + + return v, nil +} + +func (config MediaGroupConfig) method() string { + return "sendMediaGroup" +} + // LocationConfig contains information about a SendLocation request. type LocationConfig struct { BaseChat @@ -786,13 +901,17 @@ func (config EditMessageTextConfig) method() string { // EditMessageCaptionConfig allows you to modify the caption of a message. type EditMessageCaptionConfig struct { BaseEdit - Caption string + Caption string + ParseMode string } func (config EditMessageCaptionConfig) values() (url.Values, error) { v, _ := config.BaseEdit.values() v.Add("caption", config.Caption) + if config.ParseMode != "" { + v.Add("parse_mode", config.ParseMode) + } return v, nil } diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/helpers.go b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/helpers.go index b5480ead..f49cbbab 100644 --- a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/helpers.go +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/helpers.go @@ -1,7 +1,6 @@ package tgbotapi import ( - "log" "net/url" ) @@ -14,11 +13,12 @@ func NewMessage(chatID int64, text string) MessageConfig { ChatID: chatID, ReplyToMessageID: 0, }, - Text: text, + Text: text, DisableWebPagePreview: false, } } +// NewDeleteMessage creates a request to delete a message. func NewDeleteMessage(chatID int64, messageID int) DeleteMessageConfig { return DeleteMessageConfig{ ChatID: chatID, @@ -201,6 +201,35 @@ func NewVideoShare(chatID int64, fileID string) VideoConfig { } } +// NewAnimationUpload creates a new animation uploader. +// +// chatID is where to send it, file is a string path to the file, +// FileReader, or FileBytes. +func NewAnimationUpload(chatID int64, file interface{}) AnimationConfig { + return AnimationConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + File: file, + UseExisting: false, + }, + } +} + +// NewAnimationShare shares an existing animation. +// You may use this to reshare an existing animation without reuploading it. +// +// chatID is where to send it, fileID is the ID of the animation +// already uploaded. +func NewAnimationShare(chatID int64, fileID string) AnimationConfig { + return AnimationConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + FileID: fileID, + UseExisting: true, + }, + } +} + // NewVideoNoteUpload creates a new video note uploader. // // chatID is where to send it, file is a string path to the file, @@ -261,6 +290,33 @@ func NewVoiceShare(chatID int64, fileID string) VoiceConfig { } } +// NewMediaGroup creates a new media group. Files should be an array of +// two to ten InputMediaPhoto or InputMediaVideo. +func NewMediaGroup(chatID int64, files []interface{}) MediaGroupConfig { + return MediaGroupConfig{ + BaseChat: BaseChat{ + ChatID: chatID, + }, + InputMedia: files, + } +} + +// NewInputMediaPhoto creates a new InputMediaPhoto. +func NewInputMediaPhoto(media string) InputMediaPhoto { + return InputMediaPhoto{ + Type: "photo", + Media: media, + } +} + +// NewInputMediaVideo creates a new InputMediaVideo. +func NewInputMediaVideo(media string) InputMediaVideo { + return InputMediaVideo{ + Type: "video", + Media: media, + } +} + // NewContact allows you to send a shared contact. func NewContact(chatID int64, phoneNumber, firstName string) ContactConfig { return ContactConfig{ diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/log.go b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/log.go new file mode 100644 index 00000000..18725514 --- /dev/null +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/log.go @@ -0,0 +1,27 @@ +package tgbotapi + +import ( + "errors" + stdlog "log" + "os" +) + +// BotLogger is an interface that represents the required methods to log data. +// +// Instead of requiring the standard logger, we can just specify the methods we +// use and allow users to pass anything that implements these. +type BotLogger interface { + Println(v ...interface{}) + Printf(format string, v ...interface{}) +} + +var log BotLogger = stdlog.New(os.Stderr, "", stdlog.LstdFlags) + +// SetLogger specifies the logger that the package should use. +func SetLogger(logger BotLogger) error { + if logger == nil { + return errors.New("logger is nil") + } + log = logger + return nil +} diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/passport.go b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/passport.go new file mode 100644 index 00000000..5f55006d --- /dev/null +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/passport.go @@ -0,0 +1,315 @@ +package tgbotapi + +// PassportRequestInfoConfig allows you to request passport info +type PassportRequestInfoConfig struct { + BotID int `json:"bot_id"` + Scope *PassportScope `json:"scope"` + Nonce string `json:"nonce"` + PublicKey string `json:"public_key"` +} + +// PassportScopeElement supports using one or one of several elements. +type PassportScopeElement interface { + ScopeType() string +} + +// PassportScope is the requested scopes of data. +type PassportScope struct { + V int `json:"v"` + Data []PassportScopeElement `json:"data"` +} + +// PassportScopeElementOneOfSeveral allows you to request any one of the +// requested documents. +type PassportScopeElementOneOfSeveral struct { +} + +// ScopeType is the scope type. +func (eo *PassportScopeElementOneOfSeveral) ScopeType() string { + return "one_of" +} + +// PassportScopeElementOne requires the specified element be provided. +type PassportScopeElementOne struct { + Type string `json:"type"` // One of “personal_details”, “passport”, “driver_license”, “identity_card”, “internal_passport”, “address”, “utility_bill”, “bank_statement”, “rental_agreement”, “passport_registration”, “temporary_registration”, “phone_number”, “email” + Selfie bool `json:"selfie"` + Translation bool `json:"translation"` + NativeNames bool `json:"native_name"` +} + +// ScopeType is the scope type. +func (eo *PassportScopeElementOne) ScopeType() string { + return "one" +} + +type ( + // PassportData contains information about Telegram Passport data shared with + // the bot by the user. + PassportData struct { + // Array with information about documents and other Telegram Passport + // elements that was shared with the bot + Data []EncryptedPassportElement `json:"data"` + + // Encrypted credentials required to decrypt the data + Credentials *EncryptedCredentials `json:"credentials"` + } + + // PassportFile represents a file uploaded to Telegram Passport. Currently all + // Telegram Passport files are in JPEG format when decrypted and don't exceed + // 10MB. + PassportFile struct { + // Unique identifier for this file + FileID string `json:"file_id"` + + // File size + FileSize int `json:"file_size"` + + // Unix time when the file was uploaded + FileDate int64 `json:"file_date"` + } + + // EncryptedPassportElement contains information about documents or other + // Telegram Passport elements shared with the bot by the user. + EncryptedPassportElement struct { + // Element type. + Type string `json:"type"` + + // Base64-encoded encrypted Telegram Passport element data provided by + // the user, available for "personal_details", "passport", + // "driver_license", "identity_card", "identity_passport" and "address" + // types. Can be decrypted and verified using the accompanying + // EncryptedCredentials. + Data string `json:"data,omitempty"` + + // User's verified phone number, available only for "phone_number" type + PhoneNumber string `json:"phone_number,omitempty"` + + // User's verified email address, available only for "email" type + Email string `json:"email,omitempty"` + + // Array of encrypted files with documents provided by the user, + // available for "utility_bill", "bank_statement", "rental_agreement", + // "passport_registration" and "temporary_registration" types. Files can + // be decrypted and verified using the accompanying EncryptedCredentials. + Files []PassportFile `json:"files,omitempty"` + + // Encrypted file with the front side of the document, provided by the + // user. Available for "passport", "driver_license", "identity_card" and + // "internal_passport". The file can be decrypted and verified using the + // accompanying EncryptedCredentials. + FrontSide *PassportFile `json:"front_side,omitempty"` + + // Encrypted file with the reverse side of the document, provided by the + // user. Available for "driver_license" and "identity_card". The file can + // be decrypted and verified using the accompanying EncryptedCredentials. + ReverseSide *PassportFile `json:"reverse_side,omitempty"` + + // Encrypted file with the selfie of the user holding a document, + // provided by the user; available for "passport", "driver_license", + // "identity_card" and "internal_passport". The file can be decrypted + // and verified using the accompanying EncryptedCredentials. + Selfie *PassportFile `json:"selfie,omitempty"` + } + + // EncryptedCredentials contains data required for decrypting and + // authenticating EncryptedPassportElement. See the Telegram Passport + // Documentation for a complete description of the data decryption and + // authentication processes. + EncryptedCredentials struct { + // Base64-encoded encrypted JSON-serialized data with unique user's + // payload, data hashes and secrets required for EncryptedPassportElement + // decryption and authentication + Data string `json:"data"` + + // Base64-encoded data hash for data authentication + Hash string `json:"hash"` + + // Base64-encoded secret, encrypted with the bot's public RSA key, + // required for data decryption + Secret string `json:"secret"` + } + + // PassportElementError represents an error in the Telegram Passport element + // which was submitted that should be resolved by the user. + PassportElementError interface{} + + // PassportElementErrorDataField represents an issue in one of the data + // fields that was provided by the user. The error is considered resolved + // when the field's value changes. + PassportElementErrorDataField struct { + // Error source, must be data + Source string `json:"source"` + + // The section of the user's Telegram Passport which has the error, one + // of "personal_details", "passport", "driver_license", "identity_card", + // "internal_passport", "address" + Type string `json:"type"` + + // Name of the data field which has the error + FieldName string `json:"field_name"` + + // Base64-encoded data hash + DataHash string `json:"data_hash"` + + // Error message + Message string `json:"message"` + } + + // PassportElementErrorFrontSide represents an issue with the front side of + // a document. The error is considered resolved when the file with the front + // side of the document changes. + PassportElementErrorFrontSide struct { + // Error source, must be front_side + Source string `json:"source"` + + // The section of the user's Telegram Passport which has the issue, one + // of "passport", "driver_license", "identity_card", "internal_passport" + Type string `json:"type"` + + // Base64-encoded hash of the file with the front side of the document + FileHash string `json:"file_hash"` + + // Error message + Message string `json:"message"` + } + + // PassportElementErrorReverseSide represents an issue with the reverse side + // of a document. The error is considered resolved when the file with reverse + // side of the document changes. + PassportElementErrorReverseSide struct { + // Error source, must be reverse_side + Source string `json:"source"` + + // The section of the user's Telegram Passport which has the issue, one + // of "driver_license", "identity_card" + Type string `json:"type"` + + // Base64-encoded hash of the file with the reverse side of the document + FileHash string `json:"file_hash"` + + // Error message + Message string `json:"message"` + } + + // PassportElementErrorSelfie represents an issue with the selfie with a + // document. The error is considered resolved when the file with the selfie + // changes. + PassportElementErrorSelfie struct { + // Error source, must be selfie + Source string `json:"source"` + + // The section of the user's Telegram Passport which has the issue, one + // of "passport", "driver_license", "identity_card", "internal_passport" + Type string `json:"type"` + + // Base64-encoded hash of the file with the selfie + FileHash string `json:"file_hash"` + + // Error message + Message string `json:"message"` + } + + // PassportElementErrorFile represents an issue with a document scan. The + // error is considered resolved when the file with the document scan changes. + PassportElementErrorFile struct { + // Error source, must be file + Source string `json:"source"` + + // The section of the user's Telegram Passport which has the issue, one + // of "utility_bill", "bank_statement", "rental_agreement", + // "passport_registration", "temporary_registration" + Type string `json:"type"` + + // Base64-encoded file hash + FileHash string `json:"file_hash"` + + // Error message + Message string `json:"message"` + } + + // PassportElementErrorFiles represents an issue with a list of scans. The + // error is considered resolved when the list of files containing the scans + // changes. + PassportElementErrorFiles struct { + // Error source, must be files + Source string `json:"source"` + + // The section of the user's Telegram Passport which has the issue, one + // of "utility_bill", "bank_statement", "rental_agreement", + // "passport_registration", "temporary_registration" + Type string `json:"type"` + + // List of base64-encoded file hashes + FileHashes []string `json:"file_hashes"` + + // Error message + Message string `json:"message"` + } + + // Credentials contains encrypted data. + Credentials struct { + Data SecureData `json:"secure_data"` + // Nonce the same nonce given in the request + Nonce string `json:"nonce"` + } + + // SecureData is a map of the fields and their encrypted values. + SecureData map[string]*SecureValue + // PersonalDetails *SecureValue `json:"personal_details"` + // Passport *SecureValue `json:"passport"` + // InternalPassport *SecureValue `json:"internal_passport"` + // DriverLicense *SecureValue `json:"driver_license"` + // IdentityCard *SecureValue `json:"identity_card"` + // Address *SecureValue `json:"address"` + // UtilityBill *SecureValue `json:"utility_bill"` + // BankStatement *SecureValue `json:"bank_statement"` + // RentalAgreement *SecureValue `json:"rental_agreement"` + // PassportRegistration *SecureValue `json:"passport_registration"` + // TemporaryRegistration *SecureValue `json:"temporary_registration"` + + // SecureValue contains encrypted values for a SecureData item. + SecureValue struct { + Data *DataCredentials `json:"data"` + FrontSide *FileCredentials `json:"front_side"` + ReverseSide *FileCredentials `json:"reverse_side"` + Selfie *FileCredentials `json:"selfie"` + Translation []*FileCredentials `json:"translation"` + Files []*FileCredentials `json:"files"` + } + + // DataCredentials contains information required to decrypt data. + DataCredentials struct { + // DataHash checksum of encrypted data + DataHash string `json:"data_hash"` + // Secret of encrypted data + Secret string `json:"secret"` + } + + // FileCredentials contains information required to decrypt files. + FileCredentials struct { + // FileHash checksum of encrypted data + FileHash string `json:"file_hash"` + // Secret of encrypted data + Secret string `json:"secret"` + } + + // PersonalDetails https://core.telegram.org/passport#personaldetails + PersonalDetails struct { + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + MiddleName string `json:"middle_name"` + BirthDate string `json:"birth_date"` + Gender string `json:"gender"` + CountryCode string `json:"country_code"` + ResidenceCountryCode string `json:"residence_country_code"` + FirstNameNative string `json:"first_name_native"` + LastNameNative string `json:"last_name_native"` + MiddleNameNative string `json:"middle_name_native"` + } + + // IDDocumentData https://core.telegram.org/passport#iddocumentdata + IDDocumentData struct { + DocumentNumber string `json:"document_no"` + ExpiryDate string `json:"expiry_date"` + } +) diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/types.go b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/types.go index 0843ab94..d3c433fb 100644 --- a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/types.go +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/types.go @@ -144,6 +144,7 @@ type Message struct { Entities *[]MessageEntity `json:"entities"` // optional Audio *Audio `json:"audio"` // optional Document *Document `json:"document"` // optional + Animation *ChatAnimation `json:"animation"` // optional Game *Game `json:"game"` // optional Photo *[]PhotoSize `json:"photo"` // optional Sticker *Sticker `json:"sticker"` // optional @@ -167,6 +168,7 @@ type Message struct { PinnedMessage *Message `json:"pinned_message"` // optional Invoice *Invoice `json:"invoice"` // optional SuccessfulPayment *SuccessfulPayment `json:"successful_payment"` // optional + PassportData *PassportData `json:"passport_data,omitempty"` // optional } // Time converts the message timestamp into a Time. @@ -293,6 +295,18 @@ type Sticker struct { SetName string `json:"set_name"` // optional } +// ChatAnimation contains information about an animation. +type ChatAnimation struct { + FileID string `json:"file_id"` + Width int `json:"width"` + Height int `json:"height"` + Duration int `json:"duration"` + Thumbnail *PhotoSize `json:"thumb"` // optional + FileName string `json:"file_name"` // optional + MimeType string `json:"mime_type"` // optional + FileSize int `json:"file_size"` // optional +} + // Video contains information about a video. type Video struct { FileID string `json:"file_id"` @@ -511,6 +525,27 @@ func (info WebhookInfo) IsSet() bool { return info.URL != "" } +// InputMediaPhoto contains a photo for displaying as part of a media group. +type InputMediaPhoto struct { + Type string `json:"type"` + Media string `json:"media"` + Caption string `json:"caption"` + ParseMode string `json:"parse_mode"` +} + +// InputMediaVideo contains a video for displaying as part of a media group. +type InputMediaVideo struct { + Type string `json:"type"` + Media string `json:"media"` + // thumb intentionally missing as it is not currently compatible + Caption string `json:"caption"` + ParseMode string `json:"parse_mode"` + Width int `json:"width"` + Height int `json:"height"` + Duration int `json:"duration"` + SupportsStreaming bool `json:"supports_streaming"` +} + // InlineQuery is a Query from Telegram for an inline request. type InlineQuery struct { ID string `json:"id"` diff --git a/vendor/github.com/google/gops/agent/agent.go b/vendor/github.com/google/gops/agent/agent.go index 57083918..24c0b896 100644 --- a/vendor/github.com/google/gops/agent/agent.go +++ b/vendor/github.com/google/gops/agent/agent.go @@ -7,6 +7,8 @@ package agent import ( + "bufio" + "encoding/binary" "fmt" "io" "io/ioutil" @@ -14,14 +16,13 @@ import ( "os" gosignal "os/signal" "runtime" + "runtime/debug" "runtime/pprof" "runtime/trace" "strconv" "sync" "time" - "bufio" - "github.com/google/gops/internal" "github.com/google/gops/signal" "github.com/kardianos/osext" @@ -43,10 +44,16 @@ type Options struct { // Optional. Addr string - // NoShutdownCleanup tells the agent not to automatically cleanup - // resources if the running process receives an interrupt. + // ConfigDir is the directory to store the configuration file, + // PID of the gops process, filename, port as well as content. // Optional. - NoShutdownCleanup bool + ConfigDir string + + // ShutdownCleanup automatically cleans up resources if the + // running process receives an interrupt. Otherwise, users + // can call Close before shutting down. + // Optional. + ShutdownCleanup bool } // Listen starts the gops agent on a host process. Once agent started, users @@ -58,26 +65,29 @@ type Options struct { // Note: The agent exposes an endpoint via a TCP connection that can be used by // any program on the system. Review your security requirements before starting // the agent. -func Listen(opts *Options) error { +func Listen(opts Options) error { mu.Lock() defer mu.Unlock() - if opts == nil { - opts = &Options{} - } if portfile != "" { return fmt.Errorf("gops: agent already listening at: %v", listener.Addr()) } - gopsdir, err := internal.ConfigDir() + // new + gopsdir := opts.ConfigDir + if gopsdir == "" { + cfgDir, err := internal.ConfigDir() + if err != nil { + return err + } + gopsdir = cfgDir + } + + err := os.MkdirAll(gopsdir, os.ModePerm) if err != nil { return err } - err = os.MkdirAll(gopsdir, os.ModePerm) - if err != nil { - return err - } - if !opts.NoShutdownCleanup { + if opts.ShutdownCleanup { gracefulShutdown() } @@ -165,7 +175,7 @@ func formatBytes(val uint64) string { return fmt.Sprintf("%d bytes", val) } -func handle(conn io.Writer, msg []byte) error { +func handle(conn io.ReadWriter, msg []byte) error { switch msg[0] { case signal.StackTrace: return pprof.Lookup("goroutine").WriteTo(conn, 2) @@ -190,13 +200,20 @@ func handle(conn io.Writer, msg []byte) error { fmt.Fprintf(conn, "heap-objects: %v\n", s.HeapObjects) fmt.Fprintf(conn, "stack-in-use: %v\n", formatBytes(s.StackInuse)) fmt.Fprintf(conn, "stack-sys: %v\n", formatBytes(s.StackSys)) + fmt.Fprintf(conn, "stack-mspan-inuse: %v\n", formatBytes(s.MSpanInuse)) + fmt.Fprintf(conn, "stack-mspan-sys: %v\n", formatBytes(s.MSpanSys)) + fmt.Fprintf(conn, "stack-mcache-inuse: %v\n", formatBytes(s.MCacheInuse)) + fmt.Fprintf(conn, "stack-mcache-sys: %v\n", formatBytes(s.MCacheSys)) + fmt.Fprintf(conn, "other-sys: %v\n", formatBytes(s.OtherSys)) + fmt.Fprintf(conn, "gc-sys: %v\n", formatBytes(s.GCSys)) fmt.Fprintf(conn, "next-gc: when heap-alloc >= %v\n", formatBytes(s.NextGC)) lastGC := "-" if s.LastGC != 0 { lastGC = fmt.Sprint(time.Unix(0, int64(s.LastGC))) } fmt.Fprintf(conn, "last-gc: %v\n", lastGC) - fmt.Fprintf(conn, "gc-pause: %v\n", time.Duration(s.PauseTotalNs)) + fmt.Fprintf(conn, "gc-pause-total: %v\n", time.Duration(s.PauseTotalNs)) + fmt.Fprintf(conn, "gc-pause: %v\n", s.PauseNs[(s.NumGC+255)%256]) fmt.Fprintf(conn, "num-gc: %v\n", s.NumGC) fmt.Fprintf(conn, "enable-gc: %v\n", s.EnableGC) fmt.Fprintf(conn, "debug-gc: %v\n", s.DebugGC) @@ -232,6 +249,12 @@ func handle(conn io.Writer, msg []byte) error { trace.Start(conn) time.Sleep(5 * time.Second) trace.Stop() + case signal.SetGCPercent: + perc, err := binary.ReadVarint(bufio.NewReader(conn)) + if err != nil { + return err + } + fmt.Fprintf(conn, "New GC percent set to %v. Previous value was %v.\n", perc, debug.SetGCPercent(int(perc))) } return nil } diff --git a/vendor/github.com/google/gops/internal/internal.go b/vendor/github.com/google/gops/internal/internal.go index 13828220..80eac63f 100644 --- a/vendor/github.com/google/gops/internal/internal.go +++ b/vendor/github.com/google/gops/internal/internal.go @@ -1,3 +1,7 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package internal import ( @@ -11,7 +15,13 @@ import ( "strings" ) +const gopsConfigDirEnvKey = "GOPS_CONFIG_DIR" + func ConfigDir() (string, error) { + if configDir := os.Getenv(gopsConfigDirEnvKey); configDir != "" { + return configDir, nil + } + if runtime.GOOS == "windows" { return filepath.Join(os.Getenv("APPDATA"), "gops"), nil } diff --git a/vendor/github.com/google/gops/signal/signal.go b/vendor/github.com/google/gops/signal/signal.go index b2bfbe15..c70764a0 100644 --- a/vendor/github.com/google/gops/signal/signal.go +++ b/vendor/github.com/google/gops/signal/signal.go @@ -32,4 +32,7 @@ const ( // BinaryDump returns running binary file. BinaryDump = byte(0x9) + + // SetGCPercent sets the garbage collection target percentage. + SetGCPercent = byte(0x10) ) diff --git a/vendor/github.com/gorilla/schema/cache.go b/vendor/github.com/gorilla/schema/cache.go index b2c5995d..73b75f48 100644 --- a/vendor/github.com/gorilla/schema/cache.go +++ b/vendor/github.com/gorilla/schema/cache.go @@ -18,13 +18,9 @@ var invalidPath = errors.New("schema: invalid path") func newCache() *cache { c := cache{ m: make(map[reflect.Type]*structInfo), - conv: make(map[reflect.Kind]Converter), regconv: make(map[reflect.Type]Converter), tag: "schema", } - for k, v := range converters { - c.conv[k] = v - } return &c } @@ -32,11 +28,15 @@ func newCache() *cache { type cache struct { l sync.RWMutex m map[reflect.Type]*structInfo - conv map[reflect.Kind]Converter regconv map[reflect.Type]Converter tag string } +// registerConverter registers a converter function for a custom type. +func (c *cache) registerConverter(value interface{}, converterFunc Converter) { + c.regconv[reflect.TypeOf(value)] = converterFunc +} + // parsePath parses a path in dotted notation verifying that it is a valid // path to a struct field. // @@ -63,7 +63,7 @@ func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) { } // Valid field. Append index. path = append(path, field.name) - if field.ss { + if field.isSliceOfStructs && (!field.unmarshalerInfo.IsValid || (field.unmarshalerInfo.IsValid && field.unmarshalerInfo.IsSliceElement)) { // Parse a special case: slices of structs. // i+1 must be the slice index. // @@ -142,7 +142,7 @@ func (c *cache) create(t reflect.Type, info *structInfo) *structInfo { c.create(ft, info) for _, fi := range info.fields[bef:len(info.fields)] { // exclude required check because duplicated to embedded field - fi.required = false + fi.isRequired = false } } } @@ -162,6 +162,7 @@ func (c *cache) createField(field reflect.StructField, info *structInfo) { // First let's get the basic type. isSlice, isStruct := false, false ft := field.Type + m := isTextUnmarshaler(reflect.Zero(ft)) if ft.Kind() == reflect.Ptr { ft = ft.Elem() } @@ -178,29 +179,26 @@ func (c *cache) createField(field reflect.StructField, info *structInfo) { } } if isStruct = ft.Kind() == reflect.Struct; !isStruct { - if conv := c.converter(ft); conv == nil { + if c.converter(ft) == nil && builtinConverters[ft.Kind()] == nil { // Type is not supported. return } } info.fields = append(info.fields, &fieldInfo{ - typ: field.Type, - name: field.Name, - ss: isSlice && isStruct, - alias: alias, - anon: field.Anonymous, - required: options.Contains("required"), + typ: field.Type, + name: field.Name, + alias: alias, + unmarshalerInfo: m, + isSliceOfStructs: isSlice && isStruct, + isAnonymous: field.Anonymous, + isRequired: options.Contains("required"), }) } // converter returns the converter for a type. func (c *cache) converter(t reflect.Type) Converter { - conv := c.regconv[t] - if conv == nil { - conv = c.conv[t.Kind()] - } - return conv + return c.regconv[t] } // ---------------------------------------------------------------------------- @@ -219,12 +217,18 @@ func (i *structInfo) get(alias string) *fieldInfo { } type fieldInfo struct { - typ reflect.Type - name string // field name in the struct. - ss bool // true if this is a slice of structs. - alias string - anon bool // is an embedded field - required bool // tag option + typ reflect.Type + // name is the field name in the struct. + name string + alias string + // unmarshalerInfo contains information regarding the + // encoding.TextUnmarshaler implementation of the field type. + unmarshalerInfo unmarshaler + // isSliceOfStructs indicates if the field type is a slice of structs. + isSliceOfStructs bool + // isAnonymous indicates whether the field is embedded in the struct. + isAnonymous bool + isRequired bool } type pathPart struct { diff --git a/vendor/github.com/gorilla/schema/converter.go b/vendor/github.com/gorilla/schema/converter.go index b33e9423..4f2116a1 100644 --- a/vendor/github.com/gorilla/schema/converter.go +++ b/vendor/github.com/gorilla/schema/converter.go @@ -30,7 +30,7 @@ var ( ) // Default converters for basic types. -var converters = map[reflect.Kind]Converter{ +var builtinConverters = map[reflect.Kind]Converter{ boolType: convertBool, float32Type: convertFloat32, float64Type: convertFloat64, diff --git a/vendor/github.com/gorilla/schema/decoder.go b/vendor/github.com/gorilla/schema/decoder.go index b0f8cb00..5352a908 100644 --- a/vendor/github.com/gorilla/schema/decoder.go +++ b/vendor/github.com/gorilla/schema/decoder.go @@ -56,7 +56,7 @@ func (d *Decoder) IgnoreUnknownKeys(i bool) { // RegisterConverter registers a converter function for a custom type. func (d *Decoder) RegisterConverter(value interface{}, converterFunc Converter) { - d.cache.regconv[reflect.TypeOf(value)] = converterFunc + d.cache.registerConverter(value, converterFunc) } // Decode decodes a map[string][]string to a struct. @@ -90,7 +90,7 @@ func (d *Decoder) Decode(dst interface{}, src map[string][]string) error { return d.checkRequired(t, src, "") } -// checkRequired checks whether requred field empty +// checkRequired checks whether required fields are empty // // check type t recursively if t has struct fields, and prefix is same as parsePath: in dotted notation // @@ -106,7 +106,7 @@ func (d *Decoder) checkRequired(t reflect.Type, src map[string][]string, prefix if f.typ.Kind() == reflect.Struct { err := d.checkRequired(f.typ, src, prefix+f.alias+".") if err != nil { - if !f.anon { + if !f.isAnonymous { return err } // check embedded parent field. @@ -116,7 +116,7 @@ func (d *Decoder) checkRequired(t reflect.Type, src map[string][]string, prefix } } } - if f.required { + if f.isRequired { key := f.alias if prefix != "" { key = prefix + key @@ -153,7 +153,6 @@ func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values } v = v.FieldByName(name) } - // Don't even bother for unexported fields. if !v.CanSet() { return nil @@ -185,7 +184,8 @@ func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values // Get the converter early in case there is one for a slice type. conv := d.cache.converter(t) - if conv == nil && t.Kind() == reflect.Slice { + m := isTextUnmarshaler(v) + if conv == nil && t.Kind() == reflect.Slice && m.IsSliceElement { var items []reflect.Value elemT := t.Elem() isPtrElem := elemT.Kind() == reflect.Ptr @@ -196,9 +196,12 @@ func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values // Try to get a converter for the element type. conv := d.cache.converter(elemT) if conv == nil { - // As we are not dealing with slice of structs here, we don't need to check if the type - // implements TextUnmarshaler interface - return fmt.Errorf("schema: converter not found for %v", elemT) + conv = builtinConverters[elemT.Kind()] + if conv == nil { + // As we are not dealing with slice of structs here, we don't need to check if the type + // implements TextUnmarshaler interface + return fmt.Errorf("schema: converter not found for %v", elemT) + } } for key, value := range values { @@ -206,6 +209,26 @@ func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values if d.zeroEmpty { items = append(items, reflect.Zero(elemT)) } + } else if m.IsValid { + u := reflect.New(elemT) + if m.IsSliceElementPtr { + u = reflect.New(reflect.PtrTo(elemT).Elem()) + } + if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)); err != nil { + return ConversionError{ + Key: path, + Type: t, + Index: key, + Err: err, + } + } + if m.IsSliceElementPtr { + items = append(items, u.Elem().Addr()) + } else if u.Kind() == reflect.Ptr { + items = append(items, u.Elem()) + } else { + items = append(items, u) + } } else if item := conv(value); item.IsValid() { if isPtrElem { ptr := reflect.New(elemT) @@ -260,11 +283,45 @@ func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values val = values[len(values)-1] } - if val == "" { + if conv != nil { + if value := conv(val); value.IsValid() { + v.Set(value.Convert(t)) + } else { + return ConversionError{ + Key: path, + Type: t, + Index: -1, + } + } + } else if m.IsValid { + if m.IsPtr { + u := reflect.New(v.Type()) + if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(val)); err != nil { + return ConversionError{ + Key: path, + Type: t, + Index: -1, + Err: err, + } + } + v.Set(reflect.Indirect(u)) + } else { + // If the value implements the encoding.TextUnmarshaler interface + // apply UnmarshalText as the converter + if err := m.Unmarshaler.UnmarshalText([]byte(val)); err != nil { + return ConversionError{ + Key: path, + Type: t, + Index: -1, + Err: err, + } + } + } + } else if val == "" { if d.zeroEmpty { v.Set(reflect.Zero(t)) } - } else if conv != nil { + } else if conv := builtinConverters[t.Kind()]; conv != nil { if value := conv(val); value.IsValid() { v.Set(value.Convert(t)) } else { @@ -275,31 +332,71 @@ func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values } } } else { - // When there's no registered conversion for the custom type, we will check if the type - // implements the TextUnmarshaler interface. As the UnmarshalText function should be applied - // to the pointer of the type, we convert the value to pointer. - if v.CanAddr() { - v = v.Addr() - } - - if u, ok := v.Interface().(encoding.TextUnmarshaler); ok { - if err := u.UnmarshalText([]byte(val)); err != nil { - return ConversionError{ - Key: path, - Type: t, - Index: -1, - Err: err, - } - } - - } else { - return fmt.Errorf("schema: converter not found for %v", t) - } + return fmt.Errorf("schema: converter not found for %v", t) } } return nil } +func isTextUnmarshaler(v reflect.Value) unmarshaler { + // Create a new unmarshaller instance + m := unmarshaler{} + if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid { + return m + } + // As the UnmarshalText function should be applied to the pointer of the + // type, we check that type to see if it implements the necessary + // method. + if m.Unmarshaler, m.IsValid = reflect.New(v.Type()).Interface().(encoding.TextUnmarshaler); m.IsValid { + m.IsPtr = true + return m + } + + // if v is []T or *[]T create new T + t := v.Type() + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() == reflect.Slice { + // Check if the slice implements encoding.TextUnmarshaller + if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid { + return m + } + // If t is a pointer slice, check if its elements implement + // encoding.TextUnmarshaler + m.IsSliceElement = true + if t = t.Elem(); t.Kind() == reflect.Ptr { + t = reflect.PtrTo(t.Elem()) + v = reflect.Zero(t) + m.IsSliceElementPtr = true + m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler) + return m + } + } + + v = reflect.New(t) + m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler) + return m +} + +// TextUnmarshaler helpers ---------------------------------------------------- +// unmarshaller contains information about a TextUnmarshaler type +type unmarshaler struct { + Unmarshaler encoding.TextUnmarshaler + // IsValid indicates whether the resolved type indicated by the other + // flags implements the encoding.TextUnmarshaler interface. + IsValid bool + // IsPtr indicates that the resolved type is the pointer of the original + // type. + IsPtr bool + // IsSliceElement indicates that the resolved type is a slice element of + // the original type. + IsSliceElement bool + // IsSliceElementPtr indicates that the resolved type is a pointer to a + // slice element of the original type. + IsSliceElementPtr bool +} + // Errors --------------------------------------------------------------------- // ConversionError stores information about a failed conversion. diff --git a/vendor/github.com/gorilla/schema/doc.go b/vendor/github.com/gorilla/schema/doc.go index a95e87bd..aae9f33f 100644 --- a/vendor/github.com/gorilla/schema/doc.go +++ b/vendor/github.com/gorilla/schema/doc.go @@ -24,7 +24,7 @@ The basic usage is really simple. Given this struct: This is just a simple example and it doesn't make a lot of sense to create the map manually. Typically it will come from a http.Request object and -will be of type url.Values: http.Request.Form or http.Request.MultipartForm: +will be of type url.Values, http.Request.Form, or http.Request.MultipartForm: func MyHandler(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() @@ -45,7 +45,7 @@ will be of type url.Values: http.Request.Form or http.Request.MultipartForm: } Note: it is a good idea to set a Decoder instance as a package global, -because it caches meta-data about structs, and a instance can be shared safely: +because it caches meta-data about structs, and an instance can be shared safely: var decoder = schema.NewDecoder() @@ -121,7 +121,7 @@ field, we could not translate multiple values to it if we did not use an index for the parent struct. There's also the possibility to create a custom type that implements the -TextUnmarshaler interface, and in this case there's no need to registry +TextUnmarshaler interface, and in this case there's no need to register a converter, like: type Person struct { diff --git a/vendor/github.com/gorilla/schema/encoder.go b/vendor/github.com/gorilla/schema/encoder.go index aa43a5c9..bf1d511e 100644 --- a/vendor/github.com/gorilla/schema/encoder.go +++ b/vendor/github.com/gorilla/schema/encoder.go @@ -40,6 +40,34 @@ func (e *Encoder) SetAliasTag(tag string) { e.cache.tag = tag } +// isValidStructPointer test if input value is a valid struct pointer. +func isValidStructPointer(v reflect.Value) bool { + return v.Type().Kind() == reflect.Ptr && v.Elem().IsValid() && v.Elem().Type().Kind() == reflect.Struct +} + +func isZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Func: + case reflect.Map, reflect.Slice: + return v.IsNil() || v.Len() == 0 + case reflect.Array: + z := true + for i := 0; i < v.Len(); i++ { + z = z && isZero(v.Index(i)) + } + return z + case reflect.Struct: + z := true + for i := 0; i < v.NumField(); i++ { + z = z && isZero(v.Field(i)) + } + return z + } + // Compare other types directly: + z := reflect.Zero(v.Type()) + return v.Interface() == z.Interface() +} + func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error { if v.Kind() == reflect.Ptr { v = v.Elem() @@ -57,8 +85,9 @@ func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error { continue } - if v.Field(i).Type().Kind() == reflect.Struct { - e.encode(v.Field(i), dst) + // Encode struct pointer types if the field is a valid pointer and a struct. + if isValidStructPointer(v.Field(i)) { + e.encode(v.Field(i).Elem(), dst) continue } @@ -67,7 +96,7 @@ func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error { // Encode non-slice types and custom implementations immediately. if encFunc != nil { value := encFunc(v.Field(i)) - if value == "" && opts.Contains("omitempty") { + if opts.Contains("omitempty") && isZero(v.Field(i)) { continue } @@ -75,6 +104,11 @@ func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error { continue } + if v.Field(i).Type().Kind() == reflect.Struct { + e.encode(v.Field(i), dst) + continue + } + if v.Field(i).Type().Kind() == reflect.Slice { encFunc = typeEncoder(v.Field(i).Type().Elem(), e.regenc) } diff --git a/vendor/github.com/gorilla/websocket/.gitignore b/vendor/github.com/gorilla/websocket/.gitignore index ac710204..cd3fcd1e 100644 --- a/vendor/github.com/gorilla/websocket/.gitignore +++ b/vendor/github.com/gorilla/websocket/.gitignore @@ -22,4 +22,4 @@ _testmain.go *.exe .idea/ -*.iml \ No newline at end of file +*.iml diff --git a/vendor/github.com/gorilla/websocket/.travis.yml b/vendor/github.com/gorilla/websocket/.travis.yml index 3d8d29cf..a49db51c 100644 --- a/vendor/github.com/gorilla/websocket/.travis.yml +++ b/vendor/github.com/gorilla/websocket/.travis.yml @@ -3,11 +3,11 @@ sudo: false matrix: include: - - go: 1.4 - - go: 1.5 - - go: 1.6 - - go: 1.7 - - go: 1.8 + - go: 1.7.x + - go: 1.8.x + - go: 1.9.x + - go: 1.10.x + - go: 1.11.x - go: tip allow_failures: - go: tip diff --git a/vendor/github.com/gorilla/websocket/AUTHORS b/vendor/github.com/gorilla/websocket/AUTHORS index b003eca0..1931f400 100644 --- a/vendor/github.com/gorilla/websocket/AUTHORS +++ b/vendor/github.com/gorilla/websocket/AUTHORS @@ -4,5 +4,6 @@ # Please keep the list sorted. Gary Burd +Google LLC (https://opensource.google.com/) Joachim Bauch diff --git a/vendor/github.com/gorilla/websocket/README.md b/vendor/github.com/gorilla/websocket/README.md index 33c3d2be..20e391f8 100644 --- a/vendor/github.com/gorilla/websocket/README.md +++ b/vendor/github.com/gorilla/websocket/README.md @@ -51,7 +51,7 @@ subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn Write message using io.WriteCloserYesNo, see note 3 -Notes: +Notes: 1. Large messages are fragmented in [Chrome's new WebSocket implementation](http://www.ietf.org/mail-archive/web/hybi/current/msg10503.html). 2. The application can get the type of a received data message by implementing diff --git a/vendor/github.com/gorilla/websocket/client.go b/vendor/github.com/gorilla/websocket/client.go index 43a87c75..2e32fd50 100644 --- a/vendor/github.com/gorilla/websocket/client.go +++ b/vendor/github.com/gorilla/websocket/client.go @@ -5,15 +5,15 @@ package websocket import ( - "bufio" "bytes" + "context" "crypto/tls" - "encoding/base64" "errors" "io" "io/ioutil" "net" "net/http" + "net/http/httptrace" "net/url" "strings" "time" @@ -53,6 +53,10 @@ type Dialer struct { // NetDial is nil, net.Dial is used. NetDial func(network, addr string) (net.Conn, error) + // NetDialContext specifies the dial function for creating TCP connections. If + // NetDialContext is nil, net.DialContext is used. + NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error) + // Proxy specifies a function to return a proxy for a given // Request. If the function returns a non-nil error, the // request is aborted with the provided error. @@ -71,6 +75,17 @@ type Dialer struct { // do not limit the size of the messages that can be sent or received. ReadBufferSize, WriteBufferSize int + // WriteBufferPool is a pool of buffers for write operations. If the value + // is not set, then write buffers are allocated to the connection for the + // lifetime of the connection. + // + // A pool is most useful when the application has a modest volume of writes + // across a large number of connections. + // + // Applications should use a single pool for each unique value of + // WriteBufferSize. + WriteBufferPool BufferPool + // Subprotocols specifies the client's requested subprotocols. Subprotocols []string @@ -86,52 +101,13 @@ type Dialer struct { Jar http.CookieJar } -var errMalformedURL = errors.New("malformed ws or wss URL") - -// parseURL parses the URL. -// -// This function is a replacement for the standard library url.Parse function. -// In Go 1.4 and earlier, url.Parse loses information from the path. -func parseURL(s string) (*url.URL, error) { - // From the RFC: - // - // ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ] - // wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ] - var u url.URL - switch { - case strings.HasPrefix(s, "ws://"): - u.Scheme = "ws" - s = s[len("ws://"):] - case strings.HasPrefix(s, "wss://"): - u.Scheme = "wss" - s = s[len("wss://"):] - default: - return nil, errMalformedURL - } - - if i := strings.Index(s, "?"); i >= 0 { - u.RawQuery = s[i+1:] - s = s[:i] - } - - if i := strings.Index(s, "/"); i >= 0 { - u.Opaque = s[i:] - s = s[:i] - } else { - u.Opaque = "/" - } - - u.Host = s - - if strings.Contains(u.Host, "@") { - // Don't bother parsing user information because user information is - // not allowed in websocket URIs. - return nil, errMalformedURL - } - - return &u, nil +// Dial creates a new client connection by calling DialContext with a background context. +func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { + return d.DialContext(context.Background(), urlStr, requestHeader) } +var errMalformedURL = errors.New("malformed ws or wss URL") + func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) { hostPort = u.Host hostNoPort = u.Host @@ -150,26 +126,29 @@ func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) { return hostPort, hostNoPort } -// DefaultDialer is a dialer with all fields set to the default zero values. +// DefaultDialer is a dialer with all fields set to the default values. var DefaultDialer = &Dialer{ - Proxy: http.ProxyFromEnvironment, + Proxy: http.ProxyFromEnvironment, + HandshakeTimeout: 45 * time.Second, } -// Dial creates a new client connection. Use requestHeader to specify the +// nilDialer is dialer to use when receiver is nil. +var nilDialer = *DefaultDialer + +// DialContext creates a new client connection. Use requestHeader to specify the // origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie). // Use the response.Header to get the selected subprotocol // (Sec-WebSocket-Protocol) and cookies (Set-Cookie). // +// The context will be used in the request and in the Dialer +// // If the WebSocket handshake fails, ErrBadHandshake is returned along with a // non-nil *http.Response so that callers can handle redirects, authentication, // etcetera. The response body may not contain the entire response and does not // need to be closed by the application. -func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { - +func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { if d == nil { - d = &Dialer{ - Proxy: http.ProxyFromEnvironment, - } + d = &nilDialer } challengeKey, err := generateChallengeKey() @@ -177,7 +156,7 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re return nil, nil, err } - u, err := parseURL(urlStr) + u, err := url.Parse(urlStr) if err != nil { return nil, nil, err } @@ -205,6 +184,7 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re Header: make(http.Header), Host: u.Host, } + req = req.WithContext(ctx) // Set the cookies present in the cookie jar of the dialer if d.Jar != nil { @@ -237,45 +217,83 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re k == "Sec-Websocket-Extensions" || (k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0): return nil, nil, errors.New("websocket: duplicate header not allowed: " + k) + case k == "Sec-Websocket-Protocol": + req.Header["Sec-WebSocket-Protocol"] = vs default: req.Header[k] = vs } } if d.EnableCompression { - req.Header.Set("Sec-Websocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover") + req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"} + } + + if d.HandshakeTimeout != 0 { + var cancel func() + ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout) + defer cancel() + } + + // Get network dial function. + var netDial func(network, add string) (net.Conn, error) + + if d.NetDialContext != nil { + netDial = func(network, addr string) (net.Conn, error) { + return d.NetDialContext(ctx, network, addr) + } + } else if d.NetDial != nil { + netDial = d.NetDial + } else { + netDialer := &net.Dialer{} + netDial = func(network, addr string) (net.Conn, error) { + return netDialer.DialContext(ctx, network, addr) + } + } + + // If needed, wrap the dial function to set the connection deadline. + if deadline, ok := ctx.Deadline(); ok { + forwardDial := netDial + netDial = func(network, addr string) (net.Conn, error) { + c, err := forwardDial(network, addr) + if err != nil { + return nil, err + } + err = c.SetDeadline(deadline) + if err != nil { + c.Close() + return nil, err + } + return c, nil + } + } + + // If needed, wrap the dial function to connect through a proxy. + if d.Proxy != nil { + proxyURL, err := d.Proxy(req) + if err != nil { + return nil, nil, err + } + if proxyURL != nil { + dialer, err := proxy_FromURL(proxyURL, netDialerFunc(netDial)) + if err != nil { + return nil, nil, err + } + netDial = dialer.Dial + } } hostPort, hostNoPort := hostPortNoPort(u) - - var proxyURL *url.URL - // Check wether the proxy method has been configured - if d.Proxy != nil { - proxyURL, err = d.Proxy(req) - } - if err != nil { - return nil, nil, err + trace := httptrace.ContextClientTrace(ctx) + if trace != nil && trace.GetConn != nil { + trace.GetConn(hostPort) } - var targetHostPort string - if proxyURL != nil { - targetHostPort, _ = hostPortNoPort(proxyURL) - } else { - targetHostPort = hostPort + netConn, err := netDial("tcp", hostPort) + if trace != nil && trace.GotConn != nil { + trace.GotConn(httptrace.GotConnInfo{ + Conn: netConn, + }) } - - var deadline time.Time - if d.HandshakeTimeout != 0 { - deadline = time.Now().Add(d.HandshakeTimeout) - } - - netDial := d.NetDial - if netDial == nil { - netDialer := &net.Dialer{Deadline: deadline} - netDial = netDialer.Dial - } - - netConn, err := netDial("tcp", targetHostPort) if err != nil { return nil, nil, err } @@ -286,42 +304,6 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re } }() - if err := netConn.SetDeadline(deadline); err != nil { - return nil, nil, err - } - - if proxyURL != nil { - connectHeader := make(http.Header) - if user := proxyURL.User; user != nil { - proxyUser := user.Username() - if proxyPassword, passwordSet := user.Password(); passwordSet { - credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword)) - connectHeader.Set("Proxy-Authorization", "Basic "+credential) - } - } - connectReq := &http.Request{ - Method: "CONNECT", - URL: &url.URL{Opaque: hostPort}, - Host: hostPort, - Header: connectHeader, - } - - connectReq.Write(netConn) - - // Read response. - // Okay to use and discard buffered reader here, because - // TLS server will not speak until spoken to. - br := bufio.NewReader(netConn) - resp, err := http.ReadResponse(br, connectReq) - if err != nil { - return nil, nil, err - } - if resp.StatusCode != 200 { - f := strings.SplitN(resp.Status, " ", 2) - return nil, nil, errors.New(f[1]) - } - } - if u.Scheme == "https" { cfg := cloneTLSConfig(d.TLSClientConfig) if cfg.ServerName == "" { @@ -329,22 +311,31 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re } tlsConn := tls.Client(netConn, cfg) netConn = tlsConn - if err := tlsConn.Handshake(); err != nil { - return nil, nil, err + + var err error + if trace != nil { + err = doHandshakeWithTrace(trace, tlsConn, cfg) + } else { + err = doHandshake(tlsConn, cfg) } - if !cfg.InsecureSkipVerify { - if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { - return nil, nil, err - } + + if err != nil { + return nil, nil, err } } - conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize) + conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize, d.WriteBufferPool, nil, nil) if err := req.Write(netConn); err != nil { return nil, nil, err } + if trace != nil && trace.GotFirstResponseByte != nil { + if peek, err := conn.br.Peek(1); err == nil && len(peek) == 1 { + trace.GotFirstResponseByte() + } + } + resp, err := http.ReadResponse(conn.br, req) if err != nil { return nil, nil, err @@ -390,3 +381,15 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re netConn = nil // to avoid close in defer. return conn, resp, nil } + +func doHandshake(tlsConn *tls.Conn, cfg *tls.Config) error { + if err := tlsConn.Handshake(); err != nil { + return err + } + if !cfg.InsecureSkipVerify { + if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/gorilla/websocket/conn.go b/vendor/github.com/gorilla/websocket/conn.go index 97e1dbac..d2a21c14 100644 --- a/vendor/github.com/gorilla/websocket/conn.go +++ b/vendor/github.com/gorilla/websocket/conn.go @@ -76,7 +76,7 @@ const ( // is UTF-8 encoded text. PingMessage = 9 - // PongMessage denotes a ping control message. The optional message payload + // PongMessage denotes a pong control message. The optional message payload // is UTF-8 encoded text. PongMessage = 10 ) @@ -100,9 +100,8 @@ func (e *netError) Error() string { return e.msg } func (e *netError) Temporary() bool { return e.temporary } func (e *netError) Timeout() bool { return e.timeout } -// CloseError represents close frame. +// CloseError represents a close message. type CloseError struct { - // Code is defined in RFC 6455, section 11.7. Code int @@ -224,6 +223,20 @@ func isValidReceivedCloseCode(code int) bool { return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999) } +// BufferPool represents a pool of buffers. The *sync.Pool type satisfies this +// interface. The type of the value stored in a pool is not specified. +type BufferPool interface { + // Get gets a value from the pool or returns nil if the pool is empty. + Get() interface{} + // Put adds a value to the pool. + Put(interface{}) +} + +// writePoolData is the type added to the write buffer pool. This wrapper is +// used to prevent applications from peeking at and depending on the values +// added to the pool. +type writePoolData struct{ buf []byte } + // The Conn type represents a WebSocket connection. type Conn struct { conn net.Conn @@ -233,6 +246,8 @@ type Conn struct { // Write fields mu chan bool // used as mutex to protect write to conn writeBuf []byte // frame is constructed in this buffer. + writePool BufferPool + writeBufSize int writeDeadline time.Time writer io.WriteCloser // the current writer returned to the application isWriting bool // for best-effort concurrent write detection @@ -264,64 +279,29 @@ type Conn struct { newDecompressionReader func(io.Reader) io.ReadCloser } -func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn { - return newConnBRW(conn, isServer, readBufferSize, writeBufferSize, nil) -} +func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, writeBufferPool BufferPool, br *bufio.Reader, writeBuf []byte) *Conn { -type writeHook struct { - p []byte -} - -func (wh *writeHook) Write(p []byte) (int, error) { - wh.p = p - return len(p), nil -} - -func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, brw *bufio.ReadWriter) *Conn { - mu := make(chan bool, 1) - mu <- true - - var br *bufio.Reader - if readBufferSize == 0 && brw != nil && brw.Reader != nil { - // Reuse the supplied bufio.Reader if the buffer has a useful size. - // This code assumes that peek on a reader returns - // bufio.Reader.buf[:0]. - brw.Reader.Reset(conn) - if p, err := brw.Reader.Peek(0); err == nil && cap(p) >= 256 { - br = brw.Reader - } - } if br == nil { if readBufferSize == 0 { readBufferSize = defaultReadBufferSize - } - if readBufferSize < maxControlFramePayloadSize { + } else if readBufferSize < maxControlFramePayloadSize { + // must be large enough for control frame readBufferSize = maxControlFramePayloadSize } br = bufio.NewReaderSize(conn, readBufferSize) } - var writeBuf []byte - if writeBufferSize == 0 && brw != nil && brw.Writer != nil { - // Use the bufio.Writer's buffer if the buffer has a useful size. This - // code assumes that bufio.Writer.buf[:1] is passed to the - // bufio.Writer's underlying writer. - var wh writeHook - brw.Writer.Reset(&wh) - brw.Writer.WriteByte(0) - brw.Flush() - if cap(wh.p) >= maxFrameHeaderSize+256 { - writeBuf = wh.p[:cap(wh.p)] - } - } - - if writeBuf == nil { - if writeBufferSize == 0 { - writeBufferSize = defaultWriteBufferSize - } - writeBuf = make([]byte, writeBufferSize+maxFrameHeaderSize) + if writeBufferSize <= 0 { + writeBufferSize = defaultWriteBufferSize + } + writeBufferSize += maxFrameHeaderSize + + if writeBuf == nil && writeBufferPool == nil { + writeBuf = make([]byte, writeBufferSize) } + mu := make(chan bool, 1) + mu <- true c := &Conn{ isServer: isServer, br: br, @@ -329,6 +309,8 @@ func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize in mu: mu, readFinal: true, writeBuf: writeBuf, + writePool: writeBufferPool, + writeBufSize: writeBufferSize, enableWriteCompression: true, compressionLevel: defaultCompressionLevel, } @@ -343,7 +325,8 @@ func (c *Conn) Subprotocol() string { return c.subprotocol } -// Close closes the underlying network connection without sending or waiting for a close frame. +// Close closes the underlying network connection without sending or waiting +// for a close message. func (c *Conn) Close() error { return c.conn.Close() } @@ -370,7 +353,16 @@ func (c *Conn) writeFatal(err error) error { return err } -func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error { +func (c *Conn) read(n int) ([]byte, error) { + p, err := c.br.Peek(n) + if err == io.EOF { + err = errUnexpectedEOF + } + c.br.Discard(len(p)) + return p, err +} + +func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error { <-c.mu defer func() { c.mu <- true }() @@ -382,15 +374,14 @@ func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error { } c.conn.SetWriteDeadline(deadline) - for _, buf := range bufs { - if len(buf) > 0 { - _, err := c.conn.Write(buf) - if err != nil { - return c.writeFatal(err) - } - } + if len(buf1) == 0 { + _, err = c.conn.Write(buf0) + } else { + err = c.writeBufs(buf0, buf1) + } + if err != nil { + return c.writeFatal(err) } - if frameType == CloseMessage { c.writeFatal(ErrCloseSent) } @@ -476,7 +467,19 @@ func (c *Conn) prepWrite(messageType int) error { c.writeErrMu.Lock() err := c.writeErr c.writeErrMu.Unlock() - return err + if err != nil { + return err + } + + if c.writeBuf == nil { + wpd, ok := c.writePool.Get().(writePoolData) + if ok { + c.writeBuf = wpd.buf + } else { + c.writeBuf = make([]byte, c.writeBufSize) + } + } + return nil } // NextWriter returns a writer for the next message to send. The writer's Close @@ -484,6 +487,9 @@ func (c *Conn) prepWrite(messageType int) error { // // There can be at most one open writer on a connection. NextWriter closes the // previous writer if the application has not already done so. +// +// All message types (TextMessage, BinaryMessage, CloseMessage, PingMessage and +// PongMessage) are supported. func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) { if err := c.prepWrite(messageType); err != nil { return nil, err @@ -599,6 +605,10 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error { if final { c.writer = nil + if c.writePool != nil { + c.writePool.Put(writePoolData{buf: c.writeBuf}) + c.writeBuf = nil + } return nil } @@ -764,7 +774,6 @@ func (c *Conn) SetWriteDeadline(t time.Time) error { // Read methods func (c *Conn) advanceFrame() (int, error) { - // 1. Skip remainder of previous frame. if c.readRemaining > 0 { @@ -1033,7 +1042,7 @@ func (c *Conn) SetReadDeadline(t time.Time) error { } // SetReadLimit sets the maximum size for a message read from the peer. If a -// message exceeds the limit, the connection sends a close frame to the peer +// message exceeds the limit, the connection sends a close message to the peer // and returns ErrReadLimit to the application. func (c *Conn) SetReadLimit(limit int64) { c.readLimit = limit @@ -1046,24 +1055,22 @@ func (c *Conn) CloseHandler() func(code int, text string) error { // SetCloseHandler sets the handler for close messages received from the peer. // The code argument to h is the received close code or CloseNoStatusReceived -// if the close message is empty. The default close handler sends a close frame -// back to the peer. +// if the close message is empty. The default close handler sends a close +// message back to the peer. // -// The application must read the connection to process close messages as -// described in the section on Control Frames above. +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// close messages as described in the section on Control Messages above. // -// The connection read methods return a CloseError when a close frame is +// The connection read methods return a CloseError when a close message is // received. Most applications should handle close messages as part of their // normal error handling. Applications should only set a close handler when the -// application must perform some action before sending a close frame back to +// application must perform some action before sending a close message back to // the peer. func (c *Conn) SetCloseHandler(h func(code int, text string) error) { if h == nil { h = func(code int, text string) error { - message := []byte{} - if code != CloseNoStatusReceived { - message = FormatCloseMessage(code, "") - } + message := FormatCloseMessage(code, "") c.WriteControl(CloseMessage, message, time.Now().Add(writeWait)) return nil } @@ -1077,11 +1084,12 @@ func (c *Conn) PingHandler() func(appData string) error { } // SetPingHandler sets the handler for ping messages received from the peer. -// The appData argument to h is the PING frame application data. The default +// The appData argument to h is the PING message application data. The default // ping handler sends a pong to the peer. // -// The application must read the connection to process ping messages as -// described in the section on Control Frames above. +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// ping messages as described in the section on Control Messages above. func (c *Conn) SetPingHandler(h func(appData string) error) { if h == nil { h = func(message string) error { @@ -1103,11 +1111,12 @@ func (c *Conn) PongHandler() func(appData string) error { } // SetPongHandler sets the handler for pong messages received from the peer. -// The appData argument to h is the PONG frame application data. The default +// The appData argument to h is the PONG message application data. The default // pong handler does nothing. // -// The application must read the connection to process ping messages as -// described in the section on Control Frames above. +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// pong messages as described in the section on Control Messages above. func (c *Conn) SetPongHandler(h func(appData string) error) { if h == nil { h = func(string) error { return nil } @@ -1141,7 +1150,14 @@ func (c *Conn) SetCompressionLevel(level int) error { } // FormatCloseMessage formats closeCode and text as a WebSocket close message. +// An empty message is returned for code CloseNoStatusReceived. func FormatCloseMessage(closeCode int, text string) []byte { + if closeCode == CloseNoStatusReceived { + // Return empty message because it's illegal to send + // CloseNoStatusReceived. Return non-nil value in case application + // checks for nil. + return []byte{} + } buf := make([]byte, 2+len(text)) binary.BigEndian.PutUint16(buf, uint16(closeCode)) copy(buf[2:], text) diff --git a/vendor/github.com/gorilla/websocket/conn_read_legacy.go b/vendor/github.com/gorilla/websocket/conn_read_legacy.go deleted file mode 100644 index 018541cf..00000000 --- a/vendor/github.com/gorilla/websocket/conn_read_legacy.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !go1.5 - -package websocket - -import "io" - -func (c *Conn) read(n int) ([]byte, error) { - p, err := c.br.Peek(n) - if err == io.EOF { - err = errUnexpectedEOF - } - if len(p) > 0 { - // advance over the bytes just read - io.ReadFull(c.br, p) - } - return p, err -} diff --git a/vendor/github.com/gorilla/websocket/conn_read.go b/vendor/github.com/gorilla/websocket/conn_write.go similarity index 52% rename from vendor/github.com/gorilla/websocket/conn_read.go rename to vendor/github.com/gorilla/websocket/conn_write.go index 1ea15059..a509a21f 100644 --- a/vendor/github.com/gorilla/websocket/conn_read.go +++ b/vendor/github.com/gorilla/websocket/conn_write.go @@ -2,17 +2,14 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build go1.5 +// +build go1.8 package websocket -import "io" +import "net" -func (c *Conn) read(n int) ([]byte, error) { - p, err := c.br.Peek(n) - if err == io.EOF { - err = errUnexpectedEOF - } - c.br.Discard(len(p)) - return p, err +func (c *Conn) writeBufs(bufs ...[]byte) error { + b := net.Buffers(bufs) + _, err := b.WriteTo(c.conn) + return err } diff --git a/vendor/github.com/gorilla/websocket/conn_write_legacy.go b/vendor/github.com/gorilla/websocket/conn_write_legacy.go new file mode 100644 index 00000000..37edaff5 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/conn_write_legacy.go @@ -0,0 +1,18 @@ +// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !go1.8 + +package websocket + +func (c *Conn) writeBufs(bufs ...[]byte) error { + for _, buf := range bufs { + if len(buf) > 0 { + if _, err := c.conn.Write(buf); err != nil { + return err + } + } + } + return nil +} diff --git a/vendor/github.com/gorilla/websocket/doc.go b/vendor/github.com/gorilla/websocket/doc.go index e291a952..dcce1a63 100644 --- a/vendor/github.com/gorilla/websocket/doc.go +++ b/vendor/github.com/gorilla/websocket/doc.go @@ -6,9 +6,8 @@ // // Overview // -// The Conn type represents a WebSocket connection. A server application uses -// the Upgrade function from an Upgrader object with a HTTP request handler -// to get a pointer to a Conn: +// The Conn type represents a WebSocket connection. A server application calls +// the Upgrader.Upgrade method from an HTTP request handler to get a *Conn: // // var upgrader = websocket.Upgrader{ // ReadBufferSize: 1024, @@ -31,10 +30,12 @@ // for { // messageType, p, err := conn.ReadMessage() // if err != nil { +// log.Println(err) // return // } -// if err = conn.WriteMessage(messageType, p); err != nil { -// return err +// if err := conn.WriteMessage(messageType, p); err != nil { +// log.Println(err) +// return // } // } // @@ -85,20 +86,26 @@ // and pong. Call the connection WriteControl, WriteMessage or NextWriter // methods to send a control message to the peer. // -// Connections handle received close messages by sending a close message to the -// peer and returning a *CloseError from the the NextReader, ReadMessage or the -// message Read method. +// Connections handle received close messages by calling the handler function +// set with the SetCloseHandler method and by returning a *CloseError from the +// NextReader, ReadMessage or the message Read method. The default close +// handler sends a close message to the peer. // -// Connections handle received ping and pong messages by invoking callback -// functions set with SetPingHandler and SetPongHandler methods. The callback -// functions are called from the NextReader, ReadMessage and the message Read -// methods. +// Connections handle received ping messages by calling the handler function +// set with the SetPingHandler method. The default ping handler sends a pong +// message to the peer. // -// The default ping handler sends a pong to the peer. The application's reading -// goroutine can block for a short time while the handler writes the pong data -// to the connection. +// Connections handle received pong messages by calling the handler function +// set with the SetPongHandler method. The default pong handler does nothing. +// If an application sends ping messages, then the application should set a +// pong handler to receive the corresponding pong. // -// The application must read the connection to process ping, pong and close +// The control message handler functions are called from the NextReader, +// ReadMessage and message reader Read methods. The default close and ping +// handlers can block these methods for a short time when the handler writes to +// the connection. +// +// The application must read the connection to process close, ping and pong // messages sent from the peer. If the application is not otherwise interested // in messages from the peer, then the application should start a goroutine to // read and discard messages from the peer. A simple example is: @@ -137,19 +144,12 @@ // method fails the WebSocket handshake with HTTP status 403. // // If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail -// the handshake if the Origin request header is present and not equal to the -// Host request header. +// the handshake if the Origin request header is present and the Origin host is +// not equal to the Host request header. // -// An application can allow connections from any origin by specifying a -// function that always returns true: -// -// var upgrader = websocket.Upgrader{ -// CheckOrigin: func(r *http.Request) bool { return true }, -// } -// -// The deprecated Upgrade function does not enforce an origin policy. It's the -// application's responsibility to check the Origin header before calling -// Upgrade. +// The deprecated package-level Upgrade function does not perform origin +// checking. The application is responsible for checking the Origin header +// before calling the Upgrade function. // // Compression EXPERIMENTAL // diff --git a/vendor/github.com/gorilla/websocket/json.go b/vendor/github.com/gorilla/websocket/json.go index 4f0e3687..dc2c1f64 100644 --- a/vendor/github.com/gorilla/websocket/json.go +++ b/vendor/github.com/gorilla/websocket/json.go @@ -9,12 +9,14 @@ import ( "io" ) -// WriteJSON is deprecated, use c.WriteJSON instead. +// WriteJSON writes the JSON encoding of v as a message. +// +// Deprecated: Use c.WriteJSON instead. func WriteJSON(c *Conn, v interface{}) error { return c.WriteJSON(v) } -// WriteJSON writes the JSON encoding of v to the connection. +// WriteJSON writes the JSON encoding of v as a message. // // See the documentation for encoding/json Marshal for details about the // conversion of Go values to JSON. @@ -31,7 +33,10 @@ func (c *Conn) WriteJSON(v interface{}) error { return err2 } -// ReadJSON is deprecated, use c.ReadJSON instead. +// ReadJSON reads the next JSON-encoded message from the connection and stores +// it in the value pointed to by v. +// +// Deprecated: Use c.ReadJSON instead. func ReadJSON(c *Conn, v interface{}) error { return c.ReadJSON(v) } diff --git a/vendor/github.com/gorilla/websocket/mask.go b/vendor/github.com/gorilla/websocket/mask.go index 6a88bbc7..577fce9e 100644 --- a/vendor/github.com/gorilla/websocket/mask.go +++ b/vendor/github.com/gorilla/websocket/mask.go @@ -11,7 +11,6 @@ import "unsafe" const wordSize = int(unsafe.Sizeof(uintptr(0))) func maskBytes(key [4]byte, pos int, b []byte) int { - // Mask one byte at a time for small buffers. if len(b) < 2*wordSize { for i := range b { diff --git a/vendor/github.com/gorilla/websocket/prepared.go b/vendor/github.com/gorilla/websocket/prepared.go index 1efffbd1..74ec565d 100644 --- a/vendor/github.com/gorilla/websocket/prepared.go +++ b/vendor/github.com/gorilla/websocket/prepared.go @@ -19,7 +19,6 @@ import ( type PreparedMessage struct { messageType int data []byte - err error mu sync.Mutex frames map[prepareKey]*preparedFrame } diff --git a/vendor/github.com/gorilla/websocket/proxy.go b/vendor/github.com/gorilla/websocket/proxy.go new file mode 100644 index 00000000..bf2478e4 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/proxy.go @@ -0,0 +1,77 @@ +// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "encoding/base64" + "errors" + "net" + "net/http" + "net/url" + "strings" +) + +type netDialerFunc func(network, addr string) (net.Conn, error) + +func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) { + return fn(network, addr) +} + +func init() { + proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) { + return &httpProxyDialer{proxyURL: proxyURL, fowardDial: forwardDialer.Dial}, nil + }) +} + +type httpProxyDialer struct { + proxyURL *url.URL + fowardDial func(network, addr string) (net.Conn, error) +} + +func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) { + hostPort, _ := hostPortNoPort(hpd.proxyURL) + conn, err := hpd.fowardDial(network, hostPort) + if err != nil { + return nil, err + } + + connectHeader := make(http.Header) + if user := hpd.proxyURL.User; user != nil { + proxyUser := user.Username() + if proxyPassword, passwordSet := user.Password(); passwordSet { + credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword)) + connectHeader.Set("Proxy-Authorization", "Basic "+credential) + } + } + + connectReq := &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: addr}, + Host: addr, + Header: connectHeader, + } + + if err := connectReq.Write(conn); err != nil { + conn.Close() + return nil, err + } + + // Read response. It's OK to use and discard buffered reader here becaue + // the remote server does not speak until spoken to. + br := bufio.NewReader(conn) + resp, err := http.ReadResponse(br, connectReq) + if err != nil { + conn.Close() + return nil, err + } + + if resp.StatusCode != 200 { + conn.Close() + f := strings.SplitN(resp.Status, " ", 2) + return nil, errors.New(f[1]) + } + return conn, nil +} diff --git a/vendor/github.com/gorilla/websocket/server.go b/vendor/github.com/gorilla/websocket/server.go index 3495e0f1..a761824b 100644 --- a/vendor/github.com/gorilla/websocket/server.go +++ b/vendor/github.com/gorilla/websocket/server.go @@ -7,7 +7,7 @@ package websocket import ( "bufio" "errors" - "net" + "io" "net/http" "net/url" "strings" @@ -33,10 +33,23 @@ type Upgrader struct { // or received. ReadBufferSize, WriteBufferSize int + // WriteBufferPool is a pool of buffers for write operations. If the value + // is not set, then write buffers are allocated to the connection for the + // lifetime of the connection. + // + // A pool is most useful when the application has a modest volume of writes + // across a large number of connections. + // + // Applications should use a single pool for each unique value of + // WriteBufferSize. + WriteBufferPool BufferPool + // Subprotocols specifies the server's supported protocols in order of - // preference. If this field is set, then the Upgrade method negotiates a + // preference. If this field is not nil, then the Upgrade method negotiates a // subprotocol by selecting the first match in this list with a protocol - // requested by the client. + // requested by the client. If there's no match, then no protocol is + // negotiated (the Sec-Websocket-Protocol header is not included in the + // handshake response). Subprotocols []string // Error specifies the function for generating HTTP error responses. If Error @@ -44,8 +57,12 @@ type Upgrader struct { Error func(w http.ResponseWriter, r *http.Request, status int, reason error) // CheckOrigin returns true if the request Origin header is acceptable. If - // CheckOrigin is nil, the host in the Origin header must not be set or - // must match the host of the request. + // CheckOrigin is nil, then a safe default is used: return false if the + // Origin request header is present and the origin host is not equal to + // request Host header. + // + // A CheckOrigin function should carefully validate the request origin to + // prevent cross-site request forgery. CheckOrigin func(r *http.Request) bool // EnableCompression specify if the server should attempt to negotiate per @@ -76,7 +93,7 @@ func checkSameOrigin(r *http.Request) bool { if err != nil { return false } - return u.Host == r.Host + return equalASCIIFold(u.Host, r.Host) } func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string { @@ -99,42 +116,44 @@ func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header // // The responseHeader is included in the response to the client's upgrade // request. Use the responseHeader to specify cookies (Set-Cookie) and the -// application negotiated subprotocol (Sec-Websocket-Protocol). +// application negotiated subprotocol (Sec-WebSocket-Protocol). // // If the upgrade fails, then Upgrade replies to the client with an HTTP error // response. func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) { - if r.Method != "GET" { - return u.returnError(w, r, http.StatusMethodNotAllowed, "websocket: not a websocket handshake: request method is not GET") - } - - if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok { - return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-Websocket-Extensions' headers are unsupported") - } + const badHandshake = "websocket: the client is not using the websocket protocol: " if !tokenListContainsValue(r.Header, "Connection", "upgrade") { - return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'upgrade' token not found in 'Connection' header") + return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'upgrade' token not found in 'Connection' header") } if !tokenListContainsValue(r.Header, "Upgrade", "websocket") { - return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'websocket' token not found in 'Upgrade' header") + return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'websocket' token not found in 'Upgrade' header") + } + + if r.Method != "GET" { + return u.returnError(w, r, http.StatusMethodNotAllowed, badHandshake+"request method is not GET") } if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") { return u.returnError(w, r, http.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header") } + if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok { + return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-WebSocket-Extensions' headers are unsupported") + } + checkOrigin := u.CheckOrigin if checkOrigin == nil { checkOrigin = checkSameOrigin } if !checkOrigin(r) { - return u.returnError(w, r, http.StatusForbidden, "websocket: 'Origin' header value not allowed") + return u.returnError(w, r, http.StatusForbidden, "websocket: request origin not allowed by Upgrader.CheckOrigin") } challengeKey := r.Header.Get("Sec-Websocket-Key") if challengeKey == "" { - return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-Websocket-Key' header is missing or blank") + return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-WebSocket-Key' header is missing or blank") } subprotocol := u.selectSubprotocol(r, responseHeader) @@ -151,17 +170,12 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade } } - var ( - netConn net.Conn - err error - ) - h, ok := w.(http.Hijacker) if !ok { return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker") } var brw *bufio.ReadWriter - netConn, brw, err = h.Hijack() + netConn, brw, err := h.Hijack() if err != nil { return u.returnError(w, r, http.StatusInternalServerError, err.Error()) } @@ -171,7 +185,21 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade return nil, errors.New("websocket: client sent data before handshake is complete") } - c := newConnBRW(netConn, true, u.ReadBufferSize, u.WriteBufferSize, brw) + var br *bufio.Reader + if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 { + // Reuse hijacked buffered reader as connection reader. + br = brw.Reader + } + + buf := bufioWriterBuffer(netConn, brw.Writer) + + var writeBuf []byte + if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 { + // Reuse hijacked write buffer as connection buffer. + writeBuf = buf + } + + c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf) c.subprotocol = subprotocol if compress { @@ -179,17 +207,23 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade c.newDecompressionReader = decompressNoContextTakeover } - p := c.writeBuf[:0] + // Use larger of hijacked buffer and connection write buffer for header. + p := buf + if len(c.writeBuf) > len(p) { + p = c.writeBuf + } + p = p[:0] + p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...) p = append(p, computeAcceptKey(challengeKey)...) p = append(p, "\r\n"...) if c.subprotocol != "" { - p = append(p, "Sec-Websocket-Protocol: "...) + p = append(p, "Sec-WebSocket-Protocol: "...) p = append(p, c.subprotocol...) p = append(p, "\r\n"...) } if compress { - p = append(p, "Sec-Websocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...) + p = append(p, "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...) } for k, vs := range responseHeader { if k == "Sec-Websocket-Protocol" { @@ -230,13 +264,14 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade // Upgrade upgrades the HTTP server connection to the WebSocket protocol. // -// This function is deprecated, use websocket.Upgrader instead. +// Deprecated: Use websocket.Upgrader instead. // -// The application is responsible for checking the request origin before -// calling Upgrade. An example implementation of the same origin policy is: +// Upgrade does not perform origin checking. The application is responsible for +// checking the Origin header before calling Upgrade. An example implementation +// of the same origin policy check is: // // if req.Header.Get("Origin") != "http://"+req.Host { -// http.Error(w, "Origin not allowed", 403) +// http.Error(w, "Origin not allowed", http.StatusForbidden) // return // } // @@ -289,3 +324,40 @@ func IsWebSocketUpgrade(r *http.Request) bool { return tokenListContainsValue(r.Header, "Connection", "upgrade") && tokenListContainsValue(r.Header, "Upgrade", "websocket") } + +// bufioReaderSize size returns the size of a bufio.Reader. +func bufioReaderSize(originalReader io.Reader, br *bufio.Reader) int { + // This code assumes that peek on a reset reader returns + // bufio.Reader.buf[:0]. + // TODO: Use bufio.Reader.Size() after Go 1.10 + br.Reset(originalReader) + if p, err := br.Peek(0); err == nil { + return cap(p) + } + return 0 +} + +// writeHook is an io.Writer that records the last slice passed to it vio +// io.Writer.Write. +type writeHook struct { + p []byte +} + +func (wh *writeHook) Write(p []byte) (int, error) { + wh.p = p + return len(p), nil +} + +// bufioWriterBuffer grabs the buffer from a bufio.Writer. +func bufioWriterBuffer(originalWriter io.Writer, bw *bufio.Writer) []byte { + // This code assumes that bufio.Writer.buf[:1] is passed to the + // bufio.Writer's underlying writer. + var wh writeHook + bw.Reset(&wh) + bw.WriteByte(0) + bw.Flush() + + bw.Reset(originalWriter) + + return wh.p[:cap(wh.p)] +} diff --git a/vendor/github.com/gorilla/websocket/trace.go b/vendor/github.com/gorilla/websocket/trace.go new file mode 100644 index 00000000..834f122a --- /dev/null +++ b/vendor/github.com/gorilla/websocket/trace.go @@ -0,0 +1,19 @@ +// +build go1.8 + +package websocket + +import ( + "crypto/tls" + "net/http/httptrace" +) + +func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { + if trace.TLSHandshakeStart != nil { + trace.TLSHandshakeStart() + } + err := doHandshake(tlsConn, cfg) + if trace.TLSHandshakeDone != nil { + trace.TLSHandshakeDone(tlsConn.ConnectionState(), err) + } + return err +} diff --git a/vendor/github.com/gorilla/websocket/trace_17.go b/vendor/github.com/gorilla/websocket/trace_17.go new file mode 100644 index 00000000..77d05a0b --- /dev/null +++ b/vendor/github.com/gorilla/websocket/trace_17.go @@ -0,0 +1,12 @@ +// +build !go1.8 + +package websocket + +import ( + "crypto/tls" + "net/http/httptrace" +) + +func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { + return doHandshake(tlsConn, cfg) +} diff --git a/vendor/github.com/gorilla/websocket/util.go b/vendor/github.com/gorilla/websocket/util.go index 9a4908df..354001e1 100644 --- a/vendor/github.com/gorilla/websocket/util.go +++ b/vendor/github.com/gorilla/websocket/util.go @@ -11,6 +11,7 @@ import ( "io" "net/http" "strings" + "unicode/utf8" ) var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") @@ -111,14 +112,14 @@ func nextTokenOrQuoted(s string) (value string, rest string) { case escape: escape = false p[j] = b - j += 1 + j++ case b == '\\': escape = true case b == '"': return string(p[:j]), s[i+1:] default: p[j] = b - j += 1 + j++ } } return "", "" @@ -127,8 +128,31 @@ func nextTokenOrQuoted(s string) (value string, rest string) { return "", "" } +// equalASCIIFold returns true if s is equal to t with ASCII case folding. +func equalASCIIFold(s, t string) bool { + for s != "" && t != "" { + sr, size := utf8.DecodeRuneInString(s) + s = s[size:] + tr, size := utf8.DecodeRuneInString(t) + t = t[size:] + if sr == tr { + continue + } + if 'A' <= sr && sr <= 'Z' { + sr = sr + 'a' - 'A' + } + if 'A' <= tr && tr <= 'Z' { + tr = tr + 'a' - 'A' + } + if sr != tr { + return false + } + } + return s == t +} + // tokenListContainsValue returns true if the 1#token header with the given -// name contains token. +// name contains a token equal to value with ASCII case folding. func tokenListContainsValue(header http.Header, name string, value string) bool { headers: for _, s := range header[name] { @@ -142,7 +166,7 @@ headers: if s != "" && s[0] != ',' { continue headers } - if strings.EqualFold(t, value) { + if equalASCIIFold(t, value) { return true } if s == "" { @@ -154,9 +178,8 @@ headers: return false } -// parseExtensiosn parses WebSocket extensions from a header. +// parseExtensions parses WebSocket extensions from a header. func parseExtensions(header http.Header) []map[string]string { - // From RFC 6455: // // Sec-WebSocket-Extensions = extension-list diff --git a/vendor/github.com/gorilla/websocket/x_net_proxy.go b/vendor/github.com/gorilla/websocket/x_net_proxy.go new file mode 100644 index 00000000..2e668f6b --- /dev/null +++ b/vendor/github.com/gorilla/websocket/x_net_proxy.go @@ -0,0 +1,473 @@ +// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT. +//go:generate bundle -o x_net_proxy.go golang.org/x/net/proxy + +// Package proxy provides support for a variety of protocols to proxy network +// data. +// + +package websocket + +import ( + "errors" + "io" + "net" + "net/url" + "os" + "strconv" + "strings" + "sync" +) + +type proxy_direct struct{} + +// Direct is a direct proxy: one that makes network connections directly. +var proxy_Direct = proxy_direct{} + +func (proxy_direct) Dial(network, addr string) (net.Conn, error) { + return net.Dial(network, addr) +} + +// A PerHost directs connections to a default Dialer unless the host name +// requested matches one of a number of exceptions. +type proxy_PerHost struct { + def, bypass proxy_Dialer + + bypassNetworks []*net.IPNet + bypassIPs []net.IP + bypassZones []string + bypassHosts []string +} + +// NewPerHost returns a PerHost Dialer that directs connections to either +// defaultDialer or bypass, depending on whether the connection matches one of +// the configured rules. +func proxy_NewPerHost(defaultDialer, bypass proxy_Dialer) *proxy_PerHost { + return &proxy_PerHost{ + def: defaultDialer, + bypass: bypass, + } +} + +// Dial connects to the address addr on the given network through either +// defaultDialer or bypass. +func (p *proxy_PerHost) Dial(network, addr string) (c net.Conn, err error) { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + + return p.dialerForRequest(host).Dial(network, addr) +} + +func (p *proxy_PerHost) dialerForRequest(host string) proxy_Dialer { + if ip := net.ParseIP(host); ip != nil { + for _, net := range p.bypassNetworks { + if net.Contains(ip) { + return p.bypass + } + } + for _, bypassIP := range p.bypassIPs { + if bypassIP.Equal(ip) { + return p.bypass + } + } + return p.def + } + + for _, zone := range p.bypassZones { + if strings.HasSuffix(host, zone) { + return p.bypass + } + if host == zone[1:] { + // For a zone ".example.com", we match "example.com" + // too. + return p.bypass + } + } + for _, bypassHost := range p.bypassHosts { + if bypassHost == host { + return p.bypass + } + } + return p.def +} + +// AddFromString parses a string that contains comma-separated values +// specifying hosts that should use the bypass proxy. Each value is either an +// IP address, a CIDR range, a zone (*.example.com) or a host name +// (localhost). A best effort is made to parse the string and errors are +// ignored. +func (p *proxy_PerHost) AddFromString(s string) { + hosts := strings.Split(s, ",") + for _, host := range hosts { + host = strings.TrimSpace(host) + if len(host) == 0 { + continue + } + if strings.Contains(host, "/") { + // We assume that it's a CIDR address like 127.0.0.0/8 + if _, net, err := net.ParseCIDR(host); err == nil { + p.AddNetwork(net) + } + continue + } + if ip := net.ParseIP(host); ip != nil { + p.AddIP(ip) + continue + } + if strings.HasPrefix(host, "*.") { + p.AddZone(host[1:]) + continue + } + p.AddHost(host) + } +} + +// AddIP specifies an IP address that will use the bypass proxy. Note that +// this will only take effect if a literal IP address is dialed. A connection +// to a named host will never match an IP. +func (p *proxy_PerHost) AddIP(ip net.IP) { + p.bypassIPs = append(p.bypassIPs, ip) +} + +// AddNetwork specifies an IP range that will use the bypass proxy. Note that +// this will only take effect if a literal IP address is dialed. A connection +// to a named host will never match. +func (p *proxy_PerHost) AddNetwork(net *net.IPNet) { + p.bypassNetworks = append(p.bypassNetworks, net) +} + +// AddZone specifies a DNS suffix that will use the bypass proxy. A zone of +// "example.com" matches "example.com" and all of its subdomains. +func (p *proxy_PerHost) AddZone(zone string) { + if strings.HasSuffix(zone, ".") { + zone = zone[:len(zone)-1] + } + if !strings.HasPrefix(zone, ".") { + zone = "." + zone + } + p.bypassZones = append(p.bypassZones, zone) +} + +// AddHost specifies a host name that will use the bypass proxy. +func (p *proxy_PerHost) AddHost(host string) { + if strings.HasSuffix(host, ".") { + host = host[:len(host)-1] + } + p.bypassHosts = append(p.bypassHosts, host) +} + +// A Dialer is a means to establish a connection. +type proxy_Dialer interface { + // Dial connects to the given address via the proxy. + Dial(network, addr string) (c net.Conn, err error) +} + +// Auth contains authentication parameters that specific Dialers may require. +type proxy_Auth struct { + User, Password string +} + +// FromEnvironment returns the dialer specified by the proxy related variables in +// the environment. +func proxy_FromEnvironment() proxy_Dialer { + allProxy := proxy_allProxyEnv.Get() + if len(allProxy) == 0 { + return proxy_Direct + } + + proxyURL, err := url.Parse(allProxy) + if err != nil { + return proxy_Direct + } + proxy, err := proxy_FromURL(proxyURL, proxy_Direct) + if err != nil { + return proxy_Direct + } + + noProxy := proxy_noProxyEnv.Get() + if len(noProxy) == 0 { + return proxy + } + + perHost := proxy_NewPerHost(proxy, proxy_Direct) + perHost.AddFromString(noProxy) + return perHost +} + +// proxySchemes is a map from URL schemes to a function that creates a Dialer +// from a URL with such a scheme. +var proxy_proxySchemes map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error) + +// RegisterDialerType takes a URL scheme and a function to generate Dialers from +// a URL with that scheme and a forwarding Dialer. Registered schemes are used +// by FromURL. +func proxy_RegisterDialerType(scheme string, f func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) { + if proxy_proxySchemes == nil { + proxy_proxySchemes = make(map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) + } + proxy_proxySchemes[scheme] = f +} + +// FromURL returns a Dialer given a URL specification and an underlying +// Dialer for it to make network requests. +func proxy_FromURL(u *url.URL, forward proxy_Dialer) (proxy_Dialer, error) { + var auth *proxy_Auth + if u.User != nil { + auth = new(proxy_Auth) + auth.User = u.User.Username() + if p, ok := u.User.Password(); ok { + auth.Password = p + } + } + + switch u.Scheme { + case "socks5": + return proxy_SOCKS5("tcp", u.Host, auth, forward) + } + + // If the scheme doesn't match any of the built-in schemes, see if it + // was registered by another package. + if proxy_proxySchemes != nil { + if f, ok := proxy_proxySchemes[u.Scheme]; ok { + return f(u, forward) + } + } + + return nil, errors.New("proxy: unknown scheme: " + u.Scheme) +} + +var ( + proxy_allProxyEnv = &proxy_envOnce{ + names: []string{"ALL_PROXY", "all_proxy"}, + } + proxy_noProxyEnv = &proxy_envOnce{ + names: []string{"NO_PROXY", "no_proxy"}, + } +) + +// envOnce looks up an environment variable (optionally by multiple +// names) once. It mitigates expensive lookups on some platforms +// (e.g. Windows). +// (Borrowed from net/http/transport.go) +type proxy_envOnce struct { + names []string + once sync.Once + val string +} + +func (e *proxy_envOnce) Get() string { + e.once.Do(e.init) + return e.val +} + +func (e *proxy_envOnce) init() { + for _, n := range e.names { + e.val = os.Getenv(n) + if e.val != "" { + return + } + } +} + +// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given address +// with an optional username and password. See RFC 1928 and RFC 1929. +func proxy_SOCKS5(network, addr string, auth *proxy_Auth, forward proxy_Dialer) (proxy_Dialer, error) { + s := &proxy_socks5{ + network: network, + addr: addr, + forward: forward, + } + if auth != nil { + s.user = auth.User + s.password = auth.Password + } + + return s, nil +} + +type proxy_socks5 struct { + user, password string + network, addr string + forward proxy_Dialer +} + +const proxy_socks5Version = 5 + +const ( + proxy_socks5AuthNone = 0 + proxy_socks5AuthPassword = 2 +) + +const proxy_socks5Connect = 1 + +const ( + proxy_socks5IP4 = 1 + proxy_socks5Domain = 3 + proxy_socks5IP6 = 4 +) + +var proxy_socks5Errors = []string{ + "", + "general failure", + "connection forbidden", + "network unreachable", + "host unreachable", + "connection refused", + "TTL expired", + "command not supported", + "address type not supported", +} + +// Dial connects to the address addr on the given network via the SOCKS5 proxy. +func (s *proxy_socks5) Dial(network, addr string) (net.Conn, error) { + switch network { + case "tcp", "tcp6", "tcp4": + default: + return nil, errors.New("proxy: no support for SOCKS5 proxy connections of type " + network) + } + + conn, err := s.forward.Dial(s.network, s.addr) + if err != nil { + return nil, err + } + if err := s.connect(conn, addr); err != nil { + conn.Close() + return nil, err + } + return conn, nil +} + +// connect takes an existing connection to a socks5 proxy server, +// and commands the server to extend that connection to target, +// which must be a canonical address with a host and port. +func (s *proxy_socks5) connect(conn net.Conn, target string) error { + host, portStr, err := net.SplitHostPort(target) + if err != nil { + return err + } + + port, err := strconv.Atoi(portStr) + if err != nil { + return errors.New("proxy: failed to parse port number: " + portStr) + } + if port < 1 || port > 0xffff { + return errors.New("proxy: port number out of range: " + portStr) + } + + // the size here is just an estimate + buf := make([]byte, 0, 6+len(host)) + + buf = append(buf, proxy_socks5Version) + if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 { + buf = append(buf, 2 /* num auth methods */, proxy_socks5AuthNone, proxy_socks5AuthPassword) + } else { + buf = append(buf, 1 /* num auth methods */, proxy_socks5AuthNone) + } + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + if buf[0] != 5 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0]))) + } + if buf[1] == 0xff { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication") + } + + // See RFC 1929 + if buf[1] == proxy_socks5AuthPassword { + buf = buf[:0] + buf = append(buf, 1 /* password protocol version */) + buf = append(buf, uint8(len(s.user))) + buf = append(buf, s.user...) + buf = append(buf, uint8(len(s.password))) + buf = append(buf, s.password...) + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if buf[1] != 0 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password") + } + } + + buf = buf[:0] + buf = append(buf, proxy_socks5Version, proxy_socks5Connect, 0 /* reserved */) + + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + buf = append(buf, proxy_socks5IP4) + ip = ip4 + } else { + buf = append(buf, proxy_socks5IP6) + } + buf = append(buf, ip...) + } else { + if len(host) > 255 { + return errors.New("proxy: destination host name too long: " + host) + } + buf = append(buf, proxy_socks5Domain) + buf = append(buf, byte(len(host))) + buf = append(buf, host...) + } + buf = append(buf, byte(port>>8), byte(port)) + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:4]); err != nil { + return errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + failure := "unknown error" + if int(buf[1]) < len(proxy_socks5Errors) { + failure = proxy_socks5Errors[buf[1]] + } + + if len(failure) > 0 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure) + } + + bytesToDiscard := 0 + switch buf[3] { + case proxy_socks5IP4: + bytesToDiscard = net.IPv4len + case proxy_socks5IP6: + bytesToDiscard = net.IPv6len + case proxy_socks5Domain: + _, err := io.ReadFull(conn, buf[:1]) + if err != nil { + return errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + bytesToDiscard = int(buf[0]) + default: + return errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr) + } + + if cap(buf) < bytesToDiscard { + buf = make([]byte, bytesToDiscard) + } else { + buf = buf[:bytesToDiscard] + } + if _, err := io.ReadFull(conn, buf); err != nil { + return errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + // Also need to discard the port number + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + return nil +} diff --git a/vendor/github.com/hashicorp/golang-lru/2q.go b/vendor/github.com/hashicorp/golang-lru/2q.go index 337d9632..e474cd07 100644 --- a/vendor/github.com/hashicorp/golang-lru/2q.go +++ b/vendor/github.com/hashicorp/golang-lru/2q.go @@ -30,9 +30,9 @@ type TwoQueueCache struct { size int recentSize int - recent *simplelru.LRU - frequent *simplelru.LRU - recentEvict *simplelru.LRU + recent simplelru.LRUCache + frequent simplelru.LRUCache + recentEvict simplelru.LRUCache lock sync.RWMutex } @@ -84,7 +84,8 @@ func New2QParams(size int, recentRatio float64, ghostRatio float64) (*TwoQueueCa return c, nil } -func (c *TwoQueueCache) Get(key interface{}) (interface{}, bool) { +// Get looks up a key's value from the cache. +func (c *TwoQueueCache) Get(key interface{}) (value interface{}, ok bool) { c.lock.Lock() defer c.lock.Unlock() @@ -105,6 +106,7 @@ func (c *TwoQueueCache) Get(key interface{}) (interface{}, bool) { return nil, false } +// Add adds a value to the cache. func (c *TwoQueueCache) Add(key, value interface{}) { c.lock.Lock() defer c.lock.Unlock() @@ -160,12 +162,15 @@ func (c *TwoQueueCache) ensureSpace(recentEvict bool) { c.frequent.RemoveOldest() } +// Len returns the number of items in the cache. func (c *TwoQueueCache) Len() int { c.lock.RLock() defer c.lock.RUnlock() return c.recent.Len() + c.frequent.Len() } +// Keys returns a slice of the keys in the cache. +// The frequently used keys are first in the returned slice. func (c *TwoQueueCache) Keys() []interface{} { c.lock.RLock() defer c.lock.RUnlock() @@ -174,6 +179,7 @@ func (c *TwoQueueCache) Keys() []interface{} { return append(k1, k2...) } +// Remove removes the provided key from the cache. func (c *TwoQueueCache) Remove(key interface{}) { c.lock.Lock() defer c.lock.Unlock() @@ -188,6 +194,7 @@ func (c *TwoQueueCache) Remove(key interface{}) { } } +// Purge is used to completely clear the cache. func (c *TwoQueueCache) Purge() { c.lock.Lock() defer c.lock.Unlock() @@ -196,13 +203,17 @@ func (c *TwoQueueCache) Purge() { c.recentEvict.Purge() } +// Contains is used to check if the cache contains a key +// without updating recency or frequency. func (c *TwoQueueCache) Contains(key interface{}) bool { c.lock.RLock() defer c.lock.RUnlock() return c.frequent.Contains(key) || c.recent.Contains(key) } -func (c *TwoQueueCache) Peek(key interface{}) (interface{}, bool) { +// Peek is used to inspect the cache value of a key +// without updating recency or frequency. +func (c *TwoQueueCache) Peek(key interface{}) (value interface{}, ok bool) { c.lock.RLock() defer c.lock.RUnlock() if val, ok := c.frequent.Peek(key); ok { diff --git a/vendor/github.com/hashicorp/golang-lru/arc.go b/vendor/github.com/hashicorp/golang-lru/arc.go index a2a25281..555225a2 100644 --- a/vendor/github.com/hashicorp/golang-lru/arc.go +++ b/vendor/github.com/hashicorp/golang-lru/arc.go @@ -18,11 +18,11 @@ type ARCCache struct { size int // Size is the total capacity of the cache p int // P is the dynamic preference towards T1 or T2 - t1 *simplelru.LRU // T1 is the LRU for recently accessed items - b1 *simplelru.LRU // B1 is the LRU for evictions from t1 + t1 simplelru.LRUCache // T1 is the LRU for recently accessed items + b1 simplelru.LRUCache // B1 is the LRU for evictions from t1 - t2 *simplelru.LRU // T2 is the LRU for frequently accessed items - b2 *simplelru.LRU // B2 is the LRU for evictions from t2 + t2 simplelru.LRUCache // T2 is the LRU for frequently accessed items + b2 simplelru.LRUCache // B2 is the LRU for evictions from t2 lock sync.RWMutex } @@ -60,11 +60,11 @@ func NewARC(size int) (*ARCCache, error) { } // Get looks up a key's value from the cache. -func (c *ARCCache) Get(key interface{}) (interface{}, bool) { +func (c *ARCCache) Get(key interface{}) (value interface{}, ok bool) { c.lock.Lock() defer c.lock.Unlock() - // Ff the value is contained in T1 (recent), then + // If the value is contained in T1 (recent), then // promote it to T2 (frequent) if val, ok := c.t1.Peek(key); ok { c.t1.Remove(key) @@ -153,7 +153,7 @@ func (c *ARCCache) Add(key, value interface{}) { // Remove from B2 c.b2.Remove(key) - // Add the key to the frequntly used list + // Add the key to the frequently used list c.t2.Add(key, value) return } @@ -247,7 +247,7 @@ func (c *ARCCache) Contains(key interface{}) bool { // Peek is used to inspect the cache value of a key // without updating recency or frequency. -func (c *ARCCache) Peek(key interface{}) (interface{}, bool) { +func (c *ARCCache) Peek(key interface{}) (value interface{}, ok bool) { c.lock.RLock() defer c.lock.RUnlock() if val, ok := c.t1.Peek(key); ok { diff --git a/vendor/github.com/hashicorp/golang-lru/doc.go b/vendor/github.com/hashicorp/golang-lru/doc.go new file mode 100644 index 00000000..2547df97 --- /dev/null +++ b/vendor/github.com/hashicorp/golang-lru/doc.go @@ -0,0 +1,21 @@ +// Package lru provides three different LRU caches of varying sophistication. +// +// Cache is a simple LRU cache. It is based on the +// LRU implementation in groupcache: +// https://github.com/golang/groupcache/tree/master/lru +// +// TwoQueueCache tracks frequently used and recently used entries separately. +// This avoids a burst of accesses from taking out frequently used entries, +// at the cost of about 2x computational overhead and some extra bookkeeping. +// +// ARCCache is an adaptive replacement cache. It tracks recent evictions as +// well as recent usage in both the frequent and recent caches. Its +// computational overhead is comparable to TwoQueueCache, but the memory +// overhead is linear with the size of the cache. +// +// ARC has been patented by IBM, so do not use it if that is problematic for +// your program. +// +// All caches in this package take locks while operating, and are therefore +// thread-safe for consumers. +package lru diff --git a/vendor/github.com/hashicorp/golang-lru/go.mod b/vendor/github.com/hashicorp/golang-lru/go.mod new file mode 100644 index 00000000..824cb97e --- /dev/null +++ b/vendor/github.com/hashicorp/golang-lru/go.mod @@ -0,0 +1 @@ +module github.com/hashicorp/golang-lru diff --git a/vendor/github.com/hashicorp/golang-lru/lru.go b/vendor/github.com/hashicorp/golang-lru/lru.go index a6285f98..c8d9b0a2 100644 --- a/vendor/github.com/hashicorp/golang-lru/lru.go +++ b/vendor/github.com/hashicorp/golang-lru/lru.go @@ -1,6 +1,3 @@ -// This package provides a simple LRU cache. It is based on the -// LRU implementation in groupcache: -// https://github.com/golang/groupcache/tree/master/lru package lru import ( @@ -11,11 +8,11 @@ import ( // Cache is a thread-safe fixed size LRU cache. type Cache struct { - lru *simplelru.LRU + lru simplelru.LRUCache lock sync.RWMutex } -// New creates an LRU of the given size +// New creates an LRU of the given size. func New(size int) (*Cache, error) { return NewWithEvict(size, nil) } @@ -33,7 +30,7 @@ func NewWithEvict(size int, onEvicted func(key interface{}, value interface{})) return c, nil } -// Purge is used to completely clear the cache +// Purge is used to completely clear the cache. func (c *Cache) Purge() { c.lock.Lock() c.lru.Purge() @@ -41,30 +38,30 @@ func (c *Cache) Purge() { } // Add adds a value to the cache. Returns true if an eviction occurred. -func (c *Cache) Add(key, value interface{}) bool { +func (c *Cache) Add(key, value interface{}) (evicted bool) { c.lock.Lock() defer c.lock.Unlock() return c.lru.Add(key, value) } // Get looks up a key's value from the cache. -func (c *Cache) Get(key interface{}) (interface{}, bool) { +func (c *Cache) Get(key interface{}) (value interface{}, ok bool) { c.lock.Lock() defer c.lock.Unlock() return c.lru.Get(key) } -// Check if a key is in the cache, without updating the recent-ness -// or deleting it for being stale. +// Contains checks if a key is in the cache, without updating the +// recent-ness or deleting it for being stale. func (c *Cache) Contains(key interface{}) bool { c.lock.RLock() defer c.lock.RUnlock() return c.lru.Contains(key) } -// Returns the key value (or undefined if not found) without updating +// Peek returns the key value (or undefined if not found) without updating // the "recently used"-ness of the key. -func (c *Cache) Peek(key interface{}) (interface{}, bool) { +func (c *Cache) Peek(key interface{}) (value interface{}, ok bool) { c.lock.RLock() defer c.lock.RUnlock() return c.lru.Peek(key) @@ -73,16 +70,15 @@ func (c *Cache) Peek(key interface{}) (interface{}, bool) { // ContainsOrAdd checks if a key is in the cache without updating the // recent-ness or deleting it for being stale, and if not, adds the value. // Returns whether found and whether an eviction occurred. -func (c *Cache) ContainsOrAdd(key, value interface{}) (ok, evict bool) { +func (c *Cache) ContainsOrAdd(key, value interface{}) (ok, evicted bool) { c.lock.Lock() defer c.lock.Unlock() if c.lru.Contains(key) { return true, false - } else { - evict := c.lru.Add(key, value) - return false, evict } + evicted = c.lru.Add(key, value) + return false, evicted } // Remove removes the provided key from the cache. diff --git a/vendor/github.com/hashicorp/golang-lru/simplelru/lru.go b/vendor/github.com/hashicorp/golang-lru/simplelru/lru.go index cb416b39..5673773b 100644 --- a/vendor/github.com/hashicorp/golang-lru/simplelru/lru.go +++ b/vendor/github.com/hashicorp/golang-lru/simplelru/lru.go @@ -36,7 +36,7 @@ func NewLRU(size int, onEvict EvictCallback) (*LRU, error) { return c, nil } -// Purge is used to completely clear the cache +// Purge is used to completely clear the cache. func (c *LRU) Purge() { for k, v := range c.items { if c.onEvict != nil { @@ -48,7 +48,7 @@ func (c *LRU) Purge() { } // Add adds a value to the cache. Returns true if an eviction occurred. -func (c *LRU) Add(key, value interface{}) bool { +func (c *LRU) Add(key, value interface{}) (evicted bool) { // Check for existing item if ent, ok := c.items[key]; ok { c.evictList.MoveToFront(ent) @@ -78,17 +78,18 @@ func (c *LRU) Get(key interface{}) (value interface{}, ok bool) { return } -// Check if a key is in the cache, without updating the recent-ness +// Contains checks if a key is in the cache, without updating the recent-ness // or deleting it for being stale. func (c *LRU) Contains(key interface{}) (ok bool) { _, ok = c.items[key] return ok } -// Returns the key value (or undefined if not found) without updating +// Peek returns the key value (or undefined if not found) without updating // the "recently used"-ness of the key. func (c *LRU) Peek(key interface{}) (value interface{}, ok bool) { - if ent, ok := c.items[key]; ok { + var ent *list.Element + if ent, ok = c.items[key]; ok { return ent.Value.(*entry).value, true } return nil, ok @@ -96,7 +97,7 @@ func (c *LRU) Peek(key interface{}) (value interface{}, ok bool) { // Remove removes the provided key from the cache, returning if the // key was contained. -func (c *LRU) Remove(key interface{}) bool { +func (c *LRU) Remove(key interface{}) (present bool) { if ent, ok := c.items[key]; ok { c.removeElement(ent) return true @@ -105,7 +106,7 @@ func (c *LRU) Remove(key interface{}) bool { } // RemoveOldest removes the oldest item from the cache. -func (c *LRU) RemoveOldest() (interface{}, interface{}, bool) { +func (c *LRU) RemoveOldest() (key interface{}, value interface{}, ok bool) { ent := c.evictList.Back() if ent != nil { c.removeElement(ent) @@ -116,7 +117,7 @@ func (c *LRU) RemoveOldest() (interface{}, interface{}, bool) { } // GetOldest returns the oldest entry -func (c *LRU) GetOldest() (interface{}, interface{}, bool) { +func (c *LRU) GetOldest() (key interface{}, value interface{}, ok bool) { ent := c.evictList.Back() if ent != nil { kv := ent.Value.(*entry) diff --git a/vendor/github.com/hashicorp/golang-lru/simplelru/lru_interface.go b/vendor/github.com/hashicorp/golang-lru/simplelru/lru_interface.go new file mode 100644 index 00000000..74c70774 --- /dev/null +++ b/vendor/github.com/hashicorp/golang-lru/simplelru/lru_interface.go @@ -0,0 +1,36 @@ +package simplelru + +// LRUCache is the interface for simple LRU cache. +type LRUCache interface { + // Adds a value to the cache, returns true if an eviction occurred and + // updates the "recently used"-ness of the key. + Add(key, value interface{}) bool + + // Returns key's value from the cache and + // updates the "recently used"-ness of the key. #value, isFound + Get(key interface{}) (value interface{}, ok bool) + + // Check if a key exsists in cache without updating the recent-ness. + Contains(key interface{}) (ok bool) + + // Returns key's value without updating the "recently used"-ness of the key. + Peek(key interface{}) (value interface{}, ok bool) + + // Removes a key from the cache. + Remove(key interface{}) bool + + // Removes the oldest entry from cache. + RemoveOldest() (interface{}, interface{}, bool) + + // Returns the oldest entry from the cache. #key, value, isFound + GetOldest() (interface{}, interface{}, bool) + + // Returns a slice of the keys in the cache, from oldest to newest. + Keys() []interface{} + + // Returns the number of items in the cache. + Len() int + + // Clear all cache entries + Purge() +} diff --git a/vendor/github.com/hashicorp/hcl/go.mod b/vendor/github.com/hashicorp/hcl/go.mod new file mode 100644 index 00000000..4debbbe3 --- /dev/null +++ b/vendor/github.com/hashicorp/hcl/go.mod @@ -0,0 +1,3 @@ +module github.com/hashicorp/hcl + +require github.com/davecgh/go-spew v1.1.1 diff --git a/vendor/github.com/hashicorp/hcl/go.sum b/vendor/github.com/hashicorp/hcl/go.sum new file mode 100644 index 00000000..b5e2922e --- /dev/null +++ b/vendor/github.com/hashicorp/hcl/go.sum @@ -0,0 +1,2 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/vendor/github.com/hashicorp/hcl/hcl/parser/parser.go b/vendor/github.com/hashicorp/hcl/hcl/parser/parser.go index 098e1bc4..64c83bcf 100644 --- a/vendor/github.com/hashicorp/hcl/hcl/parser/parser.go +++ b/vendor/github.com/hashicorp/hcl/hcl/parser/parser.go @@ -205,6 +205,12 @@ func (p *Parser) objectItem() (*ast.ObjectItem, error) { } } + // key=#comment + // val + if p.lineComment != nil { + o.LineComment, p.lineComment = p.lineComment, nil + } + // do a look-ahead for line comment p.scan() if len(keys) > 0 && o.Val.Pos().Line == keys[0].Pos().Line && p.lineComment != nil { diff --git a/vendor/github.com/hashicorp/hcl/hcl/printer/nodes.go b/vendor/github.com/hashicorp/hcl/hcl/printer/nodes.go index c896d584..7c038d12 100644 --- a/vendor/github.com/hashicorp/hcl/hcl/printer/nodes.go +++ b/vendor/github.com/hashicorp/hcl/hcl/printer/nodes.go @@ -252,6 +252,14 @@ func (p *printer) objectItem(o *ast.ObjectItem) []byte { } } + // If key and val are on different lines, treat line comments like lead comments. + if o.LineComment != nil && o.Val.Pos().Line != o.Keys[0].Pos().Line { + for _, comment := range o.LineComment.List { + buf.WriteString(comment.Text) + buf.WriteByte(newline) + } + } + for i, k := range o.Keys { buf.WriteString(k.Token.Text) buf.WriteByte(blank) @@ -265,7 +273,7 @@ func (p *printer) objectItem(o *ast.ObjectItem) []byte { buf.Write(p.output(o.Val)) - if o.Val.Pos().Line == o.Keys[0].Pos().Line && o.LineComment != nil { + if o.LineComment != nil && o.Val.Pos().Line == o.Keys[0].Pos().Line { buf.WriteByte(blank) for _, comment := range o.LineComment.List { buf.WriteString(comment.Text) @@ -509,8 +517,13 @@ func (p *printer) alignedItems(items []*ast.ObjectItem) []byte { // list returns the printable HCL form of an list type. func (p *printer) list(l *ast.ListType) []byte { + if p.isSingleLineList(l) { + return p.singleLineList(l) + } + var buf bytes.Buffer buf.WriteString("[") + buf.WriteByte(newline) var longestLine int for _, item := range l.List { @@ -523,115 +536,112 @@ func (p *printer) list(l *ast.ListType) []byte { } } - insertSpaceBeforeItem := false - lastHadLeadComment := false + haveEmptyLine := false for i, item := range l.List { - // Keep track of whether this item is a heredoc since that has - // unique behavior. - heredoc := false + // If we have a lead comment, then we want to write that first + leadComment := false + if lit, ok := item.(*ast.LiteralType); ok && lit.LeadComment != nil { + leadComment = true + + // Ensure an empty line before every element with a + // lead comment (except the first item in a list). + if !haveEmptyLine && i != 0 { + buf.WriteByte(newline) + } + + for _, comment := range lit.LeadComment.List { + buf.Write(p.indent([]byte(comment.Text))) + buf.WriteByte(newline) + } + } + + // also indent each line + val := p.output(item) + curLen := len(val) + buf.Write(p.indent(val)) + + // if this item is a heredoc, then we output the comma on + // the next line. This is the only case this happens. + comma := []byte{','} if lit, ok := item.(*ast.LiteralType); ok && lit.Token.Type == token.HEREDOC { - heredoc = true - } - - if item.Pos().Line != l.Lbrack.Line { - // multiline list, add newline before we add each item buf.WriteByte(newline) - insertSpaceBeforeItem = false + comma = p.indent(comma) + } - // If we have a lead comment, then we want to write that first - leadComment := false - if lit, ok := item.(*ast.LiteralType); ok && lit.LeadComment != nil { - leadComment = true + buf.Write(comma) - // If this isn't the first item and the previous element - // didn't have a lead comment, then we need to add an extra - // newline to properly space things out. If it did have a - // lead comment previously then this would be done - // automatically. - if i > 0 && !lastHadLeadComment { - buf.WriteByte(newline) - } - - for _, comment := range lit.LeadComment.List { - buf.Write(p.indent([]byte(comment.Text))) - buf.WriteByte(newline) - } - } - - // also indent each line - val := p.output(item) - curLen := len(val) - buf.Write(p.indent(val)) - - // if this item is a heredoc, then we output the comma on - // the next line. This is the only case this happens. - comma := []byte{','} - if heredoc { - buf.WriteByte(newline) - comma = p.indent(comma) - } - - buf.Write(comma) - - if lit, ok := item.(*ast.LiteralType); ok && lit.LineComment != nil { - // if the next item doesn't have any comments, do not align - buf.WriteByte(blank) // align one space - for i := 0; i < longestLine-curLen; i++ { - buf.WriteByte(blank) - } - - for _, comment := range lit.LineComment.List { - buf.WriteString(comment.Text) - } - } - - lastItem := i == len(l.List)-1 - if lastItem { - buf.WriteByte(newline) - } - - if leadComment && !lastItem { - buf.WriteByte(newline) - } - - lastHadLeadComment = leadComment - } else { - if insertSpaceBeforeItem { + if lit, ok := item.(*ast.LiteralType); ok && lit.LineComment != nil { + // if the next item doesn't have any comments, do not align + buf.WriteByte(blank) // align one space + for i := 0; i < longestLine-curLen; i++ { buf.WriteByte(blank) - insertSpaceBeforeItem = false } - // Output the item itself - // also indent each line - val := p.output(item) - curLen := len(val) - buf.Write(val) - - // If this is a heredoc item we always have to output a newline - // so that it parses properly. - if heredoc { - buf.WriteByte(newline) - } - - // If this isn't the last element, write a comma. - if i != len(l.List)-1 { - buf.WriteString(",") - insertSpaceBeforeItem = true - } - - if lit, ok := item.(*ast.LiteralType); ok && lit.LineComment != nil { - // if the next item doesn't have any comments, do not align - buf.WriteByte(blank) // align one space - for i := 0; i < longestLine-curLen; i++ { - buf.WriteByte(blank) - } - - for _, comment := range lit.LineComment.List { - buf.WriteString(comment.Text) - } + for _, comment := range lit.LineComment.List { + buf.WriteString(comment.Text) } } + buf.WriteByte(newline) + + // Ensure an empty line after every element with a + // lead comment (except the first item in a list). + haveEmptyLine = leadComment && i != len(l.List)-1 + if haveEmptyLine { + buf.WriteByte(newline) + } + } + + buf.WriteString("]") + return buf.Bytes() +} + +// isSingleLineList returns true if: +// * they were previously formatted entirely on one line +// * they consist entirely of literals +// * there are either no heredoc strings or the list has exactly one element +// * there are no line comments +func (printer) isSingleLineList(l *ast.ListType) bool { + for _, item := range l.List { + if item.Pos().Line != l.Lbrack.Line { + return false + } + + lit, ok := item.(*ast.LiteralType) + if !ok { + return false + } + + if lit.Token.Type == token.HEREDOC && len(l.List) != 1 { + return false + } + + if lit.LineComment != nil { + return false + } + } + + return true +} + +// singleLineList prints a simple single line list. +// For a definition of "simple", see isSingleLineList above. +func (p *printer) singleLineList(l *ast.ListType) []byte { + buf := &bytes.Buffer{} + + buf.WriteString("[") + for i, item := range l.List { + if i != 0 { + buf.WriteString(", ") + } + + // Output the item itself + buf.Write(p.output(item)) + + // The heredoc marker needs to be at the end of line. + if lit, ok := item.(*ast.LiteralType); ok && lit.Token.Type == token.HEREDOC { + buf.WriteByte(newline) + } } buf.WriteString("]") diff --git a/vendor/github.com/hashicorp/hcl/hcl/scanner/scanner.go b/vendor/github.com/hashicorp/hcl/hcl/scanner/scanner.go index 6601ef76..624a18fe 100644 --- a/vendor/github.com/hashicorp/hcl/hcl/scanner/scanner.go +++ b/vendor/github.com/hashicorp/hcl/hcl/scanner/scanner.go @@ -74,14 +74,6 @@ func (s *Scanner) next() rune { return eof } - if ch == utf8.RuneError && size == 1 { - s.srcPos.Column++ - s.srcPos.Offset += size - s.lastCharLen = size - s.err("illegal UTF-8 encoding") - return ch - } - // remember last position s.prevPos = s.srcPos @@ -89,18 +81,27 @@ func (s *Scanner) next() rune { s.lastCharLen = size s.srcPos.Offset += size + if ch == utf8.RuneError && size == 1 { + s.err("illegal UTF-8 encoding") + return ch + } + if ch == '\n' { s.srcPos.Line++ s.lastLineLen = s.srcPos.Column s.srcPos.Column = 0 } - // If we see a null character with data left, then that is an error - if ch == '\x00' && s.buf.Len() > 0 { + if ch == '\x00' { s.err("unexpected null character (0x00)") return eof } + if ch == '\uE123' { + s.err("unicode code point U+E123 reserved for internal use") + return utf8.RuneError + } + // debug // fmt.Printf("ch: %q, offset:column: %d:%d\n", ch, s.srcPos.Offset, s.srcPos.Column) return ch @@ -432,16 +433,16 @@ func (s *Scanner) scanHeredoc() { // Read the identifier identBytes := s.src[offs : s.srcPos.Offset-s.lastCharLen] - if len(identBytes) == 0 { + if len(identBytes) == 0 || (len(identBytes) == 1 && identBytes[0] == '-') { s.err("zero-length heredoc anchor") return } var identRegexp *regexp.Regexp if identBytes[0] == '-' { - identRegexp = regexp.MustCompile(fmt.Sprintf(`[[:space:]]*%s\z`, identBytes[1:])) + identRegexp = regexp.MustCompile(fmt.Sprintf(`^[[:space:]]*%s\r*\z`, identBytes[1:])) } else { - identRegexp = regexp.MustCompile(fmt.Sprintf(`[[:space:]]*%s\z`, identBytes)) + identRegexp = regexp.MustCompile(fmt.Sprintf(`^[[:space:]]*%s\r*\z`, identBytes)) } // Read the actual string value @@ -551,7 +552,7 @@ func (s *Scanner) scanDigits(ch rune, base, n int) rune { s.err("illegal char escape") } - if n != start { + if n != start && ch != eof { // we scanned all digits, put the last non digit char back, // only if we read anything at all s.unread() diff --git a/vendor/github.com/jpillora/backoff/README.md b/vendor/github.com/jpillora/backoff/README.md index 81e77cd7..ee4d6230 100644 --- a/vendor/github.com/jpillora/backoff/README.md +++ b/vendor/github.com/jpillora/backoff/README.md @@ -116,4 +116,4 @@ https://godoc.org/github.com/jpillora/backoff #### Credits -Forked from some JavaScript written by [@tj](https://github.com/tj) \ No newline at end of file +Forked from [some JavaScript](https://github.com/segmentio/backo) written by [@tj](https://github.com/tj) diff --git a/vendor/github.com/jpillora/backoff/backoff.go b/vendor/github.com/jpillora/backoff/backoff.go index a50d0e95..b4941b6e 100644 --- a/vendor/github.com/jpillora/backoff/backoff.go +++ b/vendor/github.com/jpillora/backoff/backoff.go @@ -71,7 +71,8 @@ func (b *Backoff) ForAttempt(attempt float64) time.Duration { //keep within bounds if dur < min { return min - } else if dur > max { + } + if dur > max { return max } return dur @@ -86,3 +87,13 @@ func (b *Backoff) Reset() { func (b *Backoff) Attempt() float64 { return b.attempt } + +// Copy returns a backoff with equals constraints as the original +func (b *Backoff) Copy() *Backoff { + return &Backoff{ + Factor: b.Factor, + Jitter: b.Jitter, + Min: b.Min, + Max: b.Max, + } +} diff --git a/vendor/github.com/konsorten/go-windows-terminal-sequences/LICENSE b/vendor/github.com/konsorten/go-windows-terminal-sequences/LICENSE new file mode 100644 index 00000000..14127cd8 --- /dev/null +++ b/vendor/github.com/konsorten/go-windows-terminal-sequences/LICENSE @@ -0,0 +1,9 @@ +(The MIT License) + +Copyright (c) 2017 marvin + konsorten GmbH (open-source@konsorten.de) + +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. diff --git a/vendor/github.com/konsorten/go-windows-terminal-sequences/README.md b/vendor/github.com/konsorten/go-windows-terminal-sequences/README.md new file mode 100644 index 00000000..949b77e3 --- /dev/null +++ b/vendor/github.com/konsorten/go-windows-terminal-sequences/README.md @@ -0,0 +1,40 @@ +# Windows Terminal Sequences + +This library allow for enabling Windows terminal color support for Go. + +See [Console Virtual Terminal Sequences](https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences) for details. + +## Usage + +```go +import ( + "syscall" + + sequences "github.com/konsorten/go-windows-terminal-sequences" +) + +func main() { + sequences.EnableVirtualTerminalProcessing(syscall.Stdout, true) +} + +``` + +## Authors + +The tool is sponsored by the [marvin + konsorten GmbH](http://www.konsorten.de). + +We thank all the authors who provided code to this library: + +* Felix Kollmann + +## License + +(The MIT License) + +Copyright (c) 2018 marvin + konsorten GmbH (open-source@konsorten.de) + +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. diff --git a/vendor/github.com/konsorten/go-windows-terminal-sequences/go.mod b/vendor/github.com/konsorten/go-windows-terminal-sequences/go.mod new file mode 100644 index 00000000..716c6131 --- /dev/null +++ b/vendor/github.com/konsorten/go-windows-terminal-sequences/go.mod @@ -0,0 +1 @@ +module github.com/konsorten/go-windows-terminal-sequences diff --git a/vendor/github.com/konsorten/go-windows-terminal-sequences/sequences.go b/vendor/github.com/konsorten/go-windows-terminal-sequences/sequences.go new file mode 100644 index 00000000..ef18d8f9 --- /dev/null +++ b/vendor/github.com/konsorten/go-windows-terminal-sequences/sequences.go @@ -0,0 +1,36 @@ +// +build windows + +package sequences + +import ( + "syscall" + "unsafe" +) + +var ( + kernel32Dll *syscall.LazyDLL = syscall.NewLazyDLL("Kernel32.dll") + setConsoleMode *syscall.LazyProc = kernel32Dll.NewProc("SetConsoleMode") +) + +func EnableVirtualTerminalProcessing(stream syscall.Handle, enable bool) error { + const ENABLE_VIRTUAL_TERMINAL_PROCESSING uint32 = 0x4 + + var mode uint32 + err := syscall.GetConsoleMode(syscall.Stdout, &mode) + if err != nil { + return err + } + + if enable { + mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING + } else { + mode &^= ENABLE_VIRTUAL_TERMINAL_PROCESSING + } + + ret, _, err := setConsoleMode.Call(uintptr(unsafe.Pointer(stream)), uintptr(mode)) + if ret == 0 { + return err + } + + return nil +} diff --git a/vendor/github.com/labstack/echo/.travis.yml b/vendor/github.com/labstack/echo/.travis.yml index 951c2d39..05e53b16 100644 --- a/vendor/github.com/labstack/echo/.travis.yml +++ b/vendor/github.com/labstack/echo/.travis.yml @@ -1,7 +1,7 @@ language: go go: - - 1.8.x - 1.9.x + - 1.10.x - tip install: - make dependency diff --git a/vendor/github.com/labstack/echo/Gopkg.lock b/vendor/github.com/labstack/echo/Gopkg.lock index 272aaa52..f3c3b8d2 100644 --- a/vendor/github.com/labstack/echo/Gopkg.lock +++ b/vendor/github.com/labstack/echo/Gopkg.lock @@ -10,26 +10,26 @@ [[projects]] name = "github.com/dgrijalva/jwt-go" packages = ["."] - revision = "d2709f9f1f31ebcda9651b03077758c1f3a0018c" - version = "v3.0.0" + revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e" + version = "v3.2.0" [[projects]] name = "github.com/labstack/gommon" packages = ["bytes","color","log","random"] - revision = "1121fd3e243c202482226a7afe4dcd07ffc4139a" - version = "v0.2.1" + revision = "6fe1405d73ec4bd4cd8a4ac8e2a2b2bf95d03954" + version = "0.2.4" [[projects]] name = "github.com/mattn/go-colorable" packages = ["."] - revision = "d228849504861217f796da67fae4f6e347643f15" - version = "v0.0.7" + revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" + version = "v0.0.9" [[projects]] name = "github.com/mattn/go-isatty" packages = ["."] - revision = "fc9e8d8ef48496124e79ae0df75490096eccf6fe" - version = "v0.0.2" + revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" + version = "v0.0.3" [[projects]] name = "github.com/pmezard/go-difflib" @@ -40,8 +40,8 @@ [[projects]] name = "github.com/stretchr/testify" packages = ["assert"] - revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0" - version = "v1.1.4" + revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" + version = "v1.2.1" [[projects]] branch = "master" @@ -59,17 +59,17 @@ branch = "master" name = "golang.org/x/crypto" packages = ["acme","acme/autocert"] - revision = "e1a4589e7d3ea14a3352255d04b6f1a418845e5e" + revision = "182114d582623c1caa54f73de9c7224e23a48487" [[projects]] branch = "master" name = "golang.org/x/sys" packages = ["unix"] - revision = "b90f89a1e7a9c1f6b918820b3daa7f08488c8594" + revision = "c28acc882ebcbfbe8ce9f0f14b9ac26ee138dd51" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "5f74a2a2ba5b07475ad0faa1b4c021b973ad40b2ae749e3d94e15fe839bb440e" + inputs-digest = "9c7b45e80fe353405800cf01f429b3a203cfb8d4468a04c64a908e11a98ea764" solver-name = "gps-cdcl" solver-version = 1 diff --git a/vendor/github.com/labstack/echo/Gopkg.toml b/vendor/github.com/labstack/echo/Gopkg.toml new file mode 100644 index 00000000..61de60cb --- /dev/null +++ b/vendor/github.com/labstack/echo/Gopkg.toml @@ -0,0 +1,42 @@ + +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" + + +[[constraint]] + name = "github.com/dgrijalva/jwt-go" + version = "3.2.0" + +[[constraint]] + name = "github.com/labstack/gommon" + version = "0.2.4" + +[[constraint]] + name = "github.com/stretchr/testify" + version = "1.2.1" + +[[constraint]] + branch = "master" + name = "github.com/valyala/fasttemplate" + +[[constraint]] + branch = "master" + name = "golang.org/x/crypto" \ No newline at end of file diff --git a/vendor/github.com/labstack/echo/Makefile b/vendor/github.com/labstack/echo/Makefile index 3952d7bd..494667d8 100644 --- a/vendor/github.com/labstack/echo/Makefile +++ b/vendor/github.com/labstack/echo/Makefile @@ -11,3 +11,7 @@ test: go test -race -coverprofile=profile.out -covermode=atomic $$d || exit 1; \ [ -f profile.out ] && cat profile.out >> coverage.txt && rm profile.out; \ done + +tag: + @git tag `grep -P '^\tversion = ' echo.go|cut -f2 -d'"'` + @git tag|grep -v ^v diff --git a/vendor/github.com/labstack/echo/README.md b/vendor/github.com/labstack/echo/README.md index d840f343..0f609df4 100644 --- a/vendor/github.com/labstack/echo/README.md +++ b/vendor/github.com/labstack/echo/README.md @@ -26,9 +26,13 @@ - Automatic TLS via Let’s Encrypt - HTTP/2 support -## Performance +## Benchmarks - +Date: 2018/03/15
+Source: https://github.com/vishr/web-framework-benchmark
+Lower is better! + + ## [Guide](https://echo.labstack.com/guide) diff --git a/vendor/github.com/labstack/echo/bind.go b/vendor/github.com/labstack/echo/bind.go index 186bd83d..38e07150 100644 --- a/vendor/github.com/labstack/echo/bind.go +++ b/vendor/github.com/labstack/echo/bind.go @@ -80,7 +80,7 @@ func (b *DefaultBinder) bindData(ptr interface{}, data map[string][]string, tag val := reflect.ValueOf(ptr).Elem() if typ.Kind() != reflect.Struct { - return errors.New("Binding element must be a struct") + return errors.New("binding element must be a struct") } for i := 0; i < typ.NumField(); i++ { diff --git a/vendor/github.com/labstack/echo/context.go b/vendor/github.com/labstack/echo/context.go index 39801f00..cf780c51 100644 --- a/vendor/github.com/labstack/echo/context.go +++ b/vendor/github.com/labstack/echo/context.go @@ -206,6 +206,13 @@ const ( indexPage = "index.html" ) +func (c *context) writeContentType(value string) { + header := c.Response().Header() + if header.Get(HeaderContentType) == "" { + header.Set(HeaderContentType, value) + } +} + func (c *context) Request() *http.Request { return c.request } @@ -430,7 +437,7 @@ func (c *context) JSONP(code int, callback string, i interface{}) (err error) { } func (c *context) JSONPBlob(code int, callback string, b []byte) (err error) { - c.response.Header().Set(HeaderContentType, MIMEApplicationJavaScriptCharsetUTF8) + c.writeContentType(MIMEApplicationJavaScriptCharsetUTF8) c.response.WriteHeader(code) if _, err = c.response.Write([]byte(callback + "(")); err != nil { return @@ -463,7 +470,7 @@ func (c *context) XMLPretty(code int, i interface{}, indent string) (err error) } func (c *context) XMLBlob(code int, b []byte) (err error) { - c.response.Header().Set(HeaderContentType, MIMEApplicationXMLCharsetUTF8) + c.writeContentType(MIMEApplicationXMLCharsetUTF8) c.response.WriteHeader(code) if _, err = c.response.Write([]byte(xml.Header)); err != nil { return @@ -473,14 +480,14 @@ func (c *context) XMLBlob(code int, b []byte) (err error) { } func (c *context) Blob(code int, contentType string, b []byte) (err error) { - c.response.Header().Set(HeaderContentType, contentType) + c.writeContentType(contentType) c.response.WriteHeader(code) _, err = c.response.Write(b) return } func (c *context) Stream(code int, contentType string, r io.Reader) (err error) { - c.response.Header().Set(HeaderContentType, contentType) + c.writeContentType(contentType) c.response.WriteHeader(code) _, err = io.Copy(c.response, r) return @@ -509,18 +516,17 @@ func (c *context) File(file string) (err error) { return } -func (c *context) Attachment(file, name string) (err error) { +func (c *context) Attachment(file, name string) error { return c.contentDisposition(file, name, "attachment") } -func (c *context) Inline(file, name string) (err error) { +func (c *context) Inline(file, name string) error { return c.contentDisposition(file, name, "inline") } -func (c *context) contentDisposition(file, name, dispositionType string) (err error) { +func (c *context) contentDisposition(file, name, dispositionType string) error { c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf("%s; filename=%q", dispositionType, name)) - c.File(file) - return + return c.File(file) } func (c *context) NoContent(code int) error { diff --git a/vendor/github.com/labstack/echo/echo.go b/vendor/github.com/labstack/echo/echo.go index 4a54b31a..41ac6b5e 100644 --- a/vendor/github.com/labstack/echo/echo.go +++ b/vendor/github.com/labstack/echo/echo.go @@ -38,6 +38,7 @@ package echo import ( "bytes" + stdContext "context" "crypto/tls" "errors" "fmt" @@ -45,6 +46,7 @@ import ( stdLog "log" "net" "net/http" + "net/url" "path" "path/filepath" "reflect" @@ -81,8 +83,7 @@ type ( Binder Binder Validator Validator Renderer Renderer - // Mutex sync.RWMutex - Logger Logger + Logger Logger } // Route contains a handler and information for matching against requests. @@ -94,9 +95,9 @@ type ( // HTTPError represents an error that occurred while handling a request. HTTPError struct { - Code int - Message interface{} - Inner error // Stores the error returned by an external dependency + Code int + Message interface{} + Internal error // Stores the error returned by an external dependency } // MiddlewareFunc defines a function to process middleware. @@ -129,15 +130,16 @@ type ( // HTTP methods const ( - CONNECT = "CONNECT" - DELETE = "DELETE" - GET = "GET" - HEAD = "HEAD" - OPTIONS = "OPTIONS" - PATCH = "PATCH" - POST = "POST" - PUT = "PUT" - TRACE = "TRACE" + CONNECT = "CONNECT" + DELETE = "DELETE" + GET = "GET" + HEAD = "HEAD" + OPTIONS = "OPTIONS" + PATCH = "PATCH" + POST = "POST" + PROPFIND = "PROPFIND" + PUT = "PUT" + TRACE = "TRACE" ) // MIME types @@ -191,6 +193,7 @@ const ( HeaderXHTTPMethodOverride = "X-HTTP-Method-Override" HeaderXRealIP = "X-Real-IP" HeaderXRequestID = "X-Request-ID" + HeaderXRequestedWith = "X-Requested-With" HeaderServer = "Server" HeaderOrigin = "Origin" @@ -214,7 +217,7 @@ const ( ) const ( - version = "3.2.6" + Version = "3.3.5" website = "https://echo.labstack.com" // http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo banner = ` @@ -238,6 +241,7 @@ var ( OPTIONS, PATCH, POST, + PROPFIND, PUT, TRACE, } @@ -251,10 +255,10 @@ var ( ErrForbidden = NewHTTPError(http.StatusForbidden) ErrMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed) ErrStatusRequestEntityTooLarge = NewHTTPError(http.StatusRequestEntityTooLarge) - ErrValidatorNotRegistered = errors.New("Validator not registered") - ErrRendererNotRegistered = errors.New("Renderer not registered") - ErrInvalidRedirectCode = errors.New("Invalid redirect status code") - ErrCookieNotFound = errors.New("Cookie not found") + ErrValidatorNotRegistered = errors.New("validator not registered") + ErrRendererNotRegistered = errors.New("renderer not registered") + ErrInvalidRedirectCode = errors.New("invalid redirect status code") + ErrCookieNotFound = errors.New("cookie not found") ) // Error handlers @@ -321,8 +325,8 @@ func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) { if he, ok := err.(*HTTPError); ok { code = he.Code msg = he.Message - if he.Inner != nil { - msg = fmt.Sprintf("%v, %v", err, he.Inner) + if he.Internal != nil { + msg = fmt.Sprintf("%v, %v", err, he.Internal) } } else if e.Debug { msg = err.Error() @@ -443,7 +447,7 @@ func (e *Echo) Static(prefix, root string) *Route { func static(i i, prefix, root string) *Route { h := func(c Context) error { - p, err := PathUnescape(c.Param("*")) + p, err := url.PathUnescape(c.Param("*")) if err != nil { return err } @@ -530,7 +534,7 @@ func (e *Echo) Reverse(name string, params ...interface{}) string { // Routes returns the registered routes. func (e *Echo) Routes() []*Route { - routes := []*Route{} + routes := make([]*Route, 0, len(e.router.routes)) for _, v := range e.router.routes { routes = append(routes, v) } @@ -551,39 +555,48 @@ func (e *Echo) ReleaseContext(c Context) { // ServeHTTP implements `http.Handler` interface, which serves HTTP requests. func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // Acquire lock - // e.Mutex.RLock() - // defer e.Mutex.RUnlock() - // Acquire context c := e.pool.Get().(*context) - defer e.pool.Put(c) c.Reset(r, w) - // Middleware - h := func(c Context) error { - method := r.Method + m := r.Method + h := NotFoundHandler + + if e.premiddleware == nil { path := r.URL.RawPath if path == "" { path = r.URL.Path } - e.router.Find(method, path, c) - h := c.Handler() + e.router.Find(m, getPath(r), c) + h = c.Handler() for i := len(e.middleware) - 1; i >= 0; i-- { h = e.middleware[i](h) } - return h(c) - } - - // Premiddleware - for i := len(e.premiddleware) - 1; i >= 0; i-- { - h = e.premiddleware[i](h) + } else { + h = func(c Context) error { + path := r.URL.RawPath + if path == "" { + path = r.URL.Path + } + e.router.Find(m, getPath(r), c) + h := c.Handler() + for i := len(e.middleware) - 1; i >= 0; i-- { + h = e.middleware[i](h) + } + return h(c) + } + for i := len(e.premiddleware) - 1; i >= 0; i-- { + h = e.premiddleware[i](h) + } } // Execute chain if err := h(c); err != nil { e.HTTPErrorHandler(err, c) } + + // Release context + e.pool.Put(c) } // Start starts an HTTP server. @@ -609,6 +622,10 @@ func (e *Echo) StartTLS(address string, certFile, keyFile string) (err error) { // StartAutoTLS starts an HTTPS server using certificates automatically installed from https://letsencrypt.org. func (e *Echo) StartAutoTLS(address string) error { + if e.Listener == nil { + go http.ListenAndServe(":http", e.AutoTLSManager.HTTPHandler(nil)) + } + s := e.TLSServer s.TLSConfig = new(tls.Config) s.TLSConfig.GetCertificate = e.AutoTLSManager.GetCertificate @@ -635,7 +652,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) { } if !e.HideBanner { - e.colorer.Printf(banner, e.colorer.Red("v"+version), e.colorer.Blue(website)) + e.colorer.Printf(banner, e.colorer.Red("v"+Version), e.colorer.Blue(website)) } if s.TLSConfig == nil { @@ -663,6 +680,24 @@ func (e *Echo) StartServer(s *http.Server) (err error) { return s.Serve(e.TLSListener) } +// Close immediately stops the server. +// It internally calls `http.Server#Close()`. +func (e *Echo) Close() error { + if err := e.TLSServer.Close(); err != nil { + return err + } + return e.Server.Close() +} + +// Shutdown stops server the gracefully. +// It internally calls `http.Server#Shutdown()`. +func (e *Echo) Shutdown(ctx stdContext.Context) error { + if err := e.TLSServer.Shutdown(ctx); err != nil { + return err + } + return e.Server.Shutdown(ctx) +} + // NewHTTPError creates a new HTTPError instance. func NewHTTPError(code int, message ...interface{}) *HTTPError { he := &HTTPError{Code: code, Message: http.StatusText(code)} @@ -698,6 +733,14 @@ func WrapMiddleware(m func(http.Handler) http.Handler) MiddlewareFunc { } } +func getPath(r *http.Request) string { + path := r.URL.RawPath + if path == "" { + path = r.URL.Path + } + return path +} + func handlerName(h HandlerFunc) string { t := reflect.ValueOf(h).Type() if t.Kind() == reflect.Func { @@ -706,6 +749,11 @@ func handlerName(h HandlerFunc) string { return t.String() } +// // PathUnescape is wraps `url.PathUnescape` +// func PathUnescape(s string) (string, error) { +// return url.PathUnescape(s) +// } + // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted // connections. It's used by ListenAndServe and ListenAndServeTLS so // dead TCP connections (e.g. closing laptop mid-download) eventually diff --git a/vendor/github.com/labstack/echo/echo_go1.8.go b/vendor/github.com/labstack/echo/echo_go1.8.go deleted file mode 100644 index 340bed70..00000000 --- a/vendor/github.com/labstack/echo/echo_go1.8.go +++ /dev/null @@ -1,25 +0,0 @@ -// +build go1.8 - -package echo - -import ( - stdContext "context" -) - -// Close immediately stops the server. -// It internally calls `http.Server#Close()`. -func (e *Echo) Close() error { - if err := e.TLSServer.Close(); err != nil { - return err - } - return e.Server.Close() -} - -// Shutdown stops server the gracefully. -// It internally calls `http.Server#Shutdown()`. -func (e *Echo) Shutdown(ctx stdContext.Context) error { - if err := e.TLSServer.Shutdown(ctx); err != nil { - return err - } - return e.Server.Shutdown(ctx) -} diff --git a/vendor/github.com/labstack/echo/group.go b/vendor/github.com/labstack/echo/group.go index f7e61a2e..5257e83c 100644 --- a/vendor/github.com/labstack/echo/group.go +++ b/vendor/github.com/labstack/echo/group.go @@ -92,7 +92,7 @@ func (g *Group) Match(methods []string, path string, handler HandlerFunc, middle // Group creates a new sub-group with prefix and optional sub-group-level middleware. func (g *Group) Group(prefix string, middleware ...MiddlewareFunc) *Group { - m := []MiddlewareFunc{} + m := make([]MiddlewareFunc, 0, len(g.middleware)+len(middleware)) m = append(m, g.middleware...) m = append(m, middleware...) return g.echo.Group(g.prefix+prefix, m...) @@ -113,7 +113,7 @@ func (g *Group) Add(method, path string, handler HandlerFunc, middleware ...Midd // Combine into a new slice to avoid accidentally passing the same slice for // multiple routes, which would lead to later add() calls overwriting the // middleware from earlier calls. - m := []MiddlewareFunc{} + m := make([]MiddlewareFunc, 0, len(g.middleware)+len(middleware)) m = append(m, g.middleware...) m = append(m, middleware...) return g.echo.Add(method, g.prefix+path, handler, m...) diff --git a/vendor/github.com/labstack/echo/middleware/basic_auth.go b/vendor/github.com/labstack/echo/middleware/basic_auth.go index 6d6a37b4..e6c96324 100644 --- a/vendor/github.com/labstack/echo/middleware/basic_auth.go +++ b/vendor/github.com/labstack/echo/middleware/basic_auth.go @@ -93,10 +93,8 @@ func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc { } } - realm := "" - if config.Realm == defaultRealm { - realm = defaultRealm - } else { + realm := defaultRealm + if config.Realm != defaultRealm { realm = strconv.Quote(config.Realm) } diff --git a/vendor/github.com/labstack/echo/middleware/body_dump.go b/vendor/github.com/labstack/echo/middleware/body_dump.go index 14cf33d1..e64e5e11 100644 --- a/vendor/github.com/labstack/echo/middleware/body_dump.go +++ b/vendor/github.com/labstack/echo/middleware/body_dump.go @@ -3,12 +3,11 @@ package middleware import ( "bufio" "bytes" + "io" "io/ioutil" "net" "net/http" - "io" - "github.com/labstack/echo" ) diff --git a/vendor/github.com/labstack/echo/middleware/body_limit.go b/vendor/github.com/labstack/echo/middleware/body_limit.go index 8d8281f4..c83f57e1 100644 --- a/vendor/github.com/labstack/echo/middleware/body_limit.go +++ b/vendor/github.com/labstack/echo/middleware/body_limit.go @@ -105,6 +105,7 @@ func (r *limitedReader) Close() error { func (r *limitedReader) Reset(reader io.ReadCloser, context echo.Context) { r.reader = reader r.context = context + r.read = 0 } func limitedReaderPool(c BodyLimitConfig) sync.Pool { diff --git a/vendor/github.com/labstack/echo/middleware/csrf.go b/vendor/github.com/labstack/echo/middleware/csrf.go index 0d2b7fd6..5d1f4671 100644 --- a/vendor/github.com/labstack/echo/middleware/csrf.go +++ b/vendor/github.com/labstack/echo/middleware/csrf.go @@ -126,8 +126,8 @@ func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc { k, err := c.Cookie(config.CookieName) token := "" + // Generate token if err != nil { - // Generate token token = random.String(config.TokenLength) } else { // Reuse token @@ -143,7 +143,7 @@ func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } if !validateCSRFToken(token, clientToken) { - return echo.NewHTTPError(http.StatusForbidden, "Invalid csrf token") + return echo.NewHTTPError(http.StatusForbidden, "invalid csrf token") } } @@ -187,7 +187,7 @@ func csrfTokenFromForm(param string) csrfTokenExtractor { return func(c echo.Context) (string, error) { token := c.FormValue(param) if token == "" { - return "", errors.New("Missing csrf token in the form parameter") + return "", errors.New("missing csrf token in the form parameter") } return token, nil } @@ -199,7 +199,7 @@ func csrfTokenFromQuery(param string) csrfTokenExtractor { return func(c echo.Context) (string, error) { token := c.QueryParam(param) if token == "" { - return "", errors.New("Missing csrf token in the query string") + return "", errors.New("missing csrf token in the query string") } return token, nil } diff --git a/vendor/github.com/labstack/echo/middleware/jwt.go b/vendor/github.com/labstack/echo/middleware/jwt.go index 47d885b0..e98040ae 100644 --- a/vendor/github.com/labstack/echo/middleware/jwt.go +++ b/vendor/github.com/labstack/echo/middleware/jwt.go @@ -58,8 +58,8 @@ const ( // Errors var ( - ErrJWTMissing = echo.NewHTTPError(http.StatusBadRequest, "Missing or malformed jwt") - ErrJWTInvalid = echo.NewHTTPError(http.StatusUnauthorized, "Invalid or expired jwt") + ErrJWTMissing = echo.NewHTTPError(http.StatusBadRequest, "missing or malformed jwt") + ErrJWTInvalid = echo.NewHTTPError(http.StatusUnauthorized, "invalid or expired jwt") ) var ( @@ -116,7 +116,7 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc { config.keyFunc = func(t *jwt.Token) (interface{}, error) { // Check the signing method if t.Method.Alg() != config.SigningMethod { - return nil, fmt.Errorf("Unexpected jwt signing method=%v", t.Header["alg"]) + return nil, fmt.Errorf("unexpected jwt signing method=%v", t.Header["alg"]) } return config.SigningKey, nil } @@ -156,9 +156,9 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc { return next(c) } return &echo.HTTPError{ - Code: ErrJWTInvalid.Code, - Message: ErrJWTInvalid.Message, - Inner: err, + Code: ErrJWTInvalid.Code, + Message: ErrJWTInvalid.Message, + Internal: err, } } } diff --git a/vendor/github.com/labstack/echo/middleware/key_auth.go b/vendor/github.com/labstack/echo/middleware/key_auth.go index 4990afd9..c12f4ca9 100644 --- a/vendor/github.com/labstack/echo/middleware/key_auth.go +++ b/vendor/github.com/labstack/echo/middleware/key_auth.go @@ -114,14 +114,14 @@ func keyFromHeader(header string, authScheme string) keyExtractor { return func(c echo.Context) (string, error) { auth := c.Request().Header.Get(header) if auth == "" { - return "", errors.New("Missing key in request header") + return "", errors.New("missing key in request header") } if header == echo.HeaderAuthorization { l := len(authScheme) if len(auth) > l+1 && auth[:l] == authScheme { return auth[l+1:], nil } - return "", errors.New("Invalid key in the request header") + return "", errors.New("invalid key in the request header") } return auth, nil } @@ -132,7 +132,7 @@ func keyFromQuery(param string) keyExtractor { return func(c echo.Context) (string, error) { key := c.QueryParam(param) if key == "" { - return "", errors.New("Missing key in the query string") + return "", errors.New("missing key in the query string") } return key, nil } @@ -143,7 +143,7 @@ func keyFromForm(param string) keyExtractor { return func(c echo.Context) (string, error) { key := c.FormValue(param) if key == "" { - return "", errors.New("Missing key in the form") + return "", errors.New("missing key in the form") } return key, nil } diff --git a/vendor/github.com/labstack/echo/middleware/logger.go b/vendor/github.com/labstack/echo/middleware/logger.go index c7b80f8c..87af575f 100644 --- a/vendor/github.com/labstack/echo/middleware/logger.go +++ b/vendor/github.com/labstack/echo/middleware/logger.go @@ -47,7 +47,7 @@ type ( // Example "${remote_ip} ${status}" // // Optional. Default value DefaultLoggerConfig.Format. - Format string `yaml:"format"` + Format string `yaml:"format"` // Optional. Default value DefaultLoggerConfig.CustomTimeFormat. CustomTimeFormat string `yaml:"custom_time_format"` @@ -70,9 +70,9 @@ var ( `"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` + `"latency_human":"${latency_human}","bytes_in":${bytes_in},` + `"bytes_out":${bytes_out}}` + "\n", - CustomTimeFormat:"2006-01-02 15:04:05.00000", - Output: os.Stdout, - colorer: color.New(), + CustomTimeFormat: "2006-01-02 15:04:05.00000", + Output: os.Stdout, + colorer: color.New(), } ) diff --git a/vendor/github.com/labstack/echo/middleware/proxy.go b/vendor/github.com/labstack/echo/middleware/proxy.go index ae3ff527..f6147737 100644 --- a/vendor/github.com/labstack/echo/middleware/proxy.go +++ b/vendor/github.com/labstack/echo/middleware/proxy.go @@ -108,15 +108,15 @@ func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler { return } - errc := make(chan error, 2) + errCh := make(chan error, 2) cp := func(dst io.Writer, src io.Reader) { - _, err := io.Copy(dst, src) - errc <- err + _, err = io.Copy(dst, src) + errCh <- err } go cp(out, in) go cp(in, out) - err = <-errc + err = <-errCh if err != nil && err != io.EOF { c.Logger().Errorf("proxy raw, copy body error=%v, url=%s", t.URL, err) } diff --git a/vendor/github.com/labstack/echo/middleware/static.go b/vendor/github.com/labstack/echo/middleware/static.go index 7208c3a2..55485f34 100644 --- a/vendor/github.com/labstack/echo/middleware/static.go +++ b/vendor/github.com/labstack/echo/middleware/static.go @@ -2,13 +2,16 @@ package middleware import ( "fmt" + "html/template" "net/http" + "net/url" "os" "path" "path/filepath" "strings" "github.com/labstack/echo" + "github.com/labstack/gommon/bytes" ) type ( @@ -36,6 +39,78 @@ type ( } ) +const html = ` + + + + + + + {{ .Name }} + + + +
+ {{ .Name }} +
+
    + {{ range .Files }} +
  • + {{ if .Dir }} + {{ $name := print .Name "/" }} + {{ $name }} + {{ else }} + {{ .Name }} + {{ .Size }} + {{ end }} +
  • + {{ end }} +
+ + +` + var ( // DefaultStaticConfig is the default Static middleware config. DefaultStaticConfig = StaticConfig{ @@ -66,6 +141,12 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc { config.Index = DefaultStaticConfig.Index } + // Index template + t, err := template.New("index").Parse(html) + if err != nil { + panic(fmt.Sprintf("echo: %v", err)) + } + return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) (err error) { if config.Skipper(c) { @@ -76,7 +157,7 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc { if strings.HasSuffix(c.Path(), "*") { // When serving from a group, e.g. `/static*`. p = c.Param("*") } - p, err = echo.PathUnescape(p) + p, err = url.PathUnescape(p) if err != nil { return } @@ -103,7 +184,7 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc { if err != nil { if config.Browse { - return listDir(name, c.Response()) + return listDir(t, name, c.Response()) } if os.IsNotExist(err) { return next(c) @@ -119,32 +200,30 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc { } } -func listDir(name string, res *echo.Response) (err error) { - dir, err := os.Open(name) +func listDir(t *template.Template, name string, res *echo.Response) (err error) { + file, err := os.Open(name) if err != nil { return } - dirs, err := dir.Readdir(-1) + files, err := file.Readdir(-1) if err != nil { return } - // Create a directory index + // Create directory index res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8) - if _, err = fmt.Fprintf(res, "
\n"); err != nil {
-		return
+	data := struct {
+		Name  string
+		Files []interface{}
+	}{
+		Name: name,
 	}
-	for _, d := range dirs {
-		name := d.Name()
-		color := "#212121"
-		if d.IsDir() {
-			color = "#e91e63"
-			name += "/"
-		}
-		if _, err = fmt.Fprintf(res, "%s\n", name, color, name); err != nil {
-			return
-		}
+	for _, f := range files {
+		data.Files = append(data.Files, struct {
+			Name string
+			Dir  bool
+			Size string
+		}{f.Name(), f.IsDir(), bytes.Format(f.Size())})
 	}
-	_, err = fmt.Fprintf(res, "
\n") - return + return t.Execute(res, data) } diff --git a/vendor/github.com/labstack/echo/response.go b/vendor/github.com/labstack/echo/response.go index 93204098..6244783b 100644 --- a/vendor/github.com/labstack/echo/response.go +++ b/vendor/github.com/labstack/echo/response.go @@ -4,7 +4,6 @@ import ( "bufio" "net" "net/http" - "strconv" ) type ( @@ -12,14 +11,13 @@ type ( // by an HTTP handler to construct an HTTP response. // See: https://golang.org/pkg/net/http/#ResponseWriter Response struct { - echo *Echo - contentLength int64 - beforeFuncs []func() - afterFuncs []func() - Writer http.ResponseWriter - Status int - Size int64 - Committed bool + echo *Echo + beforeFuncs []func() + afterFuncs []func() + Writer http.ResponseWriter + Status int + Size int64 + Committed bool } ) @@ -64,7 +62,6 @@ func (r *Response) WriteHeader(code int) { r.Status = code r.Writer.WriteHeader(code) r.Committed = true - r.contentLength, _ = strconv.ParseInt(r.Header().Get(HeaderContentLength), 10, 0) } // Write writes the data to the connection as part of an HTTP reply. @@ -74,10 +71,8 @@ func (r *Response) Write(b []byte) (n int, err error) { } n, err = r.Writer.Write(b) r.Size += int64(n) - if r.Size == r.contentLength { - for _, fn := range r.afterFuncs { - fn() - } + for _, fn := range r.afterFuncs { + fn() } return } @@ -106,7 +101,6 @@ func (r *Response) CloseNotify() <-chan bool { } func (r *Response) reset(w http.ResponseWriter) { - r.contentLength = 0 r.beforeFuncs = nil r.afterFuncs = nil r.Writer = w diff --git a/vendor/github.com/labstack/echo/router.go b/vendor/github.com/labstack/echo/router.go index 3af4be0b..ff53da87 100644 --- a/vendor/github.com/labstack/echo/router.go +++ b/vendor/github.com/labstack/echo/router.go @@ -21,15 +21,16 @@ type ( kind uint8 children []*node methodHandler struct { - connect HandlerFunc - delete HandlerFunc - get HandlerFunc - head HandlerFunc - options HandlerFunc - patch HandlerFunc - post HandlerFunc - put HandlerFunc - trace HandlerFunc + connect HandlerFunc + delete HandlerFunc + get HandlerFunc + head HandlerFunc + options HandlerFunc + patch HandlerFunc + post HandlerFunc + propfind HandlerFunc + put HandlerFunc + trace HandlerFunc } ) @@ -59,8 +60,8 @@ func (r *Router) Add(method, path string, h HandlerFunc) { if path[0] != '/' { path = "/" + path } - ppath := path // Pristine path pnames := []string{} // Param names + ppath := path // Pristine path for i, l := 0, len(path); i < l; i++ { if path[i] == ':' { @@ -225,22 +226,24 @@ func (n *node) findChildByKind(t kind) *node { func (n *node) addHandler(method string, h HandlerFunc) { switch method { - case GET: - n.methodHandler.get = h - case POST: - n.methodHandler.post = h - case PUT: - n.methodHandler.put = h - case DELETE: - n.methodHandler.delete = h - case PATCH: - n.methodHandler.patch = h - case OPTIONS: - n.methodHandler.options = h - case HEAD: - n.methodHandler.head = h case CONNECT: n.methodHandler.connect = h + case DELETE: + n.methodHandler.delete = h + case GET: + n.methodHandler.get = h + case HEAD: + n.methodHandler.head = h + case OPTIONS: + n.methodHandler.options = h + case PATCH: + n.methodHandler.patch = h + case POST: + n.methodHandler.post = h + case PROPFIND: + n.methodHandler.propfind = h + case PUT: + n.methodHandler.put = h case TRACE: n.methodHandler.trace = h } @@ -248,22 +251,24 @@ func (n *node) addHandler(method string, h HandlerFunc) { func (n *node) findHandler(method string) HandlerFunc { switch method { - case GET: - return n.methodHandler.get - case POST: - return n.methodHandler.post - case PUT: - return n.methodHandler.put - case DELETE: - return n.methodHandler.delete - case PATCH: - return n.methodHandler.patch - case OPTIONS: - return n.methodHandler.options - case HEAD: - return n.methodHandler.head case CONNECT: return n.methodHandler.connect + case DELETE: + return n.methodHandler.delete + case GET: + return n.methodHandler.get + case HEAD: + return n.methodHandler.head + case OPTIONS: + return n.methodHandler.options + case PATCH: + return n.methodHandler.patch + case POST: + return n.methodHandler.post + case PROPFIND: + return n.methodHandler.propfind + case PUT: + return n.methodHandler.put case TRACE: return n.methodHandler.trace default: diff --git a/vendor/github.com/labstack/echo/util_go17.go b/vendor/github.com/labstack/echo/util_go17.go deleted file mode 100644 index eaae17e3..00000000 --- a/vendor/github.com/labstack/echo/util_go17.go +++ /dev/null @@ -1,12 +0,0 @@ -// +build go1.7, !go1.8 - -package echo - -import ( - "net/url" -) - -// PathUnescape is wraps `url.QueryUnescape` -func PathUnescape(s string) (string, error) { - return url.QueryUnescape(s) -} diff --git a/vendor/github.com/labstack/echo/util_go18.go b/vendor/github.com/labstack/echo/util_go18.go deleted file mode 100644 index 8a37785b..00000000 --- a/vendor/github.com/labstack/echo/util_go18.go +++ /dev/null @@ -1,10 +0,0 @@ -// +build go1.8 - -package echo - -import "net/url" - -// PathUnescape is wraps `url.PathUnescape` -func PathUnescape(s string) (string, error) { - return url.PathUnescape(s) -} diff --git a/vendor/github.com/lrstanley/girc/.travis.yml b/vendor/github.com/lrstanley/girc/.travis.yml index 658e65d9..96a1bb8a 100644 --- a/vendor/github.com/lrstanley/girc/.travis.yml +++ b/vendor/github.com/lrstanley/girc/.travis.yml @@ -1,14 +1,13 @@ language: go go: -- 1.8 -- 1.9 +- 1.11.x - tip before_install: -- go get -v github.com/golang/lint/golint +- go get -v golang.org/x/lint/golint script: - $HOME/gopath/bin/golint -min_confidence 0.9 -set_exit_status - GORACE="exitcode=1 halt_on_error=1" go test -v -coverprofile=coverage.txt -race -timeout 3m -count 3 -cpu 1,4 -- go tool vet -v -all . +- go vet -v . after_success: - bash <(curl -s https://codecov.io/bash) branches: diff --git a/vendor/github.com/lrstanley/girc/cap.go b/vendor/github.com/lrstanley/girc/cap.go index e7037f9f..89146484 100644 --- a/vendor/github.com/lrstanley/girc/cap.go +++ b/vendor/github.com/lrstanley/girc/cap.go @@ -102,7 +102,7 @@ func handleCAP(c *Client, e Event) { possible := possibleCapList(c) - if len(e.Params) >= 2 && len(e.Trailing) > 1 && e.Params[1] == CAP_LS { + if len(e.Params) >= 2 && e.Params[1] == CAP_LS { c.state.Lock() caps := parseCap(e.Trailing) diff --git a/vendor/github.com/lrstanley/girc/client.go b/vendor/github.com/lrstanley/girc/client.go index 4f823e16..63f47eaa 100644 --- a/vendor/github.com/lrstanley/girc/client.go +++ b/vendor/github.com/lrstanley/girc/client.go @@ -135,7 +135,7 @@ type Config struct { // PingDelay is the frequency between when the client sends a keep-alive // PING to the server, and awaits a response (and times out if the server // doesn't respond in time). This should be between 20-600 seconds. See - // Client.Lag() if you want to determine the delay between the server + // Client.Latency() if you want to determine the delay between the server // and the client. If this is set to -1, the client will not attempt to // send client -> server PING requests. PingDelay time.Duration diff --git a/vendor/github.com/lrstanley/girc/commands.go b/vendor/github.com/lrstanley/girc/commands.go index d22f7616..300db9ed 100644 --- a/vendor/github.com/lrstanley/girc/commands.go +++ b/vendor/github.com/lrstanley/girc/commands.go @@ -70,7 +70,7 @@ func (cmd *Commands) PartMessage(channel, message string) { // PRIVMSG specifically. ctcpType is the CTCP command, e.g. "FINGER", "TIME", // "VERSION", etc. func (cmd *Commands) SendCTCP(target, ctcpType, message string) { - out := encodeCTCPRaw(ctcpType, message) + out := EncodeCTCPRaw(ctcpType, message) if out == "" { panic(fmt.Sprintf("invalid CTCP: %s -> %s: %s", target, ctcpType, message)) } @@ -95,7 +95,7 @@ func (cmd *Commands) SendCTCPReplyf(target, ctcpType, format string, a ...interf // SendCTCPReply sends a CTCP response to target. Note that this method uses // NOTICE specifically. func (cmd *Commands) SendCTCPReply(target, ctcpType, message string) { - out := encodeCTCPRaw(ctcpType, message) + out := EncodeCTCPRaw(ctcpType, message) if out == "" { panic(fmt.Sprintf("invalid CTCP: %s -> %s: %s", target, ctcpType, message)) } diff --git a/vendor/github.com/lrstanley/girc/constants.go b/vendor/github.com/lrstanley/girc/constants.go index 4d3c65bc..76ba482d 100644 --- a/vendor/github.com/lrstanley/girc/constants.go +++ b/vendor/github.com/lrstanley/girc/constants.go @@ -6,6 +6,7 @@ package girc // Standard CTCP based constants. const ( + CTCP_ACTION = "ACTION" CTCP_PING = "PING" CTCP_PONG = "PONG" CTCP_VERSION = "VERSION" diff --git a/vendor/github.com/lrstanley/girc/ctcp.go b/vendor/github.com/lrstanley/girc/ctcp.go index 6076ab10..45fc6be5 100644 --- a/vendor/github.com/lrstanley/girc/ctcp.go +++ b/vendor/github.com/lrstanley/girc/ctcp.go @@ -30,18 +30,22 @@ type CTCPEvent struct { Reply bool `json:"reply"` } -// decodeCTCP decodes an incoming CTCP event, if it is CTCP. nil is returned -// if the incoming event does not match a valid CTCP. -func decodeCTCP(e *Event) *CTCPEvent { +// DecodeCTCP decodes an incoming CTCP event, if it is CTCP. nil is returned +// if the incoming event does not have valid CTCP encoding. +func DecodeCTCP(e *Event) *CTCPEvent { // http://www.irchelp.org/protocol/ctcpspec.html + if e == nil { + return nil + } + // Must be targeting a user/channel, AND trailing must have // DELIM+TAG+DELIM minimum (at least 3 chars). if len(e.Params) != 1 || len(e.Trailing) < 3 { return nil } - if (e.Command != PRIVMSG && e.Command != NOTICE) || !IsValidNick(e.Params[0]) { + if e.Command != PRIVMSG && e.Command != NOTICE { return nil } @@ -88,18 +92,18 @@ func decodeCTCP(e *Event) *CTCPEvent { } } -// encodeCTCP encodes a CTCP event into a string, including delimiters. -func encodeCTCP(ctcp *CTCPEvent) (out string) { +// EncodeCTCP encodes a CTCP event into a string, including delimiters. +func EncodeCTCP(ctcp *CTCPEvent) (out string) { if ctcp == nil { return "" } - return encodeCTCPRaw(ctcp.Command, ctcp.Text) + return EncodeCTCPRaw(ctcp.Command, ctcp.Text) } -// encodeCTCPRaw is much like encodeCTCP, however accepts a raw command and +// EncodeCTCPRaw is much like EncodeCTCP, however accepts a raw command and // string as input. -func encodeCTCPRaw(cmd, text string) (out string) { +func EncodeCTCPRaw(cmd, text string) (out string) { if len(cmd) <= 0 { return "" } @@ -145,6 +149,11 @@ func (c *CTCP) call(client *Client, event *CTCPEvent) { } if _, ok := c.handlers[event.Command]; !ok { + // If ACTION, don't do anything. + if event.Command == CTCP_ACTION { + return + } + // Send a ERRMSG reply, if we know who sent it. if event.Source != nil && IsValidNick(event.Source.Name) { client.Cmd.SendCTCPReply(event.Source.Name, CTCP_ERRMSG, "that is an unknown CTCP query") diff --git a/vendor/github.com/lrstanley/girc/event.go b/vendor/github.com/lrstanley/girc/event.go index 20182340..0b40b40b 100644 --- a/vendor/github.com/lrstanley/girc/event.go +++ b/vendor/github.com/lrstanley/girc/event.go @@ -51,7 +51,7 @@ type Event struct { // Trailing text. e.g. with a PRIVMSG, this is the message text (part // after the colon.) Trailing string `json:"trailing"` - // EmptyTrailign, if true, the text prefix (:) will be added even if + // EmptyTrailing, if true, the text prefix (:) will be added even if // Event.Trailing is empty. EmptyTrailing bool `json:"empty_trailing"` // Sensitive should be true if the message is sensitive (e.g. and should @@ -355,11 +355,15 @@ func (e *Event) Pretty() (out string, ok bool) { } if (e.Command == PRIVMSG || e.Command == NOTICE) && len(e.Params) > 0 { - if ctcp := decodeCTCP(e); ctcp != nil { + if ctcp := DecodeCTCP(e); ctcp != nil { if ctcp.Reply { return } + if ctcp.Command == CTCP_ACTION { + return fmt.Sprintf("[%s] **%s** %s", strings.Join(e.Params, ","), ctcp.Source.Name, ctcp.Text), true + } + return fmt.Sprintf("[*] CTCP query from %s: %s%s", ctcp.Source.Name, ctcp.Command, " "+ctcp.Text), true } return fmt.Sprintf("[%s] (%s) %s", strings.Join(e.Params, ","), e.Source.Name, e.Trailing), true @@ -383,12 +387,30 @@ func (e *Event) Pretty() (out string, ok bool) { return fmt.Sprintf("[*] %s has quit (%s)", e.Source.Name, e.Trailing), true } - if e.Command == KICK && len(e.Params) == 2 { + if e.Command == INVITE && len(e.Params) == 1 { + return fmt.Sprintf("[*] %s invited to %s by %s", e.Params[0], e.Trailing, e.Source.Name), true + } + + if e.Command == KICK && len(e.Params) >= 2 { + if e.Trailing == "" && len(e.Params) == 3 { + e.Trailing = e.Params[2] + } + return fmt.Sprintf("[%s] *** %s has kicked %s: %s", e.Params[0], e.Source.Name, e.Params[1], e.Trailing), true } - if e.Command == NICK && len(e.Params) == 1 { - return fmt.Sprintf("[*] %s is now known as %s", e.Source.Name, e.Params[0]), true + if e.Command == NICK { + // Workaround, see https://github.com/lrstanley/girc/pull/15#issuecomment-413845482 + var name string + if len(e.Params) == 1 { + name = e.Params[0] + } else if len(e.Trailing) > 0 { + name = e.Trailing + } + + if name != "" { + return fmt.Sprintf("[*] %s is now known as %s", e.Source.Name, name), true + } } if e.Command == TOPIC && len(e.Params) > 0 { @@ -430,23 +452,27 @@ func (e *Event) Pretty() (out string, ok bool) { return "", false } -// IsAction checks to see if the event is a PRIVMSG, and is an ACTION (/me). +// IsAction checks to see if the event is an ACTION (/me). func (e *Event) IsAction() bool { - if e.Source == nil || e.Command != PRIVMSG || len(e.Trailing) < 9 { + if e.Command != PRIVMSG { return false } - if !strings.HasPrefix(e.Trailing, "\001ACTION") || e.Trailing[len(e.Trailing)-1] != ctcpDelim { - return false - } + ok, ctcp := e.IsCTCP() + return ok && ctcp.Command == CTCP_ACTION +} - return true +// IsCTCP checks to see if the event is a CTCP event, and if so, returns the +// converted CTCP event. +func (e *Event) IsCTCP() (ok bool, ctcp *CTCPEvent) { + ctcp = DecodeCTCP(e) + return ctcp != nil, ctcp } // IsFromChannel checks to see if a message was from a channel (rather than // a private message). func (e *Event) IsFromChannel() bool { - if e.Source == nil || e.Command != PRIVMSG || len(e.Params) < 1 { + if e.Source == nil || (e.Command != PRIVMSG && e.Command != NOTICE) || len(e.Params) < 1 { return false } @@ -460,7 +486,7 @@ func (e *Event) IsFromChannel() bool { // IsFromUser checks to see if a message was from a user (rather than a // channel). func (e *Event) IsFromUser() bool { - if e.Source == nil || e.Command != PRIVMSG || len(e.Params) < 1 { + if e.Source == nil || (e.Command != PRIVMSG && e.Command != NOTICE) || len(e.Params) < 1 { return false } diff --git a/vendor/github.com/lrstanley/girc/format.go b/vendor/github.com/lrstanley/girc/format.go index b974c3bd..762f602b 100644 --- a/vendor/github.com/lrstanley/girc/format.go +++ b/vendor/github.com/lrstanley/girc/format.go @@ -113,7 +113,7 @@ func Fmt(text string) string { if last > -1 { // A-Z, a-z, and "," - if text[i] != ',' && (text[i] <= 'A' || text[i] >= 'Z') && (text[i] <= 'a' || text[i] >= 'z') { + if text[i] != ',' && (text[i] < 'A' || text[i] > 'Z') && (text[i] < 'a' || text[i] > 'z') { last = -1 continue } diff --git a/vendor/github.com/lrstanley/girc/go.mod b/vendor/github.com/lrstanley/girc/go.mod new file mode 100644 index 00000000..57b39ae8 --- /dev/null +++ b/vendor/github.com/lrstanley/girc/go.mod @@ -0,0 +1 @@ +module github.com/lrstanley/girc diff --git a/vendor/github.com/lrstanley/girc/handler.go b/vendor/github.com/lrstanley/girc/handler.go index bde08976..ec717de6 100644 --- a/vendor/github.com/lrstanley/girc/handler.go +++ b/vendor/github.com/lrstanley/girc/handler.go @@ -46,7 +46,7 @@ func (c *Client) RunHandlers(event *Event) { } // Check if it's a CTCP. - if ctcp := decodeCTCP(event.Copy()); ctcp != nil { + if ctcp := DecodeCTCP(event.Copy()); ctcp != nil { // Execute it. c.CTCP.call(c, ctcp) } diff --git a/vendor/github.com/magiconair/properties/.travis.yml b/vendor/github.com/magiconair/properties/.travis.yml index 923e8664..3e7c3d2c 100644 --- a/vendor/github.com/magiconair/properties/.travis.yml +++ b/vendor/github.com/magiconair/properties/.travis.yml @@ -6,5 +6,5 @@ go: - 1.7.x - 1.8.x - 1.9.x - - "1.10" + - "1.10.x" - tip diff --git a/vendor/github.com/magiconair/properties/CHANGELOG.md b/vendor/github.com/magiconair/properties/CHANGELOG.md index adefa17e..f83adc20 100644 --- a/vendor/github.com/magiconair/properties/CHANGELOG.md +++ b/vendor/github.com/magiconair/properties/CHANGELOG.md @@ -1,5 +1,13 @@ ## Changelog +### [1.8](https://github.com/magiconair/properties/tree/v1.8) - 15 May 2018 + + * [PR #26](https://github.com/magiconair/properties/pull/26): Disable expansion during loading + + This adds the option to disable property expansion during loading. + + Thanks to [@kmala](https://github.com/kmala) for the patch. + ### [1.7.6](https://github.com/magiconair/properties/tree/v1.7.6) - 14 Feb 2018 * [PR #29](https://github.com/magiconair/properties/pull/29): Reworked expansion logic to handle more complex cases. diff --git a/vendor/github.com/magiconair/properties/decode.go b/vendor/github.com/magiconair/properties/decode.go index 0a961bb0..3ebf8049 100644 --- a/vendor/github.com/magiconair/properties/decode.go +++ b/vendor/github.com/magiconair/properties/decode.go @@ -1,4 +1,4 @@ -// Copyright 2017 Frank Schroeder. All rights reserved. +// Copyright 2018 Frank Schroeder. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. diff --git a/vendor/github.com/magiconair/properties/doc.go b/vendor/github.com/magiconair/properties/doc.go index 36c83680..f8822da2 100644 --- a/vendor/github.com/magiconair/properties/doc.go +++ b/vendor/github.com/magiconair/properties/doc.go @@ -1,4 +1,4 @@ -// Copyright 2017 Frank Schroeder. All rights reserved. +// Copyright 2018 Frank Schroeder. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. @@ -73,7 +73,7 @@ // # refers to the users' home dir // home = ${HOME} // -// # local key takes precendence over env var: u = foo +// # local key takes precedence over env var: u = foo // USER = foo // u = ${USER} // @@ -102,7 +102,7 @@ // v = p.GetString("key", "def") // v = p.GetDuration("key", 999) // -// As an alterantive properties may be applied with the standard +// As an alternative properties may be applied with the standard // library's flag implementation at any time. // // # Standard configuration diff --git a/vendor/github.com/magiconair/properties/integrate.go b/vendor/github.com/magiconair/properties/integrate.go index 0d775e03..74d38dc6 100644 --- a/vendor/github.com/magiconair/properties/integrate.go +++ b/vendor/github.com/magiconair/properties/integrate.go @@ -1,4 +1,4 @@ -// Copyright 2017 Frank Schroeder. All rights reserved. +// Copyright 2018 Frank Schroeder. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. diff --git a/vendor/github.com/magiconair/properties/lex.go b/vendor/github.com/magiconair/properties/lex.go index c63fcc60..367166d5 100644 --- a/vendor/github.com/magiconair/properties/lex.go +++ b/vendor/github.com/magiconair/properties/lex.go @@ -1,4 +1,4 @@ -// Copyright 2017 Frank Schroeder. All rights reserved. +// Copyright 2018 Frank Schroeder. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // diff --git a/vendor/github.com/magiconair/properties/load.go b/vendor/github.com/magiconair/properties/load.go index 9c83fd63..c8e1b580 100644 --- a/vendor/github.com/magiconair/properties/load.go +++ b/vendor/github.com/magiconair/properties/load.go @@ -1,4 +1,4 @@ -// Copyright 2017 Frank Schroeder. All rights reserved. +// Copyright 2018 Frank Schroeder. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. @@ -16,21 +16,157 @@ import ( type Encoding uint const ( + // utf8Default is a private placeholder for the zero value of Encoding to + // ensure that it has the correct meaning. UTF8 is the default encoding but + // was assigned a non-zero value which cannot be changed without breaking + // existing code. Clients should continue to use the public constants. + utf8Default Encoding = iota + // UTF8 interprets the input data as UTF-8. - UTF8 Encoding = 1 << iota + UTF8 // ISO_8859_1 interprets the input data as ISO-8859-1. ISO_8859_1 ) +type Loader struct { + // Encoding determines how the data from files and byte buffers + // is interpreted. For URLs the Content-Type header is used + // to determine the encoding of the data. + Encoding Encoding + + // DisableExpansion configures the property expansion of the + // returned property object. When set to true, the property values + // will not be expanded and the Property object will not be checked + // for invalid expansion expressions. + DisableExpansion bool + + // IgnoreMissing configures whether missing files or URLs which return + // 404 are reported as errors. When set to true, missing files and 404 + // status codes are not reported as errors. + IgnoreMissing bool +} + +// Load reads a buffer into a Properties struct. +func (l *Loader) LoadBytes(buf []byte) (*Properties, error) { + return l.loadBytes(buf, l.Encoding) +} + +// LoadAll reads the content of multiple URLs or files in the given order into +// a Properties struct. If IgnoreMissing is true then a 404 status code or +// missing file will not be reported as error. Encoding sets the encoding for +// files. For the URLs see LoadURL for the Content-Type header and the +// encoding. +func (l *Loader) LoadAll(names []string) (*Properties, error) { + all := NewProperties() + for _, name := range names { + n, err := expandName(name) + if err != nil { + return nil, err + } + + var p *Properties + switch { + case strings.HasPrefix(n, "http://"): + p, err = l.LoadURL(n) + case strings.HasPrefix(n, "https://"): + p, err = l.LoadURL(n) + default: + p, err = l.LoadFile(n) + } + if err != nil { + return nil, err + } + all.Merge(p) + } + + all.DisableExpansion = l.DisableExpansion + if all.DisableExpansion { + return all, nil + } + return all, all.check() +} + +// LoadFile reads a file into a Properties struct. +// If IgnoreMissing is true then a missing file will not be +// reported as error. +func (l *Loader) LoadFile(filename string) (*Properties, error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + if l.IgnoreMissing && os.IsNotExist(err) { + LogPrintf("properties: %s not found. skipping", filename) + return NewProperties(), nil + } + return nil, err + } + return l.loadBytes(data, l.Encoding) +} + +// LoadURL reads the content of the URL into a Properties struct. +// +// The encoding is determined via the Content-Type header which +// should be set to 'text/plain'. If the 'charset' parameter is +// missing, 'iso-8859-1' or 'latin1' the encoding is set to +// ISO-8859-1. If the 'charset' parameter is set to 'utf-8' the +// encoding is set to UTF-8. A missing content type header is +// interpreted as 'text/plain; charset=utf-8'. +func (l *Loader) LoadURL(url string) (*Properties, error) { + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("properties: error fetching %q. %s", url, err) + } + + if resp.StatusCode == 404 && l.IgnoreMissing { + LogPrintf("properties: %s returned %d. skipping", url, resp.StatusCode) + return NewProperties(), nil + } + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("properties: %s returned %d", url, resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("properties: %s error reading response. %s", url, err) + } + defer resp.Body.Close() + + ct := resp.Header.Get("Content-Type") + var enc Encoding + switch strings.ToLower(ct) { + case "text/plain", "text/plain; charset=iso-8859-1", "text/plain; charset=latin1": + enc = ISO_8859_1 + case "", "text/plain; charset=utf-8": + enc = UTF8 + default: + return nil, fmt.Errorf("properties: invalid content type %s", ct) + } + + return l.loadBytes(body, enc) +} + +func (l *Loader) loadBytes(buf []byte, enc Encoding) (*Properties, error) { + p, err := parse(convert(buf, enc)) + if err != nil { + return nil, err + } + p.DisableExpansion = l.DisableExpansion + if p.DisableExpansion { + return p, nil + } + return p, p.check() +} + // Load reads a buffer into a Properties struct. func Load(buf []byte, enc Encoding) (*Properties, error) { - return loadBuf(buf, enc) + l := &Loader{Encoding: enc} + return l.LoadBytes(buf) } // LoadString reads an UTF8 string into a properties struct. func LoadString(s string) (*Properties, error) { - return loadBuf([]byte(s), UTF8) + l := &Loader{Encoding: UTF8} + return l.LoadBytes([]byte(s)) } // LoadMap creates a new Properties struct from a string map. @@ -44,34 +180,32 @@ func LoadMap(m map[string]string) *Properties { // LoadFile reads a file into a Properties struct. func LoadFile(filename string, enc Encoding) (*Properties, error) { - return loadAll([]string{filename}, enc, false) + l := &Loader{Encoding: enc} + return l.LoadAll([]string{filename}) } // LoadFiles reads multiple files in the given order into // a Properties struct. If 'ignoreMissing' is true then // non-existent files will not be reported as error. func LoadFiles(filenames []string, enc Encoding, ignoreMissing bool) (*Properties, error) { - return loadAll(filenames, enc, ignoreMissing) + l := &Loader{Encoding: enc, IgnoreMissing: ignoreMissing} + return l.LoadAll(filenames) } // LoadURL reads the content of the URL into a Properties struct. -// -// The encoding is determined via the Content-Type header which -// should be set to 'text/plain'. If the 'charset' parameter is -// missing, 'iso-8859-1' or 'latin1' the encoding is set to -// ISO-8859-1. If the 'charset' parameter is set to 'utf-8' the -// encoding is set to UTF-8. A missing content type header is -// interpreted as 'text/plain; charset=utf-8'. +// See Loader#LoadURL for details. func LoadURL(url string) (*Properties, error) { - return loadAll([]string{url}, UTF8, false) + l := &Loader{Encoding: UTF8} + return l.LoadAll([]string{url}) } // LoadURLs reads the content of multiple URLs in the given order into a -// Properties struct. If 'ignoreMissing' is true then a 404 status code will -// not be reported as error. See LoadURL for the Content-Type header +// Properties struct. If IgnoreMissing is true then a 404 status code will +// not be reported as error. See Loader#LoadURL for the Content-Type header // and the encoding. func LoadURLs(urls []string, ignoreMissing bool) (*Properties, error) { - return loadAll(urls, UTF8, ignoreMissing) + l := &Loader{Encoding: UTF8, IgnoreMissing: ignoreMissing} + return l.LoadAll(urls) } // LoadAll reads the content of multiple URLs or files in the given order into a @@ -79,7 +213,8 @@ func LoadURLs(urls []string, ignoreMissing bool) (*Properties, error) { // not be reported as error. Encoding sets the encoding for files. For the URLs please see // LoadURL for the Content-Type header and the encoding. func LoadAll(names []string, enc Encoding, ignoreMissing bool) (*Properties, error) { - return loadAll(names, enc, ignoreMissing) + l := &Loader{Encoding: enc, IgnoreMissing: ignoreMissing} + return l.LoadAll(names) } // MustLoadString reads an UTF8 string into a Properties struct and @@ -122,90 +257,6 @@ func MustLoadAll(names []string, enc Encoding, ignoreMissing bool) *Properties { return must(LoadAll(names, enc, ignoreMissing)) } -func loadBuf(buf []byte, enc Encoding) (*Properties, error) { - p, err := parse(convert(buf, enc)) - if err != nil { - return nil, err - } - return p, p.check() -} - -func loadAll(names []string, enc Encoding, ignoreMissing bool) (*Properties, error) { - result := NewProperties() - for _, name := range names { - n, err := expandName(name) - if err != nil { - return nil, err - } - var p *Properties - if strings.HasPrefix(n, "http://") || strings.HasPrefix(n, "https://") { - p, err = loadURL(n, ignoreMissing) - } else { - p, err = loadFile(n, enc, ignoreMissing) - } - if err != nil { - return nil, err - } - result.Merge(p) - - } - return result, result.check() -} - -func loadFile(filename string, enc Encoding, ignoreMissing bool) (*Properties, error) { - data, err := ioutil.ReadFile(filename) - if err != nil { - if ignoreMissing && os.IsNotExist(err) { - LogPrintf("properties: %s not found. skipping", filename) - return NewProperties(), nil - } - return nil, err - } - p, err := parse(convert(data, enc)) - if err != nil { - return nil, err - } - return p, nil -} - -func loadURL(url string, ignoreMissing bool) (*Properties, error) { - resp, err := http.Get(url) - if err != nil { - return nil, fmt.Errorf("properties: error fetching %q. %s", url, err) - } - if resp.StatusCode == 404 && ignoreMissing { - LogPrintf("properties: %s returned %d. skipping", url, resp.StatusCode) - return NewProperties(), nil - } - if resp.StatusCode != 200 { - return nil, fmt.Errorf("properties: %s returned %d", url, resp.StatusCode) - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("properties: %s error reading response. %s", url, err) - } - if err = resp.Body.Close(); err != nil { - return nil, fmt.Errorf("properties: %s error reading response. %s", url, err) - } - - ct := resp.Header.Get("Content-Type") - var enc Encoding - switch strings.ToLower(ct) { - case "text/plain", "text/plain; charset=iso-8859-1", "text/plain; charset=latin1": - enc = ISO_8859_1 - case "", "text/plain; charset=utf-8": - enc = UTF8 - default: - return nil, fmt.Errorf("properties: invalid content type %s", ct) - } - - p, err := parse(convert(body, enc)) - if err != nil { - return nil, err - } - return p, nil -} - func must(p *Properties, err error) *Properties { if err != nil { ErrorHandler(err) @@ -226,7 +277,7 @@ func expandName(name string) (string, error) { // first 256 unicode code points cover ISO-8859-1. func convert(buf []byte, enc Encoding) string { switch enc { - case UTF8: + case utf8Default, UTF8: return string(buf) case ISO_8859_1: runes := make([]rune, len(buf)) diff --git a/vendor/github.com/magiconair/properties/parser.go b/vendor/github.com/magiconair/properties/parser.go index 90f555cb..cdc4a803 100644 --- a/vendor/github.com/magiconair/properties/parser.go +++ b/vendor/github.com/magiconair/properties/parser.go @@ -1,4 +1,4 @@ -// Copyright 2017 Frank Schroeder. All rights reserved. +// Copyright 2018 Frank Schroeder. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. diff --git a/vendor/github.com/magiconair/properties/properties.go b/vendor/github.com/magiconair/properties/properties.go index 53f5b2ff..cb3d1a33 100644 --- a/vendor/github.com/magiconair/properties/properties.go +++ b/vendor/github.com/magiconair/properties/properties.go @@ -1,4 +1,4 @@ -// Copyright 2017 Frank Schroeder. All rights reserved. +// Copyright 2018 Frank Schroeder. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. @@ -83,6 +83,17 @@ func NewProperties() *Properties { } } +// Load reads a buffer into the given Properties struct. +func (p *Properties) Load(buf []byte, enc Encoding) error { + l := &Loader{Encoding: enc, DisableExpansion: p.DisableExpansion} + newProperties, err := l.LoadBytes(buf) + if err != nil { + return err + } + p.Merge(newProperties) + return nil +} + // Get returns the expanded value for the given key if exists. // Otherwise, ok is false. func (p *Properties) Get(key string) (value string, ok bool) { diff --git a/vendor/github.com/magiconair/properties/rangecheck.go b/vendor/github.com/magiconair/properties/rangecheck.go index 2e907d54..b013a2e5 100644 --- a/vendor/github.com/magiconair/properties/rangecheck.go +++ b/vendor/github.com/magiconair/properties/rangecheck.go @@ -1,4 +1,4 @@ -// Copyright 2017 Frank Schroeder. All rights reserved. +// Copyright 2018 Frank Schroeder. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. diff --git a/vendor/github.com/matterbridge/discordgo/user.go b/vendor/github.com/matterbridge/discordgo/user.go deleted file mode 100644 index a710f286..00000000 --- a/vendor/github.com/matterbridge/discordgo/user.go +++ /dev/null @@ -1,47 +0,0 @@ -package discordgo - -import ( - "fmt" - "strings" -) - -// A User stores all data for an individual Discord user. -type User struct { - ID string `json:"id"` - Email string `json:"email"` - Username string `json:"username"` - Avatar string `json:"avatar"` - Discriminator string `json:"discriminator"` - Token string `json:"token"` - Verified bool `json:"verified"` - MFAEnabled bool `json:"mfa_enabled"` - Bot bool `json:"bot"` -} - -// String returns a unique identifier of the form username#discriminator -func (u *User) String() string { - return fmt.Sprintf("%s#%s", u.Username, u.Discriminator) -} - -// Mention return a string which mentions the user -func (u *User) Mention() string { - return fmt.Sprintf("<@%s>", u.ID) -} - -// AvatarURL returns a URL to the user's avatar. -// size: The size of the user's avatar as a power of two -// if size is an empty string, no size parameter will -// be added to the URL. -func (u *User) AvatarURL(size string) string { - var URL string - if strings.HasPrefix(u.Avatar, "a_") { - URL = EndpointUserAvatarAnimated(u.ID, u.Avatar) - } else { - URL = EndpointUserAvatar(u.ID, u.Avatar) - } - - if size != "" { - return URL + "?size=" + size - } - return URL -} diff --git a/vendor/github.com/mattermost/platform/LICENSE.txt b/vendor/github.com/mattermost/mattermost-server/LICENSE.txt similarity index 99% rename from vendor/github.com/mattermost/platform/LICENSE.txt rename to vendor/github.com/mattermost/mattermost-server/LICENSE.txt index ead98cf0..76e4c455 100644 --- a/vendor/github.com/mattermost/platform/LICENSE.txt +++ b/vendor/github.com/mattermost/mattermost-server/LICENSE.txt @@ -12,7 +12,7 @@ You may be licensed to use source code to create compiled versions not produced 2. Under a commercial license available from Mattermost, Inc. by contacting commercial@mattermost.com You are licensed to use the source code in Admin Tools and Configuration Files (templates/, config/, model/, -webapp/client, webapp/fonts, webapp/i18n, webapp/images and all subdirectories thereof) under the Apache License v2.0. +plugin/ and all subdirectories thereof) under the Apache License v2.0. We promise that we will not enforce the copyleft provisions in AGPL v3.0 against you if your application (a) does not link to the Mattermost Platform directly, but exclusively uses the Mattermost Admin Tools and Configuration Files, and diff --git a/vendor/github.com/mattermost/mattermost-server/NOTICE.txt b/vendor/github.com/mattermost/mattermost-server/NOTICE.txt new file mode 100644 index 00000000..55141290 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/NOTICE.txt @@ -0,0 +1,3721 @@ +Mattermost Server +© 2015-present Mattermost, Inc. All Rights Reserved. See LICENSE.txt for license information. + +NOTICES: +-------- + +This document includes a list of open source components used in Mattermost Server, including those that have been modified. + +--- + +## NYTimes/gziphandler + +This product contains 'gziphandler' by The New York Times. + +Golang middleware to gzip HTTP responses + +* HOMEPAGE: + * https://github.com/NYTimes/gziphandler + +* LICENSE: Apache-2.0 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2016-2017 The New York Times Company + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- + +## avct/uasurfer + +This product contains 'uasurfer' by Avocet. + +Go package for fast and reliable abstraction of browser user agent strings. + +* HOMEPAGE: + * https://github.com/avct/uasurfer + +* LICENSE: Apache-2.0 + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + Copyright 2015 Avocet Systems Ltd. + http://avocet.io/opensource + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + Copyright 2015 Avocet Systems Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- + +## dgryski/dgoogauth + +This product contains 'dgoogauth' by Damian Gryski. + +Google Authenticator for Go + +* HOMEPAGE: + * https://github.com/dgryski/dgoogauth + +* LICENSE: Apache-2.0 + +Note: An original license file for this dependency is not available. We determined the type of license based on the official project website. The following text has been prepared using a template from the SPDX Workgroup (https://spdx.org) for this type of license. + +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright 2018 Damian Gryski + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--- + +## disintegration/imaging + +This product contains 'imaging' by Grigory Dryapak. + +Imaging is a simple image processing package for Go + +* HOMEPAGE: + * https://github.com/disintegration/imaging + +* LICENSE: MIT + +The MIT License (MIT) + +Copyright (c) 2012-2018 Grigory Dryapak + +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. + +--- + +## dyatlov/go-opengraph + +This product contains 'go-opengraph' by Vitaly Dyatlov. + +Golang package for parsing OpenGraph data from HTML into regular structures + +* HOMEPAGE: + * https://github.com/dyatlov/go-opengraph + +* LICENSE: MIT + +The MIT License (MIT) + +Copyright (c) 2015 Vitaly Dyatlov + +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. + +--- + +## fsnotify/fsnotify + +This product contains 'fsnotify' by GitHub user "fsnotify". + +Cross-platform file system notifications for Go. + +* HOMEPAGE: + * https://github.com/fsnotify/fsnotify + +* LICENSE: BSD-3-Clause + +Copyright (c) 2012 The Go Authors. All rights reserved. +Copyright (c) 2012 fsnotify Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +## go-ldap/ldap + +This product contains 'ldap' by GitHub user "go-ldap". + +Basic LDAP v3 functionality for the GO programming language. + +* HOMEPAGE: + * https://github.com/go-ldap/ldap + +* LICENSE: MIT + +The MIT License (MIT) + +Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com) +Portions copyright (c) 2015-2016 go-ldap Authors + +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. + +--- + +## go-redis/redis + +This product contains 'redis' by GitHub user "go-redis". + +Type-safe Redis client for Golang + +* HOMEPAGE: + * https://github.com/go-redis/redis + +* LICENSE: BSD-2-Clause + +Copyright (c) 2013 The github.com/go-redis/redis Authors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +## go-sql-driver/mysql + +This product contains 'mysql' by Go SQL Drivers. + +Go MySQL Driver is a MySQL driver for Go's (golang) database/sql package + +* HOMEPAGE: + * https://github.com/go-sql-driver/mysql + +* LICENSE: MPL-2.0 + +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + +--- + +## golang/freetype + +This product contains 'freetype' by Go. + +The Freetype font rasterizer in the Go programming language. + +* HOMEPAGE: + * https://github.com/golang/freetype + +* LICENSE: (FTL OR GPL-2.0) + +Use of the Freetype-Go software is subject to your choice of exactly one of +the following two licenses: + * The FreeType License, which is similar to the original BSD license with + an advertising clause, or + * The GNU General Public License (GPL), version 2 or later. + +The text of these licenses are available in the licenses/ftl.txt and the +licenses/gpl.txt files respectively. They are also available at +http://freetype.sourceforge.net/license.html + +The Luxi fonts in the testdata directory are licensed separately. See the +testdata/COPYING file for details. + +--- + +## gorilla/handlers + +This product contains 'handlers' by Gorilla web toolkit. + +A collection of useful handlers for Go's net/http package. + +* HOMEPAGE: + * https://github.com/gorilla/handlers + +* LICENSE: BSD-2-Clause + +Copyright (c) 2013 The Gorilla Handlers Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +## gorilla/mux + +This product contains 'mux' by Gorilla web toolkit. + +A powerful URL router and dispatcher for golang. + +* HOMEPAGE: + * https://github.com/gorilla/mux + +* LICENSE: BSD-3-Clause + +Copyright (c) 2012-2018 The Gorilla Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +## gorilla/schema + +This product contains 'schema' by Gorilla web toolkit. + +Package gorilla/schema fills a struct with form values. + +* HOMEPAGE: + * https://github.com/gorilla/schema + +* LICENSE: BSD-3-Clause + +Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +## gorilla/websocket + +This product contains 'websocket' by Gorilla web toolkit. + +A WebSocket implementation for Go. + +* HOMEPAGE: + * https://github.com/gorilla/websocket + +* LICENSE: BSD-2-Clause + +Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +## hako/durafmt + +This product contains 'durafmt' by Wesley Hill. + +:clock8: Better time duration formatting in Go! + +* HOMEPAGE: + * https://github.com/hako/durafmt + +* LICENSE: MIT + +The MIT License (MIT) + +Copyright (c) 2016 Wesley Hill + +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. + +--- + +## hashicorp/go-hclog + +This product contains 'go-hclog' by HashiCorp. + +A common logging package for HashiCorp tools + +* HOMEPAGE: + * https://github.com/hashicorp/go-hclog + +* LICENSE: MIT + +MIT License + +Copyright (c) 2017 HashiCorp + +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. + +--- + +## hashicorp/go-plugin + +This product contains 'go-plugin' by HashiCorp. + +Golang plugin system over RPC. + +* HOMEPAGE: + * https://github.com/hashicorp/go-plugin + +* LICENSE: MPL-2.0 + +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. “Contributor” + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. “Contributor Version” + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor’s Contribution. + +1.3. “Contribution” + + means Covered Software of a particular Contributor. + +1.4. “Covered Software” + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. “Incompatible With Secondary Licenses” + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of version + 1.1 or earlier of the License, but not also under the terms of a + Secondary License. + +1.6. “Executable Form” + + means any form of the work other than Source Code Form. + +1.7. “Larger Work” + + means a work that combines Covered Software with other material, in a separate + file or files, that is not Covered Software. + +1.8. “License” + + means this document. + +1.9. “Licensable” + + means having the right to grant, to the maximum extent possible, whether at the + time of the initial grant or subsequently, any and all of the rights conveyed by + this License. + +1.10. “Modifications” + + means any of the following: + + a. any file in Source Code Form that results from an addition to, deletion + from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. “Patent Claims” of a Contributor + + means any patent claim(s), including without limitation, method, process, + and apparatus claims, in any patent Licensable by such Contributor that + would be infringed, but for the grant of the License, by the making, + using, selling, offering for sale, having made, import, or transfer of + either its Contributions or its Contributor Version. + +1.12. “Secondary License” + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. “Source Code Form” + + means the form of the work preferred for making modifications. + +1.14. “You” (or “Your”) + + means an individual or a legal entity exercising rights under this + License. For legal entities, “You” includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, “control” means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or as + part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its Contributions + or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution become + effective for each Contribution on the date the Contributor first distributes + such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under this + License. No additional rights or licenses will be implied from the distribution + or licensing of Covered Software under this License. Notwithstanding Section + 2.1(b) above, no patent license is granted by a Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party’s + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of its + Contributions. + + This License does not grant any rights in the trademarks, service marks, or + logos of any Contributor (except as may be necessary to comply with the + notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this License + (see Section 10.2) or under the terms of a Secondary License (if permitted + under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its Contributions + are its original creation(s) or it has sufficient rights to grant the + rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under applicable + copyright doctrines of fair use, fair dealing, or other equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under the + terms of this License. You must inform recipients that the Source Code Form + of the Covered Software is governed by the terms of this License, and how + they can obtain a copy of this License. You may not attempt to alter or + restrict the recipients’ rights in the Source Code Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this License, + or sublicense it under different terms, provided that the license for + the Executable Form does not attempt to limit or alter the recipients’ + rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for the + Covered Software. If the Larger Work is a combination of Covered Software + with a work governed by one or more Secondary Licenses, and the Covered + Software is not Incompatible With Secondary Licenses, this License permits + You to additionally distribute such Covered Software under the terms of + such Secondary License(s), so that the recipient of the Larger Work may, at + their option, further distribute the Covered Software under the terms of + either this License or such Secondary License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices (including + copyright notices, patent notices, disclaimers of warranty, or limitations + of liability) contained within the Source Code Form of the Covered + Software, except that You may alter any license notices to the extent + required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on behalf + of any Contributor. You must make it absolutely clear that any such + warranty, support, indemnity, or liability obligation is offered by You + alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, judicial + order, or regulation then You must: (a) comply with the terms of this License + to the maximum extent possible; and (b) describe the limitations and the code + they affect. Such description must be placed in a text file included with all + distributions of the Covered Software under this License. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing basis, + if such Contributor fails to notify You of the non-compliance by some + reasonable means prior to 60 days after You have come back into compliance. + Moreover, Your grants from a particular Contributor are reinstated on an + ongoing basis if such Contributor notifies You of the non-compliance by + some reasonable means, this is the first time You have received notice of + non-compliance with this License from such Contributor, and You become + compliant prior to 30 days after Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, counter-claims, + and cross-claims) alleging that a Contributor Version directly or + indirectly infringes any patent, then the rights granted to You by any and + all Contributors for the Covered Software under Section 2.1 of this License + shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an “as is” basis, without + warranty of any kind, either expressed, implied, or statutory, including, + without limitation, warranties that the Covered Software is free of defects, + merchantable, fit for a particular purpose or non-infringing. The entire + risk as to the quality and performance of the Covered Software is with You. + Should any Covered Software prove defective in any respect, You (not any + Contributor) assume the cost of any necessary servicing, repair, or + correction. This disclaimer of warranty constitutes an essential part of this + License. No use of any Covered Software is authorized under this License + except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from such + party’s negligence to the extent applicable law prohibits such limitation. + Some jurisdictions do not allow the exclusion or limitation of incidental or + consequential damages, so this exclusion and limitation may not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts of + a jurisdiction where the defendant maintains its principal place of business + and such litigation shall be governed by laws of that jurisdiction, without + reference to its conflict-of-law provisions. Nothing in this Section shall + prevent a party’s ability to bring cross-claims or counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject matter + hereof. If any provision of this License is held to be unenforceable, such + provision shall be reformed only to the extent necessary to make it + enforceable. Any law or regulation which provides that the language of a + contract shall be construed against the drafter shall not be used to construe + this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version of + the License under which You originally received the Covered Software, or + under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a modified + version of this License if you rename the license and remove any + references to the name of the license steward (except to note that such + modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + If You choose to distribute Source Code Form that is Incompatible With + Secondary Licenses under the terms of this version of the License, the + notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, then +You may include the notice in a location (such as a LICENSE file in a relevant +directory) where a recipient would be likely to look for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - “Incompatible With Secondary Licenses” Notice + + This Source Code Form is “Incompatible + With Secondary Licenses”, as defined by + the Mozilla Public License, v. 2.0. + +--- + +## hashicorp/memberlist + +This product contains 'memberlist' by HashiCorp. + +Golang package for gossip based membership and failure detection + +* HOMEPAGE: + * https://github.com/hashicorp/memberlist + +* LICENSE: MPL-2.0 + +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. “Contributor” + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. “Contributor Version” + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor’s Contribution. + +1.3. “Contribution” + + means Covered Software of a particular Contributor. + +1.4. “Covered Software” + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. “Incompatible With Secondary Licenses” + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of version + 1.1 or earlier of the License, but not also under the terms of a + Secondary License. + +1.6. “Executable Form” + + means any form of the work other than Source Code Form. + +1.7. “Larger Work” + + means a work that combines Covered Software with other material, in a separate + file or files, that is not Covered Software. + +1.8. “License” + + means this document. + +1.9. “Licensable” + + means having the right to grant, to the maximum extent possible, whether at the + time of the initial grant or subsequently, any and all of the rights conveyed by + this License. + +1.10. “Modifications” + + means any of the following: + + a. any file in Source Code Form that results from an addition to, deletion + from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. “Patent Claims” of a Contributor + + means any patent claim(s), including without limitation, method, process, + and apparatus claims, in any patent Licensable by such Contributor that + would be infringed, but for the grant of the License, by the making, + using, selling, offering for sale, having made, import, or transfer of + either its Contributions or its Contributor Version. + +1.12. “Secondary License” + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. “Source Code Form” + + means the form of the work preferred for making modifications. + +1.14. “You” (or “Your”) + + means an individual or a legal entity exercising rights under this + License. For legal entities, “You” includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, “control” means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or as + part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its Contributions + or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution become + effective for each Contribution on the date the Contributor first distributes + such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under this + License. No additional rights or licenses will be implied from the distribution + or licensing of Covered Software under this License. Notwithstanding Section + 2.1(b) above, no patent license is granted by a Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party’s + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of its + Contributions. + + This License does not grant any rights in the trademarks, service marks, or + logos of any Contributor (except as may be necessary to comply with the + notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this License + (see Section 10.2) or under the terms of a Secondary License (if permitted + under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its Contributions + are its original creation(s) or it has sufficient rights to grant the + rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under applicable + copyright doctrines of fair use, fair dealing, or other equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under the + terms of this License. You must inform recipients that the Source Code Form + of the Covered Software is governed by the terms of this License, and how + they can obtain a copy of this License. You may not attempt to alter or + restrict the recipients’ rights in the Source Code Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this License, + or sublicense it under different terms, provided that the license for + the Executable Form does not attempt to limit or alter the recipients’ + rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for the + Covered Software. If the Larger Work is a combination of Covered Software + with a work governed by one or more Secondary Licenses, and the Covered + Software is not Incompatible With Secondary Licenses, this License permits + You to additionally distribute such Covered Software under the terms of + such Secondary License(s), so that the recipient of the Larger Work may, at + their option, further distribute the Covered Software under the terms of + either this License or such Secondary License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices (including + copyright notices, patent notices, disclaimers of warranty, or limitations + of liability) contained within the Source Code Form of the Covered + Software, except that You may alter any license notices to the extent + required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on behalf + of any Contributor. You must make it absolutely clear that any such + warranty, support, indemnity, or liability obligation is offered by You + alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, judicial + order, or regulation then You must: (a) comply with the terms of this License + to the maximum extent possible; and (b) describe the limitations and the code + they affect. Such description must be placed in a text file included with all + distributions of the Covered Software under this License. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing basis, + if such Contributor fails to notify You of the non-compliance by some + reasonable means prior to 60 days after You have come back into compliance. + Moreover, Your grants from a particular Contributor are reinstated on an + ongoing basis if such Contributor notifies You of the non-compliance by + some reasonable means, this is the first time You have received notice of + non-compliance with this License from such Contributor, and You become + compliant prior to 30 days after Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, counter-claims, + and cross-claims) alleging that a Contributor Version directly or + indirectly infringes any patent, then the rights granted to You by any and + all Contributors for the Covered Software under Section 2.1 of this License + shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an “as is” basis, without + warranty of any kind, either expressed, implied, or statutory, including, + without limitation, warranties that the Covered Software is free of defects, + merchantable, fit for a particular purpose or non-infringing. The entire + risk as to the quality and performance of the Covered Software is with You. + Should any Covered Software prove defective in any respect, You (not any + Contributor) assume the cost of any necessary servicing, repair, or + correction. This disclaimer of warranty constitutes an essential part of this + License. No use of any Covered Software is authorized under this License + except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from such + party’s negligence to the extent applicable law prohibits such limitation. + Some jurisdictions do not allow the exclusion or limitation of incidental or + consequential damages, so this exclusion and limitation may not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts of + a jurisdiction where the defendant maintains its principal place of business + and such litigation shall be governed by laws of that jurisdiction, without + reference to its conflict-of-law provisions. Nothing in this Section shall + prevent a party’s ability to bring cross-claims or counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject matter + hereof. If any provision of this License is held to be unenforceable, such + provision shall be reformed only to the extent necessary to make it + enforceable. Any law or regulation which provides that the language of a + contract shall be construed against the drafter shall not be used to construe + this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version of + the License under which You originally received the Covered Software, or + under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a modified + version of this License if you rename the license and remove any + references to the name of the license steward (except to note that such + modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + If You choose to distribute Source Code Form that is Incompatible With + Secondary Licenses under the terms of this version of the License, the + notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, then +You may include the notice in a location (such as a LICENSE file in a relevant +directory) where a recipient would be likely to look for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - “Incompatible With Secondary Licenses” Notice + + This Source Code Form is “Incompatible + With Secondary Licenses”, as defined by + the Mozilla Public License, v. 2.0. + +--- + +## icrowley/fake + +This product contains 'fake' by GitHub user "icrowley". + +Fake data generator for Go (Golang) + +* HOMEPAGE: + * https://github.com/icrowley/fake + +* LICENSE: MIT + +The MIT License (MIT) + +Copyright (c) 2014 Dmitry Afanasyev + +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. + +--- + +## jaytaylor/html2text + +This product contains 'html2text' by J. Elliot Taylor. + +Golang HTML to plaintext conversion library + +* HOMEPAGE: + * https://github.com/jaytaylor/html2text + +* LICENSE: MIT + +The MIT License (MIT) + +Copyright (c) 2015 Jay Taylor + +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. + +--- + +## lib/pq + +This product contains 'pq' by GitHub user "lib". + +Pure Go Postgres driver for database/sql + +* HOMEPAGE: + * https://github.com/lib/pq + +* LICENSE: MIT + +Copyright (c) 2011-2013, 'pq' Contributors +Portions Copyright (C) 2011 Blake Mizerany + +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. + +--- + +## mattermost/gorp + +This product contains 'gorp' by Mattermost (forked from original GitHub repo 'go-gorp/gorp' owned by GitHub user "go-gorp"). + +Go Relational Persistence - an ORM-ish library for Go + +* HOMEPAGE: + * https://github.com/mattermost/gorp + +* LICENSE: MIT + +(The MIT License) + +Copyright (c) 2012 James Cooper + +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. + +--- + +## mattermost/rsc + +This product contains 'rsc' by Mattermost (forked from original GitHub repo 'petar/rsc' owned by Petar Maymounkov). + +fork of Russ Cox's code.google.com/p/rsc + +* HOMEPAGE: + * https://github.com/mattermost/rsc + +* LICENSE: BSD-3-Clause + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +## mattermost/viper + +This product contains 'viper' by Mattermost (forked from original GitHub repo 'spf13/viper' owned by Steve Francia). + +Go configuration with fangs + +* HOMEPAGE: + * https://github.com/mattermost/viper + +* LICENSE: MIT + +The MIT License (MIT) + +Copyright (c) 2014 Steve Francia + +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. + +--- + +## minio/minio-go + +This product contains 'minio-go' by Minio Cloud Storage. + +Minio Client SDK for Go + +* HOMEPAGE: + * https://github.com/minio/minio-go + +* LICENSE: Apache-2.0 + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +* This package includes the following NOTICE: + +minio-go +Copyright 2015-2017 Minio, Inc. + +--- + +## nicksnyder/go-i18n + +This product contains 'go-i18n' by Nick Snyder. + +Translate your Go program into multiple languages. + +* HOMEPAGE: + * https://github.com/nicksnyder/go-i18n + +* LICENSE: MIT + +Copyright (c) 2014 Nick Snyder https://github.com/nicksnyder + +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. + +--- + +## pborman/uuid + +This product contains 'uuid' by GitHub user "pborman". + +Automatically exported from code.google.com/p/go-uuid + +* HOMEPAGE: + * https://github.com/pborman/uuid + +* LICENSE: BSD-3-Clause + +Copyright (c) 2009,2014 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +## pkg/errors + +This product contains 'errors' by GitHub user "pkg". + +Simple error handling primitives + +* HOMEPAGE: + * https://github.com/pkg/errors + +* LICENSE: BSD-2-Clause + +Copyright (c) 2015, Dave Cheney +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +## prometheus/client_golang + +This product contains 'client_golang' by Prometheus. + +Prometheus instrumentation library for Go applications + +* HOMEPAGE: + * https://github.com/prometheus/client_golang + +* LICENSE: Apache-2.0 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +* This package includes the following NOTICE: + +Prometheus instrumentation library for Go applications +Copyright 2012-2015 The Prometheus Authors + +This product includes software developed at +SoundCloud Ltd. (http://soundcloud.com/). + + +The following components are included in this product: + +perks - a fork of https://github.com/bmizerany/perks +https://github.com/beorn7/perks +Copyright 2013-2015 Blake Mizerany, Björn Rabenstein +See https://github.com/beorn7/perks/blob/master/README.md for license details. + +Go support for Protocol Buffers - Google's data interchange format +http://github.com/golang/protobuf/ +Copyright 2010 The Go Authors +See source code for license details. + +Support for streaming Protocol Buffer messages for the Go language (golang). +https://github.com/matttproud/golang_protobuf_extensions +Copyright 2013 Matt T. Proud +Licensed under the Apache License, Version 2.0 + +--- + +## rs/cors + +This product contains 'cors' by Olivier Poitrey. + +Go net/http configurable handler to handle CORS requests + +* HOMEPAGE: + * https://github.com/rs/cors + +* LICENSE: MIT + +Copyright (c) 2014 Olivier Poitrey + +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. + +--- + +## rwcarlsen/goexif + +This product contains 'goexif' by Robert Carlsen. + +Decode embedded EXIF meta data from image files. + +* HOMEPAGE: + * https://github.com/rwcarlsen/goexif + +* LICENSE: BSD-2-Clause + + +Copyright (c) 2012, Robert Carlsen & Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +## segmentio/analytics-go + +This product contains 'analytics-go' by Segment. + +Segment analytics client for Go + +* HOMEPAGE: + * https://github.com/segmentio/analytics-go + +* LICENSE: MIT + +The MIT License (MIT) + +Copyright (c) 2016 Segment, Inc. + +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. + +--- + +## spf13/cobra + +This product contains 'cobra' by Steve Francia. + +A Commander for modern Go CLI interactions + +* HOMEPAGE: + * https://github.com/spf13/cobra + +* LICENSE: Apache-2.0 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +--- + +## stretchr/testify + +This product contains 'testify' by Stretchr, Inc.. + +A toolkit with common assertions and mocks that plays nicely with the standard library + +* HOMEPAGE: + * https://github.com/stretchr/testify + +* LICENSE: MIT + +Copyright (c) 2012 - 2013 Mat Ryer and Tyler Bunnell + +Please consider promoting this project if you find it useful. + +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. + +--- + +## throttled/throttled + +This product contains 'throttled' by GitHub user "throttled". + +Package throttled implements rate limiting access to resources such as HTTP endpoints. + +* HOMEPAGE: + * https://github.com/throttled/throttled + +* LICENSE: BSD-3-Clause + +Copyright (c) 2018, Martin Angers and Contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +## tylerb/graceful + +This product contains 'graceful' by Tyler Stillwater. + +Graceful is a Go package enabling graceful shutdown of an http.Handler server. + +* HOMEPAGE: + * https://github.com/tylerb/graceful + +* LICENSE: MIT + +The MIT License (MIT) + +Copyright (c) 2014 Tyler Bunnell + +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. + +--- + +## zap + +This product contains 'zap' by Uber Technologies, Inc.. + +Blazing fast, structured, leveled logging in Go. + +* HOMEPAGE: + * https://github.com/uber-go/zap + +* LICENSE: MIT + +Copyright (c) 2016-2017 Uber Technologies, Inc. + +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. + +--- + +## x/crypto + +This product contains 'crypto' by The Go Authors. + +[mirror] Go supplementary cryptography libraries + +* HOMEPAGE: + * https://github.com/golang/crypto + +* LICENSE: BSD-3-Clause + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +## x/image + +This product contains 'image' by The Go Authors. + +[mirror] Go supplementary image libraries + +* HOMEPAGE: + * https://github.com/golang/image + +* LICENSE: BSD-3-Clause + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +## x/net + +This product contains 'net' by The Go Authors. + +[mirror] Go supplementary network libraries + +* HOMEPAGE: + * https://github.com/golang/net + +* LICENSE: BSD-3-Clause + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +## go-gomail/gomail + +This product contains 'gomail' by Gomail. + +The best way to send emails in Go. + +* HOMEPAGE: + * https://github.com/go-gomail/gomail + +* LICENSE: MIT + +The MIT License (MIT) + +Copyright (c) 2014 Alexandre Cesaro + +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. + +--- + +## natefinch/lumberjack + +This product contains 'lumberjack' by Nate Finch. + +lumberjack is a log rolling package for Go + +* HOMEPAGE: + * https://github.com/natefinch/lumberjack + +* LICENSE: MIT + +The MIT License (MIT) + +Copyright (c) 2014 Nate Finch + +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. + +--- + +## olivere/elastic + +This product contains 'elastic' by Oliver Eilhard. + +Elasticsearch client for Go. + +* HOMEPAGE: + * https://github.com/olivere/elastic + +* LICENSE: MIT + +The MIT License (MIT) +Copyright © 2012-2015 Oliver Eilhard + +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. + +--- + +## go-yaml/yaml + +This product contains 'yaml' by GitHub user "go-yaml". + +YAML support for the Go language. + +* HOMEPAGE: + * https://github.com/go-yaml/yaml + +* LICENSE: Apache-2.0 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +* This package includes the following NOTICE: + +Copyright 2011-2016 Canonical Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vendor/github.com/mattermost/mattermost-server/mlog/default.go b/vendor/github.com/mattermost/mattermost-server/mlog/default.go new file mode 100644 index 00000000..366d22f8 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/mlog/default.go @@ -0,0 +1,50 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package mlog + +import ( + "encoding/json" + "fmt" +) + +// defaultLog manually encodes the log to STDOUT, providing a basic, default logging implementation +// before mlog is fully configured. +func defaultLog(level, msg string, fields ...Field) { + log := struct { + Level string `json:"level"` + Message string `json:"msg"` + Fields []Field `json:"fields,omitempty"` + }{ + level, + msg, + fields, + } + + if b, err := json.Marshal(log); err != nil { + fmt.Printf(`{"level":"error","msg":"failed to encode log message"}%s`, "\n") + } else { + fmt.Printf("%s\n", b) + } +} + +func defaultDebugLog(msg string, fields ...Field) { + defaultLog("debug", msg, fields...) +} + +func defaultInfoLog(msg string, fields ...Field) { + defaultLog("info", msg, fields...) +} + +func defaultWarnLog(msg string, fields ...Field) { + defaultLog("warn", msg, fields...) +} + +func defaultErrorLog(msg string, fields ...Field) { + defaultLog("error", msg, fields...) +} + +func defaultCriticalLog(msg string, fields ...Field) { + // We map critical to error in zap, so be consistent. + defaultLog("error", msg, fields...) +} diff --git a/vendor/github.com/mattermost/mattermost-server/mlog/global.go b/vendor/github.com/mattermost/mattermost-server/mlog/global.go new file mode 100644 index 00000000..ba90ace2 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/mlog/global.go @@ -0,0 +1,44 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package mlog + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var globalLogger *Logger + +func InitGlobalLogger(logger *Logger) { + glob := *logger + glob.zap = glob.zap.WithOptions(zap.AddCallerSkip(1)) + globalLogger = &glob + Debug = globalLogger.Debug + Info = globalLogger.Info + Warn = globalLogger.Warn + Error = globalLogger.Error + Critical = globalLogger.Critical +} + +func RedirectStdLog(logger *Logger) { + zap.RedirectStdLogAt(logger.zap.With(zap.String("source", "stdlog")).WithOptions(zap.AddCallerSkip(-2)), zapcore.ErrorLevel) +} + +type LogFunc func(string, ...Field) + +// DON'T USE THIS Modify the level on the app logger +func GloballyDisableDebugLogForTest() { + globalLogger.consoleLevel.SetLevel(zapcore.ErrorLevel) +} + +// DON'T USE THIS Modify the level on the app logger +func GloballyEnableDebugLogForTest() { + globalLogger.consoleLevel.SetLevel(zapcore.DebugLevel) +} + +var Debug LogFunc = defaultDebugLog +var Info LogFunc = defaultInfoLog +var Warn LogFunc = defaultWarnLog +var Error LogFunc = defaultErrorLog +var Critical LogFunc = defaultCriticalLog diff --git a/vendor/github.com/mattermost/mattermost-server/mlog/log.go b/vendor/github.com/mattermost/mattermost-server/mlog/log.go new file mode 100644 index 00000000..e3bc38d8 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/mlog/log.go @@ -0,0 +1,173 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package mlog + +import ( + "io" + "log" + "os" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" +) + +const ( + // Very verbose messages for debugging specific issues + LevelDebug = "debug" + // Default log level, informational + LevelInfo = "info" + // Warnings are messages about possible issues + LevelWarn = "warn" + // Errors are messages about things we know are problems + LevelError = "error" +) + +// Type and function aliases from zap to limit the libraries scope into MM code +type Field = zapcore.Field + +var Int64 = zap.Int64 +var Int = zap.Int +var Uint32 = zap.Uint32 +var String = zap.String +var Any = zap.Any +var Err = zap.Error +var Bool = zap.Bool + +type LoggerConfiguration struct { + EnableConsole bool + ConsoleJson bool + ConsoleLevel string + EnableFile bool + FileJson bool + FileLevel string + FileLocation string +} + +type Logger struct { + zap *zap.Logger + consoleLevel zap.AtomicLevel + fileLevel zap.AtomicLevel +} + +func getZapLevel(level string) zapcore.Level { + switch level { + case LevelInfo: + return zapcore.InfoLevel + case LevelWarn: + return zapcore.WarnLevel + case LevelDebug: + return zapcore.DebugLevel + case LevelError: + return zapcore.ErrorLevel + default: + return zapcore.InfoLevel + } +} + +func makeEncoder(json bool) zapcore.Encoder { + encoderConfig := zap.NewProductionEncoderConfig() + if json { + return zapcore.NewJSONEncoder(encoderConfig) + } + + encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + return zapcore.NewConsoleEncoder(encoderConfig) +} + +func NewLogger(config *LoggerConfiguration) *Logger { + cores := []zapcore.Core{} + logger := &Logger{ + consoleLevel: zap.NewAtomicLevelAt(getZapLevel(config.ConsoleLevel)), + fileLevel: zap.NewAtomicLevelAt(getZapLevel(config.FileLevel)), + } + + if config.EnableConsole { + writer := zapcore.Lock(os.Stdout) + core := zapcore.NewCore(makeEncoder(config.ConsoleJson), writer, logger.consoleLevel) + cores = append(cores, core) + } + + if config.EnableFile { + writer := zapcore.AddSync(&lumberjack.Logger{ + Filename: config.FileLocation, + MaxSize: 100, + Compress: true, + }) + core := zapcore.NewCore(makeEncoder(config.FileJson), writer, logger.fileLevel) + cores = append(cores, core) + } + + combinedCore := zapcore.NewTee(cores...) + + logger.zap = zap.New(combinedCore, + zap.AddCallerSkip(1), + zap.AddCaller(), + ) + + return logger +} + +func (l *Logger) ChangeLevels(config *LoggerConfiguration) { + l.consoleLevel.SetLevel(getZapLevel(config.ConsoleLevel)) + l.fileLevel.SetLevel(getZapLevel(config.FileLevel)) +} + +func (l *Logger) SetConsoleLevel(level string) { + l.consoleLevel.SetLevel(getZapLevel(level)) +} + +func (l *Logger) With(fields ...Field) *Logger { + newlogger := *l + newlogger.zap = newlogger.zap.With(fields...) + return &newlogger +} + +func (l *Logger) StdLog(fields ...Field) *log.Logger { + return zap.NewStdLog(l.With(fields...).zap.WithOptions(getStdLogOption())) +} + +// StdLogWriter returns a writer that can be hooked up to the output of a golang standard logger +// anything written will be interpreted as log entries accordingly +func (l *Logger) StdLogWriter() io.Writer { + newLogger := *l + newLogger.zap = newLogger.zap.WithOptions(zap.AddCallerSkip(4), getStdLogOption()) + f := newLogger.Info + return &loggerWriter{f} +} + +func (l *Logger) WithCallerSkip(skip int) *Logger { + newlogger := *l + newlogger.zap = newlogger.zap.WithOptions(zap.AddCallerSkip(skip)) + return &newlogger +} + +// Made for the plugin interface, wraps mlog in a simpler interface +// at the cost of performance +func (l *Logger) Sugar() *SugarLogger { + return &SugarLogger{ + wrappedLogger: l, + zapSugar: l.zap.Sugar(), + } +} + +func (l *Logger) Debug(message string, fields ...Field) { + l.zap.Debug(message, fields...) +} + +func (l *Logger) Info(message string, fields ...Field) { + l.zap.Info(message, fields...) +} + +func (l *Logger) Warn(message string, fields ...Field) { + l.zap.Warn(message, fields...) +} + +func (l *Logger) Error(message string, fields ...Field) { + l.zap.Error(message, fields...) +} + +func (l *Logger) Critical(message string, fields ...Field) { + l.zap.Error(message, fields...) +} diff --git a/vendor/github.com/mattermost/mattermost-server/mlog/stdlog.go b/vendor/github.com/mattermost/mattermost-server/mlog/stdlog.go new file mode 100644 index 00000000..7839ddfa --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/mlog/stdlog.go @@ -0,0 +1,87 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package mlog + +import ( + "bytes" + "strings" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// Implementation of zapcore.Core to interpret log messages from a standard logger +// and translate the levels to zapcore levels. +type stdLogLevelInterpreterCore struct { + wrappedCore zapcore.Core +} + +func stdLogInterpretZapEntry(entry zapcore.Entry) zapcore.Entry { + message := entry.Message + if strings.Index(message, "[DEBUG]") == 0 { + entry.Level = zapcore.DebugLevel + entry.Message = message[7:] + } else if strings.Index(message, "[DEBG]") == 0 { + entry.Level = zapcore.DebugLevel + entry.Message = message[6:] + } else if strings.Index(message, "[WARN]") == 0 { + entry.Level = zapcore.WarnLevel + entry.Message = message[6:] + } else if strings.Index(message, "[ERROR]") == 0 { + entry.Level = zapcore.ErrorLevel + entry.Message = message[7:] + } else if strings.Index(message, "[EROR]") == 0 { + entry.Level = zapcore.ErrorLevel + entry.Message = message[6:] + } else if strings.Index(message, "[ERR]") == 0 { + entry.Level = zapcore.ErrorLevel + entry.Message = message[5:] + } else if strings.Index(message, "[INFO]") == 0 { + entry.Level = zapcore.InfoLevel + entry.Message = message[6:] + } + return entry +} + +func (s *stdLogLevelInterpreterCore) Enabled(lvl zapcore.Level) bool { + return s.wrappedCore.Enabled(lvl) +} + +func (s *stdLogLevelInterpreterCore) With(fields []zapcore.Field) zapcore.Core { + return s.wrappedCore.With(fields) +} + +func (s *stdLogLevelInterpreterCore) Check(entry zapcore.Entry, checkedEntry *zapcore.CheckedEntry) *zapcore.CheckedEntry { + entry = stdLogInterpretZapEntry(entry) + return s.wrappedCore.Check(entry, checkedEntry) +} + +func (s *stdLogLevelInterpreterCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { + entry = stdLogInterpretZapEntry(entry) + return s.wrappedCore.Write(entry, fields) +} + +func (s *stdLogLevelInterpreterCore) Sync() error { + return s.wrappedCore.Sync() +} + +func getStdLogOption() zap.Option { + return zap.WrapCore( + func(core zapcore.Core) zapcore.Core { + return &stdLogLevelInterpreterCore{core} + }, + ) +} + +type loggerWriter struct { + logFunc func(msg string, fields ...Field) +} + +func (l *loggerWriter) Write(p []byte) (int, error) { + trimmed := string(bytes.TrimSpace(p)) + for _, line := range strings.Split(trimmed, "\n") { + l.logFunc(string(line)) + } + return len(p), nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/mlog/sugar.go b/vendor/github.com/mattermost/mattermost-server/mlog/sugar.go new file mode 100644 index 00000000..c00a8bbf --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/mlog/sugar.go @@ -0,0 +1,28 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package mlog + +import "go.uber.org/zap" + +// Made for the plugin interface, use the regular logger for other uses +type SugarLogger struct { + wrappedLogger *Logger + zapSugar *zap.SugaredLogger +} + +func (l *SugarLogger) Debug(msg string, keyValuePairs ...interface{}) { + l.zapSugar.Debugw(msg, keyValuePairs...) +} + +func (l *SugarLogger) Info(msg string, keyValuePairs ...interface{}) { + l.zapSugar.Infow(msg, keyValuePairs...) +} + +func (l *SugarLogger) Error(msg string, keyValuePairs ...interface{}) { + l.zapSugar.Errorw(msg, keyValuePairs...) +} + +func (l *SugarLogger) Warn(msg string, keyValuePairs ...interface{}) { + l.zapSugar.Warnw(msg, keyValuePairs...) +} diff --git a/vendor/github.com/mattermost/platform/model/access.go b/vendor/github.com/mattermost/mattermost-server/model/access.go similarity index 83% rename from vendor/github.com/mattermost/platform/model/access.go rename to vendor/github.com/mattermost/mattermost-server/model/access.go index 551ef930..e9603c78 100644 --- a/vendor/github.com/mattermost/platform/model/access.go +++ b/vendor/github.com/mattermost/mattermost-server/model/access.go @@ -74,41 +74,23 @@ func (me *AccessData) IsExpired() bool { } func (ad *AccessData) ToJson() string { - b, err := json.Marshal(ad) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(ad) + return string(b) } func AccessDataFromJson(data io.Reader) *AccessData { - decoder := json.NewDecoder(data) - var ad AccessData - err := decoder.Decode(&ad) - if err == nil { - return &ad - } else { - return nil - } + var ad *AccessData + json.NewDecoder(data).Decode(&ad) + return ad } func (ar *AccessResponse) ToJson() string { - b, err := json.Marshal(ar) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(ar) + return string(b) } func AccessResponseFromJson(data io.Reader) *AccessResponse { - decoder := json.NewDecoder(data) - var ar AccessResponse - err := decoder.Decode(&ar) - if err == nil { - return &ar - } else { - return nil - } + var ar *AccessResponse + json.NewDecoder(data).Decode(&ar) + return ar } diff --git a/vendor/github.com/mattermost/platform/model/analytics_row.go b/vendor/github.com/mattermost/mattermost-server/model/analytics_row.go similarity index 63% rename from vendor/github.com/mattermost/platform/model/analytics_row.go rename to vendor/github.com/mattermost/mattermost-server/model/analytics_row.go index 5493163d..4615bb79 100644 --- a/vendor/github.com/mattermost/platform/model/analytics_row.go +++ b/vendor/github.com/mattermost/mattermost-server/model/analytics_row.go @@ -16,23 +16,14 @@ type AnalyticsRow struct { type AnalyticsRows []*AnalyticsRow func (me *AnalyticsRow) ToJson() string { - b, err := json.Marshal(me) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(me) + return string(b) } func AnalyticsRowFromJson(data io.Reader) *AnalyticsRow { - decoder := json.NewDecoder(data) - var me AnalyticsRow - err := decoder.Decode(&me) - if err == nil { - return &me - } else { - return nil - } + var me *AnalyticsRow + json.NewDecoder(data).Decode(&me) + return me } func (me AnalyticsRows) ToJson() string { @@ -44,12 +35,7 @@ func (me AnalyticsRows) ToJson() string { } func AnalyticsRowsFromJson(data io.Reader) AnalyticsRows { - decoder := json.NewDecoder(data) var me AnalyticsRows - err := decoder.Decode(&me) - if err == nil { - return me - } else { - return nil - } + json.NewDecoder(data).Decode(&me) + return me } diff --git a/vendor/github.com/mattermost/platform/model/audit.go b/vendor/github.com/mattermost/mattermost-server/model/audit.go similarity index 70% rename from vendor/github.com/mattermost/platform/model/audit.go rename to vendor/github.com/mattermost/mattermost-server/model/audit.go index 7699cf93..e3d1bdf9 100644 --- a/vendor/github.com/mattermost/platform/model/audit.go +++ b/vendor/github.com/mattermost/mattermost-server/model/audit.go @@ -19,21 +19,12 @@ type Audit struct { } func (o *Audit) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func AuditFromJson(data io.Reader) *Audit { - decoder := json.NewDecoder(data) - var o Audit - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *Audit + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/platform/model/audits.go b/vendor/github.com/mattermost/mattermost-server/model/audits.go similarity index 82% rename from vendor/github.com/mattermost/platform/model/audits.go rename to vendor/github.com/mattermost/mattermost-server/model/audits.go index e57e2a3a..3673eb61 100644 --- a/vendor/github.com/mattermost/platform/model/audits.go +++ b/vendor/github.com/mattermost/mattermost-server/model/audits.go @@ -28,12 +28,7 @@ func (o Audits) ToJson() string { } func AuditsFromJson(data io.Reader) Audits { - decoder := json.NewDecoder(data) var o Audits - err := decoder.Decode(&o) - if err == nil { - return o - } else { - return nil - } + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/platform/model/authorize.go b/vendor/github.com/mattermost/mattermost-server/model/authorize.go similarity index 86% rename from vendor/github.com/mattermost/platform/model/authorize.go rename to vendor/github.com/mattermost/mattermost-server/model/authorize.go index df07ff14..922adc07 100644 --- a/vendor/github.com/mattermost/platform/model/authorize.go +++ b/vendor/github.com/mattermost/mattermost-server/model/authorize.go @@ -12,6 +12,7 @@ import ( const ( AUTHCODE_EXPIRE_TIME = 60 * 10 // 10 minutes AUTHCODE_RESPONSE_TYPE = "code" + IMPLICIT_RESPONSE_TYPE = "token" DEFAULT_SCOPE = "user" ) @@ -58,11 +59,11 @@ func (ad *AuthData) IsValid() *AppError { return NewAppError("AuthData.IsValid", "model.authorize.is_valid.create_at.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest) } - if len(ad.RedirectUri) == 0 || len(ad.RedirectUri) > 256 || !IsValidHttpUrl(ad.RedirectUri) { + if len(ad.RedirectUri) > 256 || !IsValidHttpUrl(ad.RedirectUri) { return NewAppError("AuthData.IsValid", "model.authorize.is_valid.redirect_uri.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest) } - if len(ad.State) > 128 { + if len(ad.State) > 1024 { return NewAppError("AuthData.IsValid", "model.authorize.is_valid.state.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest) } @@ -89,7 +90,7 @@ func (ar *AuthorizeRequest) IsValid() *AppError { return NewAppError("AuthData.IsValid", "model.authorize.is_valid.redirect_uri.app_error", nil, "client_id="+ar.ClientId, http.StatusBadRequest) } - if len(ar.State) > 128 { + if len(ar.State) > 1024 { return NewAppError("AuthData.IsValid", "model.authorize.is_valid.state.app_error", nil, "client_id="+ar.ClientId, http.StatusBadRequest) } @@ -115,43 +116,25 @@ func (ad *AuthData) PreSave() { } func (ad *AuthData) ToJson() string { - b, err := json.Marshal(ad) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(ad) + return string(b) } func AuthDataFromJson(data io.Reader) *AuthData { - decoder := json.NewDecoder(data) - var ad AuthData - err := decoder.Decode(&ad) - if err == nil { - return &ad - } else { - return nil - } + var ad *AuthData + json.NewDecoder(data).Decode(&ad) + return ad } func (ar *AuthorizeRequest) ToJson() string { - b, err := json.Marshal(ar) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(ar) + return string(b) } func AuthorizeRequestFromJson(data io.Reader) *AuthorizeRequest { - decoder := json.NewDecoder(data) - var ar AuthorizeRequest - err := decoder.Decode(&ar) - if err == nil { - return &ar - } else { - return nil - } + var ar *AuthorizeRequest + json.NewDecoder(data).Decode(&ar) + return ar } func (ad *AuthData) IsExpired() bool { diff --git a/vendor/github.com/mattermost/platform/model/builtin.go b/vendor/github.com/mattermost/mattermost-server/model/builtin.go similarity index 100% rename from vendor/github.com/mattermost/platform/model/builtin.go rename to vendor/github.com/mattermost/mattermost-server/model/builtin.go diff --git a/vendor/github.com/mattermost/platform/model/bundle_info.go b/vendor/github.com/mattermost/mattermost-server/model/bundle_info.go similarity index 65% rename from vendor/github.com/mattermost/platform/model/bundle_info.go rename to vendor/github.com/mattermost/mattermost-server/model/bundle_info.go index 6965159c..7cb06701 100644 --- a/vendor/github.com/mattermost/platform/model/bundle_info.go +++ b/vendor/github.com/mattermost/mattermost-server/model/bundle_info.go @@ -3,6 +3,8 @@ package model +import "github.com/mattermost/mattermost-server/mlog" + type BundleInfo struct { Path string @@ -11,6 +13,13 @@ type BundleInfo struct { ManifestError error } +func (b *BundleInfo) WrapLogger(logger *mlog.Logger) *mlog.Logger { + if b.Manifest != nil { + return logger.With(mlog.String("plugin_id", b.Manifest.Id)) + } + return logger.With(mlog.String("plugin_path", b.Path)) +} + // Returns bundle info for the given path. The return value is never nil. func BundleInfoForPath(path string) *BundleInfo { m, mpath, err := FindManifest(path) diff --git a/vendor/github.com/mattermost/platform/model/channel.go b/vendor/github.com/mattermost/mattermost-server/model/channel.go similarity index 74% rename from vendor/github.com/mattermost/platform/model/channel.go rename to vendor/github.com/mattermost/mattermost-server/model/channel.go index a4c733c3..529c49d3 100644 --- a/vendor/github.com/mattermost/platform/model/channel.go +++ b/vendor/github.com/mattermost/mattermost-server/model/channel.go @@ -32,20 +32,22 @@ const ( ) type Channel struct { - Id string `json:"id"` - CreateAt int64 `json:"create_at"` - UpdateAt int64 `json:"update_at"` - DeleteAt int64 `json:"delete_at"` - TeamId string `json:"team_id"` - Type string `json:"type"` - DisplayName string `json:"display_name"` - Name string `json:"name"` - Header string `json:"header"` - Purpose string `json:"purpose"` - LastPostAt int64 `json:"last_post_at"` - TotalMsgCount int64 `json:"total_msg_count"` - ExtraUpdateAt int64 `json:"extra_update_at"` - CreatorId string `json:"creator_id"` + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + TeamId string `json:"team_id"` + Type string `json:"type"` + DisplayName string `json:"display_name"` + Name string `json:"name"` + Header string `json:"header"` + Purpose string `json:"purpose"` + LastPostAt int64 `json:"last_post_at"` + TotalMsgCount int64 `json:"total_msg_count"` + ExtraUpdateAt int64 `json:"extra_update_at"` + CreatorId string `json:"creator_id"` + SchemeId *string `json:"scheme_id"` + Props map[string]interface{} `json:"props" db:"-"` } type ChannelPatch struct { @@ -55,61 +57,47 @@ type ChannelPatch struct { Purpose *string `json:"purpose"` } +type ChannelForExport struct { + Channel + TeamName string + SchemeName *string +} + func (o *Channel) DeepCopy() *Channel { copy := *o + if copy.SchemeId != nil { + copy.SchemeId = NewString(*o.SchemeId) + } return © } func (o *Channel) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func (o *ChannelPatch) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func ChannelFromJson(data io.Reader) *Channel { - decoder := json.NewDecoder(data) - var o Channel - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *Channel + json.NewDecoder(data).Decode(&o) + return o } func ChannelPatchFromJson(data io.Reader) *ChannelPatch { - decoder := json.NewDecoder(data) - var o ChannelPatch - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *ChannelPatch + json.NewDecoder(data).Decode(&o) + return o } func (o *Channel) Etag() string { return Etag(o.Id, o.UpdateAt) } -func (o *Channel) StatsEtag() string { - return Etag(o.Id, o.ExtraUpdateAt) -} - func (o *Channel) IsValid() *AppError { - if len(o.Id) != 26 { return NewAppError("Channel.IsValid", "model.channel.is_valid.id.app_error", nil, "", http.StatusBadRequest) } @@ -156,17 +144,13 @@ func (o *Channel) PreSave() { o.CreateAt = GetMillis() o.UpdateAt = o.CreateAt - o.ExtraUpdateAt = o.CreateAt + o.ExtraUpdateAt = 0 } func (o *Channel) PreUpdate() { o.UpdateAt = GetMillis() } -func (o *Channel) ExtraUpdated() { - o.ExtraUpdateAt = GetMillis() -} - func (o *Channel) IsGroupOrDirect() bool { return o.Type == CHANNEL_DIRECT || o.Type == CHANNEL_GROUP } @@ -189,6 +173,18 @@ func (o *Channel) Patch(patch *ChannelPatch) { } } +func (o *Channel) MakeNonNil() { + if o.Props == nil { + o.Props = make(map[string]interface{}) + } +} + +func (o *Channel) AddProp(key string, value interface{}) { + o.MakeNonNil() + + o.Props[key] = value +} + func GetDMNameFromIds(userId1, userId2 string) string { if userId1 > userId2 { return userId2 + "__" + userId1 diff --git a/vendor/github.com/mattermost/platform/model/channel_count.go b/vendor/github.com/mattermost/mattermost-server/model/channel_count.go similarity index 79% rename from vendor/github.com/mattermost/platform/model/channel_count.go rename to vendor/github.com/mattermost/mattermost-server/model/channel_count.go index aecb0c71..8c6d8dd0 100644 --- a/vendor/github.com/mattermost/platform/model/channel_count.go +++ b/vendor/github.com/mattermost/mattermost-server/model/channel_count.go @@ -43,21 +43,12 @@ func (o *ChannelCounts) Etag() string { } func (o *ChannelCounts) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func ChannelCountsFromJson(data io.Reader) *ChannelCounts { - decoder := json.NewDecoder(data) - var o ChannelCounts - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *ChannelCounts + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/platform/model/channel_data.go b/vendor/github.com/mattermost/mattermost-server/model/channel_data.go similarity index 71% rename from vendor/github.com/mattermost/platform/model/channel_data.go rename to vendor/github.com/mattermost/mattermost-server/model/channel_data.go index 41b7eaa6..aae0a149 100644 --- a/vendor/github.com/mattermost/platform/model/channel_data.go +++ b/vendor/github.com/mattermost/mattermost-server/model/channel_data.go @@ -23,21 +23,12 @@ func (o *ChannelData) Etag() string { } func (o *ChannelData) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func ChannelDataFromJson(data io.Reader) *ChannelData { - decoder := json.NewDecoder(data) - var o ChannelData - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *ChannelData + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/platform/model/channel_list.go b/vendor/github.com/mattermost/mattermost-server/model/channel_list.go similarity index 74% rename from vendor/github.com/mattermost/platform/model/channel_list.go rename to vendor/github.com/mattermost/mattermost-server/model/channel_list.go index d5a4ccb7..1b3bda46 100644 --- a/vendor/github.com/mattermost/platform/model/channel_list.go +++ b/vendor/github.com/mattermost/mattermost-server/model/channel_list.go @@ -41,23 +41,13 @@ func (o *ChannelList) Etag() string { } func ChannelListFromJson(data io.Reader) *ChannelList { - decoder := json.NewDecoder(data) - var o ChannelList - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *ChannelList + json.NewDecoder(data).Decode(&o) + return o } func ChannelSliceFromJson(data io.Reader) []*Channel { - decoder := json.NewDecoder(data) var o []*Channel - err := decoder.Decode(&o) - if err == nil { - return o - } else { - return nil - } + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/platform/model/channel_member.go b/vendor/github.com/mattermost/mattermost-server/model/channel_member.go similarity index 80% rename from vendor/github.com/mattermost/platform/model/channel_member.go rename to vendor/github.com/mattermost/mattermost-server/model/channel_member.go index e7ad8232..941db62f 100644 --- a/vendor/github.com/mattermost/platform/model/channel_member.go +++ b/vendor/github.com/mattermost/mattermost-server/model/channel_member.go @@ -28,18 +28,26 @@ type ChannelUnread struct { } type ChannelMember struct { - ChannelId string `json:"channel_id"` - UserId string `json:"user_id"` - Roles string `json:"roles"` - LastViewedAt int64 `json:"last_viewed_at"` - MsgCount int64 `json:"msg_count"` - MentionCount int64 `json:"mention_count"` - NotifyProps StringMap `json:"notify_props"` - LastUpdateAt int64 `json:"last_update_at"` + ChannelId string `json:"channel_id"` + UserId string `json:"user_id"` + Roles string `json:"roles"` + LastViewedAt int64 `json:"last_viewed_at"` + MsgCount int64 `json:"msg_count"` + MentionCount int64 `json:"mention_count"` + NotifyProps StringMap `json:"notify_props"` + LastUpdateAt int64 `json:"last_update_at"` + SchemeUser bool `json:"scheme_user"` + SchemeAdmin bool `json:"scheme_admin"` + ExplicitRoles string `json:"explicit_roles"` } type ChannelMembers []ChannelMember +type ChannelMemberForExport struct { + ChannelMember + ChannelName string +} + func (o *ChannelMembers) ToJson() string { if b, err := json.Marshal(o); err != nil { return "[]" @@ -49,54 +57,31 @@ func (o *ChannelMembers) ToJson() string { } func (o *ChannelUnread) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func ChannelMembersFromJson(data io.Reader) *ChannelMembers { - decoder := json.NewDecoder(data) - var o ChannelMembers - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *ChannelMembers + json.NewDecoder(data).Decode(&o) + return o } func ChannelUnreadFromJson(data io.Reader) *ChannelUnread { - decoder := json.NewDecoder(data) - var o ChannelUnread - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *ChannelUnread + json.NewDecoder(data).Decode(&o) + return o } func (o *ChannelMember) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func ChannelMemberFromJson(data io.Reader) *ChannelMember { - decoder := json.NewDecoder(data) - var o ChannelMember - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *ChannelMember + json.NewDecoder(data).Decode(&o) + return o } func (o *ChannelMember) IsValid() *AppError { diff --git a/vendor/github.com/mattermost/platform/model/channel_member_history.go b/vendor/github.com/mattermost/mattermost-server/model/channel_member_history.go similarity index 88% rename from vendor/github.com/mattermost/platform/model/channel_member_history.go rename to vendor/github.com/mattermost/mattermost-server/model/channel_member_history.go index bc71b580..55435c32 100644 --- a/vendor/github.com/mattermost/platform/model/channel_member_history.go +++ b/vendor/github.com/mattermost/mattermost-server/model/channel_member_history.go @@ -6,7 +6,6 @@ package model type ChannelMemberHistory struct { ChannelId string UserId string - UserEmail string `db:"Email"` JoinTime int64 LeaveTime *int64 } diff --git a/vendor/github.com/mattermost/mattermost-server/model/channel_member_history_result.go b/vendor/github.com/mattermost/mattermost-server/model/channel_member_history_result.go new file mode 100644 index 00000000..ed3e7963 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/channel_member_history_result.go @@ -0,0 +1,15 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +type ChannelMemberHistoryResult struct { + ChannelId string + UserId string + JoinTime int64 + LeaveTime *int64 + + // these two fields are never set in the database - when we SELECT, we join on Users to get them + UserEmail string `db:"Email"` + Username string +} diff --git a/vendor/github.com/mattermost/mattermost-server/model/channel_mentions.go b/vendor/github.com/mattermost/mattermost-server/model/channel_mentions.go new file mode 100644 index 00000000..795ec379 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/channel_mentions.go @@ -0,0 +1,28 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "regexp" + "strings" +) + +var channelMentionRegexp = regexp.MustCompile(`\B~[a-zA-Z0-9\-_]+`) + +func ChannelMentions(message string) []string { + var names []string + + if strings.Contains(message, "~") { + alreadyMentioned := make(map[string]bool) + for _, match := range channelMentionRegexp.FindAllString(message, -1) { + name := match[1:] + if !alreadyMentioned[name] { + names = append(names, name) + alreadyMentioned[name] = true + } + } + } + + return names +} diff --git a/vendor/github.com/mattermost/platform/model/channel_search.go b/vendor/github.com/mattermost/mattermost-server/model/channel_search.go similarity index 65% rename from vendor/github.com/mattermost/platform/model/channel_search.go rename to vendor/github.com/mattermost/mattermost-server/model/channel_search.go index d915c164..593cf669 100644 --- a/vendor/github.com/mattermost/platform/model/channel_search.go +++ b/vendor/github.com/mattermost/mattermost-server/model/channel_search.go @@ -14,22 +14,13 @@ type ChannelSearch struct { // ToJson convert a Channel to a json string func (c *ChannelSearch) ToJson() string { - b, err := json.Marshal(c) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(c) + return string(b) } // ChannelSearchFromJson will decode the input and return a Channel func ChannelSearchFromJson(data io.Reader) *ChannelSearch { - decoder := json.NewDecoder(data) - var cs ChannelSearch - err := decoder.Decode(&cs) - if err == nil { - return &cs - } else { - return nil - } + var cs *ChannelSearch + json.NewDecoder(data).Decode(&cs) + return cs } diff --git a/vendor/github.com/mattermost/platform/model/channel_stats.go b/vendor/github.com/mattermost/mattermost-server/model/channel_stats.go similarity index 62% rename from vendor/github.com/mattermost/platform/model/channel_stats.go rename to vendor/github.com/mattermost/mattermost-server/model/channel_stats.go index 758aa2b1..21af920f 100644 --- a/vendor/github.com/mattermost/platform/model/channel_stats.go +++ b/vendor/github.com/mattermost/mattermost-server/model/channel_stats.go @@ -14,21 +14,12 @@ type ChannelStats struct { } func (o *ChannelStats) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func ChannelStatsFromJson(data io.Reader) *ChannelStats { - decoder := json.NewDecoder(data) - var o ChannelStats - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *ChannelStats + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/platform/model/channel_view.go b/vendor/github.com/mattermost/mattermost-server/model/channel_view.go similarity index 59% rename from vendor/github.com/mattermost/platform/model/channel_view.go rename to vendor/github.com/mattermost/mattermost-server/model/channel_view.go index e7b1de30..650d14ce 100644 --- a/vendor/github.com/mattermost/platform/model/channel_view.go +++ b/vendor/github.com/mattermost/mattermost-server/model/channel_view.go @@ -14,23 +14,14 @@ type ChannelView struct { } func (o *ChannelView) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func ChannelViewFromJson(data io.Reader) *ChannelView { - decoder := json.NewDecoder(data) - var o ChannelView - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *ChannelView + json.NewDecoder(data).Decode(&o) + return o } type ChannelViewResponse struct { @@ -39,21 +30,12 @@ type ChannelViewResponse struct { } func (o *ChannelViewResponse) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func ChannelViewResponseFromJson(data io.Reader) *ChannelViewResponse { - decoder := json.NewDecoder(data) - var o ChannelViewResponse - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *ChannelViewResponse + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/platform/model/client4.go b/vendor/github.com/mattermost/mattermost-server/model/client4.go similarity index 82% rename from vendor/github.com/mattermost/platform/model/client4.go rename to vendor/github.com/mattermost/mattermost-server/model/client4.go index e84a23e5..57c4fb7c 100644 --- a/vendor/github.com/mattermost/platform/model/client4.go +++ b/vendor/github.com/mattermost/mattermost-server/model/client4.go @@ -13,6 +13,33 @@ import ( "net/url" "strconv" "strings" + "time" +) + +const ( + HEADER_REQUEST_ID = "X-Request-ID" + HEADER_VERSION_ID = "X-Version-ID" + HEADER_CLUSTER_ID = "X-Cluster-ID" + HEADER_ETAG_SERVER = "ETag" + HEADER_ETAG_CLIENT = "If-None-Match" + HEADER_FORWARDED = "X-Forwarded-For" + HEADER_REAL_IP = "X-Real-IP" + HEADER_FORWARDED_PROTO = "X-Forwarded-Proto" + HEADER_TOKEN = "token" + HEADER_BEARER = "BEARER" + HEADER_AUTH = "Authorization" + HEADER_REQUESTED_WITH = "X-Requested-With" + HEADER_REQUESTED_WITH_XML = "XMLHttpRequest" + STATUS = "status" + STATUS_OK = "OK" + STATUS_FAIL = "FAIL" + STATUS_REMOVE = "REMOVE" + + CLIENT_DIR = "client" + + API_URL_SUFFIX_V1 = "/api/v1" + API_URL_SUFFIX_V4 = "/api/v4" + API_URL_SUFFIX = API_URL_SUFFIX_V4 ) type Response struct { @@ -30,10 +57,29 @@ type Client4 struct { HttpClient *http.Client // The http client AuthToken string AuthType string + HttpHeader map[string]string // Headers to be copied over for each request +} + +func closeBody(r *http.Response) { + if r.Body != nil { + ioutil.ReadAll(r.Body) + r.Body.Close() + } +} + +// Must is a convenience function used for testing. +func (c *Client4) Must(result interface{}, resp *Response) interface{} { + if resp.Error != nil { + + time.Sleep(time.Second) + panic(resp.Error) + } + + return result } func NewAPIv4Client(url string) *Client4 { - return &Client4{url, url + API_URL_SUFFIX, &http.Client{}, "", ""} + return &Client4{url, url + API_URL_SUFFIX, &http.Client{}, "", "", map[string]string{}} } func BuildErrorResponse(r *http.Response, err *AppError) *Response { @@ -64,6 +110,11 @@ func BuildResponse(r *http.Response) *Response { } } +func (c *Client4) MockSession(sessionToken string) { + c.AuthToken = sessionToken + c.AuthType = HEADER_BEARER +} + func (c *Client4) SetOAuthToken(token string) { c.AuthToken = token c.AuthType = HEADER_TOKEN @@ -82,6 +133,10 @@ func (c *Client4) GetUserRoute(userId string) string { return fmt.Sprintf(c.GetUsersRoute()+"/%v", userId) } +func (c *Client4) GetUserAccessTokensRoute() string { + return fmt.Sprintf(c.GetUsersRoute() + "/tokens") +} + func (c *Client4) GetUserAccessTokenRoute(tokenId string) string { return fmt.Sprintf(c.GetUsersRoute()+"/tokens/%v", tokenId) } @@ -158,6 +213,10 @@ func (c *Client4) GetPostsRoute() string { return fmt.Sprintf("/posts") } +func (c *Client4) GetPostsEphemeralRoute() string { + return fmt.Sprintf("/posts/ephemeral") +} + func (c *Client4) GetConfigRoute() string { return fmt.Sprintf("/config") } @@ -194,6 +253,10 @@ func (c *Client4) GetTestEmailRoute() string { return fmt.Sprintf("/email/test") } +func (c *Client4) GetTestS3Route() string { + return fmt.Sprintf("/file/s3_test") +} + func (c *Client4) GetDatabaseRoute() string { return fmt.Sprintf("/database") } @@ -278,6 +341,10 @@ func (c *Client4) GetEmojiRoute(emojiId string) string { return fmt.Sprintf(c.GetEmojisRoute()+"/%v", emojiId) } +func (c *Client4) GetEmojiByNameRoute(name string) string { + return fmt.Sprintf(c.GetEmojisRoute()+"/name/%v", name) +} + func (c *Client4) GetReactionsRoute() string { return fmt.Sprintf("/reactions") } @@ -298,10 +365,50 @@ func (c *Client4) GetJobsRoute() string { return fmt.Sprintf("/jobs") } +func (c *Client4) GetRolesRoute() string { + return fmt.Sprintf("/roles") +} + +func (c *Client4) GetSchemesRoute() string { + return fmt.Sprintf("/schemes") +} + +func (c *Client4) GetSchemeRoute(id string) string { + return c.GetSchemesRoute() + fmt.Sprintf("/%v", id) +} + func (c *Client4) GetAnalyticsRoute() string { return fmt.Sprintf("/analytics") } +func (c *Client4) GetTimezonesRoute() string { + return fmt.Sprintf(c.GetSystemRoute() + "/timezones") +} + +func (c *Client4) GetChannelSchemeRoute(channelId string) string { + return fmt.Sprintf(c.GetChannelsRoute()+"/%v/scheme", channelId) +} + +func (c *Client4) GetTeamSchemeRoute(teamId string) string { + return fmt.Sprintf(c.GetTeamsRoute()+"/%v/scheme", teamId) +} + +func (c *Client4) GetTotalUsersStatsRoute() string { + return fmt.Sprintf(c.GetUsersRoute() + "/stats") +} + +func (c *Client4) GetRedirectLocationRoute() string { + return fmt.Sprintf("/redirect_location") +} + +func (c *Client4) GetRegisterTermsOfServiceRoute(userId string) string { + return c.GetUserRoute(userId) + "/terms_of_service" +} + +func (c *Client4) GetTermsOfServiceRoute() string { + return "/terms_of_service" +} + func (c *Client4) DoApiGet(url string, etag string) (*http.Response, *AppError) { return c.DoApiRequest(http.MethodGet, c.ApiUrl+url, "", etag) } @@ -320,7 +427,6 @@ func (c *Client4) DoApiDelete(url string) (*http.Response, *AppError) { func (c *Client4) DoApiRequest(method, url, data, etag string) (*http.Response, *AppError) { rq, _ := http.NewRequest(method, url, strings.NewReader(data)) - rq.Close = true if len(etag) > 0 { rq.Header.Set(HEADER_ETAG_CLIENT, etag) @@ -330,6 +436,13 @@ func (c *Client4) DoApiRequest(method, url, data, etag string) (*http.Response, rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) } + if c.HttpHeader != nil && len(c.HttpHeader) > 0 { + + for k, v := range c.HttpHeader { + rq.Header.Set(k, v) + } + } + if rp, err := c.HttpClient.Do(rq); err != nil || rp == nil { return nil, NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0) } else if rp.StatusCode == 304 { @@ -345,7 +458,6 @@ func (c *Client4) DoApiRequest(method, url, data, etag string) (*http.Response, func (c *Client4) DoUploadFile(url string, data []byte, contentType string) (*FileUploadResponse, *Response) { rq, _ := http.NewRequest("POST", c.ApiUrl+url, bytes.NewReader(data)) rq.Header.Set("Content-Type", contentType) - rq.Close = true if len(c.AuthToken) > 0 { rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) @@ -367,7 +479,6 @@ func (c *Client4) DoUploadFile(url string, data []byte, contentType string) (*Fi func (c *Client4) DoEmojiUploadFile(url string, data []byte, contentType string) (*Emoji, *Response) { rq, _ := http.NewRequest("POST", c.ApiUrl+url, bytes.NewReader(data)) rq.Header.Set("Content-Type", contentType) - rq.Close = true if len(c.AuthToken) > 0 { rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) @@ -389,7 +500,6 @@ func (c *Client4) DoEmojiUploadFile(url string, data []byte, contentType string) func (c *Client4) DoUploadImportTeam(url string, data []byte, contentType string) (map[string]string, *Response) { rq, _ := http.NewRequest("POST", c.ApiUrl+url, bytes.NewReader(data)) rq.Header.Set("Content-Type", contentType) - rq.Close = true if len(c.AuthToken) > 0 { rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) @@ -506,13 +616,13 @@ func (c *Client4) CreateUser(user *User) (*User, *Response) { } } -// CreateUserWithHash creates a user in the system based on the provided user struct and hash created. -func (c *Client4) CreateUserWithHash(user *User, hash, data string) (*User, *Response) { +// CreateUserWithToken creates a user in the system based on the provided tokenId. +func (c *Client4) CreateUserWithToken(user *User, tokenId string) (*User, *Response) { var query string - if hash != "" && data != "" { - query = fmt.Sprintf("?d=%v&h=%v", url.QueryEscape(data), hash) + if tokenId != "" { + query = fmt.Sprintf("?t=%v", tokenId) } else { - err := NewAppError("MissingHashOrData", "api.user.create_user.missing_hash_or_data.app_error", nil, "", http.StatusBadRequest) + err := NewAppError("MissingHashOrData", "api.user.create_user.missing_token.app_error", nil, "", http.StatusBadRequest) return nil, &Response{StatusCode: err.StatusCode, Error: err} } if r, err := c.DoApiPost(c.GetUsersRoute()+query, user.ToJson()); err != nil { @@ -683,7 +793,7 @@ func (c *Client4) GetUsersNotInTeam(teamId string, page int, perPage int, etag s } } -// GetUsersInChannel returns a page of users on a team. Page counting starts at 0. +// GetUsersInChannel returns a page of users in a channel. Page counting starts at 0. func (c *Client4) GetUsersInChannel(channelId string, page int, perPage int, etag string) ([]*User, *Response) { query := fmt.Sprintf("?in_channel=%v&page=%v&per_page=%v", channelId, page, perPage) if r, err := c.DoApiGet(c.GetUsersRoute()+query, etag); err != nil { @@ -694,7 +804,18 @@ func (c *Client4) GetUsersInChannel(channelId string, page int, perPage int, eta } } -// GetUsersNotInChannel returns a page of users on a team. Page counting starts at 0. +// GetUsersInChannelStatus returns a page of users in a channel. Page counting starts at 0. Sorted by Status +func (c *Client4) GetUsersInChannelByStatus(channelId string, page int, perPage int, etag string) ([]*User, *Response) { + query := fmt.Sprintf("?in_channel=%v&page=%v&per_page=%v&sort=status", channelId, page, perPage) + if r, err := c.DoApiGet(c.GetUsersRoute()+query, etag); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) + } +} + +// GetUsersNotInChannel returns a page of users not in a channel. Page counting starts at 0. func (c *Client4) GetUsersNotInChannel(teamId, channelId string, page int, perPage int, etag string) ([]*User, *Response) { query := fmt.Sprintf("?in_team=%v¬_in_channel=%v&page=%v&per_page=%v", teamId, channelId, page, perPage) if r, err := c.DoApiGet(c.GetUsersRoute()+query, etag); err != nil { @@ -1001,7 +1122,6 @@ func (c *Client4) SetProfileImage(userId string, data []byte) (bool, *Response) rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetUserRoute(userId)+"/image", bytes.NewReader(body.Bytes())) rq.Header.Set("Content-Type", writer.FormDataContentType()) - rq.Close = true if len(c.AuthToken) > 0 { rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) @@ -1035,10 +1155,23 @@ func (c *Client4) CreateUserAccessToken(userId, description string) (*UserAccess } } -// GetUserAccessToken will get a user access token's id, description and the user_id -// of the user it is for. The actual token will not be returned. Must have the -// 'read_user_access_token' permission and if getting for another user, must have the -// 'edit_other_users' permission. +// GetUserAccessTokens will get a page of access tokens' id, description, is_active +// and the user_id in the system. The actual token will not be returned. Must have +// the 'manage_system' permission. +func (c *Client4) GetUserAccessTokens(page int, perPage int) ([]*UserAccessToken, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + if r, err := c.DoApiGet(c.GetUserAccessTokensRoute()+query, ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return UserAccessTokenListFromJson(r.Body), BuildResponse(r) + } +} + +// GetUserAccessToken will get a user access tokens' id, description, is_active +// and the user_id of the user it is for. The actual token will not be returned. +// Must have the 'read_user_access_token' permission and if getting for another +// user, must have the 'edit_other_users' permission. func (c *Client4) GetUserAccessToken(tokenId string) (*UserAccessToken, *Response) { if r, err := c.DoApiGet(c.GetUserAccessTokenRoute(tokenId), ""); err != nil { return nil, BuildErrorResponse(r, err) @@ -1075,6 +1208,16 @@ func (c *Client4) RevokeUserAccessToken(tokenId string) (bool, *Response) { } } +// SearchUserAccessTokens returns user access tokens matching the provided search term. +func (c *Client4) SearchUserAccessTokens(search *UserAccessTokenSearch) ([]*UserAccessToken, *Response) { + if r, err := c.DoApiPost(c.GetUsersRoute()+"/tokens/search", search.ToJson()); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return UserAccessTokenListFromJson(r.Body), BuildResponse(r) + } +} + // DisableUserAccessToken will disable a user access token by id. Must have the // 'revoke_user_access_token' permission and if disabling for another user, must have the // 'edit_other_users' permission. @@ -1196,6 +1339,16 @@ func (c *Client4) UpdateTeamMemberRoles(teamId, userId, newRoles string) (bool, } } +// UpdateTeamMemberSchemeRoles will update the scheme-derived roles on a team for a user. +func (c *Client4) UpdateTeamMemberSchemeRoles(teamId string, userId string, schemeRoles *SchemeRoles) (bool, *Response) { + if r, err := c.DoApiPut(c.GetTeamMemberRoute(teamId, userId)+"/schemeRoles", schemeRoles.ToJson()); err != nil { + return false, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) + } +} + // UpdateTeam will update a team. func (c *Client4) UpdateTeam(team *Team) (*Team, *Response) { if r, err := c.DoApiPut(c.GetTeamRoute(team.Id), team.ToJson()); err != nil { @@ -1282,16 +1435,16 @@ func (c *Client4) AddTeamMember(teamId, userId string) (*TeamMember, *Response) } // AddTeamMemberFromInvite adds a user to a team and return a team member using an invite id -// or an invite hash/data pair. -func (c *Client4) AddTeamMemberFromInvite(hash, dataToHash, inviteId string) (*TeamMember, *Response) { +// or an invite token/data pair. +func (c *Client4) AddTeamMemberFromInvite(token, inviteId string) (*TeamMember, *Response) { var query string if inviteId != "" { query += fmt.Sprintf("?invite_id=%v", inviteId) } - if hash != "" && dataToHash != "" { - query += fmt.Sprintf("?hash=%v&data=%v", hash, dataToHash) + if token != "" { + query += fmt.Sprintf("?token=%v", token) } if r, err := c.DoApiPost(c.GetTeamsRoute()+"/members/invite"+query, ""); err != nil { @@ -1339,6 +1492,17 @@ func (c *Client4) GetTeamStats(teamId, etag string) (*TeamStats, *Response) { } } +// GetTotalUsersStats returns a total system user stats. +// Must be authenticated. +func (c *Client4) GetTotalUsersStats(etag string) (*UsersStats, *Response) { + if r, err := c.DoApiGet(c.GetTotalUsersStatsRoute(), etag); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return UsersStatsFromJson(r.Body), BuildResponse(r) + } +} + // GetTeamUnread will return a TeamUnread object that contains the amount of // unread messages and mentions the user has for the specified team. // Must be authenticated. @@ -1401,6 +1565,68 @@ func (c *Client4) GetTeamInviteInfo(inviteId string) (*Team, *Response) { } } +// SetTeamIcon sets team icon of the team +func (c *Client4) SetTeamIcon(teamId string, data []byte) (bool, *Response) { + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + if part, err := writer.CreateFormFile("image", "teamIcon.png"); err != nil { + return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.no_file.app_error", nil, err.Error(), http.StatusBadRequest)} + } else if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil { + return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.no_file.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if err := writer.Close(); err != nil { + return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.writer.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetTeamRoute(teamId)+"/image", bytes.NewReader(body.Bytes())) + rq.Header.Set("Content-Type", writer.FormDataContentType()) + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + if rp, err := c.HttpClient.Do(rq); err != nil || rp == nil { + // set to http.StatusForbidden(403) + return false, &Response{StatusCode: http.StatusForbidden, Error: NewAppError(c.GetTeamRoute(teamId)+"/image", "model.client.connecting.app_error", nil, err.Error(), 403)} + } else { + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return false, BuildErrorResponse(rp, AppErrorFromJson(rp.Body)) + } else { + return CheckStatusOK(rp), BuildResponse(rp) + } + } +} + +// GetTeamIcon gets the team icon of the team +func (c *Client4) GetTeamIcon(teamId, etag string) ([]byte, *Response) { + if r, err := c.DoApiGet(c.GetTeamRoute(teamId)+"/image", etag); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + + if data, err := ioutil.ReadAll(r.Body); err != nil { + return nil, BuildErrorResponse(r, NewAppError("GetTeamIcon", "model.client.get_team_icon.app_error", nil, err.Error(), r.StatusCode)) + } else { + return data, BuildResponse(r) + } + } +} + +// RemoveTeamIcon updates LastTeamIconUpdate to 0 which indicates team icon is removed. +func (c *Client4) RemoveTeamIcon(teamId string) (bool, *Response) { + if r, err := c.DoApiDelete(c.GetTeamRoute(teamId) + "/image"); err != nil { + return false, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) + } +} + // Channel Section // CreateChannel creates a channel based on the provided channel struct. @@ -1433,6 +1659,16 @@ func (c *Client4) PatchChannel(channelId string, patch *ChannelPatch) (*Channel, } } +// ConvertChannelToPrivate converts public to private channel. +func (c *Client4) ConvertChannelToPrivate(channelId string) (*Channel, *Response) { + if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/convert", ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return ChannelFromJson(r.Body), BuildResponse(r) + } +} + // RestoreChannel restores a previously deleted channel. Any missing fields are not updated. func (c *Client4) RestoreChannel(channelId string) (*Channel, *Response) { if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/restore", ""); err != nil { @@ -1567,6 +1803,15 @@ func (c *Client4) GetChannelByName(channelName, teamId string, etag string) (*Ch } } +func (c *Client4) GetChannelByNameIncludeDeleted(channelName, teamId string, etag string) (*Channel, *Response) { + if r, err := c.DoApiGet(c.GetChannelByNameRoute(channelName, teamId)+"?include_deleted=true", etag); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return ChannelFromJson(r.Body), BuildResponse(r) + } +} + // GetChannelByNameForTeamName returns a channel based on the provided channel name and team name strings. func (c *Client4) GetChannelByNameForTeamName(channelName, teamName string, etag string) (*Channel, *Response) { if r, err := c.DoApiGet(c.GetChannelByNameForTeamNameRoute(channelName, teamName), etag); err != nil { @@ -1577,6 +1822,15 @@ func (c *Client4) GetChannelByNameForTeamName(channelName, teamName string, etag } } +func (c *Client4) GetChannelByNameForTeamNameIncludeDeleted(channelName, teamName string, etag string) (*Channel, *Response) { + if r, err := c.DoApiGet(c.GetChannelByNameForTeamNameRoute(channelName, teamName)+"?include_deleted=true", etag); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return ChannelFromJson(r.Body), BuildResponse(r) + } +} + // GetChannelMembers gets a page of channel members. func (c *Client4) GetChannelMembers(channelId string, page, perPage int, etag string) (*ChannelMembers, *Response) { query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) @@ -1652,6 +1906,16 @@ func (c *Client4) UpdateChannelRoles(channelId, userId, roles string) (bool, *Re } } +// UpdateChannelMemberSchemeRoles will update the scheme-derived roles on a channel for a user. +func (c *Client4) UpdateChannelMemberSchemeRoles(channelId string, userId string, schemeRoles *SchemeRoles) (bool, *Response) { + if r, err := c.DoApiPut(c.GetChannelMemberRoute(channelId, userId)+"/schemeRoles", schemeRoles.ToJson()); err != nil { + return false, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) + } +} + // UpdateChannelNotifyProps will update the notification properties on a channel for a user. func (c *Client4) UpdateChannelNotifyProps(channelId, userId string, props map[string]string) (bool, *Response) { if r, err := c.DoApiPut(c.GetChannelMemberRoute(channelId, userId)+"/notify_props", MapToJson(props)); err != nil { @@ -1694,11 +1958,43 @@ func (c *Client4) RemoveUserFromChannel(channelId, userId string) (bool, *Respon } } +// AutocompleteChannelsForTeam will return an ordered list of channels autocomplete suggestions +func (c *Client4) AutocompleteChannelsForTeam(teamId, name string) (*ChannelList, *Response) { + query := fmt.Sprintf("?name=%v", name) + if r, err := c.DoApiGet(c.GetChannelsForTeamRoute(teamId)+"/autocomplete"+query, ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return ChannelListFromJson(r.Body), BuildResponse(r) + } +} + +// AutocompleteChannelsForTeamForSearch will return an ordered list of your channels autocomplete suggestions +func (c *Client4) AutocompleteChannelsForTeamForSearch(teamId, name string) (*ChannelList, *Response) { + query := fmt.Sprintf("?name=%v", name) + if r, err := c.DoApiGet(c.GetChannelsForTeamRoute(teamId)+"/search_autocomplete"+query, ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return ChannelListFromJson(r.Body), BuildResponse(r) + } +} + // Post Section // CreatePost creates a post based on the provided post struct. func (c *Client4) CreatePost(post *Post) (*Post, *Response) { - if r, err := c.DoApiPost(c.GetPostsRoute(), post.ToJson()); err != nil { + if r, err := c.DoApiPost(c.GetPostsRoute(), post.ToUnsanitizedJson()); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return PostFromJson(r.Body), BuildResponse(r) + } +} + +// CreatePostEphemeral creates a ephemeral post based on the provided post struct which is send to the given user id +func (c *Client4) CreatePostEphemeral(post *PostEphemeral) (*Post, *Response) { + if r, err := c.DoApiPost(c.GetPostsEphemeralRoute(), post.ToUnsanitizedJson()); err != nil { return nil, BuildErrorResponse(r, err) } else { defer closeBody(r) @@ -1708,7 +2004,7 @@ func (c *Client4) CreatePost(post *Post) (*Post, *Response) { // UpdatePost updates a post based on the provided post struct. func (c *Client4) UpdatePost(postId string, post *Post) (*Post, *Response) { - if r, err := c.DoApiPut(c.GetPostRoute(postId), post.ToJson()); err != nil { + if r, err := c.DoApiPut(c.GetPostRoute(postId), post.ToUnsanitizedJson()); err != nil { return nil, BuildErrorResponse(r, err) } else { defer closeBody(r) @@ -1863,12 +2159,31 @@ func (c *Client4) GetPostsBefore(channelId, postId string, page, perPage int, et // SearchPosts returns any posts with matching terms string. func (c *Client4) SearchPosts(teamId string, terms string, isOrSearch bool) (*PostList, *Response) { + params := SearchParameter{ + Terms: &terms, + IsOrSearch: &isOrSearch, + } + return c.SearchPostsWithParams(teamId, ¶ms) +} + +// SearchPosts returns any posts with matching terms string. +func (c *Client4) SearchPostsWithParams(teamId string, params *SearchParameter) (*PostList, *Response) { + if r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/posts/search", params.SearchParameterToJson()); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return PostListFromJson(r.Body), BuildResponse(r) + } +} + +// SearchPosts returns any posts with matching terms string, including . +func (c *Client4) SearchPostsWithMatches(teamId string, terms string, isOrSearch bool) (*PostSearchResults, *Response) { requestBody := map[string]interface{}{"terms": terms, "is_or_search": isOrSearch} if r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/posts/search", StringInterfaceToJson(requestBody)); err != nil { return nil, BuildErrorResponse(r, err) } else { defer closeBody(r) - return PostListFromJson(r.Body), BuildResponse(r) + return PostSearchResultsFromJson(r.Body), BuildResponse(r) } } @@ -1884,7 +2199,8 @@ func (c *Client4) DoPostAction(postId, actionId string) (bool, *Response) { // File Section -// UploadFile will upload a file to a channel, to be later attached to a post. +// UploadFile will upload a file to a channel using a multipart request, to be later attached to a post. +// This method is functionally equivalent to Client4.UploadFileAsRequestBody. func (c *Client4) UploadFile(data []byte, channelId string, filename string) (*FileUploadResponse, *Response) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -1908,6 +2224,12 @@ func (c *Client4) UploadFile(data []byte, channelId string, filename string) (*F return c.DoUploadFile(c.GetFilesRoute(), body.Bytes(), writer.FormDataContentType()) } +// UploadFileAsRequestBody will upload a file to a channel as the body of a request, to be later attached +// to a post. This method is functionally equivalent to Client4.UploadFile. +func (c *Client4) UploadFileAsRequestBody(data []byte, channelId string, filename string) (*FileUploadResponse, *Response) { + return c.DoUploadFile(c.GetFilesRoute()+fmt.Sprintf("?channel_id=%v&filename=%v", url.QueryEscape(channelId), url.QueryEscape(filename)), data, http.DetectContentType(data)) +} + // GetFile gets the bytes for a file by id. func (c *Client4) GetFile(fileId string) ([]byte, *Response) { if r, err := c.DoApiGet(c.GetFileRoute(fileId), ""); err != nil { @@ -2045,8 +2367,18 @@ func (c *Client4) GetPing() (string, *Response) { } // TestEmail will attempt to connect to the configured SMTP server. -func (c *Client4) TestEmail() (bool, *Response) { - if r, err := c.DoApiPost(c.GetTestEmailRoute(), ""); err != nil { +func (c *Client4) TestEmail(config *Config) (bool, *Response) { + if r, err := c.DoApiPost(c.GetTestEmailRoute(), config.ToJson()); err != nil { + return false, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) + } +} + +// TestS3Connection will attempt to connect to the AWS S3. +func (c *Client4) TestS3Connection(config *Config) (bool, *Response) { + if r, err := c.DoApiPost(c.GetTestS3Route(), config.ToJson()); err != nil { return false, BuildErrorResponse(r, err) } else { defer closeBody(r) @@ -2085,6 +2417,18 @@ func (c *Client4) GetOldClientConfig(etag string) (map[string]string, *Response) } } +// GetEnvironmentConfig will retrieve a map mirroring the server configuration where fields +// are set to true if the corresponding config setting is set through an environment variable. +// Settings that haven't been set through environment variables will be missing from the map. +func (c *Client4) GetEnvironmentConfig() (map[string]interface{}, *Response) { + if r, err := c.DoApiGet(c.GetConfigRoute()+"/environment", ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return StringInterfaceFromJson(r.Body), BuildResponse(r) + } +} + // GetOldClientLicense will retrieve the parts of the server license needed by the // client, formatted in the old format. func (c *Client4) GetOldClientLicense(etag string) (map[string]string, *Response) { @@ -2143,7 +2487,6 @@ func (c *Client4) UploadLicenseFile(data []byte) (bool, *Response) { rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetLicenseRoute(), bytes.NewReader(body.Bytes())) rq.Header.Set("Content-Type", writer.FormDataContentType()) - rq.Close = true if len(c.AuthToken) > 0 { rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) @@ -2178,7 +2521,7 @@ func (c *Client4) RemoveLicenseFile() (bool, *Response) { // and defaults to "standard". The "teamId" argument is optional and will limit results // to a specific team. func (c *Client4) GetAnalyticsOld(name, teamId string) (AnalyticsRows, *Response) { - query := fmt.Sprintf("?name=%v&teamId=%v", name, teamId) + query := fmt.Sprintf("?name=%v&team_id=%v", name, teamId) if r, err := c.DoApiGet(c.GetAnalyticsRoute()+"/old"+query, ""); err != nil { return nil, BuildErrorResponse(r, err) } else { @@ -2531,7 +2874,6 @@ func (c *Client4) GetComplianceReport(reportId string) (*Compliance, *Response) func (c *Client4) DownloadComplianceReport(reportId string) ([]byte, *Response) { var rq *http.Request rq, _ = http.NewRequest("GET", c.ApiUrl+c.GetComplianceReportRoute(reportId), nil) - rq.Close = true if len(c.AuthToken) > 0 { rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) @@ -2636,7 +2978,6 @@ func (c *Client4) UploadBrandImage(data []byte) (bool, *Response) { rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetBrandRoute()+"/image", bytes.NewReader(body.Bytes())) rq.Header.Set("Content-Type", writer.FormDataContentType()) - rq.Close = true if len(c.AuthToken) > 0 { rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) @@ -2785,6 +3126,27 @@ func (c *Client4) DeauthorizeOAuthApp(appId string) (bool, *Response) { } } +// GetOAuthAccessToken is a test helper function for the OAuth access token endpoint. +func (c *Client4) GetOAuthAccessToken(data url.Values) (*AccessResponse, *Response) { + rq, _ := http.NewRequest(http.MethodPost, c.Url+"/oauth/access_token", strings.NewReader(data.Encode())) + rq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + if rp, err := c.HttpClient.Do(rq); err != nil || rp == nil { + return nil, &Response{StatusCode: http.StatusForbidden, Error: NewAppError(c.Url+"/oauth/access_token", "model.client.connecting.app_error", nil, err.Error(), 403)} + } else { + defer closeBody(rp) + if rp.StatusCode >= 300 { + return nil, BuildErrorResponse(rp, AppErrorFromJson(rp.Body)) + } else { + return AccessResponseFromJson(rp.Body), BuildResponse(rp) + } + } +} + // Elasticsearch Section // TestElasticsearch will attempt to connect to the configured Elasticsearch server and return OK if configured @@ -2873,7 +3235,9 @@ func (c *Client4) ExecuteCommand(channelId, command string) (*CommandResponse, * return nil, BuildErrorResponse(r, err) } else { defer closeBody(r) - return CommandResponseFromJson(r.Body), BuildResponse(r) + + response, _ := CommandResponseFromJson(r.Body) + return response, BuildResponse(r) } } @@ -2889,7 +3253,9 @@ func (c *Client4) ExecuteCommandWithTeam(channelId, teamId, command string) (*Co return nil, BuildErrorResponse(r, err) } else { defer closeBody(r) - return CommandResponseFromJson(r.Body), BuildResponse(r) + + response, _ := CommandResponseFromJson(r.Body) + return response, BuildResponse(r) } } @@ -2996,6 +3362,18 @@ func (c *Client4) GetEmojiList(page, perPage int) ([]*Emoji, *Response) { } } +// GetSortedEmojiList returns a page of custom emoji on the system sorted based on the sort +// parameter, blank for no sorting and "name" to sort by emoji names. +func (c *Client4) GetSortedEmojiList(page, perPage int, sort string) ([]*Emoji, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v&sort=%v", page, perPage, sort) + if r, err := c.DoApiGet(c.GetEmojisRoute()+query, ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return EmojiListFromJson(r.Body), BuildResponse(r) + } +} + // DeleteEmoji delete an custom emoji on the provided emoji id string. func (c *Client4) DeleteEmoji(emojiId string) (bool, *Response) { if r, err := c.DoApiDelete(c.GetEmojiRoute(emojiId)); err != nil { @@ -3006,7 +3384,7 @@ func (c *Client4) DeleteEmoji(emojiId string) (bool, *Response) { } } -// GetEmoji returns a custom emoji in the system on the provided emoji id string. +// GetEmoji returns a custom emoji based on the emojiId string. func (c *Client4) GetEmoji(emojiId string) (*Emoji, *Response) { if r, err := c.DoApiGet(c.GetEmojiRoute(emojiId), ""); err != nil { return nil, BuildErrorResponse(r, err) @@ -3016,6 +3394,16 @@ func (c *Client4) GetEmoji(emojiId string) (*Emoji, *Response) { } } +// GetEmojiByName returns a custom emoji based on the name string. +func (c *Client4) GetEmojiByName(name string) (*Emoji, *Response) { + if r, err := c.DoApiGet(c.GetEmojiByNameRoute(name), ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return EmojiFromJson(r.Body), BuildResponse(r) + } +} + // GetEmojiImage returns the emoji image. func (c *Client4) GetEmojiImage(emojiId string) ([]byte, *Response) { if r, err := c.DoApiGet(c.GetEmojiRoute(emojiId)+"/image", ""); err != nil { @@ -3031,6 +3419,27 @@ func (c *Client4) GetEmojiImage(emojiId string) ([]byte, *Response) { } } +// SearchEmoji returns a list of emoji matching some search criteria. +func (c *Client4) SearchEmoji(search *EmojiSearch) ([]*Emoji, *Response) { + if r, err := c.DoApiPost(c.GetEmojisRoute()+"/search", search.ToJson()); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return EmojiListFromJson(r.Body), BuildResponse(r) + } +} + +// AutocompleteEmoji returns a list of emoji starting with or matching name. +func (c *Client4) AutocompleteEmoji(name string, etag string) ([]*Emoji, *Response) { + query := fmt.Sprintf("?name=%v", name) + if r, err := c.DoApiGet(c.GetEmojisRoute()+"/autocomplete"+query, ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return EmojiListFromJson(r.Body), BuildResponse(r) + } +} + // Reaction Section // SaveReaction saves an emoji reaction for a post. Returns the saved reaction if successful, otherwise an error will be returned. @@ -3063,6 +3472,18 @@ func (c *Client4) DeleteReaction(reaction *Reaction) (bool, *Response) { } } +// Timezone Section + +// GetSupportedTimezone returns a page of supported timezones on the system. +func (c *Client4) GetSupportedTimezone() (SupportedTimezones, *Response) { + if r, err := c.DoApiGet(c.GetTimezonesRoute(), ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return TimezonesFromJson(r.Body), BuildResponse(r) + } +} + // Open Graph Metadata Section // OpenGraph return the open graph metadata for a particular url if the site have the metadata @@ -3130,6 +3551,120 @@ func (c *Client4) CancelJob(jobId string) (bool, *Response) { } } +// Roles Section + +// GetRole gets a single role by ID. +func (c *Client4) GetRole(id string) (*Role, *Response) { + if r, err := c.DoApiGet(c.GetRolesRoute()+fmt.Sprintf("/%v", id), ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return RoleFromJson(r.Body), BuildResponse(r) + } +} + +// GetRoleByName gets a single role by Name. +func (c *Client4) GetRoleByName(name string) (*Role, *Response) { + if r, err := c.DoApiGet(c.GetRolesRoute()+fmt.Sprintf("/name/%v", name), ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return RoleFromJson(r.Body), BuildResponse(r) + } +} + +// GetRolesByNames returns a list of roles based on the provided role names. +func (c *Client4) GetRolesByNames(roleNames []string) ([]*Role, *Response) { + if r, err := c.DoApiPost(c.GetRolesRoute()+"/names", ArrayToJson(roleNames)); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return RoleListFromJson(r.Body), BuildResponse(r) + } +} + +// PatchRole partially updates a role in the system. Any missing fields are not updated. +func (c *Client4) PatchRole(roleId string, patch *RolePatch) (*Role, *Response) { + if r, err := c.DoApiPut(c.GetRolesRoute()+fmt.Sprintf("/%v/patch", roleId), patch.ToJson()); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return RoleFromJson(r.Body), BuildResponse(r) + } +} + +// Schemes Section + +// CreateScheme creates a new Scheme. +func (c *Client4) CreateScheme(scheme *Scheme) (*Scheme, *Response) { + if r, err := c.DoApiPost(c.GetSchemesRoute(), scheme.ToJson()); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return SchemeFromJson(r.Body), BuildResponse(r) + } +} + +// GetScheme gets a single scheme by ID. +func (c *Client4) GetScheme(id string) (*Scheme, *Response) { + if r, err := c.DoApiGet(c.GetSchemeRoute(id), ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return SchemeFromJson(r.Body), BuildResponse(r) + } +} + +// Get all schemes, sorted with the most recently created first, optionally filtered by scope. +func (c *Client4) GetSchemes(scope string, page int, perPage int) ([]*Scheme, *Response) { + if r, err := c.DoApiGet(c.GetSchemesRoute()+fmt.Sprintf("?scope=%v&page=%v&per_page=%v", scope, page, perPage), ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return SchemesFromJson(r.Body), BuildResponse(r) + } +} + +// DeleteScheme deletes a single scheme by ID. +func (c *Client4) DeleteScheme(id string) (bool, *Response) { + if r, err := c.DoApiDelete(c.GetSchemeRoute(id)); err != nil { + return false, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) + } +} + +// PatchScheme partially updates a scheme in the system. Any missing fields are not updated. +func (c *Client4) PatchScheme(id string, patch *SchemePatch) (*Scheme, *Response) { + if r, err := c.DoApiPut(c.GetSchemeRoute(id)+"/patch", patch.ToJson()); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return SchemeFromJson(r.Body), BuildResponse(r) + } +} + +// Get the teams using this scheme, sorted alphabetically by display name. +func (c *Client4) GetTeamsForScheme(schemeId string, page int, perPage int) ([]*Team, *Response) { + if r, err := c.DoApiGet(c.GetSchemeRoute(schemeId)+fmt.Sprintf("/teams?page=%v&per_page=%v", page, perPage), ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return TeamListFromJson(r.Body), BuildResponse(r) + } +} + +// Get the channels using this scheme, sorted alphabetically by display name. +func (c *Client4) GetChannelsForScheme(schemeId string, page int, perPage int) (ChannelList, *Response) { + if r, err := c.DoApiGet(c.GetSchemeRoute(schemeId)+fmt.Sprintf("/channels?page=%v&per_page=%v", page, perPage), ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return *ChannelListFromJson(r.Body), BuildResponse(r) + } +} + // Plugin Section // UploadPlugin takes an io.Reader stream pointing to the contents of a .tar.gz plugin. @@ -3150,7 +3685,6 @@ func (c *Client4) UploadPlugin(file io.Reader) (*Manifest, *Response) { rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetPluginsRoute(), body) rq.Header.Set("Content-Type", writer.FormDataContentType()) - rq.Close = true if len(c.AuthToken) > 0 { rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) @@ -3180,6 +3714,18 @@ func (c *Client4) GetPlugins() (*PluginsResponse, *Response) { } } +// GetPluginStatuses will return the plugins installed on any server in the cluster, for reporting +// to the administrator via the system console. +// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. +func (c *Client4) GetPluginStatuses() (PluginStatuses, *Response) { + if r, err := c.DoApiGet(c.GetPluginsRoute(), "/statuses"); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return PluginStatusesFromJson(r.Body), BuildResponse(r) + } +} + // RemovePlugin will deactivate and delete a plugin. // WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. func (c *Client4) RemovePlugin(id string) (bool, *Response) { @@ -3204,8 +3750,8 @@ func (c *Client4) GetWebappPlugins() ([]*Manifest, *Response) { // ActivatePlugin will activate an plugin installed. // WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. -func (c *Client4) ActivatePlugin(id string) (bool, *Response) { - if r, err := c.DoApiPost(c.GetPluginRoute(id)+"/activate", ""); err != nil { +func (c *Client4) EnablePlugin(id string) (bool, *Response) { + if r, err := c.DoApiPost(c.GetPluginRoute(id)+"/enable", ""); err != nil { return false, BuildErrorResponse(r, err) } else { defer closeBody(r) @@ -3215,11 +3761,79 @@ func (c *Client4) ActivatePlugin(id string) (bool, *Response) { // DeactivatePlugin will deactivate an active plugin. // WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. -func (c *Client4) DeactivatePlugin(id string) (bool, *Response) { - if r, err := c.DoApiPost(c.GetPluginRoute(id)+"/deactivate", ""); err != nil { +func (c *Client4) DisablePlugin(id string) (bool, *Response) { + if r, err := c.DoApiPost(c.GetPluginRoute(id)+"/disable", ""); err != nil { return false, BuildErrorResponse(r, err) } else { defer closeBody(r) return CheckStatusOK(r), BuildResponse(r) } } + +// UpdateChannelScheme will update a channel's scheme. +func (c *Client4) UpdateChannelScheme(channelId, schemeId string) (bool, *Response) { + sip := &SchemeIDPatch{SchemeID: &schemeId} + if r, err := c.DoApiPut(c.GetChannelSchemeRoute(channelId), sip.ToJson()); err != nil { + return false, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) + } +} + +// UpdateTeamScheme will update a team's scheme. +func (c *Client4) UpdateTeamScheme(teamId, schemeId string) (bool, *Response) { + sip := &SchemeIDPatch{SchemeID: &schemeId} + if r, err := c.DoApiPut(c.GetTeamSchemeRoute(teamId), sip.ToJson()); err != nil { + return false, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) + } +} + +// GetRedirectLocation retrieves the value of the 'Location' header of an HTTP response for a given URL. +func (c *Client4) GetRedirectLocation(urlParam, etag string) (string, *Response) { + url := fmt.Sprintf("%s?url=%s", c.GetRedirectLocationRoute(), url.QueryEscape(urlParam)) + if r, err := c.DoApiGet(url, etag); err != nil { + return "", BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return MapFromJson(r.Body)["location"], BuildResponse(r) + } +} + +func (c *Client4) RegisteTermsOfServiceAction(userId, termsOfServiceId string, accepted bool) (*bool, *Response) { + url := c.GetRegisterTermsOfServiceRoute(userId) + data := map[string]interface{}{"termsOfServiceId": termsOfServiceId, "accepted": accepted} + + if r, err := c.DoApiPost(url, StringInterfaceToJson(data)); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return NewBool(CheckStatusOK(r)), BuildResponse(r) + } +} + +func (c *Client4) GetTermsOfService(etag string) (*TermsOfService, *Response) { + url := c.GetTermsOfServiceRoute() + + if r, err := c.DoApiGet(url, etag); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return TermsOfServiceFromJson(r.Body), BuildResponse(r) + } +} + +func (c *Client4) CreateTermsOfService(text, userId string) (*TermsOfService, *Response) { + url := c.GetTermsOfServiceRoute() + + data := map[string]string{"text": text} + if r, err := c.DoApiPost(url, MapToJson(data)); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return TermsOfServiceFromJson(r.Body), BuildResponse(r) + } +} diff --git a/vendor/github.com/mattermost/platform/model/cluster_discovery.go b/vendor/github.com/mattermost/mattermost-server/model/cluster_discovery.go similarity index 74% rename from vendor/github.com/mattermost/platform/model/cluster_discovery.go rename to vendor/github.com/mattermost/mattermost-server/model/cluster_discovery.go index 89e5fc95..796d3dda 100644 --- a/vendor/github.com/mattermost/platform/model/cluster_discovery.go +++ b/vendor/github.com/mattermost/mattermost-server/model/cluster_discovery.go @@ -86,27 +86,27 @@ func FilterClusterDiscovery(vs []*ClusterDiscovery, f func(*ClusterDiscovery) bo func (o *ClusterDiscovery) IsValid() *AppError { if len(o.Id) != 26 { - return NewAppError("Channel.IsValid", "model.channel.is_valid.id.app_error", nil, "", http.StatusBadRequest) + return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.id.app_error", nil, "", http.StatusBadRequest) } if len(o.ClusterName) == 0 { - return NewAppError("ClusterDiscovery.IsValid", "ClusterName must be set", nil, "", http.StatusBadRequest) + return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.name.app_error", nil, "", http.StatusBadRequest) } if len(o.Type) == 0 { - return NewAppError("ClusterDiscovery.IsValid", "Type must be set", nil, "", http.StatusBadRequest) + return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.type.app_error", nil, "", http.StatusBadRequest) } if len(o.Hostname) == 0 { - return NewAppError("ClusterDiscovery.IsValid", "Hostname must be set", nil, "", http.StatusBadRequest) + return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.hostname.app_error", nil, "", http.StatusBadRequest) } if o.CreateAt == 0 { - return NewAppError("ClusterDiscovery.IsValid", "CreateAt must be set", nil, "", http.StatusBadRequest) + return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.create_at.app_error", nil, "", http.StatusBadRequest) } if o.LastPingAt == 0 { - return NewAppError("ClusterDiscovery.IsValid", "LastPingAt must be set", nil, "", http.StatusBadRequest) + return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.last_ping_at.app_error", nil, "", http.StatusBadRequest) } return nil diff --git a/vendor/github.com/mattermost/platform/model/cluster_info.go b/vendor/github.com/mattermost/mattermost-server/model/cluster_info.go similarity index 62% rename from vendor/github.com/mattermost/platform/model/cluster_info.go rename to vendor/github.com/mattermost/mattermost-server/model/cluster_info.go index c4f7e89a..46a3487a 100644 --- a/vendor/github.com/mattermost/platform/model/cluster_info.go +++ b/vendor/github.com/mattermost/mattermost-server/model/cluster_info.go @@ -6,7 +6,6 @@ package model import ( "encoding/json" "io" - "strings" ) type ClusterInfo struct { @@ -18,36 +17,19 @@ type ClusterInfo struct { } func (me *ClusterInfo) ToJson() string { - b, err := json.Marshal(me) - if err != nil { - return "" - } else { - return string(b) - } -} - -func (me *ClusterInfo) Copy() *ClusterInfo { - json := me.ToJson() - return ClusterInfoFromJson(strings.NewReader(json)) + b, _ := json.Marshal(me) + return string(b) } func ClusterInfoFromJson(data io.Reader) *ClusterInfo { - decoder := json.NewDecoder(data) - var me ClusterInfo - err := decoder.Decode(&me) - if err == nil { - return &me - } else { - return nil - } + var me *ClusterInfo + json.NewDecoder(data).Decode(&me) + return me } func ClusterInfosToJson(objmap []*ClusterInfo) string { - if b, err := json.Marshal(objmap); err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(objmap) + return string(b) } func ClusterInfosFromJson(data io.Reader) []*ClusterInfo { diff --git a/vendor/github.com/mattermost/platform/model/cluster_message.go b/vendor/github.com/mattermost/mattermost-server/model/cluster_message.go similarity index 86% rename from vendor/github.com/mattermost/platform/model/cluster_message.go rename to vendor/github.com/mattermost/mattermost-server/model/cluster_message.go index a6dec2e7..d02da3ee 100644 --- a/vendor/github.com/mattermost/platform/model/cluster_message.go +++ b/vendor/github.com/mattermost/mattermost-server/model/cluster_message.go @@ -21,6 +21,8 @@ const ( CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL = "inv_channel" CLUSTER_EVENT_INVALIDATE_CACHE_FOR_USER = "inv_user" CLUSTER_EVENT_CLEAR_SESSION_CACHE_FOR_USER = "clear_session_user" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_ROLES = "inv_roles" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_SCHEMES = "inv_schemes" CLUSTER_SEND_BEST_EFFORT = "best_effort" CLUSTER_SEND_RELIABLE = "reliable" @@ -35,21 +37,12 @@ type ClusterMessage struct { } func (o *ClusterMessage) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func ClusterMessageFromJson(data io.Reader) *ClusterMessage { - decoder := json.NewDecoder(data) - var o ClusterMessage - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *ClusterMessage + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/platform/model/cluster_stats.go b/vendor/github.com/mattermost/mattermost-server/model/cluster_stats.go similarity index 70% rename from vendor/github.com/mattermost/platform/model/cluster_stats.go rename to vendor/github.com/mattermost/mattermost-server/model/cluster_stats.go index 879487c6..064f7b81 100644 --- a/vendor/github.com/mattermost/platform/model/cluster_stats.go +++ b/vendor/github.com/mattermost/mattermost-server/model/cluster_stats.go @@ -16,21 +16,12 @@ type ClusterStats struct { } func (me *ClusterStats) ToJson() string { - b, err := json.Marshal(me) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(me) + return string(b) } func ClusterStatsFromJson(data io.Reader) *ClusterStats { - decoder := json.NewDecoder(data) - var me ClusterStats - err := decoder.Decode(&me) - if err == nil { - return &me - } else { - return nil - } + var me *ClusterStats + json.NewDecoder(data).Decode(&me) + return me } diff --git a/vendor/github.com/mattermost/platform/model/command.go b/vendor/github.com/mattermost/mattermost-server/model/command.go similarity index 89% rename from vendor/github.com/mattermost/platform/model/command.go rename to vendor/github.com/mattermost/mattermost-server/model/command.go index 69da41c1..b23e5020 100644 --- a/vendor/github.com/mattermost/platform/model/command.go +++ b/vendor/github.com/mattermost/mattermost-server/model/command.go @@ -38,43 +38,25 @@ type Command struct { } func (o *Command) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func CommandFromJson(data io.Reader) *Command { - decoder := json.NewDecoder(data) - var o Command - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *Command + json.NewDecoder(data).Decode(&o) + return o } func CommandListToJson(l []*Command) string { - b, err := json.Marshal(l) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(l) + return string(b) } func CommandListFromJson(data io.Reader) []*Command { - decoder := json.NewDecoder(data) var o []*Command - err := decoder.Decode(&o) - if err == nil { - return o - } else { - return nil - } + json.NewDecoder(data).Decode(&o) + return o } func (o *Command) IsValid() *AppError { diff --git a/vendor/github.com/mattermost/platform/model/command_args.go b/vendor/github.com/mattermost/mattermost-server/model/command_args.go similarity index 77% rename from vendor/github.com/mattermost/platform/model/command_args.go rename to vendor/github.com/mattermost/mattermost-server/model/command_args.go index 76b03a2f..4a635a1a 100644 --- a/vendor/github.com/mattermost/platform/model/command_args.go +++ b/vendor/github.com/mattermost/mattermost-server/model/command_args.go @@ -23,21 +23,12 @@ type CommandArgs struct { } func (o *CommandArgs) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func CommandArgsFromJson(data io.Reader) *CommandArgs { - decoder := json.NewDecoder(data) - var o CommandArgs - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *CommandArgs + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/platform/model/command_response.go b/vendor/github.com/mattermost/mattermost-server/model/command_response.go similarity index 73% rename from vendor/github.com/mattermost/platform/model/command_response.go rename to vendor/github.com/mattermost/mattermost-server/model/command_response.go index a3a171ce..1ed5286d 100644 --- a/vendor/github.com/mattermost/platform/model/command_response.go +++ b/vendor/github.com/mattermost/mattermost-server/model/command_response.go @@ -8,6 +8,8 @@ import ( "io" "io/ioutil" "strings" + + "github.com/mattermost/mattermost-server/utils/jsonutils" ) const ( @@ -27,22 +29,18 @@ type CommandResponse struct { } func (o *CommandResponse) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } -func CommandResponseFromHTTPBody(contentType string, body io.Reader) *CommandResponse { +func CommandResponseFromHTTPBody(contentType string, body io.Reader) (*CommandResponse, error) { if strings.TrimSpace(strings.Split(contentType, ";")[0]) == "application/json" { return CommandResponseFromJson(body) } if b, err := ioutil.ReadAll(body); err == nil { - return CommandResponseFromPlainText(string(b)) + return CommandResponseFromPlainText(string(b)), nil } - return nil + return nil, nil } func CommandResponseFromPlainText(text string) *CommandResponse { @@ -51,15 +49,19 @@ func CommandResponseFromPlainText(text string) *CommandResponse { } } -func CommandResponseFromJson(data io.Reader) *CommandResponse { - decoder := json.NewDecoder(data) - var o CommandResponse +func CommandResponseFromJson(data io.Reader) (*CommandResponse, error) { + b, err := ioutil.ReadAll(data) + if err != nil { + return nil, err + } - if err := decoder.Decode(&o); err != nil { - return nil + var o CommandResponse + err = json.Unmarshal(b, &o) + if err != nil { + return nil, jsonutils.HumanizeJsonError(err, b) } o.Attachments = StringifySlackFieldValue(o.Attachments) - return &o + return &o, nil } diff --git a/vendor/github.com/mattermost/platform/model/command_webhook.go b/vendor/github.com/mattermost/mattermost-server/model/command_webhook.go similarity index 100% rename from vendor/github.com/mattermost/platform/model/command_webhook.go rename to vendor/github.com/mattermost/mattermost-server/model/command_webhook.go diff --git a/vendor/github.com/mattermost/platform/model/compliance.go b/vendor/github.com/mattermost/mattermost-server/model/compliance.go similarity index 87% rename from vendor/github.com/mattermost/platform/model/compliance.go rename to vendor/github.com/mattermost/mattermost-server/model/compliance.go index 3134ddba..5546b783 100644 --- a/vendor/github.com/mattermost/platform/model/compliance.go +++ b/vendor/github.com/mattermost/mattermost-server/model/compliance.go @@ -38,12 +38,8 @@ type Compliance struct { type Compliances []Compliance func (o *Compliance) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func (me *Compliance) PreSave() { @@ -56,7 +52,7 @@ func (me *Compliance) PreSave() { } me.Count = 0 - me.Emails = strings.ToLower(me.Emails) + me.Emails = NormalizeEmail(me.Emails) me.Keywords = strings.ToLower(me.Keywords) me.CreateAt = GetMillis() @@ -103,14 +99,9 @@ func (me *Compliance) IsValid() *AppError { } func ComplianceFromJson(data io.Reader) *Compliance { - decoder := json.NewDecoder(data) - var o Compliance - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *Compliance + json.NewDecoder(data).Decode(&o) + return o } func (o Compliances) ToJson() string { @@ -122,12 +113,7 @@ func (o Compliances) ToJson() string { } func CompliancesFromJson(data io.Reader) Compliances { - decoder := json.NewDecoder(data) var o Compliances - err := decoder.Decode(&o) - if err == nil { - return o - } else { - return nil - } + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/platform/model/compliance_post.go b/vendor/github.com/mattermost/mattermost-server/model/compliance_post.go similarity index 96% rename from vendor/github.com/mattermost/platform/model/compliance_post.go rename to vendor/github.com/mattermost/mattermost-server/model/compliance_post.go index 3751c586..75e8de1f 100644 --- a/vendor/github.com/mattermost/platform/model/compliance_post.go +++ b/vendor/github.com/mattermost/mattermost-server/model/compliance_post.go @@ -17,6 +17,7 @@ type CompliancePost struct { // From Channel ChannelName string ChannelDisplayName string + ChannelType string // From User UserUsername string @@ -45,6 +46,7 @@ func CompliancePostHeader() []string { "ChannelName", "ChannelDisplayName", + "ChannelType", "UserUsername", "UserEmail", @@ -92,6 +94,7 @@ func (me *CompliancePost) Row() []string { cleanComplianceStrings(me.ChannelName), cleanComplianceStrings(me.ChannelDisplayName), + cleanComplianceStrings(me.ChannelType), cleanComplianceStrings(me.UserUsername), cleanComplianceStrings(me.UserEmail), diff --git a/vendor/github.com/mattermost/platform/model/config.go b/vendor/github.com/mattermost/mattermost-server/model/config.go similarity index 74% rename from vendor/github.com/mattermost/platform/model/config.go rename to vendor/github.com/mattermost/mattermost-server/model/config.go index fb34d1a0..d59b8d6d 100644 --- a/vendor/github.com/mattermost/platform/model/config.go +++ b/vendor/github.com/mattermost/mattermost-server/model/config.go @@ -6,8 +6,12 @@ package model import ( "encoding/json" "io" + "math" + "net" "net/http" "net/url" + "regexp" + "strconv" "strings" "time" ) @@ -21,6 +25,7 @@ const ( IMAGE_DRIVER_LOCAL = "local" IMAGE_DRIVER_S3 = "amazons3" + DATABASE_DRIVER_SQLITE = "sqlite3" DATABASE_DRIVER_MYSQL = "mysql" DATABASE_DRIVER_POSTGRES = "postgres" @@ -35,10 +40,6 @@ const ( SERVICE_GOOGLE = "google" SERVICE_OFFICE365 = "office365" - WEBSERVER_MODE_REGULAR = "regular" - WEBSERVER_MODE_GZIP = "gzip" - WEBSERVER_MODE_DISABLED = "disabled" - GENERIC_NO_CHANNEL_NOTIFICATION = "generic_no_channel" GENERIC_NOTIFICATION = "generic" FULL_NOTIFICATION = "full" @@ -69,6 +70,10 @@ const ( ALLOW_EDIT_POST_NEVER = "never" ALLOW_EDIT_POST_TIME_LIMIT = "time_limit" + GROUP_UNREAD_CHANNELS_DISABLED = "disabled" + GROUP_UNREAD_CHANNELS_DEFAULT_ON = "default_on" + GROUP_UNREAD_CHANNELS_DEFAULT_OFF = "default_off" + EMAIL_BATCHING_BUFFER_SIZE = 256 EMAIL_BATCHING_INTERVAL = 30 @@ -85,6 +90,8 @@ const ( SERVICE_SETTINGS_DEFAULT_MAX_LOGIN_ATTEMPTS = 10 SERVICE_SETTINGS_DEFAULT_ALLOW_CORS_FROM = "" SERVICE_SETTINGS_DEFAULT_LISTEN_AND_ADDRESS = ":8065" + SERVICE_SETTINGS_DEFAULT_GFYCAT_API_KEY = "2_KtH_W5" + SERVICE_SETTINGS_DEFAULT_GFYCAT_API_SECRET = "3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof" TEAM_SETTINGS_DEFAULT_MAX_USERS_PER_TEAM = 50 TEAM_SETTINGS_DEFAULT_CUSTOM_BRAND_TEXT = "" @@ -93,17 +100,16 @@ const ( SQL_SETTINGS_DEFAULT_DATA_SOURCE = "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s" + FILE_SETTINGS_DEFAULT_DIRECTORY = "./data/" + EMAIL_SETTINGS_DEFAULT_FEEDBACK_ORGANIZATION = "" - SUPPORT_SETTINGS_DEFAULT_TERMS_OF_SERVICE_LINK = "https://about.mattermost.com/default-terms/" - SUPPORT_SETTINGS_DEFAULT_PRIVACY_POLICY_LINK = "https://about.mattermost.com/default-privacy-policy/" - SUPPORT_SETTINGS_DEFAULT_ABOUT_LINK = "https://about.mattermost.com/default-about/" - SUPPORT_SETTINGS_DEFAULT_HELP_LINK = "https://about.mattermost.com/default-help/" - SUPPORT_SETTINGS_DEFAULT_REPORT_A_PROBLEM_LINK = "https://about.mattermost.com/default-report-a-problem/" - SUPPORT_SETTINGS_DEFAULT_ADMINISTRATORS_GUIDE_LINK = "https://about.mattermost.com/administrators-guide/" - SUPPORT_SETTINGS_DEFAULT_TROUBLESHOOTING_FORUM_LINK = "https://about.mattermost.com/troubleshooting-forum/" - SUPPORT_SETTINGS_DEFAULT_COMMERCIAL_SUPPORT_LINK = "https://about.mattermost.com/commercial-support/" - SUPPORT_SETTINGS_DEFAULT_SUPPORT_EMAIL = "feedback@mattermost.com" + SUPPORT_SETTINGS_DEFAULT_TERMS_OF_SERVICE_LINK = "https://about.mattermost.com/default-terms/" + SUPPORT_SETTINGS_DEFAULT_PRIVACY_POLICY_LINK = "https://about.mattermost.com/default-privacy-policy/" + SUPPORT_SETTINGS_DEFAULT_ABOUT_LINK = "https://about.mattermost.com/default-about/" + SUPPORT_SETTINGS_DEFAULT_HELP_LINK = "https://about.mattermost.com/default-help/" + SUPPORT_SETTINGS_DEFAULT_REPORT_A_PROBLEM_LINK = "https://about.mattermost.com/default-report-a-problem/" + SUPPORT_SETTINGS_DEFAULT_SUPPORT_EMAIL = "feedback@mattermost.com" LDAP_SETTINGS_DEFAULT_FIRST_NAME_ATTRIBUTE = "" LDAP_SETTINGS_DEFAULT_LAST_NAME_ATTRIBUTE = "" @@ -114,6 +120,7 @@ const ( LDAP_SETTINGS_DEFAULT_POSITION_ATTRIBUTE = "" LDAP_SETTINGS_DEFAULT_LOGIN_FIELD_NAME = "" + SAML_SETTINGS_DEFAULT_ID_ATTRIBUTE = "" SAML_SETTINGS_DEFAULT_FIRST_NAME_ATTRIBUTE = "" SAML_SETTINGS_DEFAULT_LAST_NAME_ATTRIBUTE = "" SAML_SETTINGS_DEFAULT_EMAIL_ATTRIBUTE = "" @@ -154,72 +161,113 @@ const ( PLUGIN_SETTINGS_DEFAULT_DIRECTORY = "./plugins" PLUGIN_SETTINGS_DEFAULT_CLIENT_DIRECTORY = "./client/plugins" + + TIMEZONE_SETTINGS_DEFAULT_SUPPORTED_TIMEZONES_PATH = "timezones.json" + + COMPLIANCE_EXPORT_TYPE_CSV = "csv" + COMPLIANCE_EXPORT_TYPE_ACTIANCE = "actiance" + COMPLIANCE_EXPORT_TYPE_GLOBALRELAY = "globalrelay" + GLOBALRELAY_CUSTOMER_TYPE_A9 = "A9" + GLOBALRELAY_CUSTOMER_TYPE_A10 = "A10" + + CLIENT_SIDE_CERT_CHECK_PRIMARY_AUTH = "primary" + CLIENT_SIDE_CERT_CHECK_SECONDARY_AUTH = "secondary" ) type ServiceSettings struct { - SiteURL *string - LicenseFileLocation *string - ListenAddress *string - ConnectionSecurity *string - TLSCertFile *string - TLSKeyFile *string - UseLetsEncrypt *bool - LetsEncryptCertificateCacheFile *string - Forward80To443 *bool - ReadTimeout *int - WriteTimeout *int - MaximumLoginAttempts *int - GoroutineHealthThreshold *int - GoogleDeveloperKey string - EnableOAuthServiceProvider bool - EnableIncomingWebhooks bool - EnableOutgoingWebhooks bool - EnableCommands *bool - EnableOnlyAdminIntegrations *bool - EnablePostUsernameOverride bool - EnablePostIconOverride bool - EnableAPIv3 *bool - EnableLinkPreviews *bool - EnableTesting bool - EnableDeveloper *bool - EnableSecurityFixAlert *bool - EnableInsecureOutgoingConnections *bool - AllowedUntrustedInternalConnections *string - EnableMultifactorAuthentication *bool - EnforceMultifactorAuthentication *bool - EnableUserAccessTokens *bool - AllowCorsFrom *string - SessionLengthWebInDays *int - SessionLengthMobileInDays *int - SessionLengthSSOInDays *int - SessionCacheInMinutes *int - SessionIdleTimeoutInMinutes *int - WebsocketSecurePort *int - WebsocketPort *int - WebserverMode *string - EnableCustomEmoji *bool - EnableEmojiPicker *bool - RestrictCustomEmojiCreation *string - RestrictPostDelete *string - AllowEditPost *string - PostEditTimeLimit *int - TimeBetweenUserTypingUpdatesMilliseconds *int64 - EnablePostSearch *bool - EnableUserTypingMessages *bool - EnableChannelViewedMessages *bool - EnableUserStatuses *bool - ExperimentalEnableAuthenticationTransfer *bool - ClusterLogTimeoutMilliseconds *int - CloseUnusedDirectMessages *bool - EnablePreviewFeatures *bool - EnableTutorial *bool + SiteURL *string + WebsocketURL *string + LicenseFileLocation *string + ListenAddress *string + ConnectionSecurity *string + TLSCertFile *string + TLSKeyFile *string + UseLetsEncrypt *bool + LetsEncryptCertificateCacheFile *string + Forward80To443 *bool + ReadTimeout *int + WriteTimeout *int + MaximumLoginAttempts *int + GoroutineHealthThreshold *int + GoogleDeveloperKey string + EnableOAuthServiceProvider bool + EnableIncomingWebhooks bool + EnableOutgoingWebhooks bool + EnableCommands *bool + EnableOnlyAdminIntegrations *bool + EnablePostUsernameOverride bool + EnablePostIconOverride bool + EnableLinkPreviews *bool + EnableTesting bool + EnableDeveloper *bool + EnableSecurityFixAlert *bool + EnableInsecureOutgoingConnections *bool + AllowedUntrustedInternalConnections *string + EnableMultifactorAuthentication *bool + EnforceMultifactorAuthentication *bool + EnableUserAccessTokens *bool + AllowCorsFrom *string + CorsExposedHeaders *string + CorsAllowCredentials *bool + CorsDebug *bool + AllowCookiesForSubdomains *bool + SessionLengthWebInDays *int + SessionLengthMobileInDays *int + SessionLengthSSOInDays *int + SessionCacheInMinutes *int + SessionIdleTimeoutInMinutes *int + WebsocketSecurePort *int + WebsocketPort *int + WebserverMode *string + EnableCustomEmoji *bool + EnableEmojiPicker *bool + EnableGifPicker *bool + GfycatApiKey *string + GfycatApiSecret *string + RestrictCustomEmojiCreation *string + RestrictPostDelete *string + AllowEditPost *string + PostEditTimeLimit *int + TimeBetweenUserTypingUpdatesMilliseconds *int64 + EnablePostSearch *bool + EnableUserTypingMessages *bool + EnableChannelViewedMessages *bool + EnableUserStatuses *bool + ExperimentalEnableAuthenticationTransfer *bool + ClusterLogTimeoutMilliseconds *int + CloseUnusedDirectMessages *bool + EnablePreviewFeatures *bool + EnableTutorial *bool + ExperimentalEnableDefaultChannelLeaveJoinMessages *bool + ExperimentalGroupUnreadChannels *string + ExperimentalChannelOrganization *bool + ImageProxyType *string + ImageProxyURL *string + ImageProxyOptions *string + EnableAPITeamDeletion *bool + ExperimentalEnableHardenedMode *bool + ExperimentalLimitClientConfig *bool + EnableEmailInvitations *bool } func (s *ServiceSettings) SetDefaults() { + if s.EnableEmailInvitations == nil { + // If the site URL is also not present then assume this is a clean install + if s.SiteURL == nil { + s.EnableEmailInvitations = NewBool(false) + } else { + s.EnableEmailInvitations = NewBool(true) + } + } + if s.SiteURL == nil { s.SiteURL = NewString(SERVICE_SETTINGS_DEFAULT_SITE_URL) } + if s.WebsocketURL == nil { + s.WebsocketURL = NewString("") + } + if s.LicenseFileLocation == nil { s.LicenseFileLocation = NewString("") } @@ -228,10 +276,6 @@ func (s *ServiceSettings) SetDefaults() { s.ListenAddress = NewString(SERVICE_SETTINGS_DEFAULT_LISTEN_AND_ADDRESS) } - if s.EnableAPIv3 == nil { - s.EnableAPIv3 = NewBool(true) - } - if s.EnableLinkPreviews == nil { s.EnableLinkPreviews = NewBool(false) } @@ -249,7 +293,7 @@ func (s *ServiceSettings) SetDefaults() { } if s.AllowedUntrustedInternalConnections == nil { - s.AllowedUntrustedInternalConnections = new(string) + s.AllowedUntrustedInternalConnections = NewString("") } if s.EnableMultifactorAuthentication == nil { @@ -376,6 +420,22 @@ func (s *ServiceSettings) SetDefaults() { s.AllowCorsFrom = NewString(SERVICE_SETTINGS_DEFAULT_ALLOW_CORS_FROM) } + if s.CorsExposedHeaders == nil { + s.CorsExposedHeaders = NewString("") + } + + if s.CorsAllowCredentials == nil { + s.CorsAllowCredentials = NewBool(false) + } + + if s.CorsDebug == nil { + s.CorsDebug = NewBool(false) + } + + if s.AllowCookiesForSubdomains == nil { + s.AllowCookiesForSubdomains = NewBool(false) + } + if s.WebserverMode == nil { s.WebserverMode = NewString("gzip") } else if *s.WebserverMode == "regular" { @@ -390,6 +450,18 @@ func (s *ServiceSettings) SetDefaults() { s.EnableEmojiPicker = NewBool(true) } + if s.EnableGifPicker == nil { + s.EnableGifPicker = NewBool(false) + } + + if s.GfycatApiKey == nil || *s.GfycatApiKey == "" { + s.GfycatApiKey = NewString(SERVICE_SETTINGS_DEFAULT_GFYCAT_API_KEY) + } + + if s.GfycatApiSecret == nil || *s.GfycatApiSecret == "" { + s.GfycatApiSecret = NewString(SERVICE_SETTINGS_DEFAULT_GFYCAT_API_SECRET) + } + if s.RestrictCustomEmojiCreation == nil { s.RestrictCustomEmojiCreation = NewString(RESTRICT_EMOJI_CREATION_ALL) } @@ -407,23 +479,67 @@ func (s *ServiceSettings) SetDefaults() { } if s.PostEditTimeLimit == nil { - s.PostEditTimeLimit = NewInt(300) + s.PostEditTimeLimit = NewInt(-1) } if s.EnablePreviewFeatures == nil { s.EnablePreviewFeatures = NewBool(true) } + + if s.ExperimentalEnableDefaultChannelLeaveJoinMessages == nil { + s.ExperimentalEnableDefaultChannelLeaveJoinMessages = NewBool(true) + } + + if s.ExperimentalGroupUnreadChannels == nil { + s.ExperimentalGroupUnreadChannels = NewString(GROUP_UNREAD_CHANNELS_DISABLED) + } else if *s.ExperimentalGroupUnreadChannels == "0" { + s.ExperimentalGroupUnreadChannels = NewString(GROUP_UNREAD_CHANNELS_DISABLED) + } else if *s.ExperimentalGroupUnreadChannels == "1" { + s.ExperimentalGroupUnreadChannels = NewString(GROUP_UNREAD_CHANNELS_DEFAULT_ON) + } + + if s.ExperimentalChannelOrganization == nil { + experimentalUnreadEnabled := *s.ExperimentalGroupUnreadChannels != GROUP_UNREAD_CHANNELS_DISABLED + s.ExperimentalChannelOrganization = NewBool(experimentalUnreadEnabled) + } + + if s.ImageProxyType == nil { + s.ImageProxyType = NewString("") + } + + if s.ImageProxyURL == nil { + s.ImageProxyURL = NewString("") + } + + if s.ImageProxyOptions == nil { + s.ImageProxyOptions = NewString("") + } + + if s.EnableAPITeamDeletion == nil { + s.EnableAPITeamDeletion = NewBool(false) + } + + if s.ExperimentalEnableHardenedMode == nil { + s.ExperimentalEnableHardenedMode = NewBool(false) + } + + if s.ExperimentalLimitClientConfig == nil { + s.ExperimentalLimitClientConfig = NewBool(false) + } } type ClusterSettings struct { - Enable *bool - ClusterName *string - OverrideHostname *string - UseIpAddress *bool - UseExperimentalGossip *bool - ReadOnlyConfig *bool - GossipPort *int - StreamingPort *int + Enable *bool + ClusterName *string + OverrideHostname *string + UseIpAddress *bool + UseExperimentalGossip *bool + ReadOnlyConfig *bool + GossipPort *int + StreamingPort *int + MaxIdleConns *int + MaxIdleConnsPerHost *int + IdleConnTimeoutMilliseconds *int } func (s *ClusterSettings) SetDefaults() { @@ -458,6 +574,18 @@ func (s *ClusterSettings) SetDefaults() { if s.StreamingPort == nil { s.StreamingPort = NewInt(8075) } + + if s.MaxIdleConns == nil { + s.MaxIdleConns = NewInt(100) + } + + if s.MaxIdleConnsPerHost == nil { + s.MaxIdleConnsPerHost = NewInt(128) + } + + if s.IdleConnTimeoutMilliseconds == nil { + s.IdleConnTimeoutMilliseconds = NewInt(90000) + } } type MetricsSettings struct { @@ -480,6 +608,21 @@ func (s *MetricsSettings) SetDefaults() { } } +type ExperimentalSettings struct { + ClientSideCertEnable *bool + ClientSideCertCheck *string +} + +func (s *ExperimentalSettings) SetDefaults() { + if s.ClientSideCertEnable == nil { + s.ClientSideCertEnable = NewBool(false) + } + + if s.ClientSideCertCheck == nil { + s.ClientSideCertCheck = NewString(CLIENT_SIDE_CERT_CHECK_SECONDARY_AUTH) + } +} + type AnalyticsSettings struct { MaxUsersForStatistics *int } @@ -501,15 +644,17 @@ type SSOSettings struct { } type SqlSettings struct { - DriverName *string - DataSource *string - DataSourceReplicas []string - DataSourceSearchReplicas []string - MaxIdleConns *int - MaxOpenConns *int - Trace bool - AtRestEncryptKey string - QueryTimeout *int + DriverName *string + DataSource *string + DataSourceReplicas []string + DataSourceSearchReplicas []string + MaxIdleConns *int + ConnMaxLifetimeMilliseconds *int + MaxOpenConns *int + Trace bool + AtRestEncryptKey string + QueryTimeout *int + EnablePublicChannelsMaterialization *bool } func (s *SqlSettings) SetDefaults() { @@ -533,17 +678,26 @@ func (s *SqlSettings) SetDefaults() { s.MaxOpenConns = NewInt(300) } + if s.ConnMaxLifetimeMilliseconds == nil { + s.ConnMaxLifetimeMilliseconds = NewInt(3600000) + } + if s.QueryTimeout == nil { s.QueryTimeout = NewInt(30) } + + if s.EnablePublicChannelsMaterialization == nil { + s.EnablePublicChannelsMaterialization = NewBool(true) + } } type LogSettings struct { EnableConsole bool ConsoleLevel string + ConsoleJson *bool EnableFile bool FileLevel string - FileFormat string + FileJson *bool FileLocation string EnableWebhookDebugging bool EnableDiagnostics *bool @@ -553,6 +707,14 @@ func (s *LogSettings) SetDefaults() { if s.EnableDiagnostics == nil { s.EnableDiagnostics = NewBool(true) } + + if s.ConsoleJson == nil { + s.ConsoleJson = NewBool(true) + } + + if s.FileJson == nil { + s.FileJson = NewBool(true) + } } type PasswordSettings struct { @@ -654,12 +816,12 @@ func (s *FileSettings) SetDefaults() { } if s.InitialFont == "" { - // Defaults to "luximbi.ttf" - s.InitialFont = "luximbi.ttf" + // Defaults to "nunito-bold.ttf" + s.InitialFont = "nunito-bold.ttf" } if s.Directory == "" { - s.Directory = "./data/" + s.Directory = FILE_SETTINGS_DEFAULT_DIRECTORY } } @@ -686,6 +848,7 @@ type EmailSettings struct { EnableEmailBatching *bool EmailBatchingBufferSize *int EmailBatchingInterval *int + EnablePreviewModeBanner *bool SkipServerCertificateVerification *bool EmailNotificationContentsType *string LoginButtonColor *string @@ -738,6 +901,10 @@ func (s *EmailSettings) SetDefaults() { s.EmailBatchingInterval = NewInt(EMAIL_BATCHING_INTERVAL) } + if s.EnablePreviewModeBanner == nil { + s.EnablePreviewModeBanner = NewBool(true) + } + if s.EnableSMTPAuth == nil { s.EnableSMTPAuth = new(bool) if s.ConnectionSecurity == CONN_SECURITY_NONE { @@ -772,12 +939,28 @@ func (s *EmailSettings) SetDefaults() { } } +type ExtensionSettings struct { + EnableExperimentalExtensions *bool + AllowedExtensionsIDs []string +} + +func (s *ExtensionSettings) SetDefaults() { + if s.EnableExperimentalExtensions == nil { + s.EnableExperimentalExtensions = NewBool(false) + } + + if s.AllowedExtensionsIDs == nil { + s.AllowedExtensionsIDs = []string{} + } +} + type RateLimitSettings struct { Enable *bool PerSec *int MaxBurst *int MemoryStoreSize *int - VaryByRemoteAddr bool + VaryByRemoteAddr *bool + VaryByUser *bool VaryByHeader string } @@ -797,6 +980,14 @@ func (s *RateLimitSettings) SetDefaults() { if s.MemoryStoreSize == nil { s.MemoryStoreSize = NewInt(10000) } + + if s.VaryByRemoteAddr == nil { + s.VaryByRemoteAddr = NewBool(true) + } + + if s.VaryByUser == nil { + s.VaryByUser = NewBool(false) + } } type PrivacySettings struct { @@ -805,12 +996,13 @@ type PrivacySettings struct { } type SupportSettings struct { - TermsOfServiceLink *string - PrivacyPolicyLink *string - AboutLink *string - HelpLink *string - ReportAProblemLink *string - SupportEmail *string + TermsOfServiceLink *string + PrivacyPolicyLink *string + AboutLink *string + HelpLink *string + ReportAProblemLink *string + SupportEmail *string + CustomTermsOfServiceEnabled *bool } func (s *SupportSettings) SetDefaults() { @@ -857,6 +1049,10 @@ func (s *SupportSettings) SetDefaults() { if s.SupportEmail == nil { s.SupportEmail = NewString(SUPPORT_SETTINGS_DEFAULT_SUPPORT_EMAIL) } + + if s.CustomTermsOfServiceEnabled == nil { + s.CustomTermsOfServiceEnabled = NewBool(false) + } } type AnnouncementSettings struct { @@ -917,9 +1113,10 @@ func (s *ThemeSettings) SetDefaults() { type TeamSettings struct { SiteName string MaxUsersPerTeam *int - EnableTeamCreation bool - EnableUserCreation bool + EnableTeamCreation *bool + EnableUserCreation *bool EnableOpenServer *bool + EnableUserDeactivation *bool RestrictCreationToDomains string EnableCustomBrand *bool CustomBrandText *string @@ -939,8 +1136,12 @@ type TeamSettings struct { MaxNotificationsPerChannel *int64 EnableConfirmNotificationsToChannel *bool TeammateNameDisplay *string + ExperimentalViewArchivedChannels *bool + ExperimentalEnableAutomaticReplies *bool + ExperimentalHideTownSquareinLHS *bool ExperimentalTownSquareIsReadOnly *bool ExperimentalPrimaryTeam *string + ExperimentalDefaultChannels []string } func (s *TeamSettings) SetDefaults() { @@ -952,6 +1153,10 @@ func (s *TeamSettings) SetDefaults() { s.EnableCustomBrand = NewBool(false) } + if s.EnableUserDeactivation == nil { + s.EnableUserDeactivation = NewBool(false) + } + if s.CustomBrandText == nil { s.CustomBrandText = NewString(TEAM_SETTINGS_DEFAULT_CUSTOM_BRAND_TEXT) } @@ -1033,6 +1238,14 @@ func (s *TeamSettings) SetDefaults() { s.EnableConfirmNotificationsToChannel = NewBool(true) } + if s.ExperimentalEnableAutomaticReplies == nil { + s.ExperimentalEnableAutomaticReplies = NewBool(false) + } + + if s.ExperimentalHideTownSquareinLHS == nil { + s.ExperimentalHideTownSquareinLHS = NewBool(false) + } + if s.ExperimentalTownSquareIsReadOnly == nil { s.ExperimentalTownSquareIsReadOnly = NewBool(false) } @@ -1040,6 +1253,22 @@ func (s *TeamSettings) SetDefaults() { if s.ExperimentalPrimaryTeam == nil { s.ExperimentalPrimaryTeam = NewString("") } + + if s.ExperimentalDefaultChannels == nil { + s.ExperimentalDefaultChannels = []string{} + } + + if s.EnableTeamCreation == nil { + s.EnableTeamCreation = NewBool(true) + } + + if s.EnableUserCreation == nil { + s.EnableUserCreation = NewBool(true) + } + + if s.ExperimentalViewArchivedChannels == nil { + s.ExperimentalViewArchivedChannels = NewBool(false) + } } type ClientRequirements struct { @@ -1073,8 +1302,9 @@ type LdapSettings struct { NicknameAttribute *string IdAttribute *string PositionAttribute *string + LoginIdAttribute *string - // Syncronization + // Synchronization SyncIntervalMinutes *int // Advanced @@ -1156,6 +1386,12 @@ func (s *LdapSettings) SetDefaults() { s.PositionAttribute = NewString(LDAP_SETTINGS_DEFAULT_POSITION_ATTRIBUTE) } + // For those upgrading to the version when LoginIdAttribute was added + // they need IdAttribute == LoginIdAttribute not to break + if s.LoginIdAttribute == nil { + s.LoginIdAttribute = s.IdAttribute + } + if s.SyncIntervalMinutes == nil { s.SyncIntervalMinutes = NewInt(60) } @@ -1231,8 +1467,9 @@ func (s *LocalizationSettings) SetDefaults() { type SamlSettings struct { // Basic - Enable *bool - EnableSyncWithLdap *bool + Enable *bool + EnableSyncWithLdap *bool + EnableSyncWithLdapIncludeAuth *bool Verify *bool Encrypt *bool @@ -1241,11 +1478,15 @@ type SamlSettings struct { IdpDescriptorUrl *string AssertionConsumerServiceURL *string + ScopingIDPProviderId *string + ScopingIDPName *string + IdpCertificateFile *string PublicCertificateFile *string PrivateKeyFile *string // User Mapping + IdAttribute *string FirstNameAttribute *string LastNameAttribute *string EmailAttribute *string @@ -1270,6 +1511,10 @@ func (s *SamlSettings) SetDefaults() { s.EnableSyncWithLdap = NewBool(false) } + if s.EnableSyncWithLdapIncludeAuth == nil { + s.EnableSyncWithLdapIncludeAuth = NewBool(false) + } + if s.Verify == nil { s.Verify = NewBool(true) } @@ -1302,10 +1547,22 @@ func (s *SamlSettings) SetDefaults() { s.AssertionConsumerServiceURL = NewString("") } + if s.ScopingIDPProviderId == nil { + s.ScopingIDPProviderId = NewString("") + } + + if s.ScopingIDPName == nil { + s.ScopingIDPName = NewString("") + } + if s.LoginButtonText == nil || *s.LoginButtonText == "" { s.LoginButtonText = NewString(USER_AUTH_SERVICE_SAML_TEXT) } + if s.IdAttribute == nil { + s.IdAttribute = NewString(SAML_SETTINGS_DEFAULT_ID_ATTRIBUTE) + } + if s.FirstNameAttribute == nil { s.FirstNameAttribute = NewString(SAML_SETTINGS_DEFAULT_FIRST_NAME_ATTRIBUTE) } @@ -1541,7 +1798,7 @@ type PluginSettings struct { EnableUploads *bool Directory *string ClientDirectory *string - Plugins map[string]interface{} + Plugins map[string]map[string]interface{} PluginStates map[string]*PluginState } @@ -1571,7 +1828,7 @@ func (s *PluginSettings) SetDefaults() { } if s.Plugins == nil { - s.Plugins = make(map[string]interface{}) + s.Plugins = make(map[string]map[string]interface{}) } if s.PluginStates == nil { @@ -1579,11 +1836,37 @@ func (s *PluginSettings) SetDefaults() { } } +type GlobalRelayMessageExportSettings struct { + CustomerType *string // must be either A9 or A10, dictates SMTP server url + SmtpUsername *string + SmtpPassword *string + EmailAddress *string // the address to send messages to +} + +func (s *GlobalRelayMessageExportSettings) SetDefaults() { + if s.CustomerType == nil { + s.CustomerType = NewString(GLOBALRELAY_CUSTOMER_TYPE_A9) + } + if s.SmtpUsername == nil { + s.SmtpUsername = NewString("") + } + if s.SmtpPassword == nil { + s.SmtpPassword = NewString("") + } + if s.EmailAddress == nil { + s.EmailAddress = NewString("") + } +} + type MessageExportSettings struct { EnableExport *bool + ExportFormat *string DailyRunTime *string ExportFromTimestamp *int64 BatchSize *int + + // formatter-specific settings - these are only expected to be non-nil if ExportFormat is set to the associated format + GlobalRelaySettings *GlobalRelayMessageExportSettings } func (s *MessageExportSettings) SetDefaults() { @@ -1591,6 +1874,10 @@ func (s *MessageExportSettings) SetDefaults() { s.EnableExport = NewBool(false) } + if s.ExportFormat == nil { + s.ExportFormat = NewString(COMPLIANCE_EXPORT_TYPE_ACTIANCE) + } + if s.DailyRunTime == nil { s.DailyRunTime = NewString("01:00") } @@ -1610,6 +1897,37 @@ func (s *MessageExportSettings) SetDefaults() { if s.BatchSize == nil { s.BatchSize = NewInt(10000) } + + if s.GlobalRelaySettings == nil { + s.GlobalRelaySettings = &GlobalRelayMessageExportSettings{} + } + s.GlobalRelaySettings.SetDefaults() +} + +type DisplaySettings struct { + CustomUrlSchemes *[]string + ExperimentalTimezone *bool +} + +func (s *DisplaySettings) SetDefaults() { + if s.CustomUrlSchemes == nil { + customUrlSchemes := []string{} + s.CustomUrlSchemes = &customUrlSchemes + } + + if s.ExperimentalTimezone == nil { + s.ExperimentalTimezone = NewBool(false) + } +} + +type TimezoneSettings struct { + SupportedTimezonesPath *string +} + +func (s *TimezoneSettings) SetDefaults() { + if s.SupportedTimezonesPath == nil { + s.SupportedTimezonesPath = NewString(TIMEZONE_SETTINGS_DEFAULT_SUPPORTED_TIMEZONES_PATH) + } } type ConfigFunc func() *Config @@ -1623,6 +1941,7 @@ type Config struct { PasswordSettings PasswordSettings FileSettings FileSettings EmailSettings EmailSettings + ExtensionSettings ExtensionSettings RateLimitSettings RateLimitSettings PrivacySettings PrivacySettings SupportSettings SupportSettings @@ -1638,6 +1957,7 @@ type Config struct { NativeAppSettings NativeAppSettings ClusterSettings ClusterSettings MetricsSettings MetricsSettings + ExperimentalSettings ExperimentalSettings AnalyticsSettings AnalyticsSettings WebrtcSettings WebrtcSettings ElasticsearchSettings ElasticsearchSettings @@ -1645,6 +1965,8 @@ type Config struct { MessageExportSettings MessageExportSettings JobSettings JobSettings PluginSettings PluginSettings + DisplaySettings DisplaySettings + TimezoneSettings TimezoneSettings } func (o *Config) Clone() *Config { @@ -1656,12 +1978,8 @@ func (o *Config) Clone() *Config { } func (o *Config) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func (o *Config) GetSSOService(service string) *SSOSettings { @@ -1678,14 +1996,9 @@ func (o *Config) GetSSOService(service string) *SSOSettings { } func ConfigFromJson(data io.Reader) *Config { - decoder := json.NewDecoder(data) - var o Config - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *Config + json.NewDecoder(data).Decode(&o) + return o } func (o *Config) SetDefaults() { @@ -1707,6 +2020,7 @@ func (o *Config) SetDefaults() { o.PasswordSettings.SetDefaults() o.TeamSettings.SetDefaults() o.MetricsSettings.SetDefaults() + o.ExperimentalSettings.SetDefaults() o.SupportSettings.SetDefaults() o.AnnouncementSettings.SetDefaults() o.ThemeSettings.SetDefaults() @@ -1723,6 +2037,9 @@ func (o *Config) SetDefaults() { o.JobSettings.SetDefaults() o.WebrtcSettings.SetDefaults() o.MessageExportSettings.SetDefaults() + o.TimezoneSettings.SetDefaults() + o.DisplaySettings.SetDefaults() + o.ExtensionSettings.SetDefaults() } func (o *Config) IsValid() *AppError { @@ -1734,6 +2051,10 @@ func (o *Config) IsValid() *AppError { return NewAppError("Config.IsValid", "model.config.is_valid.cluster_email_batching.app_error", nil, "", http.StatusBadRequest) } + if len(*o.ServiceSettings.SiteURL) == 0 && *o.ServiceSettings.AllowCookiesForSubdomains { + return NewAppError("Config.IsValid", "model.config.is_valid.allow_cookies_for_subdomains.app_error", nil, "", http.StatusBadRequest) + } + if err := o.TeamSettings.isValid(); err != nil { return err } @@ -1790,6 +2111,10 @@ func (o *Config) IsValid() *AppError { return err } + if err := o.DisplaySettings.isValid(); err != nil { + return err + } + return nil } @@ -1834,6 +2159,10 @@ func (ss *SqlSettings) isValid() *AppError { return NewAppError("Config.IsValid", "model.config.is_valid.sql_idle.app_error", nil, "", http.StatusBadRequest) } + if *ss.ConnMaxLifetimeMilliseconds < 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.sql_conn_max_lifetime_milliseconds.app_error", nil, "", http.StatusBadRequest) + } + if *ss.QueryTimeout <= 0 { return NewAppError("Config.IsValid", "model.config.is_valid.sql_query_timeout.app_error", nil, "", http.StatusBadRequest) } @@ -1938,6 +2267,10 @@ func (ls *LdapSettings) isValid() *AppError { if *ls.IdAttribute == "" { return NewAppError("Config.IsValid", "model.config.is_valid.ldap_id", nil, "", http.StatusBadRequest) } + + if *ls.LoginIdAttribute == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.ldap_login_id", nil, "", http.StatusBadRequest) + } } return nil @@ -2041,10 +2374,40 @@ func (ss *ServiceSettings) isValid() *AppError { } } - if len(*ss.ListenAddress) == 0 { + if len(*ss.WebsocketURL) != 0 { + if _, err := url.ParseRequestURI(*ss.WebsocketURL); err != nil { + return NewAppError("Config.IsValid", "model.config.is_valid.websocket_url.app_error", nil, "", http.StatusBadRequest) + } + } + + host, port, _ := net.SplitHostPort(*ss.ListenAddress) + var isValidHost bool + if host == "" { + isValidHost = true + } else { + isValidHost = (net.ParseIP(host) != nil) || IsDomainName(host) + } + portInt, err := strconv.Atoi(port) + if err != nil || !isValidHost || portInt < 0 || portInt > math.MaxUint16 { return NewAppError("Config.IsValid", "model.config.is_valid.listen_address.app_error", nil, "", http.StatusBadRequest) } + if *ss.ExperimentalGroupUnreadChannels != GROUP_UNREAD_CHANNELS_DISABLED && + *ss.ExperimentalGroupUnreadChannels != GROUP_UNREAD_CHANNELS_DEFAULT_ON && + *ss.ExperimentalGroupUnreadChannels != GROUP_UNREAD_CHANNELS_DEFAULT_OFF { + return NewAppError("Config.IsValid", "model.config.is_valid.group_unread_channels.app_error", nil, "", http.StatusBadRequest) + } + + switch *ss.ImageProxyType { + case "": + case "atmos/camo": + if *ss.ImageProxyOptions == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.atmos_camo_image_proxy_options.app_error", nil, "", http.StatusBadRequest) + } + default: + return NewAppError("Config.IsValid", "model.config.is_valid.image_proxy_type.app_error", nil, "", http.StatusBadRequest) + } + return nil } @@ -2121,11 +2484,49 @@ func (mes *MessageExportSettings) isValid(fs FileSettings) *AppError { return NewAppError("Config.IsValid", "model.config.is_valid.message_export.daily_runtime.app_error", nil, err.Error(), http.StatusBadRequest) } else if mes.BatchSize == nil || *mes.BatchSize < 0 { return NewAppError("Config.IsValid", "model.config.is_valid.message_export.batch_size.app_error", nil, "", http.StatusBadRequest) + } else if mes.ExportFormat == nil || (*mes.ExportFormat != COMPLIANCE_EXPORT_TYPE_ACTIANCE && *mes.ExportFormat != COMPLIANCE_EXPORT_TYPE_GLOBALRELAY && *mes.ExportFormat != COMPLIANCE_EXPORT_TYPE_CSV) { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.export_type.app_error", nil, "", http.StatusBadRequest) + } + + if *mes.ExportFormat == COMPLIANCE_EXPORT_TYPE_GLOBALRELAY { + if mes.GlobalRelaySettings == nil { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.config_missing.app_error", nil, "", http.StatusBadRequest) + } else if mes.GlobalRelaySettings.CustomerType == nil || (*mes.GlobalRelaySettings.CustomerType != GLOBALRELAY_CUSTOMER_TYPE_A9 && *mes.GlobalRelaySettings.CustomerType != GLOBALRELAY_CUSTOMER_TYPE_A10) { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.customer_type.app_error", nil, "", http.StatusBadRequest) + } else if mes.GlobalRelaySettings.EmailAddress == nil || !strings.Contains(*mes.GlobalRelaySettings.EmailAddress, "@") { + // validating email addresses is hard - just make sure it contains an '@' sign + // see https://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.email_address.app_error", nil, "", http.StatusBadRequest) + } else if mes.GlobalRelaySettings.SmtpUsername == nil || *mes.GlobalRelaySettings.SmtpUsername == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.smtp_username.app_error", nil, "", http.StatusBadRequest) + } else if mes.GlobalRelaySettings.SmtpPassword == nil || *mes.GlobalRelaySettings.SmtpPassword == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.smtp_password.app_error", nil, "", http.StatusBadRequest) + } } } return nil } +func (ds *DisplaySettings) isValid() *AppError { + if len(*ds.CustomUrlSchemes) != 0 { + validProtocolPattern := regexp.MustCompile(`(?i)^\s*[a-z][a-z0-9-]*\s*$`) + + for _, scheme := range *ds.CustomUrlSchemes { + if !validProtocolPattern.MatchString(scheme) { + return NewAppError( + "Config.IsValid", + "model.config.is_valid.display.custom_url_schemes.app_error", + map[string]interface{}{"Scheme": scheme}, + "", + http.StatusBadRequest, + ) + } + } + } + + return nil +} + func (o *Config) GetSanitizeOptions() map[string]bool { options := map[string]bool{} options["fullname"] = o.PrivacySettings.ShowFullName diff --git a/vendor/github.com/mattermost/platform/model/data_retention_policy.go b/vendor/github.com/mattermost/mattermost-server/model/data_retention_policy.go similarity index 71% rename from vendor/github.com/mattermost/platform/model/data_retention_policy.go rename to vendor/github.com/mattermost/mattermost-server/model/data_retention_policy.go index 7284477e..dbb13374 100644 --- a/vendor/github.com/mattermost/platform/model/data_retention_policy.go +++ b/vendor/github.com/mattermost/mattermost-server/model/data_retention_policy.go @@ -16,21 +16,12 @@ type DataRetentionPolicy struct { } func (me *DataRetentionPolicy) ToJson() string { - b, err := json.Marshal(me) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(me) + return string(b) } func DataRetentionPolicyFromJson(data io.Reader) *DataRetentionPolicy { - decoder := json.NewDecoder(data) - var me DataRetentionPolicy - err := decoder.Decode(&me) - if err == nil { - return &me - } else { - return nil - } + var me *DataRetentionPolicy + json.NewDecoder(data).Decode(&me) + return me } diff --git a/vendor/github.com/mattermost/platform/model/emoji.go b/vendor/github.com/mattermost/mattermost-server/model/emoji.go similarity index 68% rename from vendor/github.com/mattermost/platform/model/emoji.go rename to vendor/github.com/mattermost/mattermost-server/model/emoji.go index 272616d9..f14af89d 100644 --- a/vendor/github.com/mattermost/platform/model/emoji.go +++ b/vendor/github.com/mattermost/mattermost-server/model/emoji.go @@ -9,6 +9,11 @@ import ( "net/http" ) +const ( + EMOJI_NAME_MAX_LENGTH = 64 + EMOJI_SORT_BY_NAME = "name" +) + type Emoji struct { Id string `json:"id"` CreateAt int64 `json:"create_at"` @@ -18,6 +23,11 @@ type Emoji struct { Name string `json:"name"` } +func inSystemEmoji(emojiName string) bool { + _, ok := SystemEmojis[emojiName] + return ok +} + func (emoji *Emoji) IsValid() *AppError { if len(emoji.Id) != 26 { return NewAppError("Emoji.IsValid", "model.emoji.id.app_error", nil, "", http.StatusBadRequest) @@ -31,11 +41,15 @@ func (emoji *Emoji) IsValid() *AppError { return NewAppError("Emoji.IsValid", "model.emoji.update_at.app_error", nil, "id="+emoji.Id, http.StatusBadRequest) } - if len(emoji.CreatorId) != 26 { + if len(emoji.CreatorId) > 26 { return NewAppError("Emoji.IsValid", "model.emoji.user_id.app_error", nil, "", http.StatusBadRequest) } - if len(emoji.Name) == 0 || len(emoji.Name) > 64 || !IsValidAlphaNumHyphenUnderscore(emoji.Name, false) { + return IsValidEmojiName(emoji.Name) +} + +func IsValidEmojiName(name string) *AppError { + if len(name) == 0 || len(name) > EMOJI_NAME_MAX_LENGTH || !IsValidAlphaNumHyphenUnderscore(name, false) || inSystemEmoji(name) { return NewAppError("Emoji.IsValid", "model.emoji.name.app_error", nil, "", http.StatusBadRequest) } @@ -51,46 +65,24 @@ func (emoji *Emoji) PreSave() { emoji.UpdateAt = emoji.CreateAt } -func (emoji *Emoji) PreUpdate() { - emoji.UpdateAt = GetMillis() -} - func (emoji *Emoji) ToJson() string { - b, err := json.Marshal(emoji) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(emoji) + return string(b) } func EmojiFromJson(data io.Reader) *Emoji { - decoder := json.NewDecoder(data) - var emoji Emoji - err := decoder.Decode(&emoji) - if err == nil { - return &emoji - } else { - return nil - } + var emoji *Emoji + json.NewDecoder(data).Decode(&emoji) + return emoji } func EmojiListToJson(emojiList []*Emoji) string { - b, err := json.Marshal(emojiList) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(emojiList) + return string(b) } func EmojiListFromJson(data io.Reader) []*Emoji { - decoder := json.NewDecoder(data) var emojiList []*Emoji - err := decoder.Decode(&emojiList) - if err == nil { - return emojiList - } else { - return nil - } + json.NewDecoder(data).Decode(&emojiList) + return emojiList } diff --git a/vendor/github.com/mattermost/mattermost-server/model/emoji_data.go b/vendor/github.com/mattermost/mattermost-server/model/emoji_data.go new file mode 100644 index 00000000..f6e62e68 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/emoji_data.go @@ -0,0 +1,6 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +var SystemEmojis = map[string]string{"policewoman": "1f46e-200d-2640-fe0f", "family_man_girl_medium_skin_tone": "1f468-1f3fd", "man_technologist": "1f468-200d-1f4bb", "family_woman_girl_medium_light_skin_tone": "1f469-1f3fc", "massage_woman_medium_light_skin_tone": "1f486-1f3fc-200d-2640-fe0f", "family_woman_woman_boy": "1f469-200d-1f469-200d-1f466", "rice_scene": "1f391", "notes": "1f3b6", "burundi": "1f1e7-1f1ee", "woman_medium_skin_tone": "1f469-1f3fd", "tipping_hand_man_medium_dark_skin_tone": "1f481-1f3fe-200d-2642-fe0f", "new_moon": "1f311", "belize": "1f1e7-1f1ff", "bhutan": "1f1e7-1f1f9", "eu": "1f1ea-1f1fa", "point_up_dark_skin_tone": "261d-1f3ff", "older_man_medium_light_skin_tone": "1f474-1f3fc", "prince": "1f934", "walking_man": "1f6b6", "telephone_receiver": "1f4de", "arrow_upper_right": "2197-fe0f", "taiwan": "1f1f9-1f1fc", "-1_light_skin_tone": "1f44e-1f3fb", "bear": "1f43b", "derelict_house": "1f3da", "blue_book": "1f4d8", "ok": "1f197", "woman_farmer_medium_light_skin_tone": "1f469-1f3fc", "man_shrugging_light_skin_tone": "1f937-1f3fb-200d-2642-fe0f", "dancing_women": "1f46f", "cd": "1f4bf", "tada": "1f389", "virgo": "264d-fe0f", "white_flower": "1f4ae", "guardswoman_medium_dark_skin_tone": "1f482-1f3fe-200d-2640-fe0f", "performing_arts": "1f3ad", "prayer_beads": "1f4ff", "congo_brazzaville": "1f1e8-1f1ec", "point_down_medium_skin_tone": "1f447-1f3fd", "raised_hand_with_fingers_splayed_light_skin_tone": "1f590-1f3fb", "man_playing_water_polo_medium_skin_tone": "1f93d-1f3fd-200d-2642-fe0f", "four_leaf_clover": "1f340", "microphone": "1f3a4", "heartpulse": "1f497", "north_korea": "1f1f0-1f1f5", "neutral_face": "1f610", "volleyball": "1f3d0", "man_playing_water_polo": "1f93d-200d-2642-fe0f", "uk": "1f1ec-1f1e7", "wallis_futuna": "1f1fc-1f1eb", "earth_africa": "1f30d", "droplet": "1f4a7", "construction_worker_man_medium_dark_skin_tone": "1f477-1f3fe-200d-2640-fe0f", "family_woman_woman_girl_boy_medium_light_skin_tone": "1f469-1f3fc", "mountain_biking_man_medium_dark_skin_tone": "1f6b5-1f3fe-200d-2640-fe0f", "vulcan_salute_light_skin_tone": "1f596-1f3fb", "woman_shrugging_dark_skin_tone": "1f937-1f3ff-200d-2640-fe0f", "walking_man_medium_light_skin_tone": "1f6b6-1f3fc-200d-2640-fe0f", "wave": "1f44b", "framed_picture": "1f5bc", "mag": "1f50d", "fist_left": "1f91b", "building_construction": "1f3d7", "clock9": "1f558", "cayman_islands": "1f1f0-1f1fe", "laos": "1f1f1-1f1e6", "woman_playing_handball_dark_skin_tone": "1f93e-1f3ff-200d-2640-fe0f", "man_office_worker": "1f468-200d-1f4bc", "family_man_woman_girl": "1f468-200d-1f469-200d-1f467", "wilted_flower": "1f940", "books": "1f4da", "rage": "1f621", "rice_ball": "1f359", "desert": "1f3dc", "malta": "1f1f2-1f1f9", "haircut_woman_dark_skin_tone": "1f487-1f3ff-200d-2640-fe0f", "symbols": "1f523", "marshall_islands": "1f1f2-1f1ed", "sierra_leone": "1f1f8-1f1f1", "crossed_fingers_medium_dark_skin_tone": "1f91e-1f3fe", "man_judge_medium_skin_tone": "1f468-1f3fd", "bamboo": "1f38d", "keyboard": "2328-fe0f", "clock10": "1f559", "massage_man_medium_skin_tone": "1f486-1f3fd-200d-2642-fe0f", "tipping_hand_man_dark_skin_tone": "1f481-1f3ff-200d-2642-fe0f", "man_facepalming_light_skin_tone": "1f926-1f3fb-200d-2642-fe0f", "train": "1f68b", "traffic_light": "1f6a5", "vietnam": "1f1fb-1f1f3", "boy_medium_light_skin_tone": "1f466-1f3fc", "man_farmer_light_skin_tone": "1f468-1f3fb", "man_singer_medium_dark_skin_tone": "1f468-1f3fe", "woman_cartwheeling_medium_light_skin_tone": "1f938-1f3fc-200d-2640-fe0f", "top": "1f51d", "gb": "1f1ec-1f1e7", "mouse2": "1f401", "do_not_litter": "1f6af", "south_sudan": "1f1f8-1f1f8", "bowing_woman_light_skin_tone": "1f647-1f3fb-200d-2640-fe0f", "family_man_man_girl_girl_medium_light_skin_tone": "1f468-1f3fc", "japanese_goblin": "1f47a", "camel": "1f42b", "taurus": "2649-fe0f", "mute": "1f507", "woman_mechanic_medium_dark_skin_tone": "1f469-1f3fe", "surfer": "1f3c4", "tipping_hand_man": "1f481-200d-2642-fe0f", "family_woman_woman_boy_boy": "1f469-200d-1f469-200d-1f466-200d-1f466", "floppy_disk": "1f4be", "atm": "1f3e7", "clock230": "1f55d", "prince_light_skin_tone": "1f934-1f3fb", "name_badge": "1f4db", "octocat": "octocat", "family_woman_woman_girl_girl_dark_skin_tone": "1f469-1f3ff", "christmas_tree": "1f384", "waxing_gibbous_moon": "1f314", "mountain_cableway": "1f6a0", "woman_scientist_medium_dark_skin_tone": "1f469-1f3fe", "haircut_man_medium_dark_skin_tone": "1f487-1f3fe-200d-2642-fe0f", "basketball_woman_medium_dark_skin_tone": "26f9-1f3fe-200d-2640-fe0f", "family_man_man_boy_medium_light_skin_tone": "1f468-1f3fc", "rowing_woman_medium_dark_skin_tone": "1f6a3-1f3fe-200d-2640-fe0f", "bowling": "1f3b3", "shinto_shrine": "26e9", "round_pushpin": "1f4cd", "cyprus": "1f1e8-1f1fe", "open_hands_dark_skin_tone": "1f450-1f3ff", "clap_medium_skin_tone": "1f44f-1f3fd", "bath_medium_dark_skin_tone": "1f6c0-1f3fe", "briefcase": "1f4bc", "tiger": "1f42f", "morocco": "1f1f2-1f1e6", "open_hands_light_skin_tone": "1f450-1f3fb", "hand_light_skin_tone": "270b-1f3fb", "weight_lifting_man_medium_dark_skin_tone": "1f3cb-1f3fe-200d-2640-fe0f", "mans_shoe": "1f45e", "poland": "1f1f5-1f1f1", "raised_hands_medium_skin_tone": "1f64c-1f3fd", "family_man_woman_girl_boy_light_skin_tone": "1f468-1f3fb", "woman_playing_handball_medium_skin_tone": "1f93e-1f3fd-200d-2640-fe0f", "office": "1f3e2", "woman_singer_medium_dark_skin_tone": "1f469-1f3fe", "family_woman_woman_boy_boy_medium_light_skin_tone": "1f469-1f3fc", "scorpion": "1f982", "tomato": "1f345", "goal_net": "1f945", "chad": "1f1f9-1f1e9", "family_man_woman_girl_boy_medium_skin_tone": "1f468-1f3fd", "mountain_biking_man_light_skin_tone": "1f6b5-1f3fb-200d-2640-fe0f", "weight_lifting_man_dark_skin_tone": "1f3cb-1f3ff-200d-2640-fe0f", "eyeglasses": "1f453", "golfing_woman": "1f3cc-fe0f-200d-2640-fe0f", "dvd": "1f4c0", "clipboard": "1f4cb", "ireland": "1f1ee-1f1ea", "woman_student_dark_skin_tone": "1f469-1f3ff", "angry": "1f620", "baby": "1f476", "women_wrestling": "1f93c-200d-2640-fe0f", "black_square_button": "1f532", "male_detective_medium_light_skin_tone": "1f575-1f3fc-200d-2640-fe0f", "dancer_dark_skin_tone": "1f483-1f3ff", "id": "1f194", "vibration_mode": "1f4f3", "handshake": "1f91d", "tiger2": "1f405", "leaves": "1f343", "baseball": "26be-fe0f", "golf": "26f3-fe0f", "toilet": "1f6bd", "male_detective_dark_skin_tone": "1f575-1f3ff-200d-2640-fe0f", "family_woman_boy": "1f469-200d-1f466", "duck": "1f986", "writing_hand_medium_dark_skin_tone": "270d-1f3fe", "woman_singer_medium_light_skin_tone": "1f469-1f3fc", "man_teacher_medium_skin_tone": "1f468-1f3fd", "lips": "1f444", "octopus": "1f419", "policeman_medium_dark_skin_tone": "1f46e-1f3fe-200d-2640-fe0f", "man_factory_worker_dark_skin_tone": "1f468-1f3ff", "man_astronaut_light_skin_tone": "1f468-1f3fb", "ok_man_dark_skin_tone": "1f646-1f3ff-200d-2642-fe0f", "couple_with_heart": "1f491", "pray_medium_skin_tone": "1f64f-1f3fd", "woman_health_worker_light_skin_tone": "1f469-1f3fb", "tipping_hand_woman_medium_light_skin_tone": "1f481-1f3fc-200d-2640-fe0f", "no_good_woman_dark_skin_tone": "1f645-1f3ff-200d-2640-fe0f", "no_good_woman_medium_dark_skin_tone": "1f645-1f3fe-200d-2640-fe0f", "thumbsdown": "1f44e", "fist": "270a", "camera_flash": "1f4f8", "azerbaijan": "1f1e6-1f1ff", "woman_with_turban_medium_light_skin_tone": "1f473-1f3fc-200d-2640-fe0f", "man_singer_medium_skin_tone": "1f468-1f3fd", "mali": "1f1f2-1f1f1", "blonde_woman_medium_dark_skin_tone": "1f471-1f3fe-200d-2640-fe0f", "family_man_man_girl_light_skin_tone": "1f468-1f3fb", "biking_woman_medium_skin_tone": "1f6b4-1f3fd-200d-2640-fe0f", "crab": "1f980", "green_salad": "1f957", "men_wrestling": "1f93c-200d-2642-fe0f", "-1_medium_dark_skin_tone": "1f44e-1f3fe", "baby_dark_skin_tone": "1f476-1f3ff", "surfing_woman_light_skin_tone": "1f3c4-1f3fb-200d-2640-fe0f", "stuck_out_tongue_winking_eye": "1f61c", "plate_with_cutlery": "1f37d", "swimmer": "1f3ca", "blonde_woman_medium_light_skin_tone": "1f471-1f3fc-200d-2640-fe0f", "curly_loop": "27b0", "india": "1f1ee-1f1f3", "norfolk_island": "1f1f3-1f1eb", "mountain_biking_woman_dark_skin_tone": "1f6b5-1f3ff-200d-2640-fe0f", "ghost": "1f47b", "boar": "1f417", "railway_track": "1f6e4", "100": "1f4af", "metal_medium_dark_skin_tone": "1f918-1f3fe", "woman_playing_handball_light_skin_tone": "1f93e-1f3fb-200d-2640-fe0f", "barber": "1f488", "clock730": "1f562", "equatorial_guinea": "1f1ec-1f1f6", "maldives": "1f1f2-1f1fb", "weight_lifting_woman_medium_dark_skin_tone": "1f3cb-1f3fe-200d-2640-fe0f", "eagle": "1f985", "tea": "1f375", "tanabata_tree": "1f38b", "night_with_stars": "1f303", "balloon": "1f388", "on": "1f51b", "lizard": "1f98e", "beer": "1f37a", "part_alternation_mark": "303d-fe0f", "white_square_button": "1f533", "clock430": "1f55f", "gibraltar": "1f1ec-1f1ee", "massage_woman_medium_skin_tone": "1f486-1f3fd-200d-2640-fe0f", "sweat": "1f613", "athletic_shoe": "1f45f", "joystick": "1f579", "biohazard": "2623-fe0f", "muscle_medium_light_skin_tone": "1f4aa-1f3fc", "bride_with_veil_medium_light_skin_tone": "1f470-1f3fc", "parasol_on_ground": "26f1", "costa_rica": "1f1e8-1f1f7", "woman_student_light_skin_tone": "1f469-1f3fb", "massage_woman_medium_dark_skin_tone": "1f486-1f3fe-200d-2640-fe0f", "surfing_woman_medium_dark_skin_tone": "1f3c4-1f3fe-200d-2640-fe0f", "rowing_woman": "1f6a3-200d-2640-fe0f", "guardsman_light_skin_tone": "1f482-1f3fb-200d-2640-fe0f", "construction_worker_man_light_skin_tone": "1f477-1f3fb-200d-2640-fe0f", "family_woman_boy_boy": "1f469-200d-1f466-200d-1f466", "small_airplane": "1f6e9", "baggage_claim": "1f6c4", "bosnia_herzegovina": "1f1e7-1f1e6", "falkland_islands": "1f1eb-1f1f0", "crossed_fingers_medium_light_skin_tone": "1f91e-1f3fc", "man_scientist_medium_light_skin_tone": "1f468-1f3fc", "family_woman_girl_boy_medium_skin_tone": "1f469-1f3fd", "satisfied": "1f606", "u5408": "1f234", "cn": "1f1e8-1f1f3", "isle_of_man": "1f1ee-1f1f2", "fist_raised_medium_light_skin_tone": "270a-1f3fc", "family_man_woman_girl_dark_skin_tone": "1f468-1f3ff", "family_woman_girl_dark_skin_tone": "1f469-1f3ff", "family_man_boy_medium_skin_tone": "1f468-1f3fd", "money_mouth_face": "1f911", "syringe": "1f489", "hand_medium_light_skin_tone": "270b-1f3fc", "writing_hand_medium_skin_tone": "270d-1f3fd", "man_farmer_medium_light_skin_tone": "1f468-1f3fc", "woman_artist_dark_skin_tone": "1f469-1f3ff", "tickets": "1f39f", "man_cartwheeling_light_skin_tone": "1f938-1f3fb-200d-2642-fe0f", "squid": "1f991", "fish": "1f41f", "memo": "1f4dd", "eye_speech_bubble": "1f441-200d-1f5e8", "+1_light_skin_tone": "1f44d-1f3fb", "tulip": "1f337", "blossom": "1f33c", "family_woman_woman_girl_medium_dark_skin_tone": "1f469-1f3fe", "triumph": "1f624", "rooster": "1f413", "ng": "1f196", "blonde_man_medium_light_skin_tone": "1f471-1f3fc-200d-2640-fe0f", "policeman_light_skin_tone": "1f46e-1f3fb-200d-2640-fe0f", "woman_cook_dark_skin_tone": "1f469-1f3ff", "pray_dark_skin_tone": "1f64f-1f3ff", "point_up_2_medium_skin_tone": "1f446-1f3fd", "busts_in_silhouette": "1f465", "tornado": "1f32a", "woman_juggling": "1f939-200d-2640-fe0f", "cupid": "1f498", "white_check_mark": "2705", "aruba": "1f1e6-1f1fc", "family_man_boy_boy_medium_dark_skin_tone": "1f468-1f3fe", "woman_cartwheeling_dark_skin_tone": "1f938-1f3ff-200d-2640-fe0f", "woman_playing_water_polo_dark_skin_tone": "1f93d-1f3ff-200d-2640-fe0f", "relaxed": "263a-fe0f", "birthday": "1f382", "high_brightness": "1f506", "couple_with_heart_woman_woman_medium_light_skin_tone": "1f469-1f3fc", "biking_man_dark_skin_tone": "1f6b4-1f3ff-200d-2640-fe0f", "disappointed_relieved": "1f625", "canary_islands": "1f1ee-1f1e8", "st_pierre_miquelon": "1f1f5-1f1f2", "trinidad_tobago": "1f1f9-1f1f9", "turkmenistan": "1f1f9-1f1f2", "pouting_woman_medium_skin_tone": "1f64e-1f3fd-200d-2640-fe0f", "man_student_dark_skin_tone": "1f468-1f3ff", "princess_dark_skin_tone": "1f478-1f3ff", "family_woman_boy_boy_medium_skin_tone": "1f469-1f3fd", "old_key": "1f5dd", "muscle_medium_skin_tone": "1f4aa-1f3fd", "ear_medium_dark_skin_tone": "1f442-1f3fe", "girl_medium_dark_skin_tone": "1f467-1f3fe", "man_pilot_dark_skin_tone": "1f468-1f3ff", "wolf": "1f43a", "gem": "1f48e", "arrow_double_up": "23eb", "woman_factory_worker_light_skin_tone": "1f469-1f3fb", "woman_mechanic_medium_skin_tone": "1f469-1f3fd", "woman_firefighter_light_skin_tone": "1f469-1f3fb", "sunglasses": "1f60e", "snake": "1f40d", "pen": "1f58a", "nose_medium_skin_tone": "1f443-1f3fd", "weight_lifting_man": "1f3cb-fe0f", "alarm_clock": "23f0", "golfing_woman_medium_light_skin_tone": "1f3cc-1f3fc-200d-2640-fe0f", "vulcan_salute": "1f596", "earth_asia": "1f30f", "+1_dark_skin_tone": "1f44d-1f3ff", "family_woman_boy_medium_dark_skin_tone": "1f469-1f3fe", "family_woman_girl_girl_light_skin_tone": "1f469-1f3fb", "weight_lifting_man_medium_light_skin_tone": "1f3cb-1f3fc-200d-2640-fe0f", "angel": "1f47c", "peach": "1f351", "truck": "1f69a", "tajikistan": "1f1f9-1f1ef", "tr": "1f1f9-1f1f7", "running_woman_medium_skin_tone": "1f3c3-1f3fd-200d-2640-fe0f", "wrench": "1f527", "black_flag": "1f3f4", "cape_verde": "1f1e8-1f1fb", "man_technologist_medium_light_skin_tone": "1f468-1f3fc", "mountain_biking_woman_medium_light_skin_tone": "1f6b5-1f3fc-200d-2640-fe0f", "man_astronaut_medium_skin_tone": "1f468-1f3fd", "man_in_tuxedo_medium_light_skin_tone": "1f935-1f3fc", "see_no_evil": "1f648", "egg": "1f95a", "1234": "1f522", "lesotho": "1f1f1-1f1f8", "middle_finger_medium_skin_tone": "1f595-1f3fd", "woman_health_worker_medium_light_skin_tone": "1f469-1f3fc", "sneezing_face": "1f927", "man_cook": "1f468-200d-1f373", "mortar_board": "1f393", "candle": "1f56f", "basketball_man_medium_skin_tone": "26f9-1f3fd-200d-2640-fe0f", "ferris_wheel": "1f3a1", "martinique": "1f1f2-1f1f6", "st_vincent_grenadines": "1f1fb-1f1e8", "yemen": "1f1fe-1f1ea", "pray_light_skin_tone": "1f64f-1f3fb", "man_in_tuxedo_medium_skin_tone": "1f935-1f3fd", "woman_pilot_medium_dark_skin_tone": "1f469-1f3fe", "pregnant_woman_dark_skin_tone": "1f930-1f3ff", "fu": "1f595", "haircut": "1f487", "boxing_glove": "1f94a", "page_with_curl": "1f4c3", "muscle_light_skin_tone": "1f4aa-1f3fb", "woman_firefighter_dark_skin_tone": "1f469-1f3ff", "ok_woman_light_skin_tone": "1f646-1f3fb-200d-2640-fe0f", "family_man_woman_boy_boy_medium_light_skin_tone": "1f468-1f3fc", "family_woman_boy_dark_skin_tone": "1f469-1f3ff", "weight_lifting_man_light_skin_tone": "1f3cb-1f3fb-200d-2640-fe0f", "blonde_man": "1f471", "woman_technologist": "1f469-200d-1f4bb", "boom": "1f4a5", "1st_place_medal": "1f947", "nine": "0039-fe0f-20e3", "czech_republic": "1f1e8-1f1ff", "meat_on_bone": "1f356", "hamburger": "1f354", "video_game": "1f3ae", "clock2": "1f551", "woman_facepalming_dark_skin_tone": "1f926-1f3ff-200d-2640-fe0f", "couplekiss_man_man_medium_dark_skin_tone": "1f468-1f3fe", "arrow_lower_right": "2198-fe0f", "haircut_woman_light_skin_tone": "1f487-1f3fb-200d-2640-fe0f", "woman_dark_skin_tone": "1f469-1f3ff", "older_man_light_skin_tone": "1f474-1f3fb", "first_quarter_moon_with_face": "1f31b", "fries": "1f35f", "restroom": "1f6bb", "zero": "0030-fe0f-20e3", "fr": "1f1eb-1f1f7", "kuwait": "1f1f0-1f1fc", "man_health_worker_medium_skin_tone": "1f468-1f3fd", "woman_judge_light_skin_tone": "1f469-1f3fb", "man_judge_dark_skin_tone": "1f468-1f3ff", "ok_woman_medium_skin_tone": "1f646-1f3fd-200d-2640-fe0f", "bike": "1f6b2", "registered": "00ae-fe0f", "blonde_woman_medium_skin_tone": "1f471-1f3fd-200d-2640-fe0f", "stuck_out_tongue_closed_eyes": "1f61d", "collision": "1f4a5", "wheelchair": "267f-fe0f", "black_circle": "26ab-fe0f", "point_up_2_light_skin_tone": "1f446-1f3fb", "older_man": "1f474", "suspension_railway": "1f69f", "libra": "264e-fe0f", "crossed_flags": "1f38c", "man_cartwheeling_medium_dark_skin_tone": "1f938-1f3fe-200d-2642-fe0f", "man_playing_water_polo_dark_skin_tone": "1f93d-1f3ff-200d-2642-fe0f", "scream": "1f631", "no_good_man": "1f645-200d-2642-fe0f", "timer_clock": "23f2", "venezuela": "1f1fb-1f1ea", "raised_back_of_hand_light_skin_tone": "1f91a-1f3fb", "woman_technologist_medium_skin_tone": "1f469-1f3fd", "popcorn": "1f37f", "romania": "1f1f7-1f1f4", "togo": "1f1f9-1f1ec", "writing_hand_dark_skin_tone": "270d-1f3ff", "woman_singer_dark_skin_tone": "1f469-1f3ff", "pouting_woman_medium_dark_skin_tone": "1f64e-1f3fe-200d-2640-fe0f", "man_health_worker_light_skin_tone": "1f468-1f3fb", "dancer_medium_light_skin_tone": "1f483-1f3fc", "phone": "260e-fe0f", "chart": "1f4b9", "repeat": "1f501", "mahjong": "1f004-fe0f", "liberia": "1f1f1-1f1f7", "rage3": "rage3", "person_frowning": "1f64d", "open_hands_medium_skin_tone": "1f450-1f3fd", "man_dark_skin_tone": "1f468-1f3ff", "man_factory_worker_medium_light_skin_tone": "1f468-1f3fc", "man_astronaut_medium_light_skin_tone": "1f468-1f3fc", "ring": "1f48d", "ok_hand_medium_skin_tone": "1f44c-1f3fd", "santa": "1f385", "beach_umbrella": "1f3d6", "finland": "1f1eb-1f1ee", "woman_facepalming_medium_skin_tone": "1f926-1f3fd-200d-2640-fe0f", "woman_shrugging_medium_light_skin_tone": "1f937-1f3fc-200d-2640-fe0f", "sunflower": "1f33b", "ok_hand_dark_skin_tone": "1f44c-1f3ff", "santa_medium_skin_tone": "1f385-1f3fd", "call_me_hand_medium_skin_tone": "1f919-1f3fd", "man_firefighter_light_skin_tone": "1f468-1f3fb", "kiss": "1f48b", "mandarin": "1f34a", "dollar": "1f4b5", "clock3": "1f552", "argentina": "1f1e6-1f1f7", "fist_left_light_skin_tone": "1f91b-1f3fb", "santa_light_skin_tone": "1f385-1f3fb", "family_man_girl_boy_medium_light_skin_tone": "1f468-1f3fc", "sushi": "1f363", "rice": "1f35a", "mailbox_with_mail": "1f4ec", "woman_cook_medium_dark_skin_tone": "1f469-1f3fe", "family_man_woman_girl_girl_medium_dark_skin_tone": "1f468-1f3fe", "straight_ruler": "1f4cf", "blue_heart": "1f499", "slightly_frowning_face": "1f641", "crossed_fingers": "1f91e", "seedling": "1f331", "herb": "1f33f", "medal_military": "1f396", "camping": "1f3d5", "arrow_backward": "25c0-fe0f", "heavy_multiplication_x": "2716-fe0f", "icecream": "1f366", "heavy_dollar_sign": "1f4b2", "frowning_woman_dark_skin_tone": "1f64d-1f3ff-200d-2640-fe0f", "family_man_woman_boy_boy_medium_dark_skin_tone": "1f468-1f3fe", "brazil": "1f1e7-1f1f7", "fist_right_medium_light_skin_tone": "1f91c-1f3fc", "man_scientist_medium_dark_skin_tone": "1f468-1f3fe", "family_woman_girl_light_skin_tone": "1f469-1f3fb", "swimming_man_medium_dark_skin_tone": "1f3ca-1f3fe-200d-2640-fe0f", "man": "1f468", "lemon": "1f34b", "japanese_castle": "1f3ef", "cinema": "1f3a6", "wave_light_skin_tone": "1f44b-1f3fb", "middle_finger_medium_light_skin_tone": "1f595-1f3fc", "five": "0035-fe0f-20e3", "boy_medium_dark_skin_tone": "1f466-1f3fe", "woman_technologist_dark_skin_tone": "1f469-1f3ff", "man_playing_handball_light_skin_tone": "1f93e-1f3fb-200d-2642-fe0f", "construction_worker_man": "1f477", "stadium": "1f3df", "biking_woman_dark_skin_tone": "1f6b4-1f3ff-200d-2640-fe0f", "trophy": "1f3c6", "arrow_left": "2b05-fe0f", "boy_dark_skin_tone": "1f466-1f3ff", "no_good_woman_light_skin_tone": "1f645-1f3fb-200d-2640-fe0f", "skull_and_crossbones": "2620-fe0f", "couple_with_heart_woman_woman": "1f469-200d-2764-fe0f-200d-1f469", "no_bicycles": "1f6b3", "bell": "1f514", "feelsgood": "feelsgood", "bowing_man_dark_skin_tone": "1f647-1f3ff-200d-2640-fe0f", "mouse": "1f42d", "anchor": "2693-fe0f", "cyclone": "1f300", "solomon_islands": "1f1f8-1f1e7", "basketball": "1f3c0", "notebook_with_decorative_cover": "1f4d4", "family_man_girl_girl_dark_skin_tone": "1f468-1f3ff", "singapore": "1f1f8-1f1ec", "golfing_man_medium_dark_skin_tone": "1f3cc-1f3fe-200d-2640-fe0f", "woman": "1f469", "fried_shrimp": "1f364", "construction": "1f6a7", "eight_pointed_black_star": "2734-fe0f", "black_joker": "1f0cf", "cambodia": "1f1f0-1f1ed", "mount_fuji": "1f5fb", "link": "1f517", "womens": "1f6ba", "family_man_man_boy_boy_medium_skin_tone": "1f468-1f3fd", "joy": "1f602", "crying_cat_face": "1f63f", "parking": "1f17f-fe0f", "barbados": "1f1e7-1f1e7", "bowing_woman_dark_skin_tone": "1f647-1f3ff-200d-2640-fe0f", "angel_medium_light_skin_tone": "1f47c-1f3fc", "waning_gibbous_moon": "1f316", "synagogue": "1f54d", "american_samoa": "1f1e6-1f1f8", "basketball_woman_medium_light_skin_tone": "26f9-1f3fc-200d-2640-fe0f", "bug": "1f41b", "woman_farmer_light_skin_tone": "1f469-1f3fb", "door": "1f6aa", "place_of_worship": "1f6d0", "eight_spoked_asterisk": "2733-fe0f", "mrs_claus_dark_skin_tone": "1f936-1f3ff", "u7a7a": "1f233", "man_astronaut_medium_dark_skin_tone": "1f468-1f3fe", "jack_o_lantern": "1f383", "lock_with_ink_pen": "1f50f", "male_detective_medium_skin_tone": "1f575-1f3fd-200d-2640-fe0f", "woman_firefighter_medium_dark_skin_tone": "1f469-1f3fe", "smiling_imp": "1f608", "tv": "1f4fa", "pouting_man": "1f64e-200d-2642-fe0f", "e-mail": "1f4e7", "package": "1f4e6", "clock130": "1f55c", "family_man_boy_light_skin_tone": "1f468-1f3fb", "cat2": "1f408", "mountain_biking_woman_medium_dark_skin_tone": "1f6b5-1f3fe-200d-2640-fe0f", "nauseated_face": "1f922", "fountain": "26f2-fe0f", "middle_finger": "1f595", "dancers": "1f46f", "cactus": "1f335", "man_student_light_skin_tone": "1f468-1f3fb", "family_man_man_girl_dark_skin_tone": "1f468-1f3ff", "family_man_girl_girl_medium_skin_tone": "1f468-1f3fd", "us_virgin_islands": "1f1fb-1f1ee", "woman_astronaut_medium_dark_skin_tone": "1f469-1f3fe", "honeybee": "1f41d", "bouquet": "1f490", "golfing_man": "1f3cc-fe0f", "u7981": "1f232", "french_guiana": "1f1ec-1f1eb", "kenya": "1f1f0-1f1ea", "melon": "1f348", "nicaragua": "1f1f3-1f1ee", "raised_hand_with_fingers_splayed_medium_light_skin_tone": "1f590-1f3fc", "bath_light_skin_tone": "1f6c0-1f3fb", "man_pilot_medium_light_skin_tone": "1f468-1f3fc", "european_post_office": "1f3e4", "mobile_phone_off": "1f4f4", "no_smoking": "1f6ad", "family_woman_girl_girl_medium_light_skin_tone": "1f469-1f3fc", "family_woman_girl_medium_dark_skin_tone": "1f469-1f3fe", "man_juggling_medium_dark_skin_tone": "1f939-1f3fe-200d-2642-fe0f", "expressionless": "1f611", "school_satchel": "1f392", "film_strip": "1f39e", "running_man_light_skin_tone": "1f3c3-1f3fb-200d-2640-fe0f", "family_man_woman_girl_medium_dark_skin_tone": "1f468-1f3fe", "family_woman_woman_boy_boy_light_skin_tone": "1f469-1f3fb", "smiley_cat": "1f63a", "chestnut": "1f330", "girl_dark_skin_tone": "1f467-1f3ff", "bride_with_veil_medium_dark_skin_tone": "1f470-1f3fe", "family_man_man_boy_boy_light_skin_tone": "1f468-1f3fb", "family_man_boy_boy_medium_light_skin_tone": "1f468-1f3fc", "wink": "1f609", "carrot": "1f955", "credit_card": "1f4b3", "triangular_ruler": "1f4d0", "question": "2753", "+1_medium_dark_skin_tone": "1f44d-1f3fe", "man_teacher_medium_dark_skin_tone": "1f468-1f3fe", "family_man_girl_girl_medium_light_skin_tone": "1f468-1f3fc", "kimono": "1f458", "bellhop_bell": "1f6ce", "red_circle": "1f534", "call_me_hand_light_skin_tone": "1f919-1f3fb", "nail_care_medium_skin_tone": "1f485-1f3fd", "woman_teacher_medium_skin_tone": "1f469-1f3fd", "woman_juggling_dark_skin_tone": "1f939-1f3ff-200d-2640-fe0f", "runner": "1f3c3", "heavy_check_mark": "2714-fe0f", "family_man_girl_girl_medium_dark_skin_tone": "1f468-1f3fe", "tent": "26fa-fe0f", "card_index_dividers": "1f5c2", "man_singer_dark_skin_tone": "1f468-1f3ff", "man_firefighter_medium_dark_skin_tone": "1f468-1f3fe", "man_playing_handball_medium_dark_skin_tone": "1f93e-1f3fe-200d-2642-fe0f", "female_detective_medium_dark_skin_tone": "1f575-1f3fe-200d-2640-fe0f", "metal": "1f918", "dark_sunglasses": "1f576", "vertical_traffic_light": "1f6a6", "four": "0034-fe0f-20e3", "wavy_dash": "3030-fe0f", "ear_medium_light_skin_tone": "1f442-1f3fc", "man_juggling_dark_skin_tone": "1f939-1f3ff-200d-2642-fe0f", "kissing_heart": "1f618", "sweet_potato": "1f360", "gift_heart": "1f49d", "man_technologist_light_skin_tone": "1f468-1f3fb", "prince_medium_dark_skin_tone": "1f934-1f3fe", "ok_man_medium_skin_tone": "1f646-1f3fd-200d-2642-fe0f", "womans_clothes": "1f45a", "roller_coaster": "1f3a2", "woman_student_medium_light_skin_tone": "1f469-1f3fc", "zipper_mouth_face": "1f910", "person_with_blond_hair": "1f471", "leftwards_arrow_with_hook": "21a9-fe0f", "white_circle": "26aa-fe0f", "afghanistan": "1f1e6-1f1eb", "face_with_thermometer": "1f912", "bow": "1f647", "kr": "1f1f0-1f1f7", "finnadie": "finnadie", "girl_light_skin_tone": "1f467-1f3fb", "woman_farmer_medium_skin_tone": "1f469-1f3fd", "umbrella": "2614-fe0f", "ice_cream": "1f368", "point_down_medium_light_skin_tone": "1f447-1f3fc", "woman_health_worker_dark_skin_tone": "1f469-1f3ff", "rowing_woman_medium_light_skin_tone": "1f6a3-1f3fc-200d-2640-fe0f", "money_with_wings": "1f4b8", "dolls": "1f38e", "surfing_man_medium_dark_skin_tone": "1f3c4-1f3fe-200d-2640-fe0f", "mrs_claus_medium_skin_tone": "1f936-1f3fd", "basketball_man_dark_skin_tone": "26f9-1f3ff-200d-2640-fe0f", "dragon_face": "1f432", "woman_cartwheeling": "1f938-200d-2640-fe0f", "aquarius": "2652-fe0f", "sos": "1f198", "clock1230": "1f567", "haiti": "1f1ed-1f1f9", "woman_facepalming": "1f926-200d-2640-fe0f", "clinking_glasses": "1f942", "trollface": "trollface", "mrs_claus_light_skin_tone": "1f936-1f3fb", "clap": "1f44f", "couplekiss_man_man": "1f468-200d-2764-fe0f-200d-1f48b-200d-1f468", "page_facing_up": "1f4c4", "belgium": "1f1e7-1f1ea", "curacao": "1f1e8-1f1fc", "family_woman_woman_boy_dark_skin_tone": "1f469-1f3ff", "two_men_holding_hands": "1f46c", "mountain_snow": "1f3d4", "wind_chime": "1f390", "person_with_pouting_face": "1f64e", "cityscape": "1f3d9", "bride_with_veil_dark_skin_tone": "1f470-1f3ff", "frowning_woman_light_skin_tone": "1f64d-1f3fb-200d-2640-fe0f", "bath_medium_light_skin_tone": "1f6c0-1f3fc", "sheep": "1f411", "sparkler": "1f387", "frowning_woman": "1f64d", "rat": "1f400", "custard": "1f36e", "video_camera": "1f4f9", "open_umbrella": "2602-fe0f", "man_with_turban_medium_light_skin_tone": "1f473-1f3fc-200d-2640-fe0f", "woman_student_medium_dark_skin_tone": "1f469-1f3fe", "ok_woman_medium_light_skin_tone": "1f646-1f3fc-200d-2640-fe0f", "swimming_man_dark_skin_tone": "1f3ca-1f3ff-200d-2640-fe0f", "man_cook_light_skin_tone": "1f468-1f3fb", "running_woman_light_skin_tone": "1f3c3-1f3fb-200d-2640-fe0f", "rabbit": "1f430", "ox": "1f402", "corn": "1f33d", "mozambique": "1f1f2-1f1ff", "point_right_light_skin_tone": "1f449-1f3fb", "nail_care_light_skin_tone": "1f485-1f3fb", "smiley": "1f603", "new_moon_with_face": "1f31a", "croatia": "1f1ed-1f1f7", "man_judge_medium_dark_skin_tone": "1f468-1f3fe", "fist_raised": "270a", "man_astronaut": "1f468-200d-1f680", "clock1130": "1f566", "st_lucia": "1f1f1-1f1e8", "princess": "1f478", "fist_left_medium_light_skin_tone": "1f91b-1f3fc", "point_left_medium_dark_skin_tone": "1f448-1f3fe", "woman_factory_worker_medium_skin_tone": "1f469-1f3fd", "angel_dark_skin_tone": "1f47c-1f3ff", "woman_cook": "1f469-200d-1f373", "koala": "1f428", "satellite": "1f4e1", "book": "1f4d6", "large_orange_diamond": "1f536", "monaco": "1f1f2-1f1e8", "spiral_notepad": "1f5d2", "capricorn": "2651-fe0f", "bacon": "1f953", "blonde_man_medium_dark_skin_tone": "1f471-1f3fe-200d-2640-fe0f", "business_suit_levitating_dark_skin_tone": "1f574-1f3ff", "call_me_hand_medium_light_skin_tone": "1f919-1f3fc", "female_detective_medium_light_skin_tone": "1f575-1f3fc-200d-2640-fe0f", "haircut_woman_medium_light_skin_tone": "1f487-1f3fc-200d-2640-fe0f", "alien": "1f47d", "baguette_bread": "1f956", "northern_mariana_islands": "1f1f2-1f1f5", "ukraine": "1f1fa-1f1e6", "flushed": "1f633", "man_scientist": "1f468-200d-1f52c", "trident": "1f531", "family_woman_woman_girl_boy_medium_skin_tone": "1f469-1f3fd", "family_man_boy_boy": "1f468-200d-1f466-200d-1f466", "tennis": "1f3be", "fire_engine": "1f692", "pushpin": "1f4cc", "man_health_worker_medium_dark_skin_tone": "1f468-1f3fe", "boy": "1f466", "headphones": "1f3a7", "fuelpump": "26fd-fe0f", "u6709": "1f236", "man_cook_medium_skin_tone": "1f468-1f3fd", "bride_with_veil_medium_skin_tone": "1f470-1f3fd", "point_up": "261d-fe0f", "necktie": "1f454", "control_knobs": "1f39b", "austria": "1f1e6-1f1f9", "papua_new_guinea": "1f1f5-1f1ec", "alembic": "2697-fe0f", "cook_islands": "1f1e8-1f1f0", "iceland": "1f1ee-1f1f8", "car": "1f697", "potable_water": "1f6b0", "haircut_man_medium_light_skin_tone": "1f487-1f3fc-200d-2642-fe0f", "couplekiss_woman_woman_medium_light_skin_tone": "1f469-1f3fc", "couplekiss_man_man_medium_skin_tone": "1f468-1f3fd", "cookie": "1f36a", "flight_departure": "1f6eb", "muscle_dark_skin_tone": "1f4aa-1f3ff", "construction_worker_man_medium_skin_tone": "1f477-1f3fd-200d-2640-fe0f", "black_medium_small_square": "25fe-fe0f", "guyana": "1f1ec-1f1fe", "file_folder": "1f4c1", "fountain_pen": "1f58b", "construction_worker_woman_medium_skin_tone": "1f477-1f3fd-200d-2640-fe0f", "family_man_woman_girl_boy_medium_light_skin_tone": "1f468-1f3fc", "poultry_leg": "1f357", "ski": "1f3bf", "guardswoman_medium_skin_tone": "1f482-1f3fd-200d-2640-fe0f", "family_man_man_girl_medium_dark_skin_tone": "1f468-1f3fe", "family_woman_boy_light_skin_tone": "1f469-1f3fb", "trumpet": "1f3ba", "no_pedestrians": "1f6b7", "heavy_minus_sign": "2796", "fist_oncoming": "1f44a", "ambulance": "1f691", "man_artist_dark_skin_tone": "1f468-1f3ff", "drum": "1f941", "train2": "1f686", "u7121": "1f21a-fe0f", "burkina_faso": "1f1e7-1f1eb", "nose_light_skin_tone": "1f443-1f3fb", "policewoman_medium_dark_skin_tone": "1f46e-1f3fe-200d-2640-fe0f", "lantern": "1f3ee", "metal_light_skin_tone": "1f918-1f3fb", "male_detective_light_skin_tone": "1f575-1f3fb-200d-2640-fe0f", "woman_juggling_medium_dark_skin_tone": "1f939-1f3fe-200d-2640-fe0f", "rowing_man_light_skin_tone": "1f6a3-1f3fb-200d-2640-fe0f", "lying_face": "1f925", "point_left": "1f448", "rosette": "1f3f5", "houses": "1f3d8", "repeat_one": "1f502", "liechtenstein": "1f1f1-1f1ee", "cloud_with_lightning": "1f329", "man_cartwheeling": "1f938-200d-2642-fe0f", "pause_button": "23f8", "arrows_clockwise": "1f503", "raised_hand_with_fingers_splayed_dark_skin_tone": "1f590-1f3ff", "clap_dark_skin_tone": "1f44f-1f3ff", "raising_hand_man_medium_skin_tone": "1f64b-1f3fd-200d-2642-fe0f", "family_woman_woman_girl_girl_medium_dark_skin_tone": "1f469-1f3fe", "dog": "1f436", "pouting_man_medium_dark_skin_tone": "1f64e-1f3fe-200d-2642-fe0f", "surfing_woman_medium_skin_tone": "1f3c4-1f3fd-200d-2640-fe0f", "confused": "1f615", "detective": "1f575-fe0f", "studio_microphone": "1f399", "fist_oncoming_medium_light_skin_tone": "1f44a-1f3fc", "man_firefighter_medium_skin_tone": "1f468-1f3fd", "tshirt": "1f455", "trolleybus": "1f68e", "norway": "1f1f3-1f1f4", "neckbeard": "neckbeard", "three": "0033-fe0f-20e3", "point_right_medium_skin_tone": "1f449-1f3fd", "man_medium_dark_skin_tone": "1f468-1f3fe", "pouting_man_medium_light_skin_tone": "1f64e-1f3fc-200d-2642-fe0f", "clock630": "1f561", "fist_raised_medium_skin_tone": "270a-1f3fd", "anguished": "1f627", "eye": "1f441", "bride_with_veil": "1f470", "hear_no_evil": "1f649", "wine_glass": "1f377", "soon": "1f51c", "family_man_boy_boy_light_skin_tone": "1f468-1f3fb", "family_woman_boy_boy_medium_light_skin_tone": "1f469-1f3fc", "dromedary_camel": "1f42a", "chipmunk": "1f43f", "soccer": "26bd-fe0f", "man_with_gua_pi_mao_medium_light_skin_tone": "1f472-1f3fc", "business_suit_levitating_medium_light_skin_tone": "1f574-1f3fc", "running_woman_medium_dark_skin_tone": "1f3c3-1f3fe-200d-2640-fe0f", "speaking_head": "1f5e3", "menorah": "1f54e", "non-potable_water": "1f6b1", "woman_with_turban_dark_skin_tone": "1f473-1f3ff-200d-2640-fe0f", "woman_shrugging_medium_skin_tone": "1f937-1f3fd-200d-2640-fe0f", "frowning_woman_medium_light_skin_tone": "1f64d-1f3fc-200d-2640-fe0f", "biking_man_medium_light_skin_tone": "1f6b4-1f3fc-200d-2640-fe0f", "woman_shrugging": "1f937-200d-2640-fe0f", "arrow_upper_left": "2196-fe0f", "metal_medium_skin_tone": "1f918-1f3fd", "woman_factory_worker_dark_skin_tone": "1f469-1f3ff", "hotsprings": "2668-fe0f", "ear_dark_skin_tone": "1f442-1f3ff", "girl_medium_skin_tone": "1f467-1f3fd", "woman_farmer_dark_skin_tone": "1f469-1f3ff", "man_student_medium_skin_tone": "1f468-1f3fd", "biking_man_medium_skin_tone": "1f6b4-1f3fd-200d-2640-fe0f", "woman_scientist": "1f469-200d-1f52c", "vs": "1f19a", "weight_lifting_man_medium_skin_tone": "1f3cb-1f3fd-200d-2640-fe0f", "arrow_right": "27a1-fe0f", "woman_juggling_medium_light_skin_tone": "1f939-1f3fc-200d-2640-fe0f", "racehorse": "1f40e", "sun_behind_large_cloud": "1f325", "convenience_store": "1f3ea", "namibia": "1f1f3-1f1e6", "raised_hands_medium_dark_skin_tone": "1f64c-1f3fe", "man_judge_medium_light_skin_tone": "1f468-1f3fc", "dancer_medium_skin_tone": "1f483-1f3fd", "fearful": "1f628", "frog": "1f438", "shopping_cart": "1f6d2", "family_man_boy_medium_light_skin_tone": "1f468-1f3fc", "basketball_man_medium_dark_skin_tone": "26f9-1f3fe-200d-2640-fe0f", "oman": "1f1f4-1f1f2", "paraguay": "1f1f5-1f1fe", "horse": "1f434", "tram": "1f68a", "wastebasket": "1f5d1", "yen": "1f4b4", "heavy_exclamation_mark": "2757-fe0f", "arrow_double_down": "23ec", "walking_woman_dark_skin_tone": "1f6b6-1f3ff-200d-2640-fe0f", "shoe": "1f45e", "ear_of_rice": "1f33e", "mountain": "26f0", "uzbekistan": "1f1fa-1f1ff", "baby_light_skin_tone": "1f476-1f3fb", "haircut_woman_medium_dark_skin_tone": "1f487-1f3fe-200d-2640-fe0f", "golfing_woman_medium_skin_tone": "1f3cc-1f3fd-200d-2640-fe0f", "earth_americas": "1f30e", "woman_playing_water_polo_medium_light_skin_tone": "1f93d-1f3fc-200d-2640-fe0f", "walking_woman": "1f6b6-200d-2640-fe0f", "fried_egg": "1f373", "rocket": "1f680", "artificial_satellite": "1f6f0", "man_shrugging_medium_skin_tone": "1f937-1f3fd-200d-2642-fe0f", "golfing_man_dark_skin_tone": "1f3cc-1f3ff-200d-2640-fe0f", "older_woman_medium_skin_tone": "1f475-1f3fd", "man_with_gua_pi_mao_medium_dark_skin_tone": "1f472-1f3fe", "persevere": "1f623", "raising_hand_woman": "1f64b", "pig": "1f437", "european_castle": "1f3f0", "department_store": "1f3ec", "fist_right_light_skin_tone": "1f91c-1f3fb", "raising_hand_woman_dark_skin_tone": "1f64b-1f3ff-200d-2640-fe0f", "paw_prints": "1f43e", "moon": "1f314", "man_medium_skin_tone": "1f468-1f3fd", "rowing_man_dark_skin_tone": "1f6a3-1f3ff-200d-2640-fe0f", "sleepy": "1f62a", "light_rail": "1f688", "peace_symbol": "262e-fe0f", "m": "24c2-fe0f", "woman_pilot_medium_skin_tone": "1f469-1f3fd", "dango": "1f361", "minibus": "1f690", "family_man_man_girl_girl_medium_dark_skin_tone": "1f468-1f3fe", "dizzy_face": "1f635", "bowing_woman": "1f647-200d-2640-fe0f", "pig2": "1f416", "factory": "1f3ed", "small_red_triangle": "1f53a", "ok_man_light_skin_tone": "1f646-1f3fb-200d-2642-fe0f", "two_women_holding_hands": "1f46d", "funeral_urn": "26b1-fe0f", "cocos_islands": "1f1e8-1f1e8", "lipstick": "1f484", "fleur_de_lis": "269c-fe0f", "man_with_gua_pi_mao_dark_skin_tone": "1f472-1f3ff", "woman_factory_worker_medium_dark_skin_tone": "1f469-1f3fe", "no_good_man_medium_light_skin_tone": "1f645-1f3fc-200d-2642-fe0f", "horse_racing_medium_dark_skin_tone": "1f3c7-1f3fe", "clock1030": "1f565", "couplekiss_man_man_dark_skin_tone": "1f468-1f3ff", "frowning_man": "1f64d-200d-2642-fe0f", "family_woman_boy_boy_dark_skin_tone": "1f469-1f3ff", "family_man_girl_boy_light_skin_tone": "1f468-1f3fb", "smile": "1f604", "clock7": "1f556", "massage_man": "1f486-200d-2642-fe0f", "guardswoman_dark_skin_tone": "1f482-1f3ff-200d-2640-fe0f", "raising_hand_man_dark_skin_tone": "1f64b-1f3ff-200d-2642-fe0f", "woman_with_turban_medium_dark_skin_tone": "1f473-1f3fe-200d-2640-fe0f", "worried": "1f61f", "no_good": "1f645", "card_index": "1f4c7", "aland_islands": "1f1e6-1f1fd", "lion": "1f981", "hammer": "1f528", "bomb": "1f4a3", "reunion": "1f1f7-1f1ea", "walking_man_light_skin_tone": "1f6b6-1f3fb-200d-2640-fe0f", "family_woman_boy_medium_light_skin_tone": "1f469-1f3fc", "pouting_cat": "1f63e", "cow": "1f42e", "motor_scooter": "1f6f5", "hong_kong": "1f1ed-1f1f0", "family_man_girl_medium_dark_skin_tone": "1f468-1f3fe", "sailboat": "26f5-fe0f", "fiji": "1f1eb-1f1ef", "raised_hands_medium_light_skin_tone": "1f64c-1f3fc", "woman_office_worker_dark_skin_tone": "1f469-1f3ff", "family_man_woman_girl_girl_medium_light_skin_tone": "1f468-1f3fc", "arrow_up": "2b06-fe0f", "walking_woman_medium_light_skin_tone": "1f6b6-1f3fc-200d-2640-fe0f", "nose_medium_light_skin_tone": "1f443-1f3fc", "basketball_woman": "26f9-fe0f-200d-2640-fe0f", "+1_medium_light_skin_tone": "1f44d-1f3fc", "crossed_fingers_medium_skin_tone": "1f91e-1f3fd", "raised_back_of_hand_dark_skin_tone": "1f91a-1f3ff", "swimming_woman_medium_light_skin_tone": "1f3ca-1f3fc-200d-2640-fe0f", "construction_worker_woman": "1f477-200d-2640-fe0f", "rugby_football": "1f3c9", "micronesia": "1f1eb-1f1f2", "point_up_2_medium_light_skin_tone": "1f446-1f3fc", "running_man_dark_skin_tone": "1f3c3-1f3ff-200d-2640-fe0f", "woman_playing_handball_medium_light_skin_tone": "1f93e-1f3fc-200d-2640-fe0f", "speaker": "1f508", "jersey": "1f1ef-1f1ea", "laughing": "1f606", "pregnant_woman": "1f930", "haircut_woman": "1f487", "blue_car": "1f699", "microscope": "1f52c", "postbox": "1f4ee", "man_firefighter_dark_skin_tone": "1f468-1f3ff", "sunny": "2600-fe0f", "beginner": "1f530", "clap_medium_light_skin_tone": "1f44f-1f3fc", "man_with_turban_dark_skin_tone": "1f473-1f3ff-200d-2640-fe0f", "rotating_light": "1f6a8", "saudi_arabia": "1f1f8-1f1e6", "family_woman_woman_girl_girl_medium_skin_tone": "1f469-1f3fd", "family_woman_girl_boy_light_skin_tone": "1f469-1f3fb", "man_with_gua_pi_mao": "1f472", "electric_plug": "1f50c", "panama": "1f1f5-1f1e6", "family_woman_woman_girl_light_skin_tone": "1f469-1f3fb", "thinking": "1f914", "point_down": "1f447", "spider": "1f577", "cloud_with_lightning_and_rain": "26c8", "ice_skate": "26f8", "ok_man_medium_dark_skin_tone": "1f646-1f3fe-200d-2642-fe0f", "netherlands": "1f1f3-1f1f1", "family_man_woman_boy": "1f46a", "orange": "1f34a", "snowboarder": "1f3c2", "passenger_ship": "1f6f3", "arrows_counterclockwise": "1f504", "tractor": "1f69c", "gambia": "1f1ec-1f1f2", "middle_finger_dark_skin_tone": "1f595-1f3ff", "tipping_hand_woman_medium_dark_skin_tone": "1f481-1f3fe-200d-2640-fe0f", "family_man_man_girl_boy_medium_light_skin_tone": "1f468-1f3fc", "thumbsup": "1f44d", "couple": "1f46b", "pouch": "1f45d", "asterisk": "002a-fe0f-20e3", "anguilla": "1f1e6-1f1ee", "woman_cook_light_skin_tone": "1f469-1f3fb", "kissing_cat": "1f63d", "nose": "1f443", "point_left_medium_skin_tone": "1f448-1f3fd", "baby_chick": "1f424", "deciduous_tree": "1f333", "u7533": "1f238", "surfing_woman_dark_skin_tone": "1f3c4-1f3ff-200d-2640-fe0f", "woman_shrugging_medium_dark_skin_tone": "1f937-1f3fe-200d-2640-fe0f", "family_woman_woman_boy_boy_dark_skin_tone": "1f469-1f3ff", "cloud_with_rain": "1f327", "oden": "1f362", "botswana": "1f1e7-1f1fc", "greenland": "1f1ec-1f1f1", "man_office_worker_light_skin_tone": "1f468-1f3fb", "raising_hand_woman_medium_dark_skin_tone": "1f64b-1f3fe-200d-2640-fe0f", "family_man_man_girl_boy_medium_dark_skin_tone": "1f468-1f3fe", "school": "1f3eb", "woman_astronaut_light_skin_tone": "1f469-1f3fb", "woman_judge_medium_skin_tone": "1f469-1f3fd", "dancing_men": "1f46f-200d-2642-fe0f", "paperclips": "1f587", "underage": "1f51e", "ok_woman_dark_skin_tone": "1f646-1f3ff-200d-2640-fe0f", "man_playing_handball_dark_skin_tone": "1f93e-1f3ff-200d-2642-fe0f", "family_man_girl_girl": "1f468-200d-1f467-200d-1f467", "wind_face": "1f32c", "banana": "1f34c", "eight": "0038-fe0f-20e3", "man_technologist_medium_dark_skin_tone": "1f468-1f3fe", "man_office_worker_medium_skin_tone": "1f468-1f3fd", "walking_man_dark_skin_tone": "1f6b6-1f3ff-200d-2640-fe0f", "family_man_man_girl_girl_medium_skin_tone": "1f468-1f3fd", "snowman": "26c4-fe0f", "basketball_man": "26f9-fe0f", "information_source": "2139-fe0f", "cote_divoire": "1f1e8-1f1ee", "man_in_tuxedo_light_skin_tone": "1f935-1f3fb", "walking_woman_light_skin_tone": "1f6b6-1f3fb-200d-2640-fe0f", "woman_playing_water_polo_light_skin_tone": "1f93d-1f3fb-200d-2640-fe0f", "bird": "1f426", "o": "2b55-fe0f", "family_woman_girl_medium_skin_tone": "1f469-1f3fd", "rowing_woman_dark_skin_tone": "1f6a3-1f3ff-200d-2640-fe0f", "facepunch": "1f44a", "railway_car": "1f683", "wave_dark_skin_tone": "1f44b-1f3ff", "man_cook_medium_dark_skin_tone": "1f468-1f3fe", "prince_medium_light_skin_tone": "1f934-1f3fc", "cowboy_hat_face": "1f920", "handbag": "1f45c", "hourglass": "231b-fe0f", "albania": "1f1e6-1f1f1", "chile": "1f1e8-1f1f1", "woman_singer_medium_skin_tone": "1f469-1f3fd", "ear_medium_skin_tone": "1f442-1f3fd", "pouting_man_medium_skin_tone": "1f64e-1f3fd-200d-2642-fe0f", "surfing_man_medium_light_skin_tone": "1f3c4-1f3fc-200d-2640-fe0f", "eggplant": "1f346", "next_track_button": "23ed", "gabon": "1f1ec-1f1e6", "western_sahara": "1f1ea-1f1ed", "raised_hands_light_skin_tone": "1f64c-1f3fb", "older_woman_medium_light_skin_tone": "1f475-1f3fc", "joy_cat": "1f639", "feet": "1f43e", "partly_sunny": "26c5-fe0f", "pig_nose": "1f43d", "wc": "1f6be", "malaysia": "1f1f2-1f1fe", "girl_medium_light_skin_tone": "1f467-1f3fc", "man_office_worker_medium_dark_skin_tone": "1f468-1f3fe", "man_mechanic_medium_light_skin_tone": "1f468-1f3fc", "shamrock": "2618-fe0f", "tumbler_glass": "1f943", "palestinian_territories": "1f1f5-1f1f8", "kissing": "1f617", "city_sunset": "1f306", "pencil2": "270f-fe0f", "cool": "1f192", "australia": "1f1e6-1f1fa", "green_heart": "1f49a", "sparkle": "2747-fe0f", "ng_woman": "1f645", "high_heel": "1f460", "hamster": "1f439", "last_quarter_moon": "1f317", "stopwatch": "23f1", "date": "1f4c5", "nail_care_dark_skin_tone": "1f485-1f3ff", "santa_dark_skin_tone": "1f385-1f3ff", "astonished": "1f632", "mushroom": "1f344", "radio": "1f4fb", "hammer_and_wrench": "1f6e0", "arrow_down": "2b07-fe0f", "speech_balloon": "1f4ac", "couple_with_heart_man_man_medium_skin_tone": "1f468-1f3fd", "euro": "1f4b6", "es": "1f1ea-1f1f8", "woman_factory_worker_medium_light_skin_tone": "1f469-1f3fc", "pouting_woman_dark_skin_tone": "1f64e-1f3ff-200d-2640-fe0f", "massage_woman": "1f486", "spades": "2660-fe0f", "blonde_woman_dark_skin_tone": "1f471-1f3ff-200d-2640-fe0f", "man_farmer_medium_skin_tone": "1f468-1f3fd", "man_mechanic_medium_skin_tone": "1f468-1f3fd", "family_man_boy_dark_skin_tone": "1f468-1f3ff", "man_juggling_medium_light_skin_tone": "1f939-1f3fc-200d-2642-fe0f", "hearts": "2665-fe0f", "clock930": "1f564", "central_african_republic": "1f1e8-1f1eb", "boy_medium_skin_tone": "1f466-1f3fd", "pregnant_woman_medium_skin_tone": "1f930-1f3fd", "woman_facepalming_medium_light_skin_tone": "1f926-1f3fc-200d-2640-fe0f", "palm_tree": "1f334", "rose": "1f339", "beers": "1f37b", "red_car": "1f697", "no_entry": "26d4-fe0f", "candy": "1f36c", "fist_oncoming_medium_skin_tone": "1f44a-1f3fd", "rowing_woman_medium_skin_tone": "1f6a3-1f3fd-200d-2640-fe0f", "sake": "1f376", "oncoming_police_car": "1f694", "woman_teacher_medium_dark_skin_tone": "1f469-1f3fe", "family_man_woman_girl_girl_medium_skin_tone": "1f468-1f3fd", "kissing_closed_eyes": "1f61a", "pager": "1f4df", "pencil": "1f4dd", "copyright": "00a9-fe0f", "wave_medium_skin_tone": "1f44b-1f3fd", "loud_sound": "1f50a", "luxembourg": "1f1f1-1f1fa", "policewoman_dark_skin_tone": "1f46e-1f3ff-200d-2640-fe0f", "woman_cartwheeling_medium_skin_tone": "1f938-1f3fd-200d-2640-fe0f", "swimming_woman_medium_dark_skin_tone": "1f3ca-1f3fe-200d-2640-fe0f", "family_man_man_girl_boy": "1f468-200d-1f468-200d-1f467-200d-1f466", "police_car": "1f693", "mailbox_with_no_mail": "1f4ed", "middle_finger_light_skin_tone": "1f595-1f3fb", "pregnant_woman_medium_light_skin_tone": "1f930-1f3fc", "raising_hand_woman_medium_skin_tone": "1f64b-1f3fd-200d-2640-fe0f", "running": "1f3c3", "sun_with_face": "1f31e", "man_teacher_dark_skin_tone": "1f468-1f3ff", "family_man_woman_girl_girl_dark_skin_tone": "1f468-1f3ff", "izakaya_lantern": "1f3ee", "comoros": "1f1f0-1f1f2", "fist_oncoming_medium_dark_skin_tone": "1f44a-1f3fe", "man_singer": "1f468-200d-1f3a4", "mountain_bicyclist": "1f6b5", "point_down_light_skin_tone": "1f447-1f3fb", "family_man_woman_girl_boy_medium_dark_skin_tone": "1f468-1f3fe", "sob": "1f62d", "ophiuchus": "26ce", "greece": "1f1ec-1f1f7", "raised_back_of_hand_medium_skin_tone": "1f91a-1f3fd", "family_man_man_boy_light_skin_tone": "1f468-1f3fb", "woman_cartwheeling_light_skin_tone": "1f938-1f3fb-200d-2640-fe0f", "massage_woman_light_skin_tone": "1f486-1f3fb-200d-2640-fe0f", "fishing_pole_and_fish": "1f3a3", "two_hearts": "1f495", "armenia": "1f1e6-1f1f2", "south_africa": "1f1ff-1f1e6", "boy_light_skin_tone": "1f466-1f3fb", "man_in_tuxedo_medium_dark_skin_tone": "1f935-1f3fe", "kiribati": "1f1f0-1f1ee", "v_dark_skin_tone": "270c-1f3ff", "frowning_man_medium_light_skin_tone": "1f64d-1f3fc-200d-2642-fe0f", "family_woman_woman_girl_boy": "1f469-200d-1f469-200d-1f467-200d-1f466", "family_woman_girl_boy_medium_dark_skin_tone": "1f469-1f3fe", "leopard": "1f406", "fireworks": "1f386", "clock6": "1f555", "bowing_man_medium_light_skin_tone": "1f647-1f3fc-200d-2640-fe0f", "raising_hand": "1f64b", "family_man_woman_girl_girl_light_skin_tone": "1f468-1f3fb", "vulcan_salute_medium_light_skin_tone": "1f596-1f3fc", "guardswoman_medium_light_skin_tone": "1f482-1f3fc-200d-2640-fe0f", "muscle": "1f4aa", "full_moon": "1f315", "pisces": "2653-fe0f", "kosovo": "1f1fd-1f1f0", "fist_left_dark_skin_tone": "1f91b-1f3ff", "point_up_2_dark_skin_tone": "1f446-1f3ff", "man_technologist_dark_skin_tone": "1f468-1f3ff", "spoon": "1f944", "nigeria": "1f1f3-1f1ec", "raised_back_of_hand_medium_light_skin_tone": "1f91a-1f3fc", "blonde_woman_light_skin_tone": "1f471-1f3fb-200d-2640-fe0f", "man_dancing_light_skin_tone": "1f57a-1f3fb", "shrimp": "1f990", "mountain_biking_man": "1f6b5", "boat": "26f5-fe0f", "egypt": "1f1ea-1f1ec", "family_woman_woman_boy_light_skin_tone": "1f469-1f3fb", "man_playing_water_polo_light_skin_tone": "1f93d-1f3fb-200d-2642-fe0f", "family_man_man_boy_boy": "1f468-200d-1f468-200d-1f466-200d-1f466", "foggy": "1f301", "construction_worker_woman_medium_light_skin_tone": "1f477-1f3fc-200d-2640-fe0f", "princess_medium_skin_tone": "1f478-1f3fd", "man_dancing_medium_dark_skin_tone": "1f57a-1f3fe", "couple_with_heart_man_man_dark_skin_tone": "1f468-1f3ff", "carousel_horse": "1f3a0", "crayon": "1f58d", "niue": "1f1f3-1f1fa", "woman_office_worker_medium_skin_tone": "1f469-1f3fd", "swimming_man_medium_skin_tone": "1f3ca-1f3fd-200d-2640-fe0f", "pensive": "1f614", "fire": "1f525", "monorail": "1f69d", "guam": "1f1ec-1f1fa", "older_woman_light_skin_tone": "1f475-1f3fb", "man_facepalming_medium_light_skin_tone": "1f926-1f3fc-200d-2642-fe0f", "family_man_man_girl": "1f468-200d-1f468-200d-1f467", "hammer_and_pick": "2692", "space_invader": "1f47e", "waning_crescent_moon": "1f318", "love_letter": "1f48c", "star_and_crescent": "262a-fe0f", "man_with_turban_light_skin_tone": "1f473-1f3fb-200d-2640-fe0f", "tipping_hand_woman_light_skin_tone": "1f481-1f3fb-200d-2640-fe0f", "dress": "1f457", "rainbow": "1f308", "cheese": "1f9c0", "bento": "1f371", "gear": "2699-fe0f", "-1_medium_skin_tone": "1f44e-1f3fd", "family_man_girl_boy_dark_skin_tone": "1f468-1f3ff", "fish_cake": "1f365", "desert_island": "1f3dd", "crystal_ball": "1f52e", "lock": "1f512", "no_good_man_medium_skin_tone": "1f645-1f3fd-200d-2642-fe0f", "small_blue_diamond": "1f539", "fist_raised_medium_dark_skin_tone": "270a-1f3fe", "man_health_worker_medium_light_skin_tone": "1f468-1f3fc", "ok_man_medium_light_skin_tone": "1f646-1f3fc-200d-2642-fe0f", "man_cartwheeling_dark_skin_tone": "1f938-1f3ff-200d-2642-fe0f", "policeman": "1f46e", "closed_lock_with_key": "1f510", "koko": "1f201", "guardswoman": "1f482-200d-2640-fe0f", "mailbox": "1f4eb", "weight_lifting_woman_light_skin_tone": "1f3cb-1f3fb-200d-2640-fe0f", "drooling_face": "1f924", "motorway": "1f6e3", "orthodox_cross": "2626-fe0f", "peru": "1f1f5-1f1ea", "woman_firefighter_medium_light_skin_tone": "1f469-1f3fc", "atom_symbol": "269b-fe0f", "benin": "1f1e7-1f1ef", "montenegro": "1f1f2-1f1ea", "tonga": "1f1f9-1f1f4", "family_man_boy_boy_medium_skin_tone": "1f468-1f3fd", "man_mechanic_light_skin_tone": "1f468-1f3fb", "female_detective": "1f575-fe0f-200d-2640-fe0f", "closed_umbrella": "1f302", "cow2": "1f404", "ballot_box": "1f5f3", "construction_worker_man_dark_skin_tone": "1f477-1f3ff-200d-2640-fe0f", "woman_technologist_medium_dark_skin_tone": "1f469-1f3fe", "indonesia": "1f1ee-1f1e9", "woman_pilot_medium_light_skin_tone": "1f469-1f3fc", "family_man_man_boy_boy_medium_light_skin_tone": "1f468-1f3fc", "call_me_hand": "1f919", "sun_behind_small_cloud": "1f324", "national_park": "1f3de", "radio_button": "1f518", "selfie_medium_light_skin_tone": "1f933-1f3fc", "woman_firefighter": "1f469-200d-1f692", "metal_dark_skin_tone": "1f918-1f3ff", "older_woman": "1f475", "man_factory_worker_medium_skin_tone": "1f468-1f3fd", "pick": "26cf", "woman_student_medium_skin_tone": "1f469-1f3fd", "mountain_biking_woman_light_skin_tone": "1f6b5-1f3fb-200d-2640-fe0f", "flags": "1f38f", "black_nib": "2712-fe0f", "rwanda": "1f1f7-1f1fc", "surfing_man_light_skin_tone": "1f3c4-1f3fb-200d-2640-fe0f", "first_quarter_moon": "1f313", "oil_drum": "1f6e2", "heart_decoration": "1f49f", "jp": "1f1ef-1f1f5", "woman_pilot": "1f469-200d-2708-fe0f", "city_sunrise": "1f307", "leo": "264c-fe0f", "arrow_up_down": "2195-fe0f", "selfie_medium_skin_tone": "1f933-1f3fd", "surfing_man_medium_skin_tone": "1f3c4-1f3fd-200d-2640-fe0f", "ramen": "1f35c", "up": "1f199", "woman_medium_light_skin_tone": "1f469-1f3fc", "woman_artist": "1f469-200d-1f3a8", "football": "1f3c8", "shopping": "1f6cd", "small_red_triangle_down": "1f53b", "crossed_fingers_light_skin_tone": "1f91e-1f3fb", "woman_artist_medium_dark_skin_tone": "1f469-1f3fe", "milk_glass": "1f95b", "clapper": "1f3ac", "star_of_david": "2721-fe0f", "dominican_republic": "1f1e9-1f1f4", "woman_teacher_light_skin_tone": "1f469-1f3fb", "man_juggling_medium_skin_tone": "1f939-1f3fd-200d-2642-fe0f", "-1": "1f44e", "wedding": "1f492", "faroe_islands": "1f1eb-1f1f4", "raising_hand_man_medium_dark_skin_tone": "1f64b-1f3fe-200d-2642-fe0f", "gemini": "264a-fe0f", "st_helena": "1f1f8-1f1ed", "running_woman_medium_light_skin_tone": "1f3c3-1f3fc-200d-2640-fe0f", "biking_woman_light_skin_tone": "1f6b4-1f3fb-200d-2640-fe0f", "paperclip": "1f4ce", "wave_medium_light_skin_tone": "1f44b-1f3fc", "man_factory_worker_medium_dark_skin_tone": "1f468-1f3fe", "woman_cartwheeling_medium_dark_skin_tone": "1f938-1f3fe-200d-2640-fe0f", "clock12": "1f55b", "ru": "1f1f7-1f1fa", "clown_face": "1f921", "pizza": "1f355", "hole": "1f573", "incoming_envelope": "1f4e8", "yin_yang": "262f-fe0f", "warning": "26a0-fe0f", "family_man_man_girl_boy_dark_skin_tone": "1f468-1f3ff", "man_cartwheeling_medium_skin_tone": "1f938-1f3fd-200d-2642-fe0f", "ram": "1f40f", "cucumber": "1f952", "heartbeat": "1f493", "swaziland": "1f1f8-1f1ff", "nail_care_medium_dark_skin_tone": "1f485-1f3fe", "bath_medium_skin_tone": "1f6c0-1f3fd", "strawberry": "1f353", "peanuts": "1f95c", "field_hockey": "1f3d1", "cricket": "1f3cf", "woman_farmer_medium_dark_skin_tone": "1f469-1f3fe", "family_man_man_girl_girl_light_skin_tone": "1f468-1f3fb", "penguin": "1f427", "star": "2b50-fe0f", "woman_shrugging_light_skin_tone": "1f937-1f3fb-200d-2640-fe0f", "golfing_man_light_skin_tone": "1f3cc-1f3fb-200d-2640-fe0f", "innocent": "1f607", "mosque": "1f54c", "calendar": "1f4c6", "canada": "1f1e8-1f1e6", "rage4": "rage4", "woman_office_worker_medium_dark_skin_tone": "1f469-1f3fe", "poodle": "1f429", "grapes": "1f347", "love_hotel": "1f3e9", "vulcan_salute_medium_skin_tone": "1f596-1f3fd", "guardsman_medium_dark_skin_tone": "1f482-1f3fe-200d-2640-fe0f", "raising_hand_man_light_skin_tone": "1f64b-1f3fb-200d-2642-fe0f", "sleeping": "1f634", "nail_care": "1f485", "monkey": "1f412", "sao_tome_principe": "1f1f8-1f1f9", "dancer_medium_dark_skin_tone": "1f483-1f3fe", "classical_building": "1f3db", "swimming_woman_medium_skin_tone": "1f3ca-1f3fd-200d-2640-fe0f", "ok_hand": "1f44c", "rice_cracker": "1f358", "moyai": "1f5ff", "rage2": "rage2", "angel_light_skin_tone": "1f47c-1f3fb", "family_man_man_boy_boy_dark_skin_tone": "1f468-1f3ff", "smile_cat": "1f638", "angola": "1f1e6-1f1f4", "cameroon": "1f1e8-1f1f2", "man_student_medium_dark_skin_tone": "1f468-1f3fe", "weight_lifting_woman_medium_light_skin_tone": "1f3cb-1f3fc-200d-2640-fe0f", "waxing_crescent_moon": "1f312", "articulated_lorry": "1f69b", "pouting_woman_light_skin_tone": "1f64e-1f3fb-200d-2640-fe0f", "running_man_medium_dark_skin_tone": "1f3c3-1f3fe-200d-2640-fe0f", "couple_with_heart_woman_woman_light_skin_tone": "1f469-1f3fb", "horse_racing_light_skin_tone": "1f3c7-1f3fb", "raised_back_of_hand": "1f91a", "saxophone": "1f3b7", "right_anger_bubble": "1f5ef", "tokelau": "1f1f9-1f1f0", "no_good_woman_medium_light_skin_tone": "1f645-1f3fc-200d-2640-fe0f", "walking_woman_medium_skin_tone": "1f6b6-1f3fd-200d-2640-fe0f", "family_woman_girl_girl": "1f469-200d-1f467-200d-1f467", "cake": "1f370", "abcd": "1f521", "tuvalu": "1f1f9-1f1fb", "suspect": "suspect", "mattermost": "mattermost", "swimming_woman_light_skin_tone": "1f3ca-1f3fb-200d-2640-fe0f", "white_medium_square": "25fb-fe0f", "haircut_woman_medium_skin_tone": "1f487-1f3fd-200d-2640-fe0f", "massage_woman_dark_skin_tone": "1f486-1f3ff-200d-2640-fe0f", "family_man_woman_girl_light_skin_tone": "1f468-1f3fb", "turks_caicos_islands": "1f1f9-1f1e8", "point_left_dark_skin_tone": "1f448-1f3ff", "family_man_man_boy_medium_dark_skin_tone": "1f468-1f3fe", "hand": "270b", "coffee": "2615-fe0f", "somalia": "1f1f8-1f1f4", "mountain_biking_man_dark_skin_tone": "1f6b5-1f3ff-200d-2640-fe0f", "hatching_chick": "1f423", "pear": "1f350", "baby_bottle": "1f37c", "ribbon": "1f380", "st_kitts_nevis": "1f1f0-1f1f3", "radioactive": "2622-fe0f", "end": "1f51a", "hand_medium_skin_tone": "270b-1f3fd", "family_woman_woman_girl_medium_light_skin_tone": "1f469-1f3fc", "3rd_place_medal": "1f949", "fist_left_medium_dark_skin_tone": "1f91b-1f3fe", "bolivia": "1f1e7-1f1f4", "point_up_light_skin_tone": "261d-1f3fb", "cherries": "1f352", "inbox_tray": "1f4e5", "pitcairn_islands": "1f1f5-1f1f3", "rage1": "rage1", "man_farmer_medium_dark_skin_tone": "1f468-1f3fe", "woman_with_turban": "1f473-200d-2640-fe0f", "unicorn": "1f984", "butterfly": "1f98b", "watch": "231a-fe0f", "arrow_up_small": "1f53c", "triangular_flag_on_post": "1f6a9", "heart_eyes": "1f60d", "shallow_pan_of_food": "1f958", "broken_heart": "1f494", "family_man_boy_boy_dark_skin_tone": "1f468-1f3ff", "golfing_woman_dark_skin_tone": "1f3cc-1f3ff-200d-2640-fe0f", "bath_dark_skin_tone": "1f6c0-1f3ff", "selfie": "1f933", "congratulations": "3297-fe0f", "baby_medium_light_skin_tone": "1f476-1f3fc", "woman_health_worker_medium_skin_tone": "1f469-1f3fd", "man_juggling": "1f939-200d-2642-fe0f", "arrow_down_small": "1f53d", "writing_hand_medium_light_skin_tone": "270d-1f3fc", "blonde_woman": "1f471-200d-2640-fe0f", "massage": "1f486", "metro": "1f687", "bath": "1f6c0", "female_detective_light_skin_tone": "1f575-1f3fb-200d-2640-fe0f", "haircut_man_light_skin_tone": "1f487-1f3fb-200d-2642-fe0f", "bowing_woman_medium_dark_skin_tone": "1f647-1f3fe-200d-2640-fe0f", "family_woman_woman_boy_medium_dark_skin_tone": "1f469-1f3fe", "shell": "1f41a", "seychelles": "1f1f8-1f1e8", "tipping_hand_man_medium_skin_tone": "1f481-1f3fd-200d-2642-fe0f", "panda_face": "1f43c", "sint_maarten": "1f1f8-1f1fd", "face_with_head_bandage": "1f915", "checkered_flag": "1f3c1", "samoa": "1f1fc-1f1f8", "v_medium_skin_tone": "270c-1f3fd", "couple_with_heart_man_man": "1f468-200d-2764-fe0f-200d-1f468", "shaved_ice": "1f367", "badminton": "1f3f8", "clock530": "1f560", "man_playing_water_polo_medium_dark_skin_tone": "1f93d-1f3fe-200d-2642-fe0f", "bulgaria": "1f1e7-1f1ec", "hurtrealbad": "hurtrealbad", "fist_oncoming_dark_skin_tone": "1f44a-1f3ff", "bat": "1f987", "signal_strength": "1f4f6", "iran": "1f1ee-1f1f7", "construction_worker_woman_medium_dark_skin_tone": "1f477-1f3fe-200d-2640-fe0f", "kiwi_fruit": "1f95d", "2nd_place_medal": "1f948", "kaaba": "1f54b", "knife": "1f52a", "ok_hand_light_skin_tone": "1f44c-1f3fb", "angel_medium_dark_skin_tone": "1f47c-1f3fe", "spider_web": "1f578", "oncoming_taxi": "1f696", "bookmark": "1f516", "u6307": "1f22f-fe0f", "za": "1f1ff-1f1e6", "fist_raised_light_skin_tone": "270a-1f3fb", "mag_right": "1f50e", "guinea": "1f1ec-1f1f3", "family_woman_woman_girl_dark_skin_tone": "1f469-1f3ff", "man_playing_handball_medium_light_skin_tone": "1f93e-1f3fc-200d-2642-fe0f", "game_die": "1f3b2", "bullettrain_front": "1f685", "speedboat": "1f6a4", "hand_dark_skin_tone": "270b-1f3ff", "selfie_light_skin_tone": "1f933-1f3fb", "family_man_woman_boy_boy_dark_skin_tone": "1f468-1f3ff", "running_man": "1f3c3", "couplekiss_woman_woman": "1f469-200d-2764-fe0f-200d-1f48b-200d-1f469", "woman_teacher": "1f469-200d-1f3eb", "running_shirt_with_sash": "1f3bd", "bowing_man_medium_skin_tone": "1f647-1f3fd-200d-2640-fe0f", "point_right_medium_dark_skin_tone": "1f449-1f3fe", "man_cartwheeling_medium_light_skin_tone": "1f938-1f3fc-200d-2642-fe0f", "pouting_man_light_skin_tone": "1f64e-1f3fb-200d-2642-fe0f", "biking_man_light_skin_tone": "1f6b4-1f3fb-200d-2640-fe0f", "oncoming_automobile": "1f698", "steam_locomotive": "1f682", "newspaper": "1f4f0", "antigua_barbuda": "1f1e6-1f1ec", "macau": "1f1f2-1f1f4", "niger": "1f1f3-1f1ea", "chicken": "1f414", "flashlight": "1f526", "family_man_woman_boy_boy_medium_skin_tone": "1f468-1f3fd", "mens": "1f6b9", "it": "1f1ee-1f1f9", "new_caledonia": "1f1f3-1f1e8", "pray_medium_light_skin_tone": "1f64f-1f3fc", "nose_medium_dark_skin_tone": "1f443-1f3fe", "man_facepalming_medium_dark_skin_tone": "1f926-1f3fe-200d-2642-fe0f", "poop": "1f4a9", "clap_light_skin_tone": "1f44f-1f3fb", "guardsman_medium_skin_tone": "1f482-1f3fd-200d-2640-fe0f", "woman_teacher_dark_skin_tone": "1f469-1f3ff", "grinning": "1f600", "aries": "2648-fe0f", "mrs_claus": "1f936", "green_book": "1f4d7", "middle_finger_medium_dark_skin_tone": "1f595-1f3fe", "rowing_woman_light_skin_tone": "1f6a3-1f3fb-200d-2640-fe0f", "tokyo_tower": "1f5fc", "printer": "1f5a8", "put_litter_in_its_place": "1f6ae", "suriname": "1f1f8-1f1f7", "woman_light_skin_tone": "1f469-1f3fb", "man_playing_water_polo_medium_light_skin_tone": "1f93d-1f3fc-200d-2642-fe0f", "dove": "1f54a", "latin_cross": "271d-fe0f", "exclamation": "2757-fe0f", "man_health_worker_dark_skin_tone": "1f468-1f3ff", "bride_with_veil_light_skin_tone": "1f470-1f3fb", "rowboat": "1f6a3", "world_map": "1f5fa", "sleeping_bed": "1f6cc", "haircut_man_dark_skin_tone": "1f487-1f3ff-200d-2642-fe0f", "surfing_man_dark_skin_tone": "1f3c4-1f3ff-200d-2640-fe0f", "couple_with_heart_woman_man": "1f491", "chart_with_upwards_trend": "1f4c8", "fist_right_dark_skin_tone": "1f91c-1f3ff", "raised_hand_with_fingers_splayed_medium_skin_tone": "1f590-1f3fd", "older_woman_medium_dark_skin_tone": "1f475-1f3fe", "couplekiss_woman_woman_medium_dark_skin_tone": "1f469-1f3fe", "hatched_chick": "1f425", "running_man_medium_skin_tone": "1f3c3-1f3fd-200d-2640-fe0f", "chocolate_bar": "1f36b", "grenada": "1f1ec-1f1e9", "man_farmer_dark_skin_tone": "1f468-1f3ff", "milky_way": "1f30c", "slovakia": "1f1f8-1f1f0", "selfie_dark_skin_tone": "1f933-1f3ff", "prince_medium_skin_tone": "1f934-1f3fd", "family_man_boy": "1f468-200d-1f466", "chains": "26d3", "british_virgin_islands": "1f1fb-1f1ec", "bow_and_arrow": "1f3f9", "ferry": "26f4", "o2": "1f17e-fe0f", "st_barthelemy": "1f1e7-1f1f1", "policewoman_medium_skin_tone": "1f46e-1f3fd-200d-2640-fe0f", "imp": "1f47f", "bathtub": "1f6c1", "anger": "1f4a2", "previous_track_button": "23ee", "pouting_woman_medium_light_skin_tone": "1f64e-1f3fc-200d-2640-fe0f", "rabbit2": "1f407", "newspaper_roll": "1f5de", "one": "0031-fe0f-20e3", "family_man_man_girl_medium_skin_tone": "1f468-1f3fd", "pouting_woman": "1f64e", "moneybag": "1f4b0", "guardsman_dark_skin_tone": "1f482-1f3ff-200d-2640-fe0f", "man_shrugging_medium_light_skin_tone": "1f937-1f3fc-200d-2642-fe0f", "smirk": "1f60f", "woman_farmer": "1f469-200d-1f33e", "ocean": "1f30a", "sweat_drops": "1f4a6", "x": "274c", "woman_judge_medium_dark_skin_tone": "1f469-1f3fe", "tropical_drink": "1f379", "brunei": "1f1e7-1f1f3", "woman_artist_medium_light_skin_tone": "1f469-1f3fc", "pregnant_woman_medium_dark_skin_tone": "1f930-1f3fe", "basketball_woman_light_skin_tone": "26f9-1f3fb-200d-2640-fe0f", "evergreen_tree": "1f332", "fax": "1f4e0", "woman_medium_dark_skin_tone": "1f469-1f3fe", "walking_woman_medium_dark_skin_tone": "1f6b6-1f3fe-200d-2640-fe0f", "lollipop": "1f36d", "bicyclist": "1f6b4", "bulb": "1f4a1", "computer": "1f4bb", "frowning_man_dark_skin_tone": "1f64d-1f3ff-200d-2642-fe0f", "guardsman_medium_light_skin_tone": "1f482-1f3fc-200d-2640-fe0f", "dancer_light_skin_tone": "1f483-1f3fb", "no_good_woman": "1f645", "cherry_blossom": "1f338", "woman_playing_water_polo": "1f93d-200d-2640-fe0f", "heavy_division_sign": "2797", "sri_lanka": "1f1f1-1f1f0", "-1_medium_light_skin_tone": "1f44e-1f3fc", "family_woman_girl_girl_dark_skin_tone": "1f469-1f3ff", "raised_hands": "1f64c", "sandal": "1f461", "rhinoceros": "1f98f", "swimming_man": "1f3ca", "scissors": "2702-fe0f", "horse_racing_dark_skin_tone": "1f3c7-1f3ff", "coffin": "26b0-fe0f", "clock1": "1f550", "eritrea": "1f1ea-1f1f7", "qatar": "1f1f6-1f1e6", "tanzania": "1f1f9-1f1ff", "pregnant_woman_light_skin_tone": "1f930-1f3fb", "cop": "1f46e", "tipping_hand_woman": "1f481", "estonia": "1f1ea-1f1ea", "man_singer_light_skin_tone": "1f468-1f3fb", "woman_judge_dark_skin_tone": "1f469-1f3ff", "business_suit_levitating_medium_skin_tone": "1f574-1f3fd", "blowfish": "1f421", "mountain_railway": "1f69e", "fast_forward": "23e9", "+1_medium_skin_tone": "1f44d-1f3fd", "goat": "1f410", "congo_kinshasa": "1f1e8-1f1e9", "point_down_dark_skin_tone": "1f447-1f3ff", "basketball_man_light_skin_tone": "26f9-1f3fb-200d-2640-fe0f", "woman_playing_handball_medium_dark_skin_tone": "1f93e-1f3fe-200d-2640-fe0f", "doughnut": "1f369", "musical_keyboard": "1f3b9", "couplekiss_woman_woman_medium_skin_tone": "1f469-1f3fd", "heavy_heart_exclamation": "2763-fe0f", "u6e80": "1f235", "woman_with_turban_medium_skin_tone": "1f473-1f3fd-200d-2640-fe0f", "horse_racing_medium_skin_tone": "1f3c7-1f3fd", "ear": "1f442", "canoe": "1f6f6", "andorra": "1f1e6-1f1e9", "ca": "1f1e8-1f1e6", "family_man_man_boy": "1f468-200d-1f468-200d-1f466", "ticket": "1f3ab", "station": "1f689", "large_blue_circle": "1f535", "palau": "1f1f5-1f1fc", "blush": "1f60a", "man_student": "1f468-200d-1f393", "woman_singer": "1f469-200d-1f3a4", "house_with_garden": "1f3e1", "smoking": "1f6ac", "b": "1f171-fe0f", "golfing_woman_light_skin_tone": "1f3cc-1f3fb-200d-2640-fe0f", "man_artist_light_skin_tone": "1f468-1f3fb", "unamused": "1f612", "japanese_ogre": "1f479", "film_projector": "1f4fd", "ballot_box_with_check": "2611-fe0f", "goberserk": "goberserk", "metal_medium_light_skin_tone": "1f918-1f3fc", "latvia": "1f1f1-1f1fb", "moldova": "1f1f2-1f1e9", "mask": "1f637", "bowing_man": "1f647", "man_shrugging": "1f937-200d-2642-fe0f", "ping_pong": "1f3d3", "trackball": "1f5b2", "six": "0036-fe0f-20e3", "point_up_medium_skin_tone": "261d-1f3fd", "hotel": "1f3e8", "bookmark_tabs": "1f4d1", "chart_with_downwards_trend": "1f4c9", "v_light_skin_tone": "270c-1f3fb", "tipping_hand_woman_medium_skin_tone": "1f481-1f3fd-200d-2640-fe0f", "heart_eyes_cat": "1f63b", "dancer": "1f483", "movie_camera": "1f3a5", "two": "0032-fe0f-20e3", "clap_medium_dark_skin_tone": "1f44f-1f3fe", "woman_astronaut_medium_skin_tone": "1f469-1f3fd", "frowning": "1f626", "cry": "1f622", "no_bell": "1f515", "hand_medium_dark_skin_tone": "270b-1f3fe", "ear_light_skin_tone": "1f442-1f3fb", "family_man_girl_boy": "1f468-200d-1f467-200d-1f466", "swimming_woman": "1f3ca-200d-2640-fe0f", "mountain_biking_woman": "1f6b5-200d-2640-fe0f", "mantelpiece_clock": "1f570", "bermuda": "1f1e7-1f1f2", "new_zealand": "1f1f3-1f1ff", "massage_man_medium_dark_skin_tone": "1f486-1f3fe-200d-2642-fe0f", "crown": "1f451", "biking_man": "1f6b4", "woman_playing_water_polo_medium_skin_tone": "1f93d-1f3fd-200d-2640-fe0f", "man_in_tuxedo_dark_skin_tone": "1f935-1f3ff", "no_entry_sign": "1f6ab", "hash": "0023-fe0f-20e3", "white_small_square": "25ab-fe0f", "iraq": "1f1ee-1f1f6", "switzerland": "1f1e8-1f1ed", "woman_mechanic_light_skin_tone": "1f469-1f3fb", "squirrel": "shipit", "woman_cook_medium_light_skin_tone": "1f469-1f3fc", "confounded": "1f616", "+1": "1f44d", "rowing_man": "1f6a3", "mailbox_closed": "1f4ea", "customs": "1f6c3", "mayotte": "1f1fe-1f1f9", "man_mechanic_medium_dark_skin_tone": "1f468-1f3fe", "man_artist_medium_skin_tone": "1f468-1f3fd", "man_playing_handball_medium_skin_tone": "1f93e-1f3fd-200d-2642-fe0f", "grimacing": "1f62c", "dart": "1f3af", "wave_medium_dark_skin_tone": "1f44b-1f3fe", "slightly_smiling_face": "1f642", "medal_sports": "1f3c5", "bank": "1f3e6", "man_student_medium_light_skin_tone": "1f468-1f3fc", "man_pilot_light_skin_tone": "1f468-1f3fb", "weight_lifting_woman_medium_skin_tone": "1f3cb-1f3fd-200d-2640-fe0f", "dash": "1f4a8", "volcano": "1f30b", "antarctica": "1f1e6-1f1f6", "woman_facepalming_light_skin_tone": "1f926-1f3fb-200d-2640-fe0f", "man_dancing_medium_light_skin_tone": "1f57a-1f3fc", "scream_cat": "1f640", "fog": "1f32b", "fist_oncoming_light_skin_tone": "1f44a-1f3fb", "man_dancing_medium_skin_tone": "1f57a-1f3fd", "burrito": "1f32f", "thought_balloon": "1f4ad", "massage_man_medium_light_skin_tone": "1f486-1f3fc-200d-2642-fe0f", "couple_with_heart_woman_woman_dark_skin_tone": "1f469-1f3ff", "writing_hand": "270d-fe0f", "zap": "26a1-fe0f", "recycle": "267b-fe0f", "policewoman_medium_light_skin_tone": "1f46e-1f3fc-200d-2640-fe0f", "frowning_woman_medium_skin_tone": "1f64d-1f3fd-200d-2640-fe0f", "massage_man_light_skin_tone": "1f486-1f3fb-200d-2642-fe0f", "woman_student": "1f469-200d-1f393", "surfing_woman": "1f3c4-200d-2640-fe0f", "sunrise": "1f305", "open_file_folder": "1f4c2", "diamonds": "2666-fe0f", "family_man_woman_girl_girl": "1f468-200d-1f469-200d-1f467-200d-1f467", "airplane": "2708-fe0f", "arrow_heading_down": "2935-fe0f", "uruguay": "1f1fa-1f1fe", "point_down_medium_dark_skin_tone": "1f447-1f3fe", "family_man_man_boy_dark_skin_tone": "1f468-1f3ff", "family_man_woman_girl_boy": "1f468-200d-1f469-200d-1f467-200d-1f466", "confetti_ball": "1f38a", "flower_playing_cards": "1f3b4", "algeria": "1f1e9-1f1ff", "man_teacher_medium_light_skin_tone": "1f468-1f3fc", "woman_artist_light_skin_tone": "1f469-1f3fb", "family_man_woman_girl_medium_skin_tone": "1f468-1f3fd", "nerd_face": "1f913", "eyes": "1f440", "boot": "1f462", "unlock": "1f513", "zzz": "1f4a4", "vatican_city": "1f1fb-1f1e6", "hot_pepper": "1f336", "slot_machine": "1f3b0", "sunrise_over_mountains": "1f304", "haircut_man_medium_skin_tone": "1f487-1f3fd-200d-2642-fe0f", "stuck_out_tongue": "1f61b", "point_up_medium_dark_skin_tone": "261d-1f3fe", "vulcan_salute_medium_dark_skin_tone": "1f596-1f3fe", "family": "1f46a", "key": "1f511", "myanmar": "1f1f2-1f1f2", "policeman_medium_light_skin_tone": "1f46e-1f3fc-200d-2640-fe0f", "man_shrugging_medium_dark_skin_tone": "1f937-1f3fe-200d-2642-fe0f", "woman_health_worker": "1f469-200d-2695-fe0f", "woman_judge": "1f469-200d-2696-fe0f", "japan": "1f5fe", "dominica": "1f1e9-1f1f2", "dragon": "1f409", "open_book": "1f4d6", "raising_hand_man": "1f64b-200d-2642-fe0f", "bikini": "1f459", "loudspeaker": "1f4e2", "woman_astronaut_medium_light_skin_tone": "1f469-1f3fc", "envelope_with_arrow": "1f4e9", "thailand": "1f1f9-1f1ed", "point_up_medium_light_skin_tone": "261d-1f3fc", "baby_medium_dark_skin_tone": "1f476-1f3fe", "man_scientist_medium_skin_tone": "1f468-1f3fd", "bowing_woman_medium_light_skin_tone": "1f647-1f3fc-200d-2640-fe0f", "construction_worker": "1f477", "nut_and_bolt": "1f529", "sparkling_heart": "1f496", "couplekiss_woman_woman_dark_skin_tone": "1f469-1f3ff", "elephant": "1f418", "bar_chart": "1f4ca", "nose_dark_skin_tone": "1f443-1f3ff", "stop_button": "23f9", "family_man_woman_boy_boy_light_skin_tone": "1f468-1f3fb", "family_man_girl_medium_light_skin_tone": "1f468-1f3fc", "relieved": "1f60c", "man_in_tuxedo": "1f935", "kick_scooter": "1f6f4", "statue_of_liberty": "1f5fd", "information_desk_person": "1f481", "sa": "1f202-fe0f", "abc": "1f524", "robot": "1f916", "cat": "1f431", "accept": "1f251", "upside_down_face": "1f643", "cloud": "2601-fe0f", "frowning_man_light_skin_tone": "1f64d-1f3fb-200d-2642-fe0f", "walking_man_medium_skin_tone": "1f6b6-1f3fd-200d-2640-fe0f", "sparkles": "2728", "u5272": "1f239", "globe_with_meridians": "1f310", "frowning_woman_medium_dark_skin_tone": "1f64d-1f3fe-200d-2640-fe0f", "grey_exclamation": "2755", "tm": "2122-fe0f", "massage_man_dark_skin_tone": "1f486-1f3ff-200d-2642-fe0f", "family_woman_woman_girl_boy_dark_skin_tone": "1f469-1f3ff", "paintbrush": "1f58c", "arrow_right_hook": "21aa-fe0f", "mauritania": "1f1f2-1f1f7", "man_scientist_light_skin_tone": "1f468-1f3fb", "woman_juggling_light_skin_tone": "1f939-1f3fb-200d-2640-fe0f", "ok_woman": "1f646", "snail": "1f40c", "hocho": "1f52a", "arrow_forward": "25b6-fe0f", "french_southern_territories": "1f1f9-1f1eb", "iphone": "1f4f1", "princess_medium_light_skin_tone": "1f478-1f3fc", "maple_leaf": "1f341", "open_hands": "1f450", "racing_car": "1f3ce", "pill": "1f48a", "cuba": "1f1e8-1f1fa", "fist_raised_dark_skin_tone": "270a-1f3ff", "blonde_man_dark_skin_tone": "1f471-1f3ff-200d-2640-fe0f", "family_woman_girl_boy_dark_skin_tone": "1f469-1f3ff", "fox_face": "1f98a", "man_playing_handball": "1f93e-200d-2642-fe0f", "bullettrain_side": "1f684", "black_small_square": "25aa-fe0f", "kazakhstan": "1f1f0-1f1ff", "vanuatu": "1f1fb-1f1fa", "older_man_medium_skin_tone": "1f474-1f3fd", "man_teacher": "1f468-200d-1f3eb", "family_man_man_boy_boy_medium_dark_skin_tone": "1f468-1f3fe", "back": "1f519", "point_up_2_medium_dark_skin_tone": "1f446-1f3fe", "woman_teacher_medium_light_skin_tone": "1f469-1f3fc", "family_woman_boy_boy_medium_dark_skin_tone": "1f469-1f3fe", "surfing_woman_medium_light_skin_tone": "1f3c4-1f3fc-200d-2640-fe0f", "portugal": "1f1f5-1f1f9", "construction_worker_woman_dark_skin_tone": "1f477-1f3ff-200d-2640-fe0f", "family_man_man_boy_medium_skin_tone": "1f468-1f3fd", "family_man_girl_dark_skin_tone": "1f468-1f3ff", "woman_mechanic": "1f469-200d-1f527", "arrow_heading_up": "2934-fe0f", "clock330": "1f55e", "malawi": "1f1f2-1f1fc", "ok_hand_medium_dark_skin_tone": "1f44c-1f3fe", "prince_dark_skin_tone": "1f934-1f3ff", "ice_hockey": "1f3d2", "pk": "1f1f5-1f1f0", "san_marino": "1f1f8-1f1f2", "point_left_light_skin_tone": "1f448-1f3fb", "woman_office_worker_medium_light_skin_tone": "1f469-1f3fc", "swimming_man_medium_light_skin_tone": "1f3ca-1f3fc-200d-2640-fe0f", "stuffed_flatbread": "1f959", "aerial_tramway": "1f6a1", "family_man_man_girl_girl_dark_skin_tone": "1f468-1f3ff", "family_woman_girl_girl_medium_dark_skin_tone": "1f469-1f3fe", "closed_book": "1f4d5", "family_woman_girl_boy_medium_light_skin_tone": "1f469-1f3fc", "family_man_man_girl_boy_medium_skin_tone": "1f468-1f3fd", "v": "270c-fe0f", "play_or_pause_button": "23ef", "el_salvador": "1f1f8-1f1fb", "woman_judge_medium_light_skin_tone": "1f469-1f3fc", "santa_medium_light_skin_tone": "1f385-1f3fc", "couplekiss_man_man_light_skin_tone": "1f468-1f3fb", "blonde_man_light_skin_tone": "1f471-1f3fb-200d-2640-fe0f", "fist_right": "1f91c", "man_with_turban": "1f473", "cancer": "264b-fe0f", "tunisia": "1f1f9-1f1f3", "open_hands_medium_light_skin_tone": "1f450-1f3fc", "call_me_hand_medium_dark_skin_tone": "1f919-1f3fe", "tired_face": "1f62b", "tongue": "1f445", "shower": "1f6bf", "british_indian_ocean_territory": "1f1ee-1f1f4", "man_firefighter_medium_light_skin_tone": "1f468-1f3fc", "couple_with_heart_woman_woman_medium_dark_skin_tone": "1f469-1f3fe", "crescent_moon": "1f319", "ecuador": "1f1ea-1f1e8", "french_polynesia": "1f1f5-1f1eb", "man_light_skin_tone": "1f468-1f3fb", "mountain_biking_woman_medium_skin_tone": "1f6b5-1f3fd-200d-2640-fe0f", "pakistan": "1f1f5-1f1f0", "open_hands_medium_dark_skin_tone": "1f450-1f3fe", "telephone": "260e-fe0f", "envelope": "2709-fe0f", "revolving_hearts": "1f49e", "mega": "1f4e3", "montserrat": "1f1f2-1f1f8", "uganda": "1f1fa-1f1ec", "tropical_fish": "1f420", "hibiscus": "1f33a", "rainbow_flag": "1f3f3-fe0f-200d-1f308", "bangladesh": "1f1e7-1f1e9", "shipit": "shipit", "no_good_man_dark_skin_tone": "1f645-1f3ff-200d-2642-fe0f", "no_mouth": "1f636", "man_farmer": "1f468-200d-1f33e", "speak_no_evil": "1f64a", "level_slider": "1f39a", "guatemala": "1f1ec-1f1f9", "woman_factory_worker": "1f469-200d-1f3ed", "fork_and_knife": "1f374", "belarus": "1f1e7-1f1fe", "family_woman_woman_girl_boy_medium_dark_skin_tone": "1f469-1f3fe", "yum": "1f60b", "helicopter": "1f681", "busstop": "1f68f", "policewoman_light_skin_tone": "1f46e-1f3fb-200d-2640-fe0f", "man_technologist_medium_skin_tone": "1f468-1f3fd", "man_with_gua_pi_mao_light_skin_tone": "1f472-1f3fb", "man_astronaut_dark_skin_tone": "1f468-1f3ff", "skull": "1f480", "smirk_cat": "1f63c", "jeans": "1f456", "flipper": "1f42c", "dizzy": "1f4ab", "cocktail": "1f378", "basketball_woman_medium_skin_tone": "26f9-1f3fd-200d-2640-fe0f", "v_medium_light_skin_tone": "270c-1f3fc", "secret": "3299-fe0f", "seven": "0037-fe0f-20e3", "ghana": "1f1ec-1f1ed", "guernsey": "1f1ec-1f1ec", "kyrgyzstan": "1f1f0-1f1ec", "godmode": "godmode", "female_detective_dark_skin_tone": "1f575-1f3ff-200d-2640-fe0f", "fallen_leaf": "1f342", "snowflake": "2744-fe0f", "raised_hand_with_fingers_splayed_medium_dark_skin_tone": "1f590-1f3fe", "woman_health_worker_medium_dark_skin_tone": "1f469-1f3fe", "man_shrugging_dark_skin_tone": "1f937-1f3ff-200d-2642-fe0f", "pout": "1f621", "stars": "1f320", "family_woman_girl_boy": "1f469-200d-1f467-200d-1f466", "gun": "1f52b", "woman_scientist_dark_skin_tone": "1f469-1f3ff", "basketball_woman_dark_skin_tone": "26f9-1f3ff-200d-2640-fe0f", "biking_woman_medium_light_skin_tone": "1f6b4-1f3fc-200d-2640-fe0f", "family_man_girl_boy_medium_dark_skin_tone": "1f468-1f3fe", "oncoming_bus": "1f68d", "seat": "1f4ba", "vhs": "1f4fc", "lithuania": "1f1f1-1f1f9", "v_medium_dark_skin_tone": "270c-1f3fe", "man_with_gua_pi_mao_medium_skin_tone": "1f472-1f3fd", "frowning_face": "2639-fe0f", "shit": "1f4a9", "ab": "1f18e", "couple_with_heart_woman_woman_medium_skin_tone": "1f469-1f3fd", "family_woman_woman_girl_girl": "1f469-200d-1f469-200d-1f467-200d-1f467", "potato": "1f954", "minidisc": "1f4bd", "libya": "1f1f1-1f1fe", "point_right_dark_skin_tone": "1f449-1f3ff", "man_artist": "1f468-200d-1f3a8", "pineapple": "1f34d", "spaghetti": "1f35d", "couch_and_lamp": "1f6cb", "free": "1f193", "jamaica": "1f1ef-1f1f2", "woman_astronaut_dark_skin_tone": "1f469-1f3ff", "man_mechanic": "1f468-200d-1f527", "curry": "1f35b", "small_orange_diamond": "1f538", "pray": "1f64f", "hotdog": "1f32d", "currency_exchange": "1f4b1", "-1_dark_skin_tone": "1f44e-1f3ff", "man_office_worker_dark_skin_tone": "1f468-1f3ff", "clock830": "1f563", "policeman_medium_skin_tone": "1f46e-1f3fd-200d-2640-fe0f", "grin": "1f601", "water_buffalo": "1f403", "older_man_dark_skin_tone": "1f474-1f3ff", "business_suit_levitating_medium_dark_skin_tone": "1f574-1f3fe", "couple_with_heart_man_man_medium_light_skin_tone": "1f468-1f3fc", "rowing_man_medium_light_skin_tone": "1f6a3-1f3fc-200d-2640-fe0f", "purse": "1f45b", "slovenia": "1f1f8-1f1ee", "tipping_hand_man_medium_light_skin_tone": "1f481-1f3fc-200d-2642-fe0f", "madagascar": "1f1f2-1f1ec", "south_georgia_south_sandwich_islands": "1f1ec-1f1f8", "punch": "1f44a", "man_pilot": "1f468-200d-2708-fe0f", "owl": "1f989", "croissant": "1f950", "email": "2709-fe0f", "outbox_tray": "1f4e4", "construction_worker_man_medium_light_skin_tone": "1f477-1f3fc-200d-2640-fe0f", "mrs_claus_medium_dark_skin_tone": "1f936-1f3fe", "family_man_woman_girl_boy_dark_skin_tone": "1f468-1f3ff", "file_cabinet": "1f5c4", "hungary": "1f1ed-1f1fa", "pray_medium_dark_skin_tone": "1f64f-1f3fe", "woman_mechanic_dark_skin_tone": "1f469-1f3ff", "angel_medium_skin_tone": "1f47c-1f3fd", "man_dancing": "1f57a", "pound": "1f4b7", "macedonia": "1f1f2-1f1f0", "man_facepalming_medium_skin_tone": "1f926-1f3fd-200d-2642-fe0f", "scroll": "1f4dc", "rescue_worker_helmet": "26d1", "desktop_computer": "1f5a5", "heavy_plus_sign": "2795", "man_with_turban_medium_skin_tone": "1f473-1f3fd-200d-2640-fe0f", "horse_racing": "1f3c7", "low_brightness": "1f505", "loop": "27bf", "man_with_turban_medium_dark_skin_tone": "1f473-1f3fe-200d-2640-fe0f", "champagne": "1f37e", "construction_worker_woman_light_skin_tone": "1f477-1f3fb-200d-2640-fe0f", "man_teacher_light_skin_tone": "1f468-1f3fb", "family_woman_woman_girl_girl_medium_light_skin_tone": "1f469-1f3fc", "footprints": "1f463", "cloud_with_snow": "1f328", "man_cook_medium_light_skin_tone": "1f468-1f3fc", "woman_mechanic_medium_light_skin_tone": "1f469-1f3fc", "point_up_2": "1f446", "circus_tent": "1f3aa", "serbia": "1f1f7-1f1f8", "fist_right_medium_dark_skin_tone": "1f91c-1f3fe", "weight_lifting_woman_dark_skin_tone": "1f3cb-1f3ff-200d-2640-fe0f", "musical_score": "1f3bc", "violin": "1f3bb", "card_file_box": "1f5c3", "tipping_hand_woman_dark_skin_tone": "1f481-1f3ff-200d-2640-fe0f", "man_facepalming_dark_skin_tone": "1f926-1f3ff-200d-2642-fe0f", "open_mouth": "1f62e", "left_right_arrow": "2194-fe0f", "no_good_man_light_skin_tone": "1f645-1f3fb-200d-2642-fe0f", "man_factory_worker": "1f468-200d-1f3ed", "man_judge": "1f468-200d-2696-fe0f", "negative_squared_cross_mark": "274e", "bowing_woman_medium_skin_tone": "1f647-1f3fd-200d-2640-fe0f", "family_woman_boy_boy_light_skin_tone": "1f469-1f3fb", "battery": "1f50b", "couplekiss_man_man_medium_light_skin_tone": "1f468-1f3fc", "clock5": "1f554", "white_flag": "1f3f3-fe0f", "guadeloupe": "1f1ec-1f1f5", "muscle_medium_dark_skin_tone": "1f4aa-1f3fe", "man_scientist_dark_skin_tone": "1f468-1f3ff", "business_suit_levitating_light_skin_tone": "1f574-1f3fb", "woman_office_worker": "1f469-200d-1f4bc", "gift": "1f381", "sound": "1f509", "clubs": "2663-fe0f", "woman_scientist_medium_light_skin_tone": "1f469-1f3fc", "female_detective_medium_skin_tone": "1f575-1f3fd-200d-2640-fe0f", "man_singer_medium_light_skin_tone": "1f468-1f3fc", "family_man_girl": "1f468-200d-1f467", "bee": "1f41d", "full_moon_with_face": "1f31d", "black_medium_square": "25fc-fe0f", "zambia": "1f1ff-1f1f2", "raised_hands_dark_skin_tone": "1f64c-1f3ff", "family_woman_woman_boy_boy_medium_skin_tone": "1f469-1f3fd", "bread": "1f35e", "clock11": "1f55a", "man_office_worker_medium_light_skin_tone": "1f468-1f3fc", "woman_firefighter_medium_skin_tone": "1f469-1f3fd", "man_dancing_dark_skin_tone": "1f57a-1f3ff", "family_man_boy_medium_dark_skin_tone": "1f468-1f3fe", "hugs": "1f917", "roll_eyes": "1f644", "raised_hand": "270b", "tangerine": "1f34a", "grey_question": "2754", "princess_light_skin_tone": "1f478-1f3fb", "motor_boat": "1f6e5", "passport_control": "1f6c2", "man_artist_medium_dark_skin_tone": "1f468-1f3fe", "golfing_man_medium_skin_tone": "1f3cc-1f3fd-200d-2640-fe0f", "shirt": "1f455", "whale": "1f433", "apple": "1f34e", "ethiopia": "1f1ea-1f1f9", "jordan": "1f1ef-1f1f4", "biking_woman_medium_dark_skin_tone": "1f6b4-1f3fe-200d-2640-fe0f", "family_woman_girl_girl_medium_skin_tone": "1f469-1f3fd", "turkey": "1f983", "snowman_with_snow": "2603-fe0f", "fist_left_medium_skin_tone": "1f91b-1f3fd", "woman_with_turban_light_skin_tone": "1f473-1f3fb-200d-2640-fe0f", "woman_pilot_dark_skin_tone": "1f469-1f3ff", "family_woman_woman_boy_medium_skin_tone": "1f469-1f3fd", "purple_heart": "1f49c", "black_heart": "1f5a4", "haircut_man": "1f487-200d-2642-fe0f", "arrow_lower_left": "2199-fe0f", "guinea_bissau": "1f1ec-1f1fc", "sudan": "1f1f8-1f1e9", "woman_scientist_light_skin_tone": "1f469-1f3fb", "bust_in_silhouette": "1f464", "walking": "1f6b6", "european_union": "1f1ea-1f1fa", "running_woman_dark_skin_tone": "1f3c3-1f3ff-200d-2640-fe0f", "om": "1f549", "rowing_man_medium_skin_tone": "1f6a3-1f3fd-200d-2640-fe0f", "ideograph_advantage": "1f250", "nepal": "1f1f3-1f1f5", "syria": "1f1f8-1f1fe", "man_pilot_medium_dark_skin_tone": "1f468-1f3fe", "princess_medium_dark_skin_tone": "1f478-1f3fe", "watermelon": "1f349", "left_luggage": "1f6c5", "us": "1f1fa-1f1f8", "point_left_medium_light_skin_tone": "1f448-1f3fc", "family_man_girl_boy_medium_skin_tone": "1f468-1f3fd", "biking_man_medium_dark_skin_tone": "1f6b4-1f3fe-200d-2640-fe0f", "keycap_ten": "1f51f", "man_medium_light_skin_tone": "1f468-1f3fc", "couple_with_heart_man_man_light_skin_tone": "1f468-1f3fb", "family_man_man_girl_boy_light_skin_tone": "1f468-1f3fb", "dog2": "1f415", "art": "1f3a8", "taxi": "1f695", "motorcycle": "1f3cd", "diamond_shape_with_a_dot_inside": "1f4a0", "writing_hand_light_skin_tone": "270d-1f3fb", "woman_playing_water_polo_medium_dark_skin_tone": "1f93d-1f3fe-200d-2640-fe0f", "martial_arts_uniform": "1f94b", "spiral_calendar": "1f5d3", "older_man_medium_dark_skin_tone": "1f474-1f3fe", "woman_artist_medium_skin_tone": "1f469-1f3fd", "no_good_man_medium_dark_skin_tone": "1f645-1f3fe-200d-2642-fe0f", "family_woman_woman_boy_medium_light_skin_tone": "1f469-1f3fc", "ship": "1f6a2", "bangbang": "203c-fe0f", "israel": "1f1ee-1f1f1", "rowing_man_medium_dark_skin_tone": "1f6a3-1f3fe-200d-2640-fe0f", "calling": "1f4f2", "scorpius": "264f-fe0f", "vulcan_salute_dark_skin_tone": "1f596-1f3ff", "woman_office_worker_light_skin_tone": "1f469-1f3fb", "man_judge_light_skin_tone": "1f468-1f3fb", "family_woman_woman_boy_boy_medium_dark_skin_tone": "1f469-1f3fe", "woman_playing_handball": "1f93e-200d-2640-fe0f", "bridge_at_night": "1f309", "stop_sign": "1f6d1", "8ball": "1f3b1", "orange_book": "1f4d9", "couplekiss_man_woman": "1f48f", "no_mobile_phones": "1f4f5", "pouting_man_dark_skin_tone": "1f64e-1f3ff-200d-2642-fe0f", "man_juggling_light_skin_tone": "1f939-1f3fb-200d-2642-fe0f", "cold_sweat": "1f630", "star2": "1f31f", "taco": "1f32e", "point_right_medium_light_skin_tone": "1f449-1f3fc", "selfie_medium_dark_skin_tone": "1f933-1f3fe", "family_woman_woman_girl_boy_light_skin_tone": "1f469-1f3fb", "hankey": "1f4a9", "monkey_face": "1f435", "sweden": "1f1f8-1f1ea", "crocodile": "1f40a", "last_quarter_moon_with_face": "1f31c", "comet": "2604-fe0f", "caribbean_netherlands": "1f1e7-1f1f6", "walking_man_medium_dark_skin_tone": "1f6b6-1f3fe-200d-2640-fe0f", "basketball_man_medium_light_skin_tone": "26f9-1f3fc-200d-2640-fe0f", "deer": "1f98c", "clock4": "1f553", "christmas_island": "1f1e8-1f1fd", "fist_right_medium_skin_tone": "1f91c-1f3fd", "man_cook_dark_skin_tone": "1f468-1f3ff", "family_man_man_girl_medium_light_skin_tone": "1f468-1f3fc", "whale2": "1f40b", "sagittarius": "2650-fe0f", "children_crossing": "1f6b8", "call_me_hand_dark_skin_tone": "1f919-1f3ff", "ok_woman_medium_dark_skin_tone": "1f646-1f3fe-200d-2640-fe0f", "man_firefighter": "1f468-200d-1f692", "rewind": "23ea", "guardswoman_light_skin_tone": "1f482-1f3fb-200d-2640-fe0f", "woman_technologist_light_skin_tone": "1f469-1f3fb", "woman_pilot_light_skin_tone": "1f469-1f3fb", "raising_hand_woman_light_skin_tone": "1f64b-1f3fb-200d-2640-fe0f", "bowing_man_light_skin_tone": "1f647-1f3fb-200d-2640-fe0f", "frowning_man_medium_skin_tone": "1f64d-1f3fd-200d-2642-fe0f", "shark": "1f988", "sun_behind_rain_cloud": "1f326", "dagger": "1f5e1", "musical_note": "1f3b5", "crossed_fingers_dark_skin_tone": "1f91e-1f3ff", "man_pilot_medium_skin_tone": "1f468-1f3fd", "family_woman_boy_medium_skin_tone": "1f469-1f3fd", "golfing_man_medium_light_skin_tone": "1f3cc-1f3fc-200d-2640-fe0f", "girl": "1f467", "family_man_woman_boy_boy": "1f468-200d-1f469-200d-1f466-200d-1f466", "biking_woman": "1f6b4-200d-2640-fe0f", "cl": "1f191", "raised_back_of_hand_medium_dark_skin_tone": "1f91a-1f3fe", "raising_hand_woman_medium_light_skin_tone": "1f64b-1f3fc-200d-2640-fe0f", "baby_medium_skin_tone": "1f476-1f3fd", "guardsman": "1f482", "woman_astronaut": "1f469-200d-1f680", "tophat": "1f3a9", "honduras": "1f1ed-1f1f3", "mexico": "1f1f2-1f1fd", "nauru": "1f1f3-1f1f7", "mrs_claus_medium_light_skin_tone": "1f936-1f3fc", "weary": "1f629", "womans_hat": "1f452", "person_fencing": "1f93a", "u6708": "1f237-fe0f", "a": "1f170-fe0f", "de": "1f1e9-1f1ea", "lebanon": "1f1f1-1f1e7", "puerto_rico": "1f1f5-1f1f7", "man_mechanic_dark_skin_tone": "1f468-1f3ff", "policeman_dark_skin_tone": "1f46e-1f3ff-200d-2640-fe0f", "kissing_smiling_eyes": "1f619", "avocado": "1f951", "six_pointed_star": "1f52f", "record_button": "23fa", "family_woman_woman_girl_girl_light_skin_tone": "1f469-1f3fb", "mountain_biking_man_medium_light_skin_tone": "1f6b5-1f3fc-200d-2640-fe0f", "couple_with_heart_man_man_medium_dark_skin_tone": "1f468-1f3fe", "family_man_girl_girl_light_skin_tone": "1f468-1f3fb", "post_office": "1f3e3", "telescope": "1f52d", "baby_symbol": "1f6bc", "capital_abcd": "1f520", "woman_singer_light_skin_tone": "1f469-1f3fb", "woman_facepalming_medium_dark_skin_tone": "1f926-1f3fe-200d-2640-fe0f", "ant": "1f41c", "house": "1f3e0", "shield": "1f6e1", "yellow_heart": "1f49b", "u55b6": "1f23a", "senegal": "1f1f8-1f1f3", "united_arab_emirates": "1f1e6-1f1ea", "no_good_woman_medium_skin_tone": "1f645-1f3fd-200d-2640-fe0f", "running_man_medium_light_skin_tone": "1f3c3-1f3fc-200d-2640-fe0f", "beetle": "1f41e", "bus": "1f68c", "flight_arrival": "1f6ec", "black_large_square": "2b1b-fe0f", "white_large_square": "2b1c-fe0f", "woman_technologist_medium_light_skin_tone": "1f469-1f3fc", "skier": "26f7", "ok_hand_medium_light_skin_tone": "1f44c-1f3fc", "rofl": "1f923", "hushed": "1f62f", "ng_man": "1f645-200d-2642-fe0f", "running_woman": "1f3c3-200d-2640-fe0f", "family_man_man_girl_girl": "1f468-200d-1f468-200d-1f467-200d-1f467", "gorilla": "1f98d", "horse_racing_medium_light_skin_tone": "1f3c7-1f3fc", "mountain_biking_man_medium_skin_tone": "1f6b5-1f3fd-200d-2640-fe0f", "disappointed": "1f61e", "dolphin": "1f42c", "green_apple": "1f34f", "honey_pot": "1f36f", "georgia": "1f1ec-1f1ea", "business_suit_levitating": "1f574", "camera": "1f4f7", "ledger": "1f4d2", "woman_cook_medium_skin_tone": "1f469-1f3fd", "bahamas": "1f1e7-1f1f8", "family_woman_woman_girl": "1f469-200d-1f469-200d-1f467", "man_factory_worker_light_skin_tone": "1f468-1f3fb", "golfing_woman_medium_dark_skin_tone": "1f3cc-1f3fe-200d-2640-fe0f", "family_woman_girl": "1f469-200d-1f467", "turtle": "1f422", "mauritius": "1f1f2-1f1fa", "family_man_girl_light_skin_tone": "1f468-1f3fb", "hospital": "1f3e5", "church": "26ea-fe0f", "wheel_of_dharma": "2638-fe0f", "mongolia": "1f1f2-1f1f3", "man_facepalming": "1f926-200d-2642-fe0f", "bed": "1f6cf", "man_artist_medium_light_skin_tone": "1f468-1f3fc", "santa_medium_dark_skin_tone": "1f385-1f3fe", "family_man_woman_girl_medium_light_skin_tone": "1f468-1f3fc", "large_blue_diamond": "1f537", "colombia": "1f1e8-1f1f4", "philippines": "1f1f5-1f1ed", "older_woman_dark_skin_tone": "1f475-1f3ff", "woman_scientist_medium_skin_tone": "1f469-1f3fd", "couplekiss_woman_woman_light_skin_tone": "1f469-1f3fb", "male_detective": "1f575-fe0f", "crossed_swords": "2694-fe0f", "notebook": "1f4d3", "nail_care_medium_light_skin_tone": "1f485-1f3fc", "blonde_man_medium_skin_tone": "1f471-1f3fd-200d-2640-fe0f", "tipping_hand_man_light_skin_tone": "1f481-1f3fb-200d-2642-fe0f", "sweat_smile": "1f605", "white_medium_small_square": "25fd-fe0f", "bowtie": "bowtie", "reminder_ribbon": "1f397", "clamp": "1f5dc", "balance_scale": "2696-fe0f", "postal_horn": "1f4ef", "swimming_woman_dark_skin_tone": "1f3ca-1f3ff-200d-2640-fe0f", "raised_hand_with_fingers_splayed": "1f590", "ok_man": "1f646-200d-2642-fe0f", "new": "1f195", "male_detective_medium_dark_skin_tone": "1f575-1f3fe-200d-2640-fe0f", "computer_mouse": "1f5b1", "hourglass_flowing_sand": "23f3", "bahrain": "1f1e7-1f1ed", "djibouti": "1f1e9-1f1ef", "zimbabwe": "1f1ff-1f1fc", "swimming_man_light_skin_tone": "1f3ca-1f3fb-200d-2640-fe0f", "interrobang": "2049-fe0f", "clock8": "1f557", "pancakes": "1f95e", "thermometer": "1f321", "label": "1f3f7", "denmark": "1f1e9-1f1f0", "raising_hand_man_medium_light_skin_tone": "1f64b-1f3fc-200d-2642-fe0f", "frowning_man_medium_dark_skin_tone": "1f64d-1f3fe-200d-2642-fe0f", "point_right": "1f449", "guitar": "1f3b8", "family_woman_woman_girl_medium_skin_tone": "1f469-1f3fd", "woman_juggling_medium_skin_tone": "1f939-1f3fd-200d-2640-fe0f", "man_health_worker": "1f468-200d-2695-fe0f", "stew": "1f372", "surfing_man": "1f3c4", "twisted_rightwards_arrows": "1f500", "timor_leste": "1f1f9-1f1f1", "weight_lifting_woman": "1f3cb-fe0f-200d-2640-fe0f", "amphora": "1f3fa", "heart": "2764-fe0f", "bowing_man_medium_dark_skin_tone": "1f647-1f3fe-200d-2640-fe0f"} diff --git a/vendor/github.com/mattermost/mattermost-server/model/emoji_search.go b/vendor/github.com/mattermost/mattermost-server/model/emoji_search.go new file mode 100644 index 00000000..3a768a57 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/emoji_search.go @@ -0,0 +1,25 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type EmojiSearch struct { + Term string `json:"term"` + PrefixOnly bool `json:"prefix_only"` +} + +func (es *EmojiSearch) ToJson() string { + b, _ := json.Marshal(es) + return string(b) +} + +func EmojiSearchFromJson(data io.Reader) *EmojiSearch { + var es *EmojiSearch + json.NewDecoder(data).Decode(&es) + return es +} diff --git a/vendor/github.com/mattermost/platform/model/file.go b/vendor/github.com/mattermost/mattermost-server/model/file.go similarity index 76% rename from vendor/github.com/mattermost/platform/model/file.go rename to vendor/github.com/mattermost/mattermost-server/model/file.go index f9258146..c7ffbf0b 100644 --- a/vendor/github.com/mattermost/platform/model/file.go +++ b/vendor/github.com/mattermost/mattermost-server/model/file.go @@ -23,21 +23,12 @@ type FileUploadResponse struct { } func FileUploadResponseFromJson(data io.Reader) *FileUploadResponse { - decoder := json.NewDecoder(data) - var o FileUploadResponse - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *FileUploadResponse + json.NewDecoder(data).Decode(&o) + return o } func (o *FileUploadResponse) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } diff --git a/vendor/github.com/mattermost/platform/model/file_info.go b/vendor/github.com/mattermost/mattermost-server/model/file_info.go similarity index 95% rename from vendor/github.com/mattermost/platform/model/file_info.go rename to vendor/github.com/mattermost/mattermost-server/model/file_info.go index 0ee2c50d..e0bbfcfc 100644 --- a/vendor/github.com/mattermost/platform/model/file_info.go +++ b/vendor/github.com/mattermost/mattermost-server/model/file_info.go @@ -35,12 +35,8 @@ type FileInfo struct { } func (info *FileInfo) ToJson() string { - b, err := json.Marshal(info) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(info) + return string(b) } func FileInfoFromJson(data io.Reader) *FileInfo { @@ -55,12 +51,8 @@ func FileInfoFromJson(data io.Reader) *FileInfo { } func FileInfosToJson(infos []*FileInfo) string { - b, err := json.Marshal(infos) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(infos) + return string(b) } func FileInfosFromJson(data io.Reader) []*FileInfo { diff --git a/vendor/github.com/mattermost/platform/model/gitlab.go b/vendor/github.com/mattermost/mattermost-server/model/gitlab.go similarity index 100% rename from vendor/github.com/mattermost/platform/model/gitlab.go rename to vendor/github.com/mattermost/mattermost-server/model/gitlab.go diff --git a/vendor/github.com/mattermost/platform/model/incoming_webhook.go b/vendor/github.com/mattermost/mattermost-server/model/incoming_webhook.go similarity index 85% rename from vendor/github.com/mattermost/platform/model/incoming_webhook.go rename to vendor/github.com/mattermost/mattermost-server/model/incoming_webhook.go index 3e0488d2..3856d22f 100644 --- a/vendor/github.com/mattermost/platform/model/incoming_webhook.go +++ b/vendor/github.com/mattermost/mattermost-server/model/incoming_webhook.go @@ -16,17 +16,18 @@ const ( ) type IncomingWebhook struct { - Id string `json:"id"` - CreateAt int64 `json:"create_at"` - UpdateAt int64 `json:"update_at"` - DeleteAt int64 `json:"delete_at"` - UserId string `json:"user_id"` - ChannelId string `json:"channel_id"` - TeamId string `json:"team_id"` - DisplayName string `json:"display_name"` - Description string `json:"description"` - Username string `json:"username"` - IconURL string `json:"icon_url"` + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + UserId string `json:"user_id"` + ChannelId string `json:"channel_id"` + TeamId string `json:"team_id"` + DisplayName string `json:"display_name"` + Description string `json:"description"` + Username string `json:"username"` + IconURL string `json:"icon_url"` + ChannelLocked bool `json:"channel_locked"` } type IncomingWebhookRequest struct { @@ -40,43 +41,25 @@ type IncomingWebhookRequest struct { } func (o *IncomingWebhook) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func IncomingWebhookFromJson(data io.Reader) *IncomingWebhook { - decoder := json.NewDecoder(data) - var o IncomingWebhook - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *IncomingWebhook + json.NewDecoder(data).Decode(&o) + return o } func IncomingWebhookListToJson(l []*IncomingWebhook) string { - b, err := json.Marshal(l) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(l) + return string(b) } func IncomingWebhookListFromJson(data io.Reader) []*IncomingWebhook { - decoder := json.NewDecoder(data) var o []*IncomingWebhook - err := decoder.Decode(&o) - if err == nil { - return o - } else { - return nil - } + json.NewDecoder(data).Decode(&o) + return o } func (o *IncomingWebhook) IsValid() *AppError { @@ -110,7 +93,7 @@ func (o *IncomingWebhook) IsValid() *AppError { return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.display_name.app_error", nil, "", http.StatusBadRequest) } - if len(o.Description) > 128 { + if len(o.Description) > 500 { return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.description.app_error", nil, "", http.StatusBadRequest) } @@ -214,7 +197,7 @@ func IncomingWebhookRequestFromJson(data io.Reader) (*IncomingWebhookRequest, *A if err != nil { o, err = decodeIncomingWebhookRequest(escapeControlCharsFromPayload(by)) if err != nil { - return nil, NewAppError("IncomingWebhookRequestFromJson", "Unable to parse incoming data", nil, err.Error(), http.StatusBadRequest) + return nil, NewAppError("IncomingWebhookRequestFromJson", "model.incoming_hook.parse_data.app_error", nil, err.Error(), http.StatusBadRequest) } } @@ -222,3 +205,12 @@ func IncomingWebhookRequestFromJson(data io.Reader) (*IncomingWebhookRequest, *A return o, nil } + +func (o *IncomingWebhookRequest) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} diff --git a/vendor/github.com/mattermost/platform/model/initial_load.go b/vendor/github.com/mattermost/mattermost-server/model/initial_load.go similarity index 74% rename from vendor/github.com/mattermost/platform/model/initial_load.go rename to vendor/github.com/mattermost/mattermost-server/model/initial_load.go index 71ba1769..3be68044 100644 --- a/vendor/github.com/mattermost/platform/model/initial_load.go +++ b/vendor/github.com/mattermost/mattermost-server/model/initial_load.go @@ -19,21 +19,12 @@ type InitialLoad struct { } func (me *InitialLoad) ToJson() string { - b, err := json.Marshal(me) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(me) + return string(b) } func InitialLoadFromJson(data io.Reader) *InitialLoad { - decoder := json.NewDecoder(data) - var o InitialLoad - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *InitialLoad + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/platform/model/job.go b/vendor/github.com/mattermost/mattermost-server/model/job.go similarity index 91% rename from vendor/github.com/mattermost/platform/model/job.go rename to vendor/github.com/mattermost/mattermost-server/model/job.go index 9a756602..c1661495 100644 --- a/vendor/github.com/mattermost/platform/model/job.go +++ b/vendor/github.com/mattermost/mattermost-server/model/job.go @@ -16,6 +16,7 @@ const ( JOB_TYPE_ELASTICSEARCH_POST_INDEXING = "elasticsearch_post_indexing" JOB_TYPE_ELASTICSEARCH_POST_AGGREGATION = "elasticsearch_post_aggregation" JOB_TYPE_LDAP_SYNC = "ldap_sync" + JOB_TYPE_MIGRATIONS = "migrations" JOB_STATUS_PENDING = "pending" JOB_STATUS_IN_PROGRESS = "in_progress" @@ -52,6 +53,7 @@ func (j *Job) IsValid() *AppError { case JOB_TYPE_ELASTICSEARCH_POST_AGGREGATION: case JOB_TYPE_LDAP_SYNC: case JOB_TYPE_MESSAGE_EXPORT: + case JOB_TYPE_MIGRATIONS: default: return NewAppError("Job.IsValid", "model.job.is_valid.type.app_error", nil, "id="+j.Id, http.StatusBadRequest) } @@ -71,11 +73,8 @@ func (j *Job) IsValid() *AppError { } func (js *Job) ToJson() string { - if b, err := json.Marshal(js); err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(js) + return string(b) } func JobFromJson(data io.Reader) *Job { @@ -88,11 +87,8 @@ func JobFromJson(data io.Reader) *Job { } func JobsToJson(jobs []*Job) string { - if b, err := json.Marshal(jobs); err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(jobs) + return string(b) } func JobsFromJson(data io.Reader) []*Job { @@ -105,11 +101,8 @@ func JobsFromJson(data io.Reader) []*Job { } func (js *Job) DataToJson() string { - if b, err := json.Marshal(js.Data); err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(js.Data) + return string(b) } type Worker interface { diff --git a/vendor/github.com/mattermost/platform/model/ldap.go b/vendor/github.com/mattermost/mattermost-server/model/ldap.go similarity index 78% rename from vendor/github.com/mattermost/platform/model/ldap.go rename to vendor/github.com/mattermost/mattermost-server/model/ldap.go index 1453a4ad..9051c5a3 100644 --- a/vendor/github.com/mattermost/platform/model/ldap.go +++ b/vendor/github.com/mattermost/mattermost-server/model/ldap.go @@ -5,5 +5,4 @@ package model const ( USER_AUTH_SERVICE_LDAP = "ldap" - LDAP_SYNC_TASK_NAME = "LDAP Syncronization" ) diff --git a/vendor/github.com/mattermost/platform/model/license.go b/vendor/github.com/mattermost/mattermost-server/model/license.go similarity index 83% rename from vendor/github.com/mattermost/platform/model/license.go rename to vendor/github.com/mattermost/mattermost-server/model/license.go index a81f882c..c30fecf7 100644 --- a/vendor/github.com/mattermost/platform/model/license.go +++ b/vendor/github.com/mattermost/mattermost-server/model/license.go @@ -46,16 +46,16 @@ type Features struct { Compliance *bool `json:"compliance"` Cluster *bool `json:"cluster"` Metrics *bool `json:"metrics"` - CustomBrand *bool `json:"custom_brand"` MHPNS *bool `json:"mhpns"` SAML *bool `json:"saml"` - PasswordRequirements *bool `json:"password_requirements"` Elasticsearch *bool `json:"elastic_search"` Announcement *bool `json:"announcement"` ThemeManagement *bool `json:"theme_management"` EmailNotificationContents *bool `json:"email_notification_contents"` DataRetention *bool `json:"data_retention"` MessageExport *bool `json:"message_export"` + CustomPermissionsSchemes *bool `json:"custom_permissions_schemes"` + CustomTermsOfService *bool `json:"custom_terms_of_service"` // after we enabled more features for webrtc we'll need to control them with this FutureFeatures *bool `json:"future_features"` @@ -70,14 +70,13 @@ func (f *Features) ToMap() map[string]interface{} { "compliance": *f.Compliance, "cluster": *f.Cluster, "metrics": *f.Metrics, - "custom_brand": *f.CustomBrand, "mhpns": *f.MHPNS, "saml": *f.SAML, - "password": *f.PasswordRequirements, "elastic_search": *f.Elasticsearch, "email_notification_contents": *f.EmailNotificationContents, "data_retention": *f.DataRetention, "message_export": *f.MessageExport, + "custom_permissions_schemes": *f.CustomPermissionsSchemes, "future": *f.FutureFeatures, } } @@ -119,10 +118,6 @@ func (f *Features) SetDefaults() { f.Metrics = NewBool(*f.FutureFeatures) } - if f.CustomBrand == nil { - f.CustomBrand = NewBool(*f.FutureFeatures) - } - if f.MHPNS == nil { f.MHPNS = NewBool(*f.FutureFeatures) } @@ -131,10 +126,6 @@ func (f *Features) SetDefaults() { f.SAML = NewBool(*f.FutureFeatures) } - if f.PasswordRequirements == nil { - f.PasswordRequirements = NewBool(*f.FutureFeatures) - } - if f.Elasticsearch == nil { f.Elasticsearch = NewBool(*f.FutureFeatures) } @@ -158,6 +149,14 @@ func (f *Features) SetDefaults() { if f.MessageExport == nil { f.MessageExport = NewBool(*f.FutureFeatures) } + + if f.CustomPermissionsSchemes == nil { + f.CustomPermissionsSchemes = NewBool(*f.FutureFeatures) + } + + if f.CustomTermsOfService == nil { + f.CustomTermsOfService = NewBool(*f.FutureFeatures) + } } func (l *License) IsExpired() bool { @@ -169,23 +168,33 @@ func (l *License) IsStarted() bool { } func (l *License) ToJson() string { - b, err := json.Marshal(l) - if err != nil { - return "" - } else { - return string(b) + b, _ := json.Marshal(l) + return string(b) +} + +// NewTestLicense returns a license that expires in the future and has the given features. +func NewTestLicense(features ...string) *License { + ret := &License{ + ExpiresAt: GetMillis() + 90*24*60*60*1000, + Customer: &Customer{}, + Features: &Features{}, } + ret.Features.SetDefaults() + + featureMap := map[string]bool{} + for _, feature := range features { + featureMap[feature] = true + } + featureJson, _ := json.Marshal(featureMap) + json.Unmarshal(featureJson, &ret.Features) + + return ret } func LicenseFromJson(data io.Reader) *License { - decoder := json.NewDecoder(data) - var o License - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *License + json.NewDecoder(data).Decode(&o) + return o } func (lr *LicenseRecord) IsValid() *AppError { diff --git a/vendor/github.com/mattermost/platform/model/manifest.go b/vendor/github.com/mattermost/mattermost-server/model/manifest.go similarity index 62% rename from vendor/github.com/mattermost/platform/model/manifest.go rename to vendor/github.com/mattermost/mattermost-server/model/manifest.go index 03d78f84..6a7df59f 100644 --- a/vendor/github.com/mattermost/platform/model/manifest.go +++ b/vendor/github.com/mattermost/mattermost-server/model/manifest.go @@ -5,23 +5,16 @@ package model import ( "encoding/json" + "fmt" "io" "io/ioutil" "os" "path/filepath" + "strings" "gopkg.in/yaml.v2" ) -const ( - PLUGIN_CONFIG_TYPE_TEXT = "text" - PLUGIN_CONFIG_TYPE_BOOL = "bool" - PLUGIN_CONFIG_TYPE_RADIO = "radio" - PLUGIN_CONFIG_TYPE_DROPDOWN = "dropdown" - PLUGIN_CONFIG_TYPE_GENERATED = "generated" - PLUGIN_CONFIG_TYPE_USERNAME = "username" -) - type PluginOption struct { // The display name for the option. DisplayName string `json:"display_name" yaml:"display_name"` @@ -92,7 +85,7 @@ type PluginSettingsSchema struct { // id: com.mycompany.myplugin // name: My Plugin // description: This is my plugin. It does stuff. -// backend: +// server: // executable: myplugin // settings_schema: // settings: @@ -102,9 +95,9 @@ type PluginSettingsSchema struct { // help_text: When true, an extra thing will be enabled! // default: false type Manifest struct { - // The id is a globally unique identifier that represents your plugin. Ids are limited - // to 190 characters. Reverse-DNS notation using a name you control is a good option. - // For example, "com.mycompany.myplugin". + // The id is a globally unique identifier that represents your plugin. Ids must be at least + // 3 characters, at most 190 characters and must match ^[a-zA-Z0-9-_\.]+$. + // Reverse-DNS notation using a name you control is a good option, e.g. "com.mycompany.myplugin". Id string `json:"id" yaml:"id"` // The name to be displayed for the plugin. @@ -116,8 +109,11 @@ type Manifest struct { // A version number for your plugin. Semantic versioning is recommended: http://semver.org Version string `json:"version" yaml:"version"` - // If your plugin extends the server, you'll need define backend. - Backend *ManifestBackend `json:"backend,omitempty" yaml:"backend,omitempty"` + // Server defines the server-side portion of your plugin. + Server *ManifestServer `json:"server,omitempty" yaml:"server,omitempty"` + + // Backend is a deprecated flag for defining the server-side portion of your plugin. Going forward, use Server instead. + Backend *ManifestServer `json:"backend,omitempty" yaml:"backend,omitempty"` // If your plugin extends the web app, you'll need to define webapp. Webapp *ManifestWebapp `json:"webapp,omitempty" yaml:"webapp,omitempty"` @@ -127,58 +123,60 @@ type Manifest struct { SettingsSchema *PluginSettingsSchema `json:"settings_schema,omitempty" yaml:"settings_schema,omitempty"` } -type ManifestBackend struct { - // The path to your executable binary. This should be relative to the root of your bundle and the - // location of the manifest file. +type ManifestServer struct { + // Executables are the paths to your executable binaries, specifying multiple entry points + // for different platforms when bundled together in a single plugin. + Executables *ManifestExecutables `json:"executables,omitempty" yaml:"executables,omitempty"` + + // Executable is the path to your executable binary. This should be relative to the root + // of your bundle and the location of the manifest file. // // On Windows, this file must have a ".exe" extension. + // + // If your plugin is compiled for multiple platforms, consider bundling them together + // and using the Executables field instead. Executable string `json:"executable" yaml:"executable"` } +type ManifestExecutables struct { + // LinuxAmd64 is the path to your executable binary for the corresponding platform + LinuxAmd64 string `json:"linux-amd64,omitempty" yaml:"linux-amd64,omitempty"` + // DarwinAmd64 is the path to your executable binary for the corresponding platform + DarwinAmd64 string `json:"darwin-amd64,omitempty" yaml:"darwin-amd64,omitempty"` + // WindowsAmd64 is the path to your executable binary for the corresponding platform + // This file must have a ".exe" extension + WindowsAmd64 string `json:"windows-amd64,omitempty" yaml:"windows-amd64,omitempty"` +} + type ManifestWebapp struct { // The path to your webapp bundle. This should be relative to the root of your bundle and the // location of the manifest file. BundlePath string `json:"bundle_path" yaml:"bundle_path"` + + // BundleHash is the 64-bit FNV-1a hash of the webapp bundle, computed when the plugin is loaded + BundleHash []byte `json:"-"` } func (m *Manifest) ToJson() string { - b, err := json.Marshal(m) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(m) + return string(b) } func ManifestListToJson(m []*Manifest) string { - b, err := json.Marshal(m) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(m) + return string(b) } func ManifestFromJson(data io.Reader) *Manifest { - decoder := json.NewDecoder(data) - var m Manifest - err := decoder.Decode(&m) - if err == nil { - return &m - } else { - return nil - } + var m *Manifest + json.NewDecoder(data).Decode(&m) + return m } func ManifestListFromJson(data io.Reader) []*Manifest { - decoder := json.NewDecoder(data) var manifests []*Manifest - err := decoder.Decode(&manifests) - if err == nil { - return manifests - } else { - return nil - } + json.NewDecoder(data).Decode(&manifests) + return manifests } func (m *Manifest) HasClient() bool { @@ -190,10 +188,58 @@ func (m *Manifest) ClientManifest() *Manifest { *cm = *m cm.Name = "" cm.Description = "" - cm.Backend = nil + cm.Server = nil + if cm.Webapp != nil { + cm.Webapp = new(ManifestWebapp) + *cm.Webapp = *m.Webapp + cm.Webapp.BundlePath = "/static/" + m.Id + "/" + fmt.Sprintf("%s_%x_bundle.js", m.Id, m.Webapp.BundleHash) + } return cm } +// GetExecutableForRuntime returns the path to the executable for the given runtime architecture. +// +// If the manifest defines multiple executables, but none match, or if only a single executable +// is defined, the Executable field will be returned. This method does not guarantee that the +// resulting binary can actually execute on the given platform. +func (m *Manifest) GetExecutableForRuntime(goOs, goArch string) string { + server := m.Server + + // Support the deprecated backend parameter. + if server == nil { + server = m.Backend + } + + if server == nil { + return "" + } + + var executable string + if server.Executables != nil { + if goOs == "linux" && goArch == "amd64" { + executable = server.Executables.LinuxAmd64 + } else if goOs == "darwin" && goArch == "amd64" { + executable = server.Executables.DarwinAmd64 + } else if goOs == "windows" && goArch == "amd64" { + executable = server.Executables.WindowsAmd64 + } + } + + if executable == "" { + executable = server.Executable + } + + return executable +} + +func (m *Manifest) HasServer() bool { + return m.Server != nil || m.Backend != nil +} + +func (m *Manifest) HasWebapp() bool { + return m.Webapp != nil +} + // FindManifest will find and parse the manifest in a given directory. // // In all cases other than a does-not-exist error, path is set to the path of the manifest file that was @@ -223,6 +269,7 @@ func FindManifest(dir string) (manifest *Manifest, path string, err error) { return } manifest = &parsed + manifest.Id = strings.ToLower(manifest.Id) return } @@ -242,5 +289,6 @@ func FindManifest(dir string) (manifest *Manifest, path string, err error) { return } manifest = &parsed + manifest.Id = strings.ToLower(manifest.Id) return } diff --git a/vendor/github.com/mattermost/mattermost-server/model/message_export.go b/vendor/github.com/mattermost/mattermost-server/model/message_export.go new file mode 100644 index 00000000..1cf764a6 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/message_export.go @@ -0,0 +1,27 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +type MessageExport struct { + TeamId *string + TeamName *string + TeamDisplayName *string + + ChannelId *string + ChannelName *string + ChannelDisplayName *string + ChannelType *string + + UserId *string + UserEmail *string + Username *string + + PostId *string + PostCreateAt *int64 + PostMessage *string + PostType *string + PostRootId *string + PostOriginalId *string + PostFileIds StringArray +} diff --git a/vendor/github.com/mattermost/platform/model/mfa_secret.go b/vendor/github.com/mattermost/mattermost-server/model/mfa_secret.go similarity index 60% rename from vendor/github.com/mattermost/platform/model/mfa_secret.go rename to vendor/github.com/mattermost/mattermost-server/model/mfa_secret.go index 99580f5f..23a903c8 100644 --- a/vendor/github.com/mattermost/platform/model/mfa_secret.go +++ b/vendor/github.com/mattermost/mattermost-server/model/mfa_secret.go @@ -14,21 +14,12 @@ type MfaSecret struct { } func (me *MfaSecret) ToJson() string { - b, err := json.Marshal(me) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(me) + return string(b) } func MfaSecretFromJson(data io.Reader) *MfaSecret { - decoder := json.NewDecoder(data) - var me MfaSecret - err := decoder.Decode(&me) - if err == nil { - return &me - } else { - return nil - } + var me *MfaSecret + json.NewDecoder(data).Decode(&me) + return me } diff --git a/vendor/github.com/mattermost/mattermost-server/model/migration.go b/vendor/github.com/mattermost/mattermost-server/model/migration.go new file mode 100644 index 00000000..ead7acce --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/migration.go @@ -0,0 +1,8 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +const ( + MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2 = "migration_advanced_permissions_phase_2" +) diff --git a/vendor/github.com/mattermost/platform/model/oauth.go b/vendor/github.com/mattermost/mattermost-server/model/oauth.go similarity index 82% rename from vendor/github.com/mattermost/platform/model/oauth.go rename to vendor/github.com/mattermost/mattermost-server/model/oauth.go index 3139aefe..6f662a5a 100644 --- a/vendor/github.com/mattermost/platform/model/oauth.go +++ b/vendor/github.com/mattermost/mattermost-server/model/oauth.go @@ -17,6 +17,7 @@ const ( OAUTH_ACTION_EMAIL_TO_SSO = "email_to_sso" OAUTH_ACTION_SSO_TO_EMAIL = "sso_to_email" OAUTH_ACTION_MOBILE = "mobile" + OAUTH_ACTION_CLIENT = "client" ) type OAuthApp struct { @@ -108,14 +109,9 @@ func (a *OAuthApp) PreUpdate() { a.UpdateAt = GetMillis() } -// ToJson convert a User to a json string func (a *OAuthApp) ToJson() string { - b, err := json.Marshal(a) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(a) + return string(b) } // Generate a valid strong etag so the browser can cache the results @@ -138,54 +134,19 @@ func (a *OAuthApp) IsValidRedirectURL(url string) bool { return false } -// OAuthAppFromJson will decode the input and return a User func OAuthAppFromJson(data io.Reader) *OAuthApp { - decoder := json.NewDecoder(data) - var app OAuthApp - err := decoder.Decode(&app) - if err == nil { - return &app - } else { - return nil - } -} - -func OAuthAppMapToJson(a map[string]*OAuthApp) string { - b, err := json.Marshal(a) - if err != nil { - return "" - } else { - return string(b) - } -} - -func OAuthAppMapFromJson(data io.Reader) map[string]*OAuthApp { - decoder := json.NewDecoder(data) - var apps map[string]*OAuthApp - err := decoder.Decode(&apps) - if err == nil { - return apps - } else { - return nil - } + var app *OAuthApp + json.NewDecoder(data).Decode(&app) + return app } func OAuthAppListToJson(l []*OAuthApp) string { - b, err := json.Marshal(l) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(l) + return string(b) } func OAuthAppListFromJson(data io.Reader) []*OAuthApp { - decoder := json.NewDecoder(data) var o []*OAuthApp - err := decoder.Decode(&o) - if err == nil { - return o - } else { - return nil - } + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/platform/model/outgoing_webhook.go b/vendor/github.com/mattermost/mattermost-server/model/outgoing_webhook.go similarity index 84% rename from vendor/github.com/mattermost/platform/model/outgoing_webhook.go rename to vendor/github.com/mattermost/mattermost-server/model/outgoing_webhook.go index 477a277d..5f7a67d0 100644 --- a/vendor/github.com/mattermost/platform/model/outgoing_webhook.go +++ b/vendor/github.com/mattermost/mattermost-server/model/outgoing_webhook.go @@ -28,6 +28,8 @@ type OutgoingWebhook struct { DisplayName string `json:"display_name"` Description string `json:"description"` ContentType string `json:"content_type"` + Username string `json:"username"` + IconURL string `json:"icon_url"` } type OutgoingWebhookPayload struct { @@ -46,23 +48,20 @@ type OutgoingWebhookPayload struct { } type OutgoingWebhookResponse struct { - Text *string `json:"text"` - Username string `json:"username"` - IconURL string `json:"icon_url"` - Props StringInterface `json:"props"` - Type string `json:"type"` - ResponseType string `json:"response_type"` + Text *string `json:"text"` + Username string `json:"username"` + IconURL string `json:"icon_url"` + Props StringInterface `json:"props"` + Attachments []*SlackAttachment `json:"attachments"` + Type string `json:"type"` + ResponseType string `json:"response_type"` } const OUTGOING_HOOK_RESPONSE_TYPE_COMMENT = "comment" func (o *OutgoingWebhookPayload) ToJSON() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func (o *OutgoingWebhookPayload) ToFormValues() string { @@ -84,63 +83,36 @@ func (o *OutgoingWebhookPayload) ToFormValues() string { } func (o *OutgoingWebhook) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func OutgoingWebhookFromJson(data io.Reader) *OutgoingWebhook { - decoder := json.NewDecoder(data) - var o OutgoingWebhook - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *OutgoingWebhook + json.NewDecoder(data).Decode(&o) + return o } func OutgoingWebhookListToJson(l []*OutgoingWebhook) string { - b, err := json.Marshal(l) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(l) + return string(b) } func OutgoingWebhookListFromJson(data io.Reader) []*OutgoingWebhook { - decoder := json.NewDecoder(data) var o []*OutgoingWebhook - err := decoder.Decode(&o) - if err == nil { - return o - } else { - return nil - } + json.NewDecoder(data).Decode(&o) + return o } func (o *OutgoingWebhookResponse) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func OutgoingWebhookResponseFromJson(data io.Reader) *OutgoingWebhookResponse { - decoder := json.NewDecoder(data) - var o OutgoingWebhookResponse - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *OutgoingWebhookResponse + json.NewDecoder(data).Decode(&o) + return o } func (o *OutgoingWebhook) IsValid() *AppError { @@ -199,7 +171,7 @@ func (o *OutgoingWebhook) IsValid() *AppError { return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.display_name.app_error", nil, "", http.StatusBadRequest) } - if len(o.Description) > 128 { + if len(o.Description) > 500 { return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.description.app_error", nil, "", http.StatusBadRequest) } @@ -211,6 +183,14 @@ func (o *OutgoingWebhook) IsValid() *AppError { return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.content_type.app_error", nil, "", http.StatusBadRequest) } + if len(o.Username) > 64 { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.username.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.IconURL) > 1024 { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.icon_url.app_error", nil, "", http.StatusBadRequest) + } + return nil } diff --git a/vendor/github.com/mattermost/platform/model/authorization.go b/vendor/github.com/mattermost/mattermost-server/model/permission.go similarity index 65% rename from vendor/github.com/mattermost/platform/model/authorization.go rename to vendor/github.com/mattermost/mattermost-server/model/permission.go index 9f4e36ea..737321cc 100644 --- a/vendor/github.com/mattermost/platform/model/authorization.go +++ b/vendor/github.com/mattermost/mattermost-server/model/permission.go @@ -3,17 +3,17 @@ package model +const ( + PERMISSION_SCOPE_SYSTEM = "system_scope" + PERMISSION_SCOPE_TEAM = "team_scope" + PERMISSION_SCOPE_CHANNEL = "channel_scope" +) + type Permission struct { Id string `json:"id"` Name string `json:"name"` Description string `json:"description"` -} - -type Role struct { - Id string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Permissions []string `json:"permissions"` + Scope string `json:"scope"` } var PERMISSION_INVITE_USER *Permission @@ -40,6 +40,9 @@ var PERMISSION_DELETE_PRIVATE_CHANNEL *Permission var PERMISSION_EDIT_OTHER_USERS *Permission var PERMISSION_READ_CHANNEL *Permission var PERMISSION_READ_PUBLIC_CHANNEL *Permission +var PERMISSION_ADD_REACTION *Permission +var PERMISSION_REMOVE_REACTION *Permission +var PERMISSION_REMOVE_OTHERS_REACTIONS *Permission var PERMISSION_PERMANENT_DELETE_USER *Permission var PERMISSION_UPLOAD_FILE *Permission var PERMISSION_GET_PUBLIC_LINK *Permission @@ -47,8 +50,11 @@ var PERMISSION_MANAGE_WEBHOOKS *Permission var PERMISSION_MANAGE_OTHERS_WEBHOOKS *Permission var PERMISSION_MANAGE_OAUTH *Permission var PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH *Permission +var PERMISSION_MANAGE_EMOJIS *Permission +var PERMISSION_MANAGE_OTHERS_EMOJIS *Permission var PERMISSION_CREATE_POST *Permission var PERMISSION_CREATE_POST_PUBLIC *Permission +var PERMISSION_CREATE_POST_EPHEMERAL *Permission var PERMISSION_EDIT_POST *Permission var PERMISSION_EDIT_OTHERS_POSTS *Permission var PERMISSION_DELETE_POST *Permission @@ -64,459 +70,397 @@ var PERMISSION_CREATE_USER_ACCESS_TOKEN *Permission var PERMISSION_READ_USER_ACCESS_TOKEN *Permission var PERMISSION_REVOKE_USER_ACCESS_TOKEN *Permission -// General permission that encompases all system admin functions +// General permission that encompasses all system admin functions // in the future this could be broken up to allow access to some // admin functions but not others var PERMISSION_MANAGE_SYSTEM *Permission -const ( - SYSTEM_USER_ROLE_ID = "system_user" - SYSTEM_ADMIN_ROLE_ID = "system_admin" - SYSTEM_POST_ALL_ROLE_ID = "system_post_all" - SYSTEM_POST_ALL_PUBLIC_ROLE_ID = "system_post_all_public" - SYSTEM_USER_ACCESS_TOKEN_ROLE_ID = "system_user_access_token" - - TEAM_USER_ROLE_ID = "team_user" - TEAM_ADMIN_ROLE_ID = "team_admin" - TEAM_POST_ALL_ROLE_ID = "team_post_all" - TEAM_POST_ALL_PUBLIC_ROLE_ID = "team_post_all_public" - - CHANNEL_USER_ROLE_ID = "channel_user" - CHANNEL_ADMIN_ROLE_ID = "channel_admin" - CHANNEL_GUEST_ROLE_ID = "guest" -) +var ALL_PERMISSIONS []*Permission func initializePermissions() { PERMISSION_INVITE_USER = &Permission{ "invite_user", "authentication.permissions.team_invite_user.name", "authentication.permissions.team_invite_user.description", + PERMISSION_SCOPE_TEAM, } PERMISSION_ADD_USER_TO_TEAM = &Permission{ "add_user_to_team", "authentication.permissions.add_user_to_team.name", "authentication.permissions.add_user_to_team.description", + PERMISSION_SCOPE_TEAM, } PERMISSION_USE_SLASH_COMMANDS = &Permission{ "use_slash_commands", "authentication.permissions.team_use_slash_commands.name", "authentication.permissions.team_use_slash_commands.description", + PERMISSION_SCOPE_CHANNEL, } PERMISSION_MANAGE_SLASH_COMMANDS = &Permission{ "manage_slash_commands", "authentication.permissions.manage_slash_commands.name", "authentication.permissions.manage_slash_commands.description", + PERMISSION_SCOPE_TEAM, } PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS = &Permission{ "manage_others_slash_commands", "authentication.permissions.manage_others_slash_commands.name", "authentication.permissions.manage_others_slash_commands.description", + PERMISSION_SCOPE_TEAM, } PERMISSION_CREATE_PUBLIC_CHANNEL = &Permission{ "create_public_channel", "authentication.permissions.create_public_channel.name", "authentication.permissions.create_public_channel.description", + PERMISSION_SCOPE_TEAM, } PERMISSION_CREATE_PRIVATE_CHANNEL = &Permission{ "create_private_channel", "authentication.permissions.create_private_channel.name", "authentication.permissions.create_private_channel.description", + PERMISSION_SCOPE_TEAM, } PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS = &Permission{ "manage_public_channel_members", "authentication.permissions.manage_public_channel_members.name", "authentication.permissions.manage_public_channel_members.description", + PERMISSION_SCOPE_CHANNEL, } PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS = &Permission{ "manage_private_channel_members", "authentication.permissions.manage_private_channel_members.name", "authentication.permissions.manage_private_channel_members.description", + PERMISSION_SCOPE_CHANNEL, } PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE = &Permission{ "assign_system_admin_role", "authentication.permissions.assign_system_admin_role.name", "authentication.permissions.assign_system_admin_role.description", + PERMISSION_SCOPE_SYSTEM, } PERMISSION_MANAGE_ROLES = &Permission{ "manage_roles", "authentication.permissions.manage_roles.name", "authentication.permissions.manage_roles.description", + PERMISSION_SCOPE_SYSTEM, } PERMISSION_MANAGE_TEAM_ROLES = &Permission{ "manage_team_roles", "authentication.permissions.manage_team_roles.name", "authentication.permissions.manage_team_roles.description", + PERMISSION_SCOPE_TEAM, } PERMISSION_MANAGE_CHANNEL_ROLES = &Permission{ "manage_channel_roles", "authentication.permissions.manage_channel_roles.name", "authentication.permissions.manage_channel_roles.description", + PERMISSION_SCOPE_CHANNEL, } PERMISSION_MANAGE_SYSTEM = &Permission{ "manage_system", "authentication.permissions.manage_system.name", "authentication.permissions.manage_system.description", + PERMISSION_SCOPE_SYSTEM, } PERMISSION_CREATE_DIRECT_CHANNEL = &Permission{ "create_direct_channel", "authentication.permissions.create_direct_channel.name", "authentication.permissions.create_direct_channel.description", + PERMISSION_SCOPE_SYSTEM, } PERMISSION_CREATE_GROUP_CHANNEL = &Permission{ "create_group_channel", "authentication.permissions.create_group_channel.name", "authentication.permissions.create_group_channel.description", + PERMISSION_SCOPE_SYSTEM, } PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES = &Permission{ - "manage__publicchannel_properties", + "manage_public_channel_properties", "authentication.permissions.manage_public_channel_properties.name", "authentication.permissions.manage_public_channel_properties.description", + PERMISSION_SCOPE_CHANNEL, } PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES = &Permission{ "manage_private_channel_properties", "authentication.permissions.manage_private_channel_properties.name", "authentication.permissions.manage_private_channel_properties.description", + PERMISSION_SCOPE_CHANNEL, } PERMISSION_LIST_TEAM_CHANNELS = &Permission{ "list_team_channels", "authentication.permissions.list_team_channels.name", "authentication.permissions.list_team_channels.description", + PERMISSION_SCOPE_TEAM, } PERMISSION_JOIN_PUBLIC_CHANNELS = &Permission{ "join_public_channels", "authentication.permissions.join_public_channels.name", "authentication.permissions.join_public_channels.description", + PERMISSION_SCOPE_TEAM, } PERMISSION_DELETE_PUBLIC_CHANNEL = &Permission{ "delete_public_channel", "authentication.permissions.delete_public_channel.name", "authentication.permissions.delete_public_channel.description", + PERMISSION_SCOPE_CHANNEL, } PERMISSION_DELETE_PRIVATE_CHANNEL = &Permission{ "delete_private_channel", "authentication.permissions.delete_private_channel.name", "authentication.permissions.delete_private_channel.description", + PERMISSION_SCOPE_CHANNEL, } PERMISSION_EDIT_OTHER_USERS = &Permission{ "edit_other_users", "authentication.permissions.edit_other_users.name", "authentication.permissions.edit_other_users.description", + PERMISSION_SCOPE_SYSTEM, } PERMISSION_READ_CHANNEL = &Permission{ "read_channel", "authentication.permissions.read_channel.name", "authentication.permissions.read_channel.description", + PERMISSION_SCOPE_CHANNEL, } PERMISSION_READ_PUBLIC_CHANNEL = &Permission{ "read_public_channel", "authentication.permissions.read_public_channel.name", "authentication.permissions.read_public_channel.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_ADD_REACTION = &Permission{ + "add_reaction", + "authentication.permissions.add_reaction.name", + "authentication.permissions.add_reaction.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_REMOVE_REACTION = &Permission{ + "remove_reaction", + "authentication.permissions.remove_reaction.name", + "authentication.permissions.remove_reaction.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_REMOVE_OTHERS_REACTIONS = &Permission{ + "remove_others_reactions", + "authentication.permissions.remove_others_reactions.name", + "authentication.permissions.remove_others_reactions.description", + PERMISSION_SCOPE_CHANNEL, } PERMISSION_PERMANENT_DELETE_USER = &Permission{ "permanent_delete_user", "authentication.permissions.permanent_delete_user.name", "authentication.permissions.permanent_delete_user.description", + PERMISSION_SCOPE_SYSTEM, } PERMISSION_UPLOAD_FILE = &Permission{ "upload_file", "authentication.permissions.upload_file.name", "authentication.permissions.upload_file.description", + PERMISSION_SCOPE_CHANNEL, } PERMISSION_GET_PUBLIC_LINK = &Permission{ "get_public_link", "authentication.permissions.get_public_link.name", "authentication.permissions.get_public_link.description", + PERMISSION_SCOPE_SYSTEM, } PERMISSION_MANAGE_WEBHOOKS = &Permission{ "manage_webhooks", "authentication.permissions.manage_webhooks.name", "authentication.permissions.manage_webhooks.description", + PERMISSION_SCOPE_TEAM, } PERMISSION_MANAGE_OTHERS_WEBHOOKS = &Permission{ "manage_others_webhooks", "authentication.permissions.manage_others_webhooks.name", "authentication.permissions.manage_others_webhooks.description", + PERMISSION_SCOPE_TEAM, } PERMISSION_MANAGE_OAUTH = &Permission{ "manage_oauth", "authentication.permissions.manage_oauth.name", "authentication.permissions.manage_oauth.description", + PERMISSION_SCOPE_SYSTEM, } PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH = &Permission{ - "manage_sytem_wide_oauth", - "authentication.permissions.manage_sytem_wide_oauth.name", - "authentication.permissions.manage_sytem_wide_oauth.description", + "manage_system_wide_oauth", + "authentication.permissions.manage_system_wide_oauth.name", + "authentication.permissions.manage_system_wide_oauth.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_MANAGE_EMOJIS = &Permission{ + "manage_emojis", + "authentication.permissions.manage_emojis.name", + "authentication.permissions.manage_emojis.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_MANAGE_OTHERS_EMOJIS = &Permission{ + "manage_others_emojis", + "authentication.permissions.manage_others_emojis.name", + "authentication.permissions.manage_others_emojis.description", + PERMISSION_SCOPE_TEAM, } PERMISSION_CREATE_POST = &Permission{ "create_post", "authentication.permissions.create_post.name", "authentication.permissions.create_post.description", + PERMISSION_SCOPE_CHANNEL, } PERMISSION_CREATE_POST_PUBLIC = &Permission{ "create_post_public", "authentication.permissions.create_post_public.name", "authentication.permissions.create_post_public.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_CREATE_POST_EPHEMERAL = &Permission{ + "create_post_ephemeral", + "authentication.permissions.create_post_ephemeral.name", + "authentication.permissions.create_post_ephemeral.description", + PERMISSION_SCOPE_CHANNEL, } PERMISSION_EDIT_POST = &Permission{ "edit_post", "authentication.permissions.edit_post.name", "authentication.permissions.edit_post.description", + PERMISSION_SCOPE_CHANNEL, } PERMISSION_EDIT_OTHERS_POSTS = &Permission{ "edit_others_posts", "authentication.permissions.edit_others_posts.name", "authentication.permissions.edit_others_posts.description", + PERMISSION_SCOPE_CHANNEL, } PERMISSION_DELETE_POST = &Permission{ "delete_post", "authentication.permissions.delete_post.name", "authentication.permissions.delete_post.description", + PERMISSION_SCOPE_CHANNEL, } PERMISSION_DELETE_OTHERS_POSTS = &Permission{ "delete_others_posts", "authentication.permissions.delete_others_posts.name", "authentication.permissions.delete_others_posts.description", + PERMISSION_SCOPE_CHANNEL, } PERMISSION_REMOVE_USER_FROM_TEAM = &Permission{ "remove_user_from_team", "authentication.permissions.remove_user_from_team.name", "authentication.permissions.remove_user_from_team.description", + PERMISSION_SCOPE_TEAM, } PERMISSION_CREATE_TEAM = &Permission{ "create_team", "authentication.permissions.create_team.name", "authentication.permissions.create_team.description", + PERMISSION_SCOPE_SYSTEM, } PERMISSION_MANAGE_TEAM = &Permission{ "manage_team", "authentication.permissions.manage_team.name", "authentication.permissions.manage_team.description", + PERMISSION_SCOPE_TEAM, } PERMISSION_IMPORT_TEAM = &Permission{ "import_team", "authentication.permissions.import_team.name", "authentication.permissions.import_team.description", + PERMISSION_SCOPE_TEAM, } PERMISSION_VIEW_TEAM = &Permission{ "view_team", "authentication.permissions.view_team.name", "authentication.permissions.view_team.description", + PERMISSION_SCOPE_TEAM, } PERMISSION_LIST_USERS_WITHOUT_TEAM = &Permission{ "list_users_without_team", "authentication.permissions.list_users_without_team.name", "authentication.permissions.list_users_without_team.description", + PERMISSION_SCOPE_SYSTEM, } PERMISSION_CREATE_USER_ACCESS_TOKEN = &Permission{ "create_user_access_token", "authentication.permissions.create_user_access_token.name", "authentication.permissions.create_user_access_token.description", + PERMISSION_SCOPE_SYSTEM, } PERMISSION_READ_USER_ACCESS_TOKEN = &Permission{ "read_user_access_token", "authentication.permissions.read_user_access_token.name", "authentication.permissions.read_user_access_token.description", + PERMISSION_SCOPE_SYSTEM, } PERMISSION_REVOKE_USER_ACCESS_TOKEN = &Permission{ "revoke_user_access_token", "authentication.permissions.revoke_user_access_token.name", "authentication.permissions.revoke_user_access_token.description", + PERMISSION_SCOPE_SYSTEM, } PERMISSION_MANAGE_JOBS = &Permission{ "manage_jobs", "authentication.permisssions.manage_jobs.name", "authentication.permisssions.manage_jobs.description", - } -} - -var DefaultRoles map[string]*Role - -func initializeDefaultRoles() { - DefaultRoles = make(map[string]*Role) - - DefaultRoles[CHANNEL_USER_ROLE_ID] = &Role{ - "channel_user", - "authentication.roles.channel_user.name", - "authentication.roles.channel_user.description", - []string{ - PERMISSION_READ_CHANNEL.Id, - PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id, - PERMISSION_UPLOAD_FILE.Id, - PERMISSION_GET_PUBLIC_LINK.Id, - PERMISSION_CREATE_POST.Id, - PERMISSION_EDIT_POST.Id, - PERMISSION_USE_SLASH_COMMANDS.Id, - }, + PERMISSION_SCOPE_SYSTEM, } - DefaultRoles[CHANNEL_ADMIN_ROLE_ID] = &Role{ - "channel_admin", - "authentication.roles.channel_admin.name", - "authentication.roles.channel_admin.description", - []string{ - PERMISSION_MANAGE_CHANNEL_ROLES.Id, - }, + ALL_PERMISSIONS = []*Permission{ + PERMISSION_INVITE_USER, + PERMISSION_ADD_USER_TO_TEAM, + PERMISSION_USE_SLASH_COMMANDS, + PERMISSION_MANAGE_SLASH_COMMANDS, + PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS, + PERMISSION_CREATE_PUBLIC_CHANNEL, + PERMISSION_CREATE_PRIVATE_CHANNEL, + PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS, + PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS, + PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE, + PERMISSION_MANAGE_ROLES, + PERMISSION_MANAGE_TEAM_ROLES, + PERMISSION_MANAGE_CHANNEL_ROLES, + PERMISSION_CREATE_DIRECT_CHANNEL, + PERMISSION_CREATE_GROUP_CHANNEL, + PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES, + PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES, + PERMISSION_LIST_TEAM_CHANNELS, + PERMISSION_JOIN_PUBLIC_CHANNELS, + PERMISSION_DELETE_PUBLIC_CHANNEL, + PERMISSION_DELETE_PRIVATE_CHANNEL, + PERMISSION_EDIT_OTHER_USERS, + PERMISSION_READ_CHANNEL, + PERMISSION_READ_PUBLIC_CHANNEL, + PERMISSION_ADD_REACTION, + PERMISSION_REMOVE_REACTION, + PERMISSION_REMOVE_OTHERS_REACTIONS, + PERMISSION_PERMANENT_DELETE_USER, + PERMISSION_UPLOAD_FILE, + PERMISSION_GET_PUBLIC_LINK, + PERMISSION_MANAGE_WEBHOOKS, + PERMISSION_MANAGE_OTHERS_WEBHOOKS, + PERMISSION_MANAGE_OAUTH, + PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH, + PERMISSION_MANAGE_EMOJIS, + PERMISSION_MANAGE_OTHERS_EMOJIS, + PERMISSION_CREATE_POST, + PERMISSION_CREATE_POST_PUBLIC, + PERMISSION_CREATE_POST_EPHEMERAL, + PERMISSION_EDIT_POST, + PERMISSION_EDIT_OTHERS_POSTS, + PERMISSION_DELETE_POST, + PERMISSION_DELETE_OTHERS_POSTS, + PERMISSION_REMOVE_USER_FROM_TEAM, + PERMISSION_CREATE_TEAM, + PERMISSION_MANAGE_TEAM, + PERMISSION_IMPORT_TEAM, + PERMISSION_VIEW_TEAM, + PERMISSION_LIST_USERS_WITHOUT_TEAM, + PERMISSION_MANAGE_JOBS, + PERMISSION_CREATE_USER_ACCESS_TOKEN, + PERMISSION_READ_USER_ACCESS_TOKEN, + PERMISSION_REVOKE_USER_ACCESS_TOKEN, + PERMISSION_MANAGE_SYSTEM, } - - DefaultRoles[CHANNEL_GUEST_ROLE_ID] = &Role{ - "guest", - "authentication.roles.global_guest.name", - "authentication.roles.global_guest.description", - []string{}, - } - - DefaultRoles[TEAM_USER_ROLE_ID] = &Role{ - "team_user", - "authentication.roles.team_user.name", - "authentication.roles.team_user.description", - []string{ - PERMISSION_LIST_TEAM_CHANNELS.Id, - PERMISSION_JOIN_PUBLIC_CHANNELS.Id, - PERMISSION_READ_PUBLIC_CHANNEL.Id, - PERMISSION_VIEW_TEAM.Id, - }, - } - - DefaultRoles[TEAM_POST_ALL_ROLE_ID] = &Role{ - "team_post_all", - "authentication.roles.team_post_all.name", - "authentication.roles.team_post_all.description", - []string{ - PERMISSION_CREATE_POST.Id, - }, - } - - DefaultRoles[TEAM_POST_ALL_PUBLIC_ROLE_ID] = &Role{ - "team_post_all_public", - "authentication.roles.team_post_all_public.name", - "authentication.roles.team_post_all_public.description", - []string{ - PERMISSION_CREATE_POST_PUBLIC.Id, - }, - } - - DefaultRoles[TEAM_ADMIN_ROLE_ID] = &Role{ - "team_admin", - "authentication.roles.team_admin.name", - "authentication.roles.team_admin.description", - []string{ - PERMISSION_EDIT_OTHERS_POSTS.Id, - PERMISSION_REMOVE_USER_FROM_TEAM.Id, - PERMISSION_MANAGE_TEAM.Id, - PERMISSION_IMPORT_TEAM.Id, - PERMISSION_MANAGE_TEAM_ROLES.Id, - PERMISSION_MANAGE_CHANNEL_ROLES.Id, - PERMISSION_MANAGE_OTHERS_WEBHOOKS.Id, - PERMISSION_MANAGE_SLASH_COMMANDS.Id, - PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS.Id, - PERMISSION_MANAGE_WEBHOOKS.Id, - }, - } - - DefaultRoles[SYSTEM_USER_ROLE_ID] = &Role{ - "system_user", - "authentication.roles.global_user.name", - "authentication.roles.global_user.description", - []string{ - PERMISSION_CREATE_DIRECT_CHANNEL.Id, - PERMISSION_CREATE_GROUP_CHANNEL.Id, - PERMISSION_PERMANENT_DELETE_USER.Id, - }, - } - - DefaultRoles[SYSTEM_POST_ALL_ROLE_ID] = &Role{ - "system_post_all", - "authentication.roles.system_post_all.name", - "authentication.roles.system_post_all.description", - []string{ - PERMISSION_CREATE_POST.Id, - }, - } - - DefaultRoles[SYSTEM_POST_ALL_PUBLIC_ROLE_ID] = &Role{ - "system_post_all_public", - "authentication.roles.system_post_all_public.name", - "authentication.roles.system_post_all_public.description", - []string{ - PERMISSION_CREATE_POST_PUBLIC.Id, - }, - } - - DefaultRoles[SYSTEM_USER_ACCESS_TOKEN_ROLE_ID] = &Role{ - "system_user_access_token", - "authentication.roles.system_user_access_token.name", - "authentication.roles.system_user_access_token.description", - []string{ - PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, - PERMISSION_READ_USER_ACCESS_TOKEN.Id, - PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, - }, - } - - DefaultRoles[SYSTEM_ADMIN_ROLE_ID] = &Role{ - "system_admin", - "authentication.roles.global_admin.name", - "authentication.roles.global_admin.description", - // System admins can do anything channel and team admins can do - // plus everything members of teams and channels can do to all teams - // and channels on the system - append( - append( - append( - append( - []string{ - PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE.Id, - PERMISSION_MANAGE_SYSTEM.Id, - PERMISSION_MANAGE_ROLES.Id, - PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES.Id, - PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id, - PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id, - PERMISSION_DELETE_PUBLIC_CHANNEL.Id, - PERMISSION_CREATE_PUBLIC_CHANNEL.Id, - PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES.Id, - PERMISSION_DELETE_PRIVATE_CHANNEL.Id, - PERMISSION_CREATE_PRIVATE_CHANNEL.Id, - PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH.Id, - PERMISSION_MANAGE_OTHERS_WEBHOOKS.Id, - PERMISSION_EDIT_OTHER_USERS.Id, - PERMISSION_MANAGE_OAUTH.Id, - PERMISSION_INVITE_USER.Id, - PERMISSION_DELETE_POST.Id, - PERMISSION_DELETE_OTHERS_POSTS.Id, - PERMISSION_CREATE_TEAM.Id, - PERMISSION_ADD_USER_TO_TEAM.Id, - PERMISSION_LIST_USERS_WITHOUT_TEAM.Id, - PERMISSION_MANAGE_JOBS.Id, - PERMISSION_CREATE_POST_PUBLIC.Id, - PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, - PERMISSION_READ_USER_ACCESS_TOKEN.Id, - PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, - }, - DefaultRoles[TEAM_USER_ROLE_ID].Permissions..., - ), - DefaultRoles[CHANNEL_USER_ROLE_ID].Permissions..., - ), - DefaultRoles[TEAM_ADMIN_ROLE_ID].Permissions..., - ), - DefaultRoles[CHANNEL_ADMIN_ROLE_ID].Permissions..., - ), - } -} - -func RoleIdsToString(roles []string) string { - output := "" - for _, role := range roles { - output += role + ", " - } - - if output == "" { - return "[]" - } - - return output[:len(output)-1] } func init() { initializePermissions() - initializeDefaultRoles() } diff --git a/vendor/github.com/mattermost/platform/model/plugin_key_value.go b/vendor/github.com/mattermost/mattermost-server/model/plugin_key_value.go similarity index 100% rename from vendor/github.com/mattermost/platform/model/plugin_key_value.go rename to vendor/github.com/mattermost/mattermost-server/model/plugin_key_value.go diff --git a/vendor/github.com/mattermost/mattermost-server/model/plugin_status.go b/vendor/github.com/mattermost/mattermost-server/model/plugin_status.go new file mode 100644 index 00000000..db276402 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/plugin_status.go @@ -0,0 +1,42 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + PluginStateNotRunning = 0 + PluginStateStarting = 1 // unused by server + PluginStateRunning = 2 + PluginStateFailedToStart = 3 + PluginStateFailedToStayRunning = 4 // unused by server + PluginStateStopping = 5 // unused by server +) + +// PluginStatus provides a cluster-aware view of installed plugins. +type PluginStatus struct { + PluginId string `json:"plugin_id"` + ClusterId string `json:"cluster_id"` + PluginPath string `json:"plugin_path"` + State int `json:"state"` + Name string `json:"name"` + Description string `json:"description"` + Version string `json:"version"` +} + +type PluginStatuses []*PluginStatus + +func (m *PluginStatuses) ToJson() string { + b, _ := json.Marshal(m) + return string(b) +} + +func PluginStatusesFromJson(data io.Reader) PluginStatuses { + var m PluginStatuses + json.NewDecoder(data).Decode(&m) + return m +} diff --git a/vendor/github.com/mattermost/platform/model/plugins_response.go b/vendor/github.com/mattermost/mattermost-server/model/plugins_response.go similarity index 61% rename from vendor/github.com/mattermost/platform/model/plugins_response.go rename to vendor/github.com/mattermost/mattermost-server/model/plugins_response.go index 74c89af2..177cfe6c 100644 --- a/vendor/github.com/mattermost/platform/model/plugins_response.go +++ b/vendor/github.com/mattermost/mattermost-server/model/plugins_response.go @@ -10,7 +10,6 @@ import ( type PluginInfo struct { Manifest - Prepackaged bool `json:"prepackaged"` } type PluginsResponse struct { @@ -19,21 +18,12 @@ type PluginsResponse struct { } func (m *PluginsResponse) ToJson() string { - b, err := json.Marshal(m) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(m) + return string(b) } func PluginsResponseFromJson(data io.Reader) *PluginsResponse { - decoder := json.NewDecoder(data) - var m PluginsResponse - err := decoder.Decode(&m) - if err == nil { - return &m - } else { - return nil - } + var m *PluginsResponse + json.NewDecoder(data).Decode(&m) + return m } diff --git a/vendor/github.com/mattermost/mattermost-server/model/post.go b/vendor/github.com/mattermost/mattermost-server/model/post.go new file mode 100644 index 00000000..5d2438fc --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/post.go @@ -0,0 +1,587 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "net/http" + "sort" + "strings" + "unicode/utf8" + + "github.com/mattermost/mattermost-server/utils/markdown" +) + +const ( + POST_SYSTEM_MESSAGE_PREFIX = "system_" + POST_DEFAULT = "" + POST_SLACK_ATTACHMENT = "slack_attachment" + POST_SYSTEM_GENERIC = "system_generic" + POST_JOIN_LEAVE = "system_join_leave" // Deprecated, use POST_JOIN_CHANNEL or POST_LEAVE_CHANNEL instead + POST_JOIN_CHANNEL = "system_join_channel" + POST_LEAVE_CHANNEL = "system_leave_channel" + POST_JOIN_TEAM = "system_join_team" + POST_LEAVE_TEAM = "system_leave_team" + POST_AUTO_RESPONDER = "system_auto_responder" + POST_ADD_REMOVE = "system_add_remove" // Deprecated, use POST_ADD_TO_CHANNEL or POST_REMOVE_FROM_CHANNEL instead + POST_ADD_TO_CHANNEL = "system_add_to_channel" + POST_REMOVE_FROM_CHANNEL = "system_remove_from_channel" + POST_MOVE_CHANNEL = "system_move_channel" + POST_ADD_TO_TEAM = "system_add_to_team" + POST_REMOVE_FROM_TEAM = "system_remove_from_team" + POST_HEADER_CHANGE = "system_header_change" + POST_DISPLAYNAME_CHANGE = "system_displayname_change" + POST_CONVERT_CHANNEL = "system_convert_channel" + POST_PURPOSE_CHANGE = "system_purpose_change" + POST_CHANNEL_DELETED = "system_channel_deleted" + POST_EPHEMERAL = "system_ephemeral" + POST_CHANGE_CHANNEL_PRIVACY = "system_change_chan_privacy" + POST_FILEIDS_MAX_RUNES = 150 + POST_FILENAMES_MAX_RUNES = 4000 + POST_HASHTAGS_MAX_RUNES = 1000 + POST_MESSAGE_MAX_RUNES_V1 = 4000 + POST_MESSAGE_MAX_BYTES_V2 = 65535 // Maximum size of a TEXT column in MySQL + POST_MESSAGE_MAX_RUNES_V2 = POST_MESSAGE_MAX_BYTES_V2 / 4 // Assume a worst-case representation + POST_PROPS_MAX_RUNES = 8000 + POST_PROPS_MAX_USER_RUNES = POST_PROPS_MAX_RUNES - 400 // Leave some room for system / pre-save modifications + POST_CUSTOM_TYPE_PREFIX = "custom_" + PROPS_ADD_CHANNEL_MEMBER = "add_channel_member" + POST_PROPS_ADDED_USER_ID = "addedUserId" + POST_PROPS_DELETE_BY = "deleteBy" + POST_ACTION_TYPE_BUTTON = "button" + POST_ACTION_TYPE_SELECT = "select" +) + +type Post struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + EditAt int64 `json:"edit_at"` + DeleteAt int64 `json:"delete_at"` + IsPinned bool `json:"is_pinned"` + UserId string `json:"user_id"` + ChannelId string `json:"channel_id"` + RootId string `json:"root_id"` + ParentId string `json:"parent_id"` + OriginalId string `json:"original_id"` + + Message string `json:"message"` + + // MessageSource will contain the message as submitted by the user if Message has been modified + // by Mattermost for presentation (e.g if an image proxy is being used). It should be used to + // populate edit boxes if present. + MessageSource string `json:"message_source,omitempty" db:"-"` + + Type string `json:"type"` + Props StringInterface `json:"props"` + Hashtags string `json:"hashtags"` + Filenames StringArray `json:"filenames,omitempty"` // Deprecated, do not use this field any more + FileIds StringArray `json:"file_ids,omitempty"` + PendingPostId string `json:"pending_post_id" db:"-"` + HasReactions bool `json:"has_reactions,omitempty"` +} + +type PostEphemeral struct { + UserID string `json:"user_id"` + Post *Post `json:"post"` +} + +type PostPatch struct { + IsPinned *bool `json:"is_pinned"` + Message *string `json:"message"` + Props *StringInterface `json:"props"` + FileIds *StringArray `json:"file_ids"` + HasReactions *bool `json:"has_reactions"` +} + +type SearchParameter struct { + Terms *string `json:"terms"` + IsOrSearch *bool `json:"is_or_search"` + TimeZoneOffset *int `json:"time_zone_offset"` + Page *int `json:"page"` + PerPage *int `json:"per_page"` + IncludeDeletedChannels *bool `json:"include_deleted_channels"` +} + +func (o *PostPatch) WithRewrittenImageURLs(f func(string) string) *PostPatch { + copy := *o + if copy.Message != nil { + *copy.Message = RewriteImageURLs(*o.Message, f) + } + return © +} + +type PostForExport struct { + Post + TeamName string + ChannelName string + Username string + ReplyCount int +} + +type ReplyForExport struct { + Post + Username string +} + +type PostForIndexing struct { + Post + TeamId string `json:"team_id"` + ParentCreateAt *int64 `json:"parent_create_at"` +} + +type DoPostActionRequest struct { + SelectedOption string `json:"selected_option"` +} + +type PostAction struct { + Id string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + DataSource string `json:"data_source"` + Options []*PostActionOptions `json:"options"` + Integration *PostActionIntegration `json:"integration,omitempty"` +} + +type PostActionOptions struct { + Text string `json:"text"` + Value string `json:"value"` +} + +type PostActionIntegration struct { + URL string `json:"url,omitempty"` + Context StringInterface `json:"context,omitempty"` +} + +type PostActionIntegrationRequest struct { + UserId string `json:"user_id"` + ChannelId string `json:"channel_id"` + TeamId string `json:"team_id"` + PostId string `json:"post_id"` + Type string `json:"type"` + DataSource string `json:"data_source"` + Context StringInterface `json:"context,omitempty"` +} + +type PostActionIntegrationResponse struct { + Update *Post `json:"update"` + EphemeralText string `json:"ephemeral_text"` +} + +func (o *Post) ToJson() string { + copy := *o + copy.StripActionIntegrations() + b, _ := json.Marshal(©) + return string(b) +} + +func (o *Post) ToUnsanitizedJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func PostFromJson(data io.Reader) *Post { + var o *Post + json.NewDecoder(data).Decode(&o) + return o +} + +func (o *Post) Etag() string { + return Etag(o.Id, o.UpdateAt) +} + +func (o *Post) IsValid(maxPostSize int) *AppError { + + if len(o.Id) != 26 { + return NewAppError("Post.IsValid", "model.post.is_valid.id.app_error", nil, "", http.StatusBadRequest) + } + + if o.CreateAt == 0 { + return NewAppError("Post.IsValid", "model.post.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if o.UpdateAt == 0 { + return NewAppError("Post.IsValid", "model.post.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if len(o.UserId) != 26 { + return NewAppError("Post.IsValid", "model.post.is_valid.user_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.ChannelId) != 26 { + return NewAppError("Post.IsValid", "model.post.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest) + } + + if !(len(o.RootId) == 26 || len(o.RootId) == 0) { + return NewAppError("Post.IsValid", "model.post.is_valid.root_id.app_error", nil, "", http.StatusBadRequest) + } + + if !(len(o.ParentId) == 26 || len(o.ParentId) == 0) { + return NewAppError("Post.IsValid", "model.post.is_valid.parent_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.ParentId) == 26 && len(o.RootId) == 0 { + return NewAppError("Post.IsValid", "model.post.is_valid.root_parent.app_error", nil, "", http.StatusBadRequest) + } + + if !(len(o.OriginalId) == 26 || len(o.OriginalId) == 0) { + return NewAppError("Post.IsValid", "model.post.is_valid.original_id.app_error", nil, "", http.StatusBadRequest) + } + + if utf8.RuneCountInString(o.Message) > maxPostSize { + return NewAppError("Post.IsValid", "model.post.is_valid.msg.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if utf8.RuneCountInString(o.Hashtags) > POST_HASHTAGS_MAX_RUNES { + return NewAppError("Post.IsValid", "model.post.is_valid.hashtags.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + switch o.Type { + case + POST_DEFAULT, + POST_JOIN_LEAVE, + POST_AUTO_RESPONDER, + POST_ADD_REMOVE, + POST_JOIN_CHANNEL, + POST_LEAVE_CHANNEL, + POST_JOIN_TEAM, + POST_LEAVE_TEAM, + POST_ADD_TO_CHANNEL, + POST_REMOVE_FROM_CHANNEL, + POST_MOVE_CHANNEL, + POST_ADD_TO_TEAM, + POST_REMOVE_FROM_TEAM, + POST_SLACK_ATTACHMENT, + POST_HEADER_CHANGE, + POST_PURPOSE_CHANGE, + POST_DISPLAYNAME_CHANGE, + POST_CONVERT_CHANNEL, + POST_CHANNEL_DELETED, + POST_CHANGE_CHANNEL_PRIVACY: + default: + if !strings.HasPrefix(o.Type, POST_CUSTOM_TYPE_PREFIX) { + return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest) + } + } + + if utf8.RuneCountInString(ArrayToJson(o.Filenames)) > POST_FILENAMES_MAX_RUNES { + return NewAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if utf8.RuneCountInString(ArrayToJson(o.FileIds)) > POST_FILEIDS_MAX_RUNES { + return NewAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if utf8.RuneCountInString(StringInterfaceToJson(o.Props)) > POST_PROPS_MAX_RUNES { + return NewAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + return nil +} + +func (o *Post) SanitizeProps() { + membersToSanitize := []string{ + PROPS_ADD_CHANNEL_MEMBER, + } + + for _, member := range membersToSanitize { + if _, ok := o.Props[member]; ok { + delete(o.Props, member) + } + } +} + +func (o *Post) PreSave() { + if o.Id == "" { + o.Id = NewId() + } + + o.OriginalId = "" + + if o.CreateAt == 0 { + o.CreateAt = GetMillis() + } + + o.UpdateAt = o.CreateAt + o.PreCommit() +} + +func (o *Post) PreCommit() { + if o.Props == nil { + o.Props = make(map[string]interface{}) + } + + if o.Filenames == nil { + o.Filenames = []string{} + } + + if o.FileIds == nil { + o.FileIds = []string{} + } + + o.GenerateActionIds() +} + +func (o *Post) MakeNonNil() { + if o.Props == nil { + o.Props = make(map[string]interface{}) + } +} + +func (o *Post) AddProp(key string, value interface{}) { + + o.MakeNonNil() + + o.Props[key] = value +} + +func (o *Post) IsSystemMessage() bool { + return len(o.Type) >= len(POST_SYSTEM_MESSAGE_PREFIX) && o.Type[:len(POST_SYSTEM_MESSAGE_PREFIX)] == POST_SYSTEM_MESSAGE_PREFIX +} + +func (p *Post) Patch(patch *PostPatch) { + if patch.IsPinned != nil { + p.IsPinned = *patch.IsPinned + } + + if patch.Message != nil { + p.Message = *patch.Message + } + + if patch.Props != nil { + p.Props = *patch.Props + } + + if patch.FileIds != nil { + p.FileIds = *patch.FileIds + } + + if patch.HasReactions != nil { + p.HasReactions = *patch.HasReactions + } +} + +func (o *PostPatch) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } + + return string(b) +} + +func PostPatchFromJson(data io.Reader) *PostPatch { + decoder := json.NewDecoder(data) + var post PostPatch + err := decoder.Decode(&post) + if err != nil { + return nil + } + + return &post +} + +func (o *SearchParameter) SearchParameterToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } + + return string(b) +} + +func SearchParameterFromJson(data io.Reader) *SearchParameter { + decoder := json.NewDecoder(data) + var searchParam SearchParameter + err := decoder.Decode(&searchParam) + if err != nil { + return nil + } + + return &searchParam +} + +func (o *Post) ChannelMentions() []string { + return ChannelMentions(o.Message) +} + +func (r *PostActionIntegrationRequest) ToJson() string { + b, _ := json.Marshal(r) + return string(b) +} + +func PostActionIntegrationRequesteFromJson(data io.Reader) *PostActionIntegrationRequest { + var o *PostActionIntegrationRequest + err := json.NewDecoder(data).Decode(&o) + if err != nil { + return nil + } + return o +} + +func (r *PostActionIntegrationResponse) ToJson() string { + b, _ := json.Marshal(r) + return string(b) +} + +func PostActionIntegrationResponseFromJson(data io.Reader) *PostActionIntegrationResponse { + var o *PostActionIntegrationResponse + err := json.NewDecoder(data).Decode(&o) + if err != nil { + return nil + } + return o +} + +func (o *Post) Attachments() []*SlackAttachment { + if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok { + return attachments + } + var ret []*SlackAttachment + if attachments, ok := o.Props["attachments"].([]interface{}); ok { + for _, attachment := range attachments { + if enc, err := json.Marshal(attachment); err == nil { + var decoded SlackAttachment + if json.Unmarshal(enc, &decoded) == nil { + ret = append(ret, &decoded) + } + } + } + } + return ret +} + +func (o *Post) StripActionIntegrations() { + attachments := o.Attachments() + if o.Props["attachments"] != nil { + o.Props["attachments"] = attachments + } + for _, attachment := range attachments { + for _, action := range attachment.Actions { + action.Integration = nil + } + } +} + +func (o *Post) GetAction(id string) *PostAction { + for _, attachment := range o.Attachments() { + for _, action := range attachment.Actions { + if action.Id == id { + return action + } + } + } + return nil +} + +func (o *Post) GenerateActionIds() { + if o.Props["attachments"] != nil { + o.Props["attachments"] = o.Attachments() + } + if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok { + for _, attachment := range attachments { + for _, action := range attachment.Actions { + if action.Id == "" { + action.Id = NewId() + } + } + } + } +} + +var markdownDestinationEscaper = strings.NewReplacer( + `\`, `\\`, + `<`, `\<`, + `>`, `\>`, + `(`, `\(`, + `)`, `\)`, +) + +// WithRewrittenImageURLs returns a new shallow copy of the post where the message has been +// rewritten via RewriteImageURLs. +func (o *Post) WithRewrittenImageURLs(f func(string) string) *Post { + copy := *o + copy.Message = RewriteImageURLs(o.Message, f) + if copy.MessageSource == "" && copy.Message != o.Message { + copy.MessageSource = o.Message + } + return © +} + +func (o *PostEphemeral) ToUnsanitizedJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func DoPostActionRequestFromJson(data io.Reader) *DoPostActionRequest { + var o *DoPostActionRequest + json.NewDecoder(data).Decode(&o) + return o +} + +// RewriteImageURLs takes a message and returns a copy that has all of the image URLs replaced +// according to the function f. For each image URL, f will be invoked, and the resulting markdown +// will contain the URL returned by that invocation instead. +// +// Image URLs are destination URLs used in inline images or reference definitions that are used +// anywhere in the input markdown as an image. +func RewriteImageURLs(message string, f func(string) string) string { + if !strings.Contains(message, "![") { + return message + } + + var ranges []markdown.Range + + markdown.Inspect(message, func(blockOrInline interface{}) bool { + switch v := blockOrInline.(type) { + case *markdown.ReferenceImage: + ranges = append(ranges, v.ReferenceDefinition.RawDestination) + case *markdown.InlineImage: + ranges = append(ranges, v.RawDestination) + default: + return true + } + return true + }) + + if ranges == nil { + return message + } + + sort.Slice(ranges, func(i, j int) bool { + return ranges[i].Position < ranges[j].Position + }) + + copyRanges := make([]markdown.Range, 0, len(ranges)) + urls := make([]string, 0, len(ranges)) + resultLength := len(message) + + start := 0 + for i, r := range ranges { + switch { + case i == 0: + case r.Position != ranges[i-1].Position: + start = ranges[i-1].End + default: + continue + } + original := message[r.Position:r.End] + replacement := markdownDestinationEscaper.Replace(f(markdown.Unescape(original))) + resultLength += len(replacement) - len(original) + copyRanges = append(copyRanges, markdown.Range{Position: start, End: r.Position}) + urls = append(urls, replacement) + } + + result := make([]byte, resultLength) + + offset := 0 + for i, r := range copyRanges { + offset += copy(result[offset:], message[r.Position:r.End]) + offset += copy(result[offset:], urls[i]) + } + copy(result[offset:], message[ranges[len(ranges)-1].End:]) + + return string(result) +} diff --git a/vendor/github.com/mattermost/platform/model/post_list.go b/vendor/github.com/mattermost/mattermost-server/model/post_list.go similarity index 88% rename from vendor/github.com/mattermost/platform/model/post_list.go rename to vendor/github.com/mattermost/mattermost-server/model/post_list.go index 018f7d14..27c22e7b 100644 --- a/vendor/github.com/mattermost/platform/model/post_list.go +++ b/vendor/github.com/mattermost/mattermost-server/model/post_list.go @@ -21,6 +21,15 @@ func NewPostList() *PostList { } } +func (o *PostList) WithRewrittenImageURLs(f func(string) string) *PostList { + copy := *o + copy.Posts = make(map[string]*Post) + for id, post := range o.Posts { + copy.Posts[id] = post.WithRewrittenImageURLs(f) + } + return © +} + func (o *PostList) StripActionIntegrations() { posts := o.Posts o.Posts = make(map[string]*Post) @@ -123,12 +132,7 @@ func (o *PostList) IsChannelId(channelId string) bool { } func PostListFromJson(data io.Reader) *PostList { - decoder := json.NewDecoder(data) - var o PostList - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *PostList + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/mattermost-server/model/post_search_results.go b/vendor/github.com/mattermost/mattermost-server/model/post_search_results.go new file mode 100644 index 00000000..2317f183 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/post_search_results.go @@ -0,0 +1,40 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type PostSearchMatches map[string][]string + +type PostSearchResults struct { + *PostList + Matches PostSearchMatches `json:"matches"` +} + +func MakePostSearchResults(posts *PostList, matches PostSearchMatches) *PostSearchResults { + return &PostSearchResults{ + posts, + matches, + } +} + +func (o *PostSearchResults) ToJson() string { + copy := *o + copy.PostList.StripActionIntegrations() + b, err := json.Marshal(©) + if err != nil { + return "" + } else { + return string(b) + } +} + +func PostSearchResultsFromJson(data io.Reader) *PostSearchResults { + var o *PostSearchResults + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/platform/model/preference.go b/vendor/github.com/mattermost/mattermost-server/model/preference.go similarity index 89% rename from vendor/github.com/mattermost/platform/model/preference.go rename to vendor/github.com/mattermost/mattermost-server/model/preference.go index 5d462de8..6f13c38e 100644 --- a/vendor/github.com/mattermost/platform/model/preference.go +++ b/vendor/github.com/mattermost/mattermost-server/model/preference.go @@ -18,9 +18,14 @@ const ( PREFERENCE_CATEGORY_ADVANCED_SETTINGS = "advanced_settings" PREFERENCE_CATEGORY_FLAGGED_POST = "flagged_post" PREFERENCE_CATEGORY_FAVORITE_CHANNEL = "favorite_channel" + PREFERENCE_CATEGORY_SIDEBAR_SETTINGS = "sidebar_settings" PREFERENCE_CATEGORY_DISPLAY_SETTINGS = "display_settings" + PREFERENCE_NAME_CHANNEL_DISPLAY_MODE = "channel_display_mode" PREFERENCE_NAME_COLLAPSE_SETTING = "collapse_previews" + PREFERENCE_NAME_MESSAGE_DISPLAY = "message_display" + PREFERENCE_NAME_NAME_FORMAT = "name_format" + PREFERENCE_NAME_USE_MILITARY_TIME = "use_military_time" PREFERENCE_CATEGORY_THEME = "theme" // the name for theme props is the team id @@ -47,23 +52,14 @@ type Preference struct { } func (o *Preference) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func PreferenceFromJson(data io.Reader) *Preference { - decoder := json.NewDecoder(data) - var o Preference - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *Preference + json.NewDecoder(data).Decode(&o) + return o } func (o *Preference) IsValid() *AppError { diff --git a/vendor/github.com/mattermost/platform/model/preferences.go b/vendor/github.com/mattermost/mattermost-server/model/preferences.go similarity index 83% rename from vendor/github.com/mattermost/platform/model/preferences.go rename to vendor/github.com/mattermost/mattermost-server/model/preferences.go index b982e309..172e1aa8 100644 --- a/vendor/github.com/mattermost/platform/model/preferences.go +++ b/vendor/github.com/mattermost/mattermost-server/model/preferences.go @@ -11,12 +11,8 @@ import ( type Preferences []Preference func (o *Preferences) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func PreferencesFromJson(data io.Reader) (Preferences, error) { diff --git a/vendor/github.com/mattermost/platform/model/push_notification.go b/vendor/github.com/mattermost/mattermost-server/model/push_notification.go similarity index 88% rename from vendor/github.com/mattermost/platform/model/push_notification.go rename to vendor/github.com/mattermost/mattermost-server/model/push_notification.go index 69719e74..5268a98f 100644 --- a/vendor/github.com/mattermost/platform/model/push_notification.go +++ b/vendor/github.com/mattermost/mattermost-server/model/push_notification.go @@ -17,6 +17,7 @@ const ( PUSH_TYPE_MESSAGE = "message" PUSH_TYPE_CLEAR = "clear" + PUSH_MESSAGE_V2 = "v2" // The category is set to handle a set of interactive Actions // with the push notifications @@ -44,15 +45,12 @@ type PushNotification struct { OverrideUsername string `json:"override_username"` OverrideIconUrl string `json:"override_icon_url"` FromWebhook string `json:"from_webhook"` + Version string `json:"version"` } func (me *PushNotification) ToJson() string { - b, err := json.Marshal(me) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(me) + return string(b) } func (me *PushNotification) SetDeviceIdAndPlatform(deviceId string) { @@ -66,12 +64,7 @@ func (me *PushNotification) SetDeviceIdAndPlatform(deviceId string) { } func PushNotificationFromJson(data io.Reader) *PushNotification { - decoder := json.NewDecoder(data) - var me PushNotification - err := decoder.Decode(&me) - if err == nil { - return &me - } else { - return nil - } + var me *PushNotification + json.NewDecoder(data).Decode(&me) + return me } diff --git a/vendor/github.com/mattermost/platform/model/push_response.go b/vendor/github.com/mattermost/mattermost-server/model/push_response.go similarity index 92% rename from vendor/github.com/mattermost/platform/model/push_response.go rename to vendor/github.com/mattermost/mattermost-server/model/push_response.go index 095986f3..1434a2b1 100644 --- a/vendor/github.com/mattermost/platform/model/push_response.go +++ b/vendor/github.com/mattermost/mattermost-server/model/push_response.go @@ -38,11 +38,8 @@ func NewErrorPushResponse(message string) PushResponse { } func (me *PushResponse) ToJson() string { - if b, err := json.Marshal(me); err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(me) + return string(b) } func PushResponseFromJson(data io.Reader) PushResponse { diff --git a/vendor/github.com/mattermost/platform/model/reaction.go b/vendor/github.com/mattermost/mattermost-server/model/reaction.go similarity index 85% rename from vendor/github.com/mattermost/platform/model/reaction.go rename to vendor/github.com/mattermost/mattermost-server/model/reaction.go index 4b72dd44..c1b9c499 100644 --- a/vendor/github.com/mattermost/platform/model/reaction.go +++ b/vendor/github.com/mattermost/mattermost-server/model/reaction.go @@ -18,11 +18,8 @@ type Reaction struct { } func (o *Reaction) ToJson() string { - if b, err := json.Marshal(o); err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func ReactionFromJson(data io.Reader) *Reaction { @@ -36,11 +33,8 @@ func ReactionFromJson(data io.Reader) *Reaction { } func ReactionsToJson(o []*Reaction) string { - if b, err := json.Marshal(o); err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func ReactionsFromJson(data io.Reader) []*Reaction { @@ -64,7 +58,7 @@ func (o *Reaction) IsValid() *AppError { validName := regexp.MustCompile(`^[a-zA-Z0-9\-\+_]+$`) - if len(o.EmojiName) == 0 || len(o.EmojiName) > 64 || !validName.MatchString(o.EmojiName) { + if len(o.EmojiName) == 0 || len(o.EmojiName) > EMOJI_NAME_MAX_LENGTH || !validName.MatchString(o.EmojiName) { return NewAppError("Reaction.IsValid", "model.reaction.is_valid.emoji_name.app_error", nil, "emoji_name="+o.EmojiName, http.StatusBadRequest) } diff --git a/vendor/github.com/mattermost/mattermost-server/model/role.go b/vendor/github.com/mattermost/mattermost-server/model/role.go new file mode 100644 index 00000000..27b32ed6 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/role.go @@ -0,0 +1,363 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "strings" +) + +const ( + SYSTEM_USER_ROLE_ID = "system_user" + SYSTEM_ADMIN_ROLE_ID = "system_admin" + SYSTEM_POST_ALL_ROLE_ID = "system_post_all" + SYSTEM_POST_ALL_PUBLIC_ROLE_ID = "system_post_all_public" + SYSTEM_USER_ACCESS_TOKEN_ROLE_ID = "system_user_access_token" + + TEAM_USER_ROLE_ID = "team_user" + TEAM_ADMIN_ROLE_ID = "team_admin" + TEAM_POST_ALL_ROLE_ID = "team_post_all" + TEAM_POST_ALL_PUBLIC_ROLE_ID = "team_post_all_public" + + CHANNEL_USER_ROLE_ID = "channel_user" + CHANNEL_ADMIN_ROLE_ID = "channel_admin" + + ROLE_NAME_MAX_LENGTH = 64 + ROLE_DISPLAY_NAME_MAX_LENGTH = 128 + ROLE_DESCRIPTION_MAX_LENGTH = 1024 +) + +type Role struct { + Id string `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + Description string `json:"description"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + Permissions []string `json:"permissions"` + SchemeManaged bool `json:"scheme_managed"` + BuiltIn bool `json:"built_in"` +} + +type RolePatch struct { + Permissions *[]string `json:"permissions"` +} + +func (role *Role) ToJson() string { + b, _ := json.Marshal(role) + return string(b) +} + +func RoleFromJson(data io.Reader) *Role { + var role *Role + json.NewDecoder(data).Decode(&role) + return role +} + +func RoleListToJson(r []*Role) string { + b, _ := json.Marshal(r) + return string(b) +} + +func RoleListFromJson(data io.Reader) []*Role { + var roles []*Role + json.NewDecoder(data).Decode(&roles) + return roles +} + +func (r *RolePatch) ToJson() string { + b, _ := json.Marshal(r) + return string(b) +} + +func RolePatchFromJson(data io.Reader) *RolePatch { + var rolePatch *RolePatch + json.NewDecoder(data).Decode(&rolePatch) + return rolePatch +} + +func (o *Role) Patch(patch *RolePatch) { + if patch.Permissions != nil { + o.Permissions = *patch.Permissions + } +} + +// Returns an array of permissions that are in either role.Permissions +// or patch.Permissions, but not both. +func PermissionsChangedByPatch(role *Role, patch *RolePatch) []string { + var result []string + + if patch.Permissions == nil { + return result + } + + roleMap := make(map[string]bool) + patchMap := make(map[string]bool) + + for _, permission := range role.Permissions { + roleMap[permission] = true + } + + for _, permission := range *patch.Permissions { + patchMap[permission] = true + } + + for _, permission := range role.Permissions { + if !patchMap[permission] { + result = append(result, permission) + } + } + + for _, permission := range *patch.Permissions { + if !roleMap[permission] { + result = append(result, permission) + } + } + + return result +} + +func (role *Role) IsValid() bool { + if len(role.Id) != 26 { + return false + } + + return role.IsValidWithoutId() +} + +func (role *Role) IsValidWithoutId() bool { + if !IsValidRoleName(role.Name) { + return false + } + + if len(role.DisplayName) == 0 || len(role.DisplayName) > ROLE_DISPLAY_NAME_MAX_LENGTH { + return false + } + + if len(role.Description) > ROLE_DESCRIPTION_MAX_LENGTH { + return false + } + + for _, permission := range role.Permissions { + permissionValidated := false + for _, p := range ALL_PERMISSIONS { + if permission == p.Id { + permissionValidated = true + break + } + } + + if !permissionValidated { + return false + } + } + + return true +} + +func IsValidRoleName(roleName string) bool { + if len(roleName) <= 0 || len(roleName) > ROLE_NAME_MAX_LENGTH { + return false + } + + if strings.TrimLeft(roleName, "abcdefghijklmnopqrstuvwxyz0123456789_") != "" { + return false + } + + return true +} + +func MakeDefaultRoles() map[string]*Role { + roles := make(map[string]*Role) + + roles[CHANNEL_USER_ROLE_ID] = &Role{ + Name: "channel_user", + DisplayName: "authentication.roles.channel_user.name", + Description: "authentication.roles.channel_user.description", + Permissions: []string{ + PERMISSION_READ_CHANNEL.Id, + PERMISSION_ADD_REACTION.Id, + PERMISSION_REMOVE_REACTION.Id, + PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id, + PERMISSION_UPLOAD_FILE.Id, + PERMISSION_GET_PUBLIC_LINK.Id, + PERMISSION_CREATE_POST.Id, + PERMISSION_USE_SLASH_COMMANDS.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[CHANNEL_ADMIN_ROLE_ID] = &Role{ + Name: "channel_admin", + DisplayName: "authentication.roles.channel_admin.name", + Description: "authentication.roles.channel_admin.description", + Permissions: []string{ + PERMISSION_MANAGE_CHANNEL_ROLES.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[TEAM_USER_ROLE_ID] = &Role{ + Name: "team_user", + DisplayName: "authentication.roles.team_user.name", + Description: "authentication.roles.team_user.description", + Permissions: []string{ + PERMISSION_LIST_TEAM_CHANNELS.Id, + PERMISSION_JOIN_PUBLIC_CHANNELS.Id, + PERMISSION_READ_PUBLIC_CHANNEL.Id, + PERMISSION_VIEW_TEAM.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[TEAM_POST_ALL_ROLE_ID] = &Role{ + Name: "team_post_all", + DisplayName: "authentication.roles.team_post_all.name", + Description: "authentication.roles.team_post_all.description", + Permissions: []string{ + PERMISSION_CREATE_POST.Id, + }, + SchemeManaged: false, + BuiltIn: true, + } + + roles[TEAM_POST_ALL_PUBLIC_ROLE_ID] = &Role{ + Name: "team_post_all_public", + DisplayName: "authentication.roles.team_post_all_public.name", + Description: "authentication.roles.team_post_all_public.description", + Permissions: []string{ + PERMISSION_CREATE_POST_PUBLIC.Id, + }, + SchemeManaged: false, + BuiltIn: true, + } + + roles[TEAM_ADMIN_ROLE_ID] = &Role{ + Name: "team_admin", + DisplayName: "authentication.roles.team_admin.name", + Description: "authentication.roles.team_admin.description", + Permissions: []string{ + PERMISSION_REMOVE_USER_FROM_TEAM.Id, + PERMISSION_MANAGE_TEAM.Id, + PERMISSION_IMPORT_TEAM.Id, + PERMISSION_MANAGE_TEAM_ROLES.Id, + PERMISSION_MANAGE_CHANNEL_ROLES.Id, + PERMISSION_MANAGE_OTHERS_WEBHOOKS.Id, + PERMISSION_MANAGE_SLASH_COMMANDS.Id, + PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS.Id, + PERMISSION_MANAGE_WEBHOOKS.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[SYSTEM_USER_ROLE_ID] = &Role{ + Name: "system_user", + DisplayName: "authentication.roles.global_user.name", + Description: "authentication.roles.global_user.description", + Permissions: []string{ + PERMISSION_CREATE_DIRECT_CHANNEL.Id, + PERMISSION_CREATE_GROUP_CHANNEL.Id, + PERMISSION_PERMANENT_DELETE_USER.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[SYSTEM_POST_ALL_ROLE_ID] = &Role{ + Name: "system_post_all", + DisplayName: "authentication.roles.system_post_all.name", + Description: "authentication.roles.system_post_all.description", + Permissions: []string{ + PERMISSION_CREATE_POST.Id, + }, + SchemeManaged: false, + BuiltIn: true, + } + + roles[SYSTEM_POST_ALL_PUBLIC_ROLE_ID] = &Role{ + Name: "system_post_all_public", + DisplayName: "authentication.roles.system_post_all_public.name", + Description: "authentication.roles.system_post_all_public.description", + Permissions: []string{ + PERMISSION_CREATE_POST_PUBLIC.Id, + }, + SchemeManaged: false, + BuiltIn: true, + } + + roles[SYSTEM_USER_ACCESS_TOKEN_ROLE_ID] = &Role{ + Name: "system_user_access_token", + DisplayName: "authentication.roles.system_user_access_token.name", + Description: "authentication.roles.system_user_access_token.description", + Permissions: []string{ + PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, + PERMISSION_READ_USER_ACCESS_TOKEN.Id, + PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, + }, + SchemeManaged: false, + BuiltIn: true, + } + + roles[SYSTEM_ADMIN_ROLE_ID] = &Role{ + Name: "system_admin", + DisplayName: "authentication.roles.global_admin.name", + Description: "authentication.roles.global_admin.description", + // System admins can do anything channel and team admins can do + // plus everything members of teams and channels can do to all teams + // and channels on the system + Permissions: append( + append( + append( + append( + []string{ + PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE.Id, + PERMISSION_MANAGE_SYSTEM.Id, + PERMISSION_MANAGE_ROLES.Id, + PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES.Id, + PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id, + PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id, + PERMISSION_DELETE_PUBLIC_CHANNEL.Id, + PERMISSION_CREATE_PUBLIC_CHANNEL.Id, + PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES.Id, + PERMISSION_DELETE_PRIVATE_CHANNEL.Id, + PERMISSION_CREATE_PRIVATE_CHANNEL.Id, + PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH.Id, + PERMISSION_MANAGE_OTHERS_WEBHOOKS.Id, + PERMISSION_EDIT_OTHER_USERS.Id, + PERMISSION_EDIT_OTHERS_POSTS.Id, + PERMISSION_MANAGE_OAUTH.Id, + PERMISSION_INVITE_USER.Id, + PERMISSION_DELETE_POST.Id, + PERMISSION_DELETE_OTHERS_POSTS.Id, + PERMISSION_CREATE_TEAM.Id, + PERMISSION_ADD_USER_TO_TEAM.Id, + PERMISSION_LIST_USERS_WITHOUT_TEAM.Id, + PERMISSION_MANAGE_JOBS.Id, + PERMISSION_CREATE_POST_PUBLIC.Id, + PERMISSION_CREATE_POST_EPHEMERAL.Id, + PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, + PERMISSION_READ_USER_ACCESS_TOKEN.Id, + PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, + PERMISSION_REMOVE_OTHERS_REACTIONS.Id, + }, + roles[TEAM_USER_ROLE_ID].Permissions..., + ), + roles[CHANNEL_USER_ROLE_ID].Permissions..., + ), + roles[TEAM_ADMIN_ROLE_ID].Permissions..., + ), + roles[CHANNEL_ADMIN_ROLE_ID].Permissions..., + ), + SchemeManaged: true, + BuiltIn: true, + } + + return roles +} diff --git a/vendor/github.com/mattermost/platform/model/saml.go b/vendor/github.com/mattermost/mattermost-server/model/saml.go similarity index 67% rename from vendor/github.com/mattermost/platform/model/saml.go rename to vendor/github.com/mattermost/mattermost-server/model/saml.go index f5826a95..528ac45c 100644 --- a/vendor/github.com/mattermost/platform/model/saml.go +++ b/vendor/github.com/mattermost/mattermost-server/model/saml.go @@ -11,9 +11,6 @@ import ( const ( USER_AUTH_SERVICE_SAML = "saml" USER_AUTH_SERVICE_SAML_TEXT = "With SAML" - SAML_IDP_CERTIFICATE = 1 - SAML_PRIVATE_KEY = 2 - SAML_PUBLIC_CERT = 3 ) type SamlAuthRequest struct { @@ -29,21 +26,12 @@ type SamlCertificateStatus struct { } func (s *SamlCertificateStatus) ToJson() string { - b, err := json.Marshal(s) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(s) + return string(b) } func SamlCertificateStatusFromJson(data io.Reader) *SamlCertificateStatus { - decoder := json.NewDecoder(data) - var status SamlCertificateStatus - err := decoder.Decode(&status) - if err == nil { - return &status - } else { - return nil - } + var status *SamlCertificateStatus + json.NewDecoder(data).Decode(&status) + return status } diff --git a/vendor/github.com/mattermost/mattermost-server/model/scheduled_task.go b/vendor/github.com/mattermost/mattermost-server/model/scheduled_task.go new file mode 100644 index 00000000..f3529ded --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/scheduled_task.go @@ -0,0 +1,77 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "fmt" + "time" +) + +type TaskFunc func() + +type ScheduledTask struct { + Name string `json:"name"` + Interval time.Duration `json:"interval"` + Recurring bool `json:"recurring"` + function func() + cancel chan struct{} + cancelled chan struct{} +} + +func CreateTask(name string, function TaskFunc, timeToExecution time.Duration) *ScheduledTask { + return createTask(name, function, timeToExecution, false) +} + +func CreateRecurringTask(name string, function TaskFunc, interval time.Duration) *ScheduledTask { + return createTask(name, function, interval, true) +} + +func createTask(name string, function TaskFunc, interval time.Duration, recurring bool) *ScheduledTask { + task := &ScheduledTask{ + Name: name, + Interval: interval, + Recurring: recurring, + function: function, + cancel: make(chan struct{}), + cancelled: make(chan struct{}), + } + + go func() { + defer close(task.cancelled) + + ticker := time.NewTicker(interval) + defer func() { + ticker.Stop() + }() + + for { + select { + case <-ticker.C: + function() + case <-task.cancel: + return + } + + if !task.Recurring { + break + } + } + }() + + return task +} + +func (task *ScheduledTask) Cancel() { + close(task.cancel) + <-task.cancelled +} + +func (task *ScheduledTask) String() string { + return fmt.Sprintf( + "%s\nInterval: %s\nRecurring: %t\n", + task.Name, + task.Interval.String(), + task.Recurring, + ) +} diff --git a/vendor/github.com/mattermost/mattermost-server/model/scheme.go b/vendor/github.com/mattermost/mattermost-server/model/scheme.go new file mode 100644 index 00000000..0c38b560 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/scheme.go @@ -0,0 +1,208 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" + "regexp" +) + +const ( + SCHEME_DISPLAY_NAME_MAX_LENGTH = 128 + SCHEME_NAME_MAX_LENGTH = 64 + SCHEME_DESCRIPTION_MAX_LENGTH = 1024 + SCHEME_SCOPE_TEAM = "team" + SCHEME_SCOPE_CHANNEL = "channel" +) + +type Scheme struct { + Id string `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + Description string `json:"description"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + Scope string `json:"scope"` + DefaultTeamAdminRole string `json:"default_team_admin_role"` + DefaultTeamUserRole string `json:"default_team_user_role"` + DefaultChannelAdminRole string `json:"default_channel_admin_role"` + DefaultChannelUserRole string `json:"default_channel_user_role"` +} + +type SchemePatch struct { + Name *string `json:"name"` + DisplayName *string `json:"display_name"` + Description *string `json:"description"` +} + +type SchemeIDPatch struct { + SchemeID *string `json:"scheme_id"` +} + +// SchemeConveyor is used for importing and exporting a Scheme and its associated Roles. +type SchemeConveyor struct { + Name string `json:"name"` + DisplayName string `json:"display_name"` + Description string `json:"description"` + Scope string `json:"scope"` + TeamAdmin string `json:"default_team_admin_role"` + TeamUser string `json:"default_team_user_role"` + ChannelAdmin string `json:"default_channel_admin_role"` + ChannelUser string `json:"default_channel_user_role"` + Roles []*Role `json:"roles"` +} + +func (sc *SchemeConveyor) Scheme() *Scheme { + return &Scheme{ + DisplayName: sc.DisplayName, + Name: sc.Name, + Description: sc.Description, + Scope: sc.Scope, + DefaultTeamAdminRole: sc.TeamAdmin, + DefaultTeamUserRole: sc.TeamUser, + DefaultChannelAdminRole: sc.ChannelAdmin, + DefaultChannelUserRole: sc.ChannelUser, + } +} + +type SchemeRoles struct { + SchemeAdmin bool `json:"scheme_admin"` + SchemeUser bool `json:"scheme_user"` +} + +func (scheme *Scheme) ToJson() string { + b, _ := json.Marshal(scheme) + return string(b) +} + +func SchemeFromJson(data io.Reader) *Scheme { + var scheme *Scheme + json.NewDecoder(data).Decode(&scheme) + return scheme +} + +func SchemesToJson(schemes []*Scheme) string { + b, _ := json.Marshal(schemes) + return string(b) +} + +func SchemesFromJson(data io.Reader) []*Scheme { + var schemes []*Scheme + if err := json.NewDecoder(data).Decode(&schemes); err == nil { + return schemes + } else { + return nil + } +} + +func (scheme *Scheme) IsValid() bool { + if len(scheme.Id) != 26 { + return false + } + + return scheme.IsValidForCreate() +} + +func (scheme *Scheme) IsValidForCreate() bool { + if len(scheme.DisplayName) == 0 || len(scheme.DisplayName) > SCHEME_DISPLAY_NAME_MAX_LENGTH { + return false + } + + if !IsValidSchemeName(scheme.Name) { + return false + } + + if len(scheme.Description) > SCHEME_DESCRIPTION_MAX_LENGTH { + return false + } + + switch scheme.Scope { + case SCHEME_SCOPE_TEAM, SCHEME_SCOPE_CHANNEL: + default: + return false + } + + if !IsValidRoleName(scheme.DefaultChannelAdminRole) { + return false + } + + if !IsValidRoleName(scheme.DefaultChannelUserRole) { + return false + } + + if scheme.Scope == SCHEME_SCOPE_TEAM { + if !IsValidRoleName(scheme.DefaultTeamAdminRole) { + return false + } + + if !IsValidRoleName(scheme.DefaultTeamUserRole) { + return false + } + } + + if scheme.Scope == SCHEME_SCOPE_CHANNEL { + if len(scheme.DefaultTeamAdminRole) != 0 { + return false + } + + if len(scheme.DefaultTeamUserRole) != 0 { + return false + } + } + + return true +} + +func (scheme *Scheme) Patch(patch *SchemePatch) { + if patch.DisplayName != nil { + scheme.DisplayName = *patch.DisplayName + } + if patch.Name != nil { + scheme.Name = *patch.Name + } + if patch.Description != nil { + scheme.Description = *patch.Description + } +} + +func (patch *SchemePatch) ToJson() string { + b, _ := json.Marshal(patch) + return string(b) +} + +func SchemePatchFromJson(data io.Reader) *SchemePatch { + var patch *SchemePatch + json.NewDecoder(data).Decode(&patch) + return patch +} + +func SchemeIDFromJson(data io.Reader) *string { + var p *SchemeIDPatch + json.NewDecoder(data).Decode(&p) + return p.SchemeID +} + +func (p *SchemeIDPatch) ToJson() string { + b, _ := json.Marshal(p) + return string(b) +} + +func IsValidSchemeName(name string) bool { + re := regexp.MustCompile(fmt.Sprintf("^[a-z0-9_]{2,%d}$", SCHEME_NAME_MAX_LENGTH)) + return re.MatchString(name) +} + +func (schemeRoles *SchemeRoles) ToJson() string { + b, _ := json.Marshal(schemeRoles) + return string(b) +} + +func SchemeRolesFromJson(data io.Reader) *SchemeRoles { + var schemeRoles *SchemeRoles + json.NewDecoder(data).Decode(&schemeRoles) + return schemeRoles +} diff --git a/vendor/github.com/mattermost/platform/model/search_params.go b/vendor/github.com/mattermost/mattermost-server/model/search_params.go similarity index 54% rename from vendor/github.com/mattermost/platform/model/search_params.go rename to vendor/github.com/mattermost/mattermost-server/model/search_params.go index 2feea8da..65358066 100644 --- a/vendor/github.com/mattermost/platform/model/search_params.go +++ b/vendor/github.com/mattermost/mattermost-server/model/search_params.go @@ -4,32 +4,64 @@ package model import ( - "encoding/json" "regexp" "strings" + "time" ) var searchTermPuncStart = regexp.MustCompile(`^[^\pL\d\s#"]+`) var searchTermPuncEnd = regexp.MustCompile(`[^\pL\d\s*"]+$`) type SearchParams struct { - Terms string - IsHashtag bool - InChannels []string - FromUsers []string - OrTerms bool + Terms string + IsHashtag bool + InChannels []string + FromUsers []string + AfterDate string + BeforeDate string + OnDate string + OrTerms bool + IncludeDeletedChannels bool + TimeZoneOffset int } -func (o *SearchParams) ToJson() string { - b, err := json.Marshal(o) +// Returns the epoch timestamp of the start of the day specified by SearchParams.AfterDate +func (p *SearchParams) GetAfterDateMillis() int64 { + date, err := time.Parse("2006-01-02", PadDateStringZeros(p.AfterDate)) if err != nil { - return "" - } else { - return string(b) + date = time.Now() } + + // travel forward 1 day + oneDay := time.Hour * 24 + afterDate := date.Add(oneDay) + return GetStartOfDayMillis(afterDate, p.TimeZoneOffset) } -var searchFlags = [...]string{"from", "channel", "in"} +// Returns the epoch timestamp of the end of the day specified by SearchParams.BeforeDate +func (p *SearchParams) GetBeforeDateMillis() int64 { + date, err := time.Parse("2006-01-02", PadDateStringZeros(p.BeforeDate)) + if err != nil { + return 0 + } + + // travel back 1 day + oneDay := time.Hour * -24 + beforeDate := date.Add(oneDay) + return GetEndOfDayMillis(beforeDate, p.TimeZoneOffset) +} + +// Returns the epoch timestamps of the start and end of the day specified by SearchParams.OnDate +func (p *SearchParams) GetOnDateMillis() (int64, int64) { + date, err := time.Parse("2006-01-02", PadDateStringZeros(p.OnDate)) + if err != nil { + return 0, 0 + } + + return GetStartOfDayMillis(date, p.TimeZoneOffset), GetEndOfDayMillis(date, p.TimeZoneOffset) +} + +var searchFlags = [...]string{"from", "channel", "in", "before", "after", "on"} func splitWords(text string) []string { words := []string{} @@ -110,7 +142,7 @@ func parseSearchFlags(input []string) ([]string, [][2]string) { return words, flags } -func ParseSearchParams(text string) []*SearchParams { +func ParseSearchParams(text string, timeZoneOffset int) []*SearchParams { words, flags := parseSearchFlags(splitWords(text)) hashtagTermList := []string{} @@ -129,6 +161,9 @@ func ParseSearchParams(text string) []*SearchParams { inChannels := []string{} fromUsers := []string{} + afterDate := "" + beforeDate := "" + onDate := "" for _, flagPair := range flags { flag := flagPair[0] @@ -138,6 +173,12 @@ func ParseSearchParams(text string) []*SearchParams { inChannels = append(inChannels, value) } else if flag == "from" { fromUsers = append(fromUsers, value) + } else if flag == "after" { + afterDate = value + } else if flag == "before" { + beforeDate = value + } else if flag == "on" { + onDate = value } } @@ -145,29 +186,41 @@ func ParseSearchParams(text string) []*SearchParams { if len(plainTerms) > 0 { paramsList = append(paramsList, &SearchParams{ - Terms: plainTerms, - IsHashtag: false, - InChannels: inChannels, - FromUsers: fromUsers, + Terms: plainTerms, + IsHashtag: false, + InChannels: inChannels, + FromUsers: fromUsers, + AfterDate: afterDate, + BeforeDate: beforeDate, + OnDate: onDate, + TimeZoneOffset: timeZoneOffset, }) } if len(hashtagTerms) > 0 { paramsList = append(paramsList, &SearchParams{ - Terms: hashtagTerms, - IsHashtag: true, - InChannels: inChannels, - FromUsers: fromUsers, + Terms: hashtagTerms, + IsHashtag: true, + InChannels: inChannels, + FromUsers: fromUsers, + AfterDate: afterDate, + BeforeDate: beforeDate, + OnDate: onDate, + TimeZoneOffset: timeZoneOffset, }) } // special case for when no terms are specified but we still have a filter - if len(plainTerms) == 0 && len(hashtagTerms) == 0 && (len(inChannels) != 0 || len(fromUsers) != 0) { + if len(plainTerms) == 0 && len(hashtagTerms) == 0 && (len(inChannels) != 0 || len(fromUsers) != 0 || len(afterDate) != 0 || len(beforeDate) != 0 || len(onDate) != 0) { paramsList = append(paramsList, &SearchParams{ - Terms: "", - IsHashtag: false, - InChannels: inChannels, - FromUsers: fromUsers, + Terms: "", + IsHashtag: false, + InChannels: inChannels, + FromUsers: fromUsers, + AfterDate: afterDate, + BeforeDate: beforeDate, + OnDate: onDate, + TimeZoneOffset: timeZoneOffset, }) } diff --git a/vendor/github.com/mattermost/platform/model/security_bulletin.go b/vendor/github.com/mattermost/mattermost-server/model/security_bulletin.go similarity index 66% rename from vendor/github.com/mattermost/platform/model/security_bulletin.go rename to vendor/github.com/mattermost/mattermost-server/model/security_bulletin.go index b8c1dc48..958b9c9e 100644 --- a/vendor/github.com/mattermost/platform/model/security_bulletin.go +++ b/vendor/github.com/mattermost/mattermost-server/model/security_bulletin.go @@ -16,23 +16,14 @@ type SecurityBulletin struct { type SecurityBulletins []SecurityBulletin func (me *SecurityBulletin) ToJson() string { - b, err := json.Marshal(me) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(me) + return string(b) } func SecurityBulletinFromJson(data io.Reader) *SecurityBulletin { - decoder := json.NewDecoder(data) - var o SecurityBulletin - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *SecurityBulletin + json.NewDecoder(data).Decode(&o) + return o } func (me SecurityBulletins) ToJson() string { @@ -44,12 +35,7 @@ func (me SecurityBulletins) ToJson() string { } func SecurityBulletinsFromJson(data io.Reader) SecurityBulletins { - decoder := json.NewDecoder(data) var o SecurityBulletins - err := decoder.Decode(&o) - if err == nil { - return o - } else { - return nil - } + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/platform/model/session.go b/vendor/github.com/mattermost/mattermost-server/model/session.go similarity index 80% rename from vendor/github.com/mattermost/platform/model/session.go rename to vendor/github.com/mattermost/mattermost-server/model/session.go index 704af067..d59e9b18 100644 --- a/vendor/github.com/mattermost/platform/model/session.go +++ b/vendor/github.com/mattermost/mattermost-server/model/session.go @@ -38,28 +38,32 @@ type Session struct { } func (me *Session) DeepCopy() *Session { - copy := *me - return © + copySession := *me + + if me.Props != nil { + copySession.Props = CopyStringMap(me.Props) + } + + if me.TeamMembers != nil { + copySession.TeamMembers = make([]*TeamMember, len(me.TeamMembers)) + for index, tm := range me.TeamMembers { + copySession.TeamMembers[index] = new(TeamMember) + *copySession.TeamMembers[index] = *tm + } + } + + return ©Session } func (me *Session) ToJson() string { - b, err := json.Marshal(me) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(me) + return string(b) } func SessionFromJson(data io.Reader) *Session { - decoder := json.NewDecoder(data) - var me Session - err := decoder.Decode(&me) - if err == nil { - return &me - } else { - return nil - } + var me *Session + json.NewDecoder(data).Decode(&me) + return me } func (me *Session) PreSave() { @@ -131,6 +135,20 @@ func (me *Session) GetUserRoles() []string { return strings.Fields(me.Roles) } +func (me *Session) GenerateCSRF() string { + token := NewId() + me.AddProp("csrf", token) + return token +} + +func (me *Session) GetCSRF() string { + if me.Props == nil { + return "" + } + + return me.Props["csrf"] +} + func SessionsToJson(o []*Session) string { if b, err := json.Marshal(o); err != nil { return "[]" @@ -140,12 +158,7 @@ func SessionsToJson(o []*Session) string { } func SessionsFromJson(data io.Reader) []*Session { - decoder := json.NewDecoder(data) var o []*Session - err := decoder.Decode(&o) - if err == nil { - return o - } else { - return nil - } + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/platform/model/slack_attachment.go b/vendor/github.com/mattermost/mattermost-server/model/slack_attachment.go similarity index 70% rename from vendor/github.com/mattermost/platform/model/slack_attachment.go rename to vendor/github.com/mattermost/mattermost-server/model/slack_attachment.go index 197d3f0f..827bf35b 100644 --- a/vendor/github.com/mattermost/platform/model/slack_attachment.go +++ b/vendor/github.com/mattermost/mattermost-server/model/slack_attachment.go @@ -5,8 +5,11 @@ package model import ( "fmt" + "regexp" ) +var linkWithTextRegex = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) + type SlackAttachment struct { Id int64 `json:"id"` Fallback string `json:"fallback"` @@ -57,3 +60,25 @@ func StringifySlackFieldValue(a []*SlackAttachment) []*SlackAttachment { } return nonNilAttachments } + +// This method only parses and processes the attachments, +// all else should be set in the post which is passed +func ParseSlackAttachment(post *Post, attachments []*SlackAttachment) { + post.Type = POST_SLACK_ATTACHMENT + + for _, attachment := range attachments { + attachment.Text = ParseSlackLinksToMarkdown(attachment.Text) + attachment.Pretext = ParseSlackLinksToMarkdown(attachment.Pretext) + + for _, field := range attachment.Fields { + if value, ok := field.Value.(string); ok { + field.Value = ParseSlackLinksToMarkdown(value) + } + } + } + post.AddProp("attachments", attachments) +} + +func ParseSlackLinksToMarkdown(text string) string { + return linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})") +} diff --git a/vendor/github.com/mattermost/platform/model/status.go b/vendor/github.com/mattermost/mattermost-server/model/status.go similarity index 62% rename from vendor/github.com/mattermost/platform/model/status.go rename to vendor/github.com/mattermost/mattermost-server/model/status.go index 6da6161e..7888c60a 100644 --- a/vendor/github.com/mattermost/platform/model/status.go +++ b/vendor/github.com/mattermost/mattermost-server/model/status.go @@ -9,6 +9,7 @@ import ( ) const ( + STATUS_OUT_OF_OFFICE = "ooo" STATUS_OFFLINE = "offline" STATUS_AWAY = "away" STATUS_DND = "dnd" @@ -23,47 +24,48 @@ type Status struct { Status string `json:"status"` Manual bool `json:"manual"` LastActivityAt int64 `json:"last_activity_at"` - ActiveChannel string `json:"-" db:"-"` + ActiveChannel string `json:"active_channel,omitempty" db:"-"` } func (o *Status) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + tempChannelId := o.ActiveChannel + o.ActiveChannel = "" + b, _ := json.Marshal(o) + o.ActiveChannel = tempChannelId + return string(b) +} + +func (o *Status) ToClusterJson() string { + b, _ := json.Marshal(o) + return string(b) } func StatusFromJson(data io.Reader) *Status { - decoder := json.NewDecoder(data) - var o Status - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *Status + json.NewDecoder(data).Decode(&o) + return o } func StatusListToJson(u []*Status) string { - b, err := json.Marshal(u) - if err != nil { - return "" - } else { - return string(b) + activeChannels := make([]string, len(u)) + for index, s := range u { + activeChannels[index] = s.ActiveChannel + s.ActiveChannel = "" } + + b, _ := json.Marshal(u) + + for index, s := range u { + s.ActiveChannel = activeChannels[index] + } + + return string(b) } func StatusListFromJson(data io.Reader) []*Status { - decoder := json.NewDecoder(data) var statuses []*Status - err := decoder.Decode(&statuses) - if err == nil { - return statuses - } else { - return nil - } + json.NewDecoder(data).Decode(&statuses) + return statuses } func StatusMapToInterfaceMap(statusMap map[string]*Status) map[string]interface{} { diff --git a/vendor/github.com/mattermost/platform/model/suggest_command.go b/vendor/github.com/mattermost/mattermost-server/model/suggest_command.go similarity index 63% rename from vendor/github.com/mattermost/platform/model/suggest_command.go rename to vendor/github.com/mattermost/mattermost-server/model/suggest_command.go index 3d066499..44f46bf7 100644 --- a/vendor/github.com/mattermost/platform/model/suggest_command.go +++ b/vendor/github.com/mattermost/mattermost-server/model/suggest_command.go @@ -14,21 +14,12 @@ type SuggestCommand struct { } func (o *SuggestCommand) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func SuggestCommandFromJson(data io.Reader) *SuggestCommand { - decoder := json.NewDecoder(data) - var o SuggestCommand - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *SuggestCommand + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/platform/model/switch_request.go b/vendor/github.com/mattermost/mattermost-server/model/switch_request.go similarity index 83% rename from vendor/github.com/mattermost/platform/model/switch_request.go rename to vendor/github.com/mattermost/mattermost-server/model/switch_request.go index 10039294..2a522f49 100644 --- a/vendor/github.com/mattermost/platform/model/switch_request.go +++ b/vendor/github.com/mattermost/mattermost-server/model/switch_request.go @@ -15,27 +15,18 @@ type SwitchRequest struct { Password string `json:"password"` NewPassword string `json:"new_password"` MfaCode string `json:"mfa_code"` - LdapId string `json:"ldap_id"` + LdapLoginId string `json:"ldap_id"` } func (o *SwitchRequest) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func SwitchRequestFromJson(data io.Reader) *SwitchRequest { - decoder := json.NewDecoder(data) - var o SwitchRequest - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *SwitchRequest + json.NewDecoder(data).Decode(&o) + return o } func (o *SwitchRequest) EmailToOAuth() bool { diff --git a/vendor/github.com/mattermost/mattermost-server/model/system.go b/vendor/github.com/mattermost/mattermost-server/model/system.go new file mode 100644 index 00000000..4228516d --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/system.go @@ -0,0 +1,47 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "math/big" +) + +const ( + SYSTEM_DIAGNOSTIC_ID = "DiagnosticId" + SYSTEM_RAN_UNIT_TESTS = "RanUnitTests" + SYSTEM_LAST_SECURITY_TIME = "LastSecurityTime" + SYSTEM_ACTIVE_LICENSE_ID = "ActiveLicenseId" + SYSTEM_LAST_COMPLIANCE_TIME = "LastComplianceTime" + SYSTEM_ASYMMETRIC_SIGNING_KEY = "AsymmetricSigningKey" + SYSTEM_INSTALLATION_DATE_KEY = "InstallationDate" +) + +type System struct { + Name string `json:"name"` + Value string `json:"value"` +} + +func (o *System) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func SystemFromJson(data io.Reader) *System { + var o *System + json.NewDecoder(data).Decode(&o) + return o +} + +type SystemAsymmetricSigningKey struct { + ECDSAKey *SystemECDSAKey `json:"ecdsa_key,omitempty"` +} + +type SystemECDSAKey struct { + Curve string `json:"curve"` + X *big.Int `json:"x"` + Y *big.Int `json:"y"` + D *big.Int `json:"d,omitempty"` +} diff --git a/vendor/github.com/mattermost/platform/model/team.go b/vendor/github.com/mattermost/mattermost-server/model/team.go similarity index 78% rename from vendor/github.com/mattermost/platform/model/team.go rename to vendor/github.com/mattermost/mattermost-server/model/team.go index 4fe03f2f..eadd0522 100644 --- a/vendor/github.com/mattermost/platform/model/team.go +++ b/vendor/github.com/mattermost/mattermost-server/model/team.go @@ -26,42 +26,45 @@ const ( ) type Team struct { - Id string `json:"id"` - CreateAt int64 `json:"create_at"` - UpdateAt int64 `json:"update_at"` - DeleteAt int64 `json:"delete_at"` - DisplayName string `json:"display_name"` - Name string `json:"name"` - Description string `json:"description"` - Email string `json:"email"` - Type string `json:"type"` - CompanyName string `json:"company_name"` - AllowedDomains string `json:"allowed_domains"` - InviteId string `json:"invite_id"` - AllowOpenInvite bool `json:"allow_open_invite"` + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + DisplayName string `json:"display_name"` + Name string `json:"name"` + Description string `json:"description"` + Email string `json:"email"` + Type string `json:"type"` + CompanyName string `json:"company_name"` + AllowedDomains string `json:"allowed_domains"` + InviteId string `json:"invite_id"` + AllowOpenInvite bool `json:"allow_open_invite"` + LastTeamIconUpdate int64 `json:"last_team_icon_update,omitempty"` + SchemeId *string `json:"scheme_id"` } type TeamPatch struct { DisplayName *string `json:"display_name"` Description *string `json:"description"` CompanyName *string `json:"company_name"` + AllowedDomains *string `json:"allowed_domains"` InviteId *string `json:"invite_id"` AllowOpenInvite *bool `json:"allow_open_invite"` } +type TeamForExport struct { + Team + SchemeName *string +} + type Invites struct { Invites []map[string]string `json:"invites"` } func InvitesFromJson(data io.Reader) *Invites { - decoder := json.NewDecoder(data) - var o Invites - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *Invites + json.NewDecoder(data).Decode(&o) + return o } func (o *Invites) ToEmailList() []string { @@ -73,72 +76,41 @@ func (o *Invites) ToEmailList() []string { } func (o *Invites) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func (o *Team) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func TeamFromJson(data io.Reader) *Team { - decoder := json.NewDecoder(data) - var o Team - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *Team + json.NewDecoder(data).Decode(&o) + return o } func TeamMapToJson(u map[string]*Team) string { - b, err := json.Marshal(u) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(u) + return string(b) } func TeamMapFromJson(data io.Reader) map[string]*Team { - decoder := json.NewDecoder(data) var teams map[string]*Team - err := decoder.Decode(&teams) - if err == nil { - return teams - } else { - return nil - } + json.NewDecoder(data).Decode(&teams) + return teams } func TeamListToJson(t []*Team) string { - b, err := json.Marshal(t) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(t) + return string(b) } func TeamListFromJson(data io.Reader) []*Team { - decoder := json.NewDecoder(data) var teams []*Team - err := decoder.Decode(&teams) - if err == nil { - return teams - } else { - return nil - } + json.NewDecoder(data).Decode(&teams) + return teams } func (o *Team) Etag() string { @@ -275,16 +247,6 @@ func CleanTeamName(s string) string { func (o *Team) Sanitize() { o.Email = "" - o.AllowedDomains = "" -} - -func (o *Team) SanitizeForNotLoggedIn() { - o.Email = "" - o.AllowedDomains = "" - o.CompanyName = "" - if !o.AllowOpenInvite { - o.InviteId = "" - } } func (t *Team) Patch(patch *TeamPatch) { @@ -300,6 +262,10 @@ func (t *Team) Patch(patch *TeamPatch) { t.CompanyName = *patch.CompanyName } + if patch.AllowedDomains != nil { + t.AllowedDomains = *patch.AllowedDomains + } + if patch.InviteId != nil { t.InviteId = *patch.InviteId } diff --git a/vendor/github.com/mattermost/platform/model/team_member.go b/vendor/github.com/mattermost/mattermost-server/model/team_member.go similarity index 64% rename from vendor/github.com/mattermost/platform/model/team_member.go rename to vendor/github.com/mattermost/mattermost-server/model/team_member.go index 6c70b75e..3bae3d7e 100644 --- a/vendor/github.com/mattermost/platform/model/team_member.go +++ b/vendor/github.com/mattermost/mattermost-server/model/team_member.go @@ -11,10 +11,13 @@ import ( ) type TeamMember struct { - TeamId string `json:"team_id"` - UserId string `json:"user_id"` - Roles string `json:"roles"` - DeleteAt int64 `json:"delete_at"` + TeamId string `json:"team_id"` + UserId string `json:"user_id"` + Roles string `json:"roles"` + DeleteAt int64 `json:"delete_at"` + SchemeUser bool `json:"scheme_user"` + SchemeAdmin bool `json:"scheme_admin"` + ExplicitRoles string `json:"explicit_roles"` } type TeamUnread struct { @@ -23,44 +26,31 @@ type TeamUnread struct { MentionCount int64 `json:"mention_count"` } +type TeamMemberForExport struct { + TeamMember + TeamName string +} + func (o *TeamMember) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func (o *TeamUnread) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func TeamMemberFromJson(data io.Reader) *TeamMember { - decoder := json.NewDecoder(data) - var o TeamMember - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *TeamMember + json.NewDecoder(data).Decode(&o) + return o } func TeamUnreadFromJson(data io.Reader) *TeamUnread { - decoder := json.NewDecoder(data) - var o TeamUnread - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *TeamUnread + json.NewDecoder(data).Decode(&o) + return o } func TeamMembersToJson(o []*TeamMember) string { @@ -72,14 +62,9 @@ func TeamMembersToJson(o []*TeamMember) string { } func TeamMembersFromJson(data io.Reader) []*TeamMember { - decoder := json.NewDecoder(data) var o []*TeamMember - err := decoder.Decode(&o) - if err == nil { - return o - } else { - return nil - } + json.NewDecoder(data).Decode(&o) + return o } func TeamsUnreadToJson(o []*TeamUnread) string { @@ -91,14 +76,9 @@ func TeamsUnreadToJson(o []*TeamUnread) string { } func TeamsUnreadFromJson(data io.Reader) []*TeamUnread { - decoder := json.NewDecoder(data) var o []*TeamUnread - err := decoder.Decode(&o) - if err == nil { - return o - } else { - return nil - } + json.NewDecoder(data).Decode(&o) + return o } func (o *TeamMember) IsValid() *AppError { diff --git a/vendor/github.com/mattermost/platform/model/team_search.go b/vendor/github.com/mattermost/mattermost-server/model/team_search.go similarity index 100% rename from vendor/github.com/mattermost/platform/model/team_search.go rename to vendor/github.com/mattermost/mattermost-server/model/team_search.go diff --git a/vendor/github.com/mattermost/platform/model/team_stats.go b/vendor/github.com/mattermost/mattermost-server/model/team_stats.go similarity index 66% rename from vendor/github.com/mattermost/platform/model/team_stats.go rename to vendor/github.com/mattermost/mattermost-server/model/team_stats.go index e94ed37b..0d688b80 100644 --- a/vendor/github.com/mattermost/platform/model/team_stats.go +++ b/vendor/github.com/mattermost/mattermost-server/model/team_stats.go @@ -15,21 +15,12 @@ type TeamStats struct { } func (o *TeamStats) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func TeamStatsFromJson(data io.Reader) *TeamStats { - decoder := json.NewDecoder(data) - var o TeamStats - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *TeamStats + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/mattermost-server/model/terms_of_service.go b/vendor/github.com/mattermost/mattermost-server/model/terms_of_service.go new file mode 100644 index 00000000..c99a7856 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/terms_of_service.go @@ -0,0 +1,70 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "unicode/utf8" +) + +// we only ever need the latest version of terms of service +const TERMS_OF_SERVICE_CACHE_SIZE = 1 + +type TermsOfService struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UserId string `json:"user_id"` + Text string `json:"text"` +} + +func (t *TermsOfService) IsValid() *AppError { + if len(t.Id) != 26 { + return InvalidTermsOfServiceError("id", "") + } + + if t.CreateAt == 0 { + return InvalidTermsOfServiceError("create_at", t.Id) + } + + if len(t.UserId) != 26 { + return InvalidTermsOfServiceError("user_id", t.Id) + } + + if utf8.RuneCountInString(t.Text) > POST_MESSAGE_MAX_RUNES_V2 { + return InvalidTermsOfServiceError("text", t.Id) + } + + return nil +} + +func (t *TermsOfService) ToJson() string { + b, _ := json.Marshal(t) + return string(b) +} + +func TermsOfServiceFromJson(data io.Reader) *TermsOfService { + var termsOfService *TermsOfService + json.NewDecoder(data).Decode(&termsOfService) + return termsOfService +} + +func InvalidTermsOfServiceError(fieldName string, termsOfServiceId string) *AppError { + id := fmt.Sprintf("model.terms_of_service.is_valid.%s.app_error", fieldName) + details := "" + if termsOfServiceId != "" { + details = "terms_of_service_id=" + termsOfServiceId + } + return NewAppError("TermsOfServiceStore.IsValid", id, map[string]interface{}{"MaxLength": POST_MESSAGE_MAX_RUNES_V2}, details, http.StatusBadRequest) +} + +func (t *TermsOfService) PreSave() { + if t.Id == "" { + t.Id = NewId() + } + + t.CreateAt = GetMillis() +} diff --git a/vendor/github.com/mattermost/mattermost-server/model/timezone.go b/vendor/github.com/mattermost/mattermost-server/model/timezone.go new file mode 100644 index 00000000..420b9d2e --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/timezone.go @@ -0,0 +1,628 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type SupportedTimezones []string + +func TimezonesToJson(timezoneList []string) string { + b, _ := json.Marshal(timezoneList) + return string(b) +} + +func TimezonesFromJson(data io.Reader) SupportedTimezones { + var timezones SupportedTimezones + json.NewDecoder(data).Decode(&timezones) + return timezones +} + +func DefaultUserTimezone() map[string]string { + defaultTimezone := make(map[string]string) + defaultTimezone["useAutomaticTimezone"] = "true" + defaultTimezone["automaticTimezone"] = "" + defaultTimezone["manualTimezone"] = "" + + return defaultTimezone +} + +var DefaultSupportedTimezones = []string{ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Asmera", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Timbuktu", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/ComodRivadavia", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Atka", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Buenos_Aires", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Catamarca", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Coral_Harbour", + "America/Cordoba", + "America/Costa_Rica", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Ensenada", + "America/Fort_Nelson", + "America/Fort_Wayne", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Jujuy", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Knox_IN", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Louisville", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Mendoza", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montreal", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Acre", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Rosario", + "America/Santa_Isabel", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Shiprock", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Virgin", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/South_Pole", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Ashkhabad", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Calcutta", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Chongqing", + "Asia/Chungking", + "Asia/Colombo", + "Asia/Dacca", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Harbin", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kashgar", + "Asia/Kathmandu", + "Asia/Katmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macao", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qyzylorda", + "Asia/Rangoon", + "Asia/Riyadh", + "Asia/Saigon", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Tel_Aviv", + "Asia/Thimbu", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ujung_Pandang", + "Asia/Ulaanbaatar", + "Asia/Ulan_Bator", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faeroe", + "Atlantic/Faroe", + "Atlantic/Jan_Mayen", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/ACT", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Canberra", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/LHI", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/NSW", + "Australia/North", + "Australia/Perth", + "Australia/Queensland", + "Australia/South", + "Australia/Sydney", + "Australia/Tasmania", + "Australia/Victoria", + "Australia/West", + "Australia/Yancowinna", + "Brazil/Acre", + "Brazil/DeNoronha", + "Brazil/East", + "Brazil/West", + "CET", + "CST6CDT", + "Canada/Atlantic", + "Canada/Central", + "Canada/East-Saskatchewan", + "Canada/Eastern", + "Canada/Mountain", + "Canada/Newfoundland", + "Canada/Pacific", + "Canada/Saskatchewan", + "Canada/Yukon", + "Chile/Continental", + "Chile/EasterIsland", + "Cuba", + "EET", + "EST", + "EST5EDT", + "Egypt", + "Eire", + "Etc/GMT", + "Etc/GMT+0", + "Etc/GMT+1", + "Etc/GMT+10", + "Etc/GMT+11", + "Etc/GMT+12", + "Etc/GMT+2", + "Etc/GMT+3", + "Etc/GMT+4", + "Etc/GMT+5", + "Etc/GMT+6", + "Etc/GMT+7", + "Etc/GMT+8", + "Etc/GMT+9", + "Etc/GMT-0", + "Etc/GMT-1", + "Etc/GMT-10", + "Etc/GMT-11", + "Etc/GMT-12", + "Etc/GMT-13", + "Etc/GMT-14", + "Etc/GMT-2", + "Etc/GMT-3", + "Etc/GMT-4", + "Etc/GMT-5", + "Etc/GMT-6", + "Etc/GMT-7", + "Etc/GMT-8", + "Etc/GMT-9", + "Etc/GMT0", + "Etc/Greenwich", + "Etc/UCT", + "Etc/UTC", + "Etc/Universal", + "Etc/Zulu", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belfast", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Nicosia", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Tiraspol", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "Factory", + "GB", + "GB-Eire", + "GMT", + "GMT+0", + "GMT-0", + "GMT0", + "Greenwich", + "HST", + "Hongkong", + "Iceland", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Iran", + "Israel", + "Jamaica", + "Japan", + "Kwajalein", + "Libya", + "MET", + "MST", + "MST7MDT", + "Mexico/BajaNorte", + "Mexico/BajaSur", + "Mexico/General", + "NZ", + "NZ-CHAT", + "Navajo", + "PRC", + "PST8PDT", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Johnston", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Ponape", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Samoa", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Truk", + "Pacific/Wake", + "Pacific/Wallis", + "Pacific/Yap", + "Poland", + "Portugal", + "ROC", + "ROK", + "Singapore", + "Turkey", + "UCT", + "US/Alaska", + "US/Aleutian", + "US/Arizona", + "US/Central", + "US/East-Indiana", + "US/Eastern", + "US/Hawaii", + "US/Indiana-Starke", + "US/Michigan", + "US/Mountain", + "US/Pacific", + "US/Pacific-New", + "US/Samoa", + "UTC", + "Universal", + "W-SU", + "WET", + "Zulu", +} diff --git a/vendor/github.com/mattermost/platform/model/token.go b/vendor/github.com/mattermost/mattermost-server/model/token.go similarity index 100% rename from vendor/github.com/mattermost/platform/model/token.go rename to vendor/github.com/mattermost/mattermost-server/model/token.go diff --git a/vendor/github.com/mattermost/platform/model/user.go b/vendor/github.com/mattermost/mattermost-server/model/user.go similarity index 76% rename from vendor/github.com/mattermost/platform/model/user.go rename to vendor/github.com/mattermost/mattermost-server/model/user.go index 7e767fd5..51f54c1b 100644 --- a/vendor/github.com/mattermost/platform/model/user.go +++ b/vendor/github.com/mattermost/mattermost-server/model/user.go @@ -22,7 +22,6 @@ const ( USER_NOTIFY_NONE = "none" DESKTOP_NOTIFY_PROP = "desktop" DESKTOP_SOUND_NOTIFY_PROP = "desktop_sound" - DESKTOP_DURATION_NOTIFY_PROP = "desktop_duration" MARK_UNREAD_NOTIFY_PROP = "mark_unread" PUSH_NOTIFY_PROP = "push" PUSH_STATUS_NOTIFY_PROP = "push_status" @@ -39,7 +38,7 @@ const ( USER_EMAIL_MAX_LENGTH = 128 USER_NICKNAME_MAX_RUNES = 64 - USER_POSITION_MAX_RUNES = 64 + USER_POSITION_MAX_RUNES = 128 USER_FIRST_NAME_MAX_RUNES = 64 USER_LAST_NAME_MAX_RUNES = 64 USER_AUTH_DATA_MAX_LENGTH = 128 @@ -49,31 +48,33 @@ const ( ) type User struct { - Id string `json:"id"` - CreateAt int64 `json:"create_at,omitempty"` - UpdateAt int64 `json:"update_at,omitempty"` - DeleteAt int64 `json:"delete_at"` - Username string `json:"username"` - Password string `json:"password,omitempty"` - AuthData *string `json:"auth_data,omitempty"` - AuthService string `json:"auth_service"` - Email string `json:"email"` - EmailVerified bool `json:"email_verified,omitempty"` - Nickname string `json:"nickname"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Position string `json:"position"` - Roles string `json:"roles"` - AllowMarketing bool `json:"allow_marketing,omitempty"` - Props StringMap `json:"props,omitempty"` - NotifyProps StringMap `json:"notify_props,omitempty"` - LastPasswordUpdate int64 `json:"last_password_update,omitempty"` - LastPictureUpdate int64 `json:"last_picture_update,omitempty"` - FailedAttempts int `json:"failed_attempts,omitempty"` - Locale string `json:"locale"` - MfaActive bool `json:"mfa_active,omitempty"` - MfaSecret string `json:"mfa_secret,omitempty"` - LastActivityAt int64 `db:"-" json:"last_activity_at,omitempty"` + Id string `json:"id"` + CreateAt int64 `json:"create_at,omitempty"` + UpdateAt int64 `json:"update_at,omitempty"` + DeleteAt int64 `json:"delete_at"` + Username string `json:"username"` + Password string `json:"password,omitempty"` + AuthData *string `json:"auth_data,omitempty"` + AuthService string `json:"auth_service"` + Email string `json:"email"` + EmailVerified bool `json:"email_verified,omitempty"` + Nickname string `json:"nickname"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Position string `json:"position"` + Roles string `json:"roles"` + AllowMarketing bool `json:"allow_marketing,omitempty"` + Props StringMap `json:"props,omitempty"` + NotifyProps StringMap `json:"notify_props,omitempty"` + LastPasswordUpdate int64 `json:"last_password_update,omitempty"` + LastPictureUpdate int64 `json:"last_picture_update,omitempty"` + FailedAttempts int `json:"failed_attempts,omitempty"` + Locale string `json:"locale"` + Timezone StringMap `json:"timezone"` + MfaActive bool `json:"mfa_active,omitempty"` + MfaSecret string `json:"mfa_secret,omitempty"` + LastActivityAt int64 `db:"-" json:"last_activity_at,omitempty"` + AcceptedTermsOfServiceId string `json:"accepted_terms_of_service_id,omitempty"` // TODO remove this field when new TOS user action table is created } type UserPatch struct { @@ -86,6 +87,7 @@ type UserPatch struct { Props StringMap `json:"props,omitempty"` NotifyProps StringMap `json:"notify_props,omitempty"` Locale *string `json:"locale"` + Timezone StringMap `json:"timezone"` } type UserAuth struct { @@ -94,6 +96,23 @@ type UserAuth struct { AuthService string `json:"auth_service,omitempty"` } +func (u *User) DeepCopy() *User { + copyUser := *u + if u.AuthData != nil { + copyUser.AuthData = NewString(*u.AuthData) + } + if u.Props != nil { + copyUser.Props = CopyStringMap(u.Props) + } + if u.NotifyProps != nil { + copyUser.NotifyProps = CopyStringMap(u.NotifyProps) + } + if u.Timezone != nil { + copyUser.Timezone = CopyStringMap(u.Timezone) + } + return ©User +} + // IsValid validates the user and returns an error if it isn't configured // correctly. func (u *User) IsValid() *AppError { @@ -114,7 +133,7 @@ func (u *User) IsValid() *AppError { return InvalidUserError("username", u.Id) } - if len(u.Email) > USER_EMAIL_MAX_LENGTH || len(u.Email) == 0 { + if len(u.Email) > USER_EMAIL_MAX_LENGTH || len(u.Email) == 0 || !IsValidEmail(u.Email) { return InvalidUserError("email", u.Id) } @@ -162,6 +181,14 @@ func InvalidUserError(fieldName string, userId string) *AppError { return NewAppError("User.IsValid", id, nil, details, http.StatusBadRequest) } +func NormalizeUsername(username string) string { + return strings.ToLower(username) +} + +func NormalizeEmail(email string) string { + return strings.ToLower(email) +} + // PreSave will set the Id and Username if missing. It will also fill // in the CreateAt, UpdateAt times. It will also hash the password. It should // be run before saving the user to the db. @@ -178,8 +205,8 @@ func (u *User) PreSave() { u.AuthData = nil } - u.Username = strings.ToLower(u.Username) - u.Email = strings.ToLower(u.Email) + u.Username = NormalizeUsername(u.Username) + u.Email = NormalizeEmail(u.Email) u.CreateAt = GetMillis() u.UpdateAt = u.CreateAt @@ -200,6 +227,10 @@ func (u *User) PreSave() { u.SetDefaultNotifications() } + if u.Timezone == nil { + u.Timezone = DefaultUserTimezone() + } + if len(u.Password) > 0 { u.Password = HashPassword(u.Password) } @@ -207,8 +238,8 @@ func (u *User) PreSave() { // PreUpdate should be run before updating the user in the db. func (u *User) PreUpdate() { - u.Username = strings.ToLower(u.Username) - u.Email = strings.ToLower(u.Email) + u.Username = NormalizeUsername(u.Username) + u.Email = NormalizeEmail(u.Email) u.UpdateAt = GetMillis() if u.AuthData != nil && *u.AuthData == "" { @@ -294,34 +325,26 @@ func (u *User) Patch(patch *UserPatch) { if patch.Locale != nil { u.Locale = *patch.Locale } + + if patch.Timezone != nil { + u.Timezone = patch.Timezone + } } // ToJson convert a User to a json string func (u *User) ToJson() string { - b, err := json.Marshal(u) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(u) + return string(b) } func (u *UserPatch) ToJson() string { - b, err := json.Marshal(u) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(u) + return string(b) } func (u *UserAuth) ToJson() string { - b, err := json.Marshal(u) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(u) + return string(b) } // Generate a valid strong etag so the browser can cache the results @@ -377,12 +400,6 @@ func (u *User) MakeNonNil() { } } -func (u *User) AddProp(key string, value string) { - u.MakeNonNil() - - u.Props[key] = value -} - func (u *User) AddNotifyProp(key string, value string) { u.MakeNonNil() @@ -390,11 +407,11 @@ func (u *User) AddNotifyProp(key string, value string) { } func (u *User) GetFullName() string { - if u.FirstName != "" && u.LastName != "" { + if len(u.FirstName) > 0 && len(u.LastName) > 0 { return u.FirstName + " " + u.LastName - } else if u.FirstName != "" { + } else if len(u.FirstName) > 0 { return u.FirstName - } else if u.LastName != "" { + } else if len(u.LastName) > 0 { return u.LastName } else { return "" @@ -405,13 +422,13 @@ func (u *User) GetDisplayName(nameFormat string) string { displayName := u.Username if nameFormat == SHOW_NICKNAME_FULLNAME { - if u.Nickname != "" { + if len(u.Nickname) > 0 { displayName = u.Nickname - } else if fullName := u.GetFullName(); fullName != "" { + } else if fullName := u.GetFullName(); len(fullName) > 0 { displayName = fullName } } else if nameFormat == SHOW_FULLNAME { - if fullName := u.GetFullName(); fullName != "" { + if fullName := u.GetFullName(); len(fullName) > 0 { displayName = fullName } } @@ -432,7 +449,7 @@ func IsValidUserRoles(userRoles string) bool { roles := strings.Fields(userRoles) for _, r := range roles { - if !isValidRole(r) { + if !IsValidRoleName(r) { return false } } @@ -445,11 +462,6 @@ func IsValidUserRoles(userRoles string) bool { return true } -func isValidRole(roleId string) bool { - _, ok := DefaultRoles[roleId] - return ok -} - // Make sure you acually want to use this function. In context.go there are functions to check permissions // This function should not be used to check permissions. func (u *User) IsInRole(inRole string) bool { @@ -486,78 +498,53 @@ func (u *User) IsSAMLUser() bool { return u.AuthService == USER_AUTH_SERVICE_SAML } +func (u *User) GetPreferredTimezone() string { + if u.Timezone["useAutomaticTimezone"] == "true" { + return u.Timezone["automaticTimezone"] + } + + return u.Timezone["manualTimezone"] +} + // UserFromJson will decode the input and return a User func UserFromJson(data io.Reader) *User { - decoder := json.NewDecoder(data) - var user User - err := decoder.Decode(&user) - if err == nil { - return &user - } else { - return nil - } + var user *User + json.NewDecoder(data).Decode(&user) + return user } func UserPatchFromJson(data io.Reader) *UserPatch { - decoder := json.NewDecoder(data) - var user UserPatch - err := decoder.Decode(&user) - if err == nil { - return &user - } else { - return nil - } + var user *UserPatch + json.NewDecoder(data).Decode(&user) + return user } func UserAuthFromJson(data io.Reader) *UserAuth { - decoder := json.NewDecoder(data) - var user UserAuth - err := decoder.Decode(&user) - if err == nil { - return &user - } else { - return nil - } + var user *UserAuth + json.NewDecoder(data).Decode(&user) + return user } func UserMapToJson(u map[string]*User) string { - b, err := json.Marshal(u) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(u) + return string(b) } func UserMapFromJson(data io.Reader) map[string]*User { - decoder := json.NewDecoder(data) var users map[string]*User - err := decoder.Decode(&users) - if err == nil { - return users - } else { - return nil - } + json.NewDecoder(data).Decode(&users) + return users } func UserListToJson(u []*User) string { - b, err := json.Marshal(u) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(u) + return string(b) } func UserListFromJson(data io.Reader) []*User { - decoder := json.NewDecoder(data) var users []*User - err := decoder.Decode(&users) - if err == nil { - return users - } else { - return nil - } + json.NewDecoder(data).Decode(&users) + return users } // HashPassword generates a hash using the bcrypt.GenerateFromPassword @@ -587,6 +574,7 @@ var restrictedUsernames = []string{ "all", "channel", "matterbot", + "system", } func IsValidUsername(s string) bool { @@ -608,7 +596,7 @@ func IsValidUsername(s string) bool { } func CleanUsername(s string) string { - s = strings.ToLower(strings.Replace(s, " ", "-", -1)) + s = NormalizeUsername(strings.Replace(s, " ", "-", -1)) for _, value := range reservedName { if s == value { diff --git a/vendor/github.com/mattermost/platform/model/user_access_token.go b/vendor/github.com/mattermost/mattermost-server/model/user_access_token.go similarity index 77% rename from vendor/github.com/mattermost/platform/model/user_access_token.go rename to vendor/github.com/mattermost/mattermost-server/model/user_access_token.go index e189ec23..bffd9fcb 100644 --- a/vendor/github.com/mattermost/platform/model/user_access_token.go +++ b/vendor/github.com/mattermost/mattermost-server/model/user_access_token.go @@ -43,41 +43,23 @@ func (t *UserAccessToken) PreSave() { } func (t *UserAccessToken) ToJson() string { - b, err := json.Marshal(t) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(t) + return string(b) } func UserAccessTokenFromJson(data io.Reader) *UserAccessToken { - decoder := json.NewDecoder(data) - var t UserAccessToken - err := decoder.Decode(&t) - if err == nil { - return &t - } else { - return nil - } + var t *UserAccessToken + json.NewDecoder(data).Decode(&t) + return t } func UserAccessTokenListToJson(t []*UserAccessToken) string { - b, err := json.Marshal(t) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(t) + return string(b) } func UserAccessTokenListFromJson(data io.Reader) []*UserAccessToken { - decoder := json.NewDecoder(data) var t []*UserAccessToken - err := decoder.Decode(&t) - if err == nil { - return t - } else { - return nil - } + json.NewDecoder(data).Decode(&t) + return t } diff --git a/vendor/github.com/mattermost/mattermost-server/model/user_access_token_search.go b/vendor/github.com/mattermost/mattermost-server/model/user_access_token_search.go new file mode 100644 index 00000000..1b0146ed --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/user_access_token_search.go @@ -0,0 +1,35 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type UserAccessTokenSearch struct { + Term string `json:"term"` +} + +// ToJson convert a UserAccessTokenSearch to json string +func (c *UserAccessTokenSearch) ToJson() string { + b, err := json.Marshal(c) + if err != nil { + return "" + } + + return string(b) +} + +// UserAccessTokenSearchJson decodes the input and returns a UserAccessTokenSearch +func UserAccessTokenSearchFromJson(data io.Reader) *UserAccessTokenSearch { + decoder := json.NewDecoder(data) + var cs UserAccessTokenSearch + err := decoder.Decode(&cs) + if err == nil { + return &cs + } + + return nil +} diff --git a/vendor/github.com/mattermost/platform/model/user_autocomplete.go b/vendor/github.com/mattermost/mattermost-server/model/user_autocomplete.go similarity index 65% rename from vendor/github.com/mattermost/platform/model/user_autocomplete.go rename to vendor/github.com/mattermost/mattermost-server/model/user_autocomplete.go index 43c030b8..b5edb45b 100644 --- a/vendor/github.com/mattermost/platform/model/user_autocomplete.go +++ b/vendor/github.com/mattermost/mattermost-server/model/user_autocomplete.go @@ -23,12 +23,8 @@ type UserAutocomplete struct { } func (o *UserAutocomplete) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func UserAutocompleteFromJson(data io.Reader) *UserAutocomplete { @@ -43,41 +39,23 @@ func UserAutocompleteFromJson(data io.Reader) *UserAutocomplete { } func (o *UserAutocompleteInChannel) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func UserAutocompleteInChannelFromJson(data io.Reader) *UserAutocompleteInChannel { - decoder := json.NewDecoder(data) - var o UserAutocompleteInChannel - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *UserAutocompleteInChannel + json.NewDecoder(data).Decode(&o) + return o } func (o *UserAutocompleteInTeam) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func UserAutocompleteInTeamFromJson(data io.Reader) *UserAutocompleteInTeam { - decoder := json.NewDecoder(data) - var o UserAutocompleteInTeam - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *UserAutocompleteInTeam + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/platform/model/user_search.go b/vendor/github.com/mattermost/mattermost-server/model/user_search.go similarity index 75% rename from vendor/github.com/mattermost/platform/model/user_search.go rename to vendor/github.com/mattermost/mattermost-server/model/user_search.go index 6213b16e..94596bdc 100644 --- a/vendor/github.com/mattermost/platform/model/user_search.go +++ b/vendor/github.com/mattermost/mattermost-server/model/user_search.go @@ -20,22 +20,13 @@ type UserSearch struct { // ToJson convert a User to a json string func (u *UserSearch) ToJson() string { - b, err := json.Marshal(u) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(u) + return string(b) } // UserSearchFromJson will decode the input and return a User func UserSearchFromJson(data io.Reader) *UserSearch { - decoder := json.NewDecoder(data) - var us UserSearch - err := decoder.Decode(&us) - if err == nil { - return &us - } else { - return nil - } + var us *UserSearch + json.NewDecoder(data).Decode(&us) + return us } diff --git a/vendor/github.com/mattermost/mattermost-server/model/users_stats.go b/vendor/github.com/mattermost/mattermost-server/model/users_stats.go new file mode 100644 index 00000000..49c882e3 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/users_stats.go @@ -0,0 +1,24 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type UsersStats struct { + TotalUsersCount int64 `json:"total_users_count"` +} + +func (o *UsersStats) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func UsersStatsFromJson(data io.Reader) *UsersStats { + var o *UsersStats + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/platform/model/utils.go b/vendor/github.com/mattermost/mattermost-server/model/utils.go similarity index 71% rename from vendor/github.com/mattermost/platform/model/utils.go rename to vendor/github.com/mattermost/mattermost-server/model/utils.go index e84d44f7..172b7824 100644 --- a/vendor/github.com/mattermost/platform/model/utils.go +++ b/vendor/github.com/mattermost/mattermost-server/model/utils.go @@ -36,6 +36,12 @@ type StringInterface map[string]interface{} type StringMap map[string]string type StringArray []string +var translateFunc goi18n.TranslateFunc = nil + +func AppErrorInit(t goi18n.TranslateFunc) { + translateFunc = t +} + type AppError struct { Id string `json:"id"` Message string `json:"message"` // Message to be display to the end user without debugging information @@ -52,6 +58,11 @@ func (er *AppError) Error() string { } func (er *AppError) Translate(T goi18n.TranslateFunc) { + if T == nil { + er.Message = er.Id + return + } + if er.params == nil { er.Message = T(er.Id) } else { @@ -68,12 +79,8 @@ func (er *AppError) SystemMessage(T goi18n.TranslateFunc) string { } func (er *AppError) ToJson() string { - b, err := json.Marshal(er) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(er) + return string(b) } // AppErrorFromJson will decode the input and return an AppError @@ -105,6 +112,7 @@ func NewAppError(where string, id string, params map[string]interface{}, details ap.DetailedError = details ap.StatusCode = status ap.IsOAuth = false + ap.Translate(translateFunc) return ap } @@ -133,27 +141,60 @@ func NewRandomString(length int) string { return b.String() } -// GetMillis is a convience method to get milliseconds since epoch. +// GetMillis is a convenience method to get milliseconds since epoch. func GetMillis() int64 { return time.Now().UnixNano() / int64(time.Millisecond) } +// GetMillisForTime is a convenience method to get milliseconds since epoch for provided Time. +func GetMillisForTime(thisTime time.Time) int64 { + return thisTime.UnixNano() / int64(time.Millisecond) +} + +// PadDateStringZeros is a convenience method to pad 2 digit date parts with zeros to meet ISO 8601 format +func PadDateStringZeros(dateString string) string { + parts := strings.Split(dateString, "-") + for index, part := range parts { + if len(part) == 1 { + parts[index] = "0" + part + } + } + dateString = strings.Join(parts[:], "-") + return dateString +} + +// GetStartOfDayMillis is a convenience method to get milliseconds since epoch for provided date's start of day +func GetStartOfDayMillis(thisTime time.Time, timeZoneOffset int) int64 { + localSearchTimeZone := time.FixedZone("Local Search Time Zone", timeZoneOffset) + resultTime := time.Date(thisTime.Year(), thisTime.Month(), thisTime.Day(), 0, 0, 0, 0, localSearchTimeZone) + return GetMillisForTime(resultTime) +} + +// GetEndOfDayMillis is a convenience method to get milliseconds since epoch for provided date's end of day +func GetEndOfDayMillis(thisTime time.Time, timeZoneOffset int) int64 { + localSearchTimeZone := time.FixedZone("Local Search Time Zone", timeZoneOffset) + resultTime := time.Date(thisTime.Year(), thisTime.Month(), thisTime.Day(), 23, 59, 59, 999999999, localSearchTimeZone) + return GetMillisForTime(resultTime) +} + +func CopyStringMap(originalMap map[string]string) map[string]string { + copyMap := make(map[string]string) + for k, v := range originalMap { + copyMap[k] = v + } + return copyMap +} + // MapToJson converts a map to a json string func MapToJson(objmap map[string]string) string { - if b, err := json.Marshal(objmap); err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(objmap) + return string(b) } // MapToJson converts a map to a json string func MapBoolToJson(objmap map[string]bool) string { - if b, err := json.Marshal(objmap); err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(objmap) + return string(b) } // MapFromJson will decode the key/value pair map @@ -181,11 +222,8 @@ func MapBoolFromJson(data io.Reader) map[string]bool { } func ArrayToJson(objmap []string) string { - if b, err := json.Marshal(objmap); err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(objmap) + return string(b) } func ArrayFromJson(data io.Reader) []string { @@ -217,11 +255,8 @@ func ArrayFromInterface(data interface{}) []string { } func StringInterfaceToJson(objmap map[string]interface{}) string { - if b, err := json.Marshal(objmap); err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(objmap) + return string(b) } func StringInterfaceFromJson(data io.Reader) map[string]interface{} { @@ -236,12 +271,8 @@ func StringInterfaceFromJson(data io.Reader) map[string]interface{} { } func StringToJson(s string) string { - b, err := json.Marshal(s) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(s) + return string(b) } func StringFromJson(data io.Reader) string { @@ -261,7 +292,7 @@ func GetServerIpAddress() string { } else { for _, addr := range addrs { - if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() { + if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() && !ip.IP.IsLinkLocalUnicast() && !ip.IP.IsLinkLocalMulticast() { if ip.IP.To4() != nil { return ip.IP.String() } @@ -277,16 +308,18 @@ func IsLower(s string) bool { } func IsValidEmail(email string) bool { - if !IsLower(email) { return false } - if _, err := mail.ParseAddress(email); err == nil { - return true + if addr, err := mail.ParseAddress(email); err != nil { + return false + } else if addr.Name != "" { + // mail.ParseAddress accepts input of the form "Billy Bob " which we don't allow + return false } - return false + return true } var reservedName = []string{ @@ -402,9 +435,6 @@ func ClearMentionTags(post string) string { return post } -var UrlRegex = regexp.MustCompile(`^((?:[a-z]+:\/\/)?(?:(?:[a-z0-9\-]+\.)+(?:[a-z]{2}|aero|arpa|biz|com|coop|edu|gov|info|int|jobs|mil|museum|name|nato|net|org|pro|travel|local|internal))(:[0-9]{1,5})?(?:\/[a-z0-9_\-\.~]+)*(\/([a-z0-9_\-\.]*)(?:\?[a-z0-9+_~\-\.%=&]*)?)?(?:#[a-zA-Z0-9!$&'()*+.=-_~:@/?]*)?)(?:\s+|$)$`) -var PartialUrlRegex = regexp.MustCompile(`/([A-Za-z0-9]{26})/([A-Za-z0-9]{26})/((?:[A-Za-z0-9]{26})?.+(?:\.[A-Za-z0-9]{3,})?)`) - func IsValidHttpUrl(rawUrl string) bool { if strings.Index(rawUrl, "http://") != 0 && strings.Index(rawUrl, "https://") != 0 { return false @@ -417,18 +447,6 @@ func IsValidHttpUrl(rawUrl string) bool { return true } -func IsValidHttpsUrl(rawUrl string) bool { - if strings.Index(rawUrl, "https://") != 0 { - return false - } - - if _, err := url.ParseRequestURI(rawUrl); err != nil { - return false - } - - return true -} - func IsValidTurnOrStunServer(rawUri string) bool { if strings.Index(rawUri, "turn:") != 0 && strings.Index(rawUri, "stun:") != 0 { return false @@ -492,3 +510,57 @@ func IsValidId(value string) bool { return true } + +// Copied from https://golang.org/src/net/dnsclient.go#L119 +func IsDomainName(s string) bool { + // See RFC 1035, RFC 3696. + // Presentation format has dots before every label except the first, and the + // terminal empty label is optional here because we assume fully-qualified + // (absolute) input. We must therefore reserve space for the first and last + // labels' length octets in wire format, where they are necessary and the + // maximum total length is 255. + // So our _effective_ maximum is 253, but 254 is not rejected if the last + // character is a dot. + l := len(s) + if l == 0 || l > 254 || l == 254 && s[l-1] != '.' { + return false + } + + last := byte('.') + ok := false // Ok once we've seen a letter. + partlen := 0 + for i := 0; i < len(s); i++ { + c := s[i] + switch { + default: + return false + case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_': + ok = true + partlen++ + case '0' <= c && c <= '9': + // fine + partlen++ + case c == '-': + // Byte before dash cannot be dot. + if last == '.' { + return false + } + partlen++ + case c == '.': + // Byte before dot cannot be dot, dash. + if last == '.' || last == '-' { + return false + } + if partlen > 63 || partlen == 0 { + return false + } + partlen = 0 + } + last = c + } + if last == '-' || partlen > 63 { + return false + } + + return ok +} diff --git a/vendor/github.com/mattermost/platform/model/version.go b/vendor/github.com/mattermost/mattermost-server/model/version.go similarity index 91% rename from vendor/github.com/mattermost/platform/model/version.go rename to vendor/github.com/mattermost/mattermost-server/model/version.go index 430924ee..1b09b0ad 100644 --- a/vendor/github.com/mattermost/platform/model/version.go +++ b/vendor/github.com/mattermost/mattermost-server/model/version.go @@ -9,10 +9,23 @@ import ( "strings" ) -// This is a list of all the current viersions including any patches. -// It should be maitained in chronological order with most current +// This is a list of all the current versions including any patches. +// It should be maintained in chronological order with most current // release at the front of the list. var versions = []string{ + "5.5.0", + "5.4.0", + "5.3.0", + "5.2.0", + "5.1.0", + "5.0.0", + "4.10.0", + "4.9.0", + "4.8.1", + "4.8.0", + "4.7.2", + "4.7.1", + "4.7.0", "4.6.0", "4.5.0", "4.4.0", @@ -104,10 +117,6 @@ func GetPreviousVersion(version string) string { return "" } -func IsOfficalBuild() bool { - return BuildNumber != "_BUILD_NUMBER_" -} - func IsCurrentVersion(versionToCheck string) bool { currentMajor, currentMinor, _ := SplitVersion(CurrentVersion) toCheckMajor, toCheckMinor, _ := SplitVersion(versionToCheck) diff --git a/vendor/github.com/mattermost/platform/model/webrtc.go b/vendor/github.com/mattermost/mattermost-server/model/webrtc.go similarity index 66% rename from vendor/github.com/mattermost/platform/model/webrtc.go rename to vendor/github.com/mattermost/mattermost-server/model/webrtc.go index fa15a4b7..59797a5b 100644 --- a/vendor/github.com/mattermost/platform/model/webrtc.go +++ b/vendor/github.com/mattermost/mattermost-server/model/webrtc.go @@ -22,32 +22,18 @@ type GatewayResponse struct { } func GatewayResponseFromJson(data io.Reader) *GatewayResponse { - decoder := json.NewDecoder(data) - var o GatewayResponse - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *GatewayResponse + json.NewDecoder(data).Decode(&o) + return o } func (o *WebrtcInfoResponse) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func WebrtcInfoResponseFromJson(data io.Reader) *WebrtcInfoResponse { - decoder := json.NewDecoder(data) - var o WebrtcInfoResponse - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *WebrtcInfoResponse + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/platform/model/websocket_client.go b/vendor/github.com/mattermost/mattermost-server/model/websocket_client.go similarity index 57% rename from vendor/github.com/mattermost/platform/model/websocket_client.go rename to vendor/github.com/mattermost/mattermost-server/model/websocket_client.go index e5c44dde..4e6c1d8c 100644 --- a/vendor/github.com/mattermost/platform/model/websocket_client.go +++ b/vendor/github.com/mattermost/mattermost-server/model/websocket_client.go @@ -6,57 +6,42 @@ package model import ( "encoding/json" "net/http" + "time" "github.com/gorilla/websocket" ) const ( - SOCKET_MAX_MESSAGE_SIZE_KB = 8 * 1024 // 8KB + SOCKET_MAX_MESSAGE_SIZE_KB = 8 * 1024 // 8KB + PING_TIMEOUT_BUFFER_SECONDS = 5 ) type WebSocketClient struct { - Url string // The location of the server like "ws://localhost:8065" - ApiUrl string // The api location of the server like "ws://localhost:8065/api/v3" - ConnectUrl string // The websocket URL to connect to like "ws://localhost:8065/api/v3/path/to/websocket" - Conn *websocket.Conn // The WebSocket connection - AuthToken string // The token used to open the WebSocket - Sequence int64 // The ever-incrementing sequence attached to each WebSocket action - EventChannel chan *WebSocketEvent - ResponseChannel chan *WebSocketResponse - ListenError *AppError + Url string // The location of the server like "ws://localhost:8065" + ApiUrl string // The api location of the server like "ws://localhost:8065/api/v3" + ConnectUrl string // The websocket URL to connect to like "ws://localhost:8065/api/v3/path/to/websocket" + Conn *websocket.Conn // The WebSocket connection + AuthToken string // The token used to open the WebSocket + Sequence int64 // The ever-incrementing sequence attached to each WebSocket action + PingTimeoutChannel chan bool // The channel used to signal ping timeouts + EventChannel chan *WebSocketEvent + ResponseChannel chan *WebSocketResponse + ListenError *AppError + pingTimeoutTimer *time.Timer } -// NewWebSocketClient constructs a new WebSocket client with convienence +// NewWebSocketClient constructs a new WebSocket client with convenience // methods for talking to the server. func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) { - conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX_V3+"/users/websocket", nil) - if err != nil { - return nil, NewAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - client := &WebSocketClient{ - url, - url + API_URL_SUFFIX_V3, - url + API_URL_SUFFIX_V3 + "/users/websocket", - conn, - authToken, - 1, - make(chan *WebSocketEvent, 100), - make(chan *WebSocketResponse, 100), - nil, - } - - client.SendMessage(WEBSOCKET_AUTHENTICATION_CHALLENGE, map[string]interface{}{"token": authToken}) - - return client, nil + return NewWebSocketClientWithDialer(websocket.DefaultDialer, url, authToken) } -// NewWebSocketClient4 constructs a new WebSocket client with convienence -// methods for talking to the server. Uses the v4 endpoint. -func NewWebSocketClient4(url, authToken string) (*WebSocketClient, *AppError) { - conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX+"/websocket", nil) +// NewWebSocketClientWithDialer constructs a new WebSocket client with convenience +// methods for talking to the server using a custom dialer. +func NewWebSocketClientWithDialer(dialer *websocket.Dialer, url, authToken string) (*WebSocketClient, *AppError) { + conn, _, err := dialer.Dial(url+API_URL_SUFFIX+"/websocket", nil) if err != nil { - return nil, NewAppError("NewWebSocketClient4", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError) + return nil, NewAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError) } client := &WebSocketClient{ @@ -66,23 +51,45 @@ func NewWebSocketClient4(url, authToken string) (*WebSocketClient, *AppError) { conn, authToken, 1, + make(chan bool, 1), make(chan *WebSocketEvent, 100), make(chan *WebSocketResponse, 100), nil, + nil, } + client.configurePingHandling() + client.SendMessage(WEBSOCKET_AUTHENTICATION_CHALLENGE, map[string]interface{}{"token": authToken}) return client, nil } +// NewWebSocketClient4 constructs a new WebSocket client with convenience +// methods for talking to the server. Uses the v4 endpoint. +func NewWebSocketClient4(url, authToken string) (*WebSocketClient, *AppError) { + return NewWebSocketClient4WithDialer(websocket.DefaultDialer, url, authToken) +} + +// NewWebSocketClient4WithDialer constructs a new WebSocket client with convenience +// methods for talking to the server using a custom dialer. Uses the v4 endpoint. +func NewWebSocketClient4WithDialer(dialer *websocket.Dialer, url, authToken string) (*WebSocketClient, *AppError) { + return NewWebSocketClientWithDialer(dialer, url, authToken) +} + func (wsc *WebSocketClient) Connect() *AppError { + return wsc.ConnectWithDialer(websocket.DefaultDialer) +} + +func (wsc *WebSocketClient) ConnectWithDialer(dialer *websocket.Dialer) *AppError { var err error - wsc.Conn, _, err = websocket.DefaultDialer.Dial(wsc.ConnectUrl, nil) + wsc.Conn, _, err = dialer.Dial(wsc.ConnectUrl, nil) if err != nil { return NewAppError("Connect", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError) } + wsc.configurePingHandling() + wsc.EventChannel = make(chan *WebSocketEvent, 100) wsc.ResponseChannel = make(chan *WebSocketResponse, 100) @@ -165,3 +172,24 @@ func (wsc *WebSocketClient) GetStatusesByIds(userIds []string) { } wsc.SendMessage("get_statuses_by_ids", data) } + +func (wsc *WebSocketClient) configurePingHandling() { + wsc.Conn.SetPingHandler(wsc.pingHandler) + wsc.pingTimeoutTimer = time.NewTimer(time.Second * (60 + PING_TIMEOUT_BUFFER_SECONDS)) + go wsc.pingWatchdog() +} + +func (wsc *WebSocketClient) pingHandler(appData string) error { + if !wsc.pingTimeoutTimer.Stop() { + <-wsc.pingTimeoutTimer.C + } + + wsc.pingTimeoutTimer.Reset(time.Second * (60 + PING_TIMEOUT_BUFFER_SECONDS)) + wsc.Conn.WriteMessage(websocket.PongMessage, []byte{}) + return nil +} + +func (wsc *WebSocketClient) pingWatchdog() { + <-wsc.pingTimeoutTimer.C + wsc.PingTimeoutChannel <- true +} diff --git a/vendor/github.com/mattermost/mattermost-server/model/websocket_message.go b/vendor/github.com/mattermost/mattermost-server/model/websocket_message.go new file mode 100644 index 00000000..ea8872d7 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/model/websocket_message.go @@ -0,0 +1,165 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" +) + +const ( + WEBSOCKET_EVENT_TYPING = "typing" + WEBSOCKET_EVENT_POSTED = "posted" + WEBSOCKET_EVENT_POST_EDITED = "post_edited" + WEBSOCKET_EVENT_POST_DELETED = "post_deleted" + WEBSOCKET_EVENT_CHANNEL_CONVERTED = "channel_converted" + WEBSOCKET_EVENT_CHANNEL_CREATED = "channel_created" + WEBSOCKET_EVENT_CHANNEL_DELETED = "channel_deleted" + WEBSOCKET_EVENT_CHANNEL_UPDATED = "channel_updated" + WEBSOCKET_EVENT_CHANNEL_MEMBER_UPDATED = "channel_member_updated" + WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added" + WEBSOCKET_EVENT_GROUP_ADDED = "group_added" + WEBSOCKET_EVENT_NEW_USER = "new_user" + WEBSOCKET_EVENT_ADDED_TO_TEAM = "added_to_team" + WEBSOCKET_EVENT_LEAVE_TEAM = "leave_team" + WEBSOCKET_EVENT_UPDATE_TEAM = "update_team" + WEBSOCKET_EVENT_DELETE_TEAM = "delete_team" + WEBSOCKET_EVENT_USER_ADDED = "user_added" + WEBSOCKET_EVENT_USER_UPDATED = "user_updated" + WEBSOCKET_EVENT_USER_ROLE_UPDATED = "user_role_updated" + WEBSOCKET_EVENT_MEMBERROLE_UPDATED = "memberrole_updated" + WEBSOCKET_EVENT_USER_REMOVED = "user_removed" + WEBSOCKET_EVENT_PREFERENCE_CHANGED = "preference_changed" + WEBSOCKET_EVENT_PREFERENCES_CHANGED = "preferences_changed" + WEBSOCKET_EVENT_PREFERENCES_DELETED = "preferences_deleted" + WEBSOCKET_EVENT_EPHEMERAL_MESSAGE = "ephemeral_message" + WEBSOCKET_EVENT_STATUS_CHANGE = "status_change" + WEBSOCKET_EVENT_HELLO = "hello" + WEBSOCKET_EVENT_WEBRTC = "webrtc" + WEBSOCKET_AUTHENTICATION_CHALLENGE = "authentication_challenge" + WEBSOCKET_EVENT_REACTION_ADDED = "reaction_added" + WEBSOCKET_EVENT_REACTION_REMOVED = "reaction_removed" + WEBSOCKET_EVENT_RESPONSE = "response" + WEBSOCKET_EVENT_EMOJI_ADDED = "emoji_added" + WEBSOCKET_EVENT_CHANNEL_VIEWED = "channel_viewed" + WEBSOCKET_EVENT_PLUGIN_STATUSES_CHANGED = "plugin_statuses_changed" + WEBSOCKET_EVENT_PLUGIN_ENABLED = "plugin_enabled" + WEBSOCKET_EVENT_PLUGIN_DISABLED = "plugin_disabled" + WEBSOCKET_EVENT_ROLE_UPDATED = "role_updated" + WEBSOCKET_EVENT_LICENSE_CHANGED = "license_changed" + WEBSOCKET_EVENT_CONFIG_CHANGED = "config_changed" +) + +type WebSocketMessage interface { + ToJson() string + IsValid() bool + EventType() string +} + +type WebsocketBroadcast struct { + OmitUsers map[string]bool `json:"omit_users"` // broadcast is omitted for users listed here + UserId string `json:"user_id"` // broadcast only occurs for this user + ChannelId string `json:"channel_id"` // broadcast only occurs for users in this channel + TeamId string `json:"team_id"` // broadcast only occurs for users in this team + ContainsSanitizedData bool `json:"-"` + ContainsSensitiveData bool `json:"-"` +} + +type precomputedWebSocketEventJSON struct { + Event json.RawMessage + Data json.RawMessage + Broadcast json.RawMessage +} + +type WebSocketEvent struct { + Event string `json:"event"` + Data map[string]interface{} `json:"data"` + Broadcast *WebsocketBroadcast `json:"broadcast"` + Sequence int64 `json:"seq"` + + precomputedJSON *precomputedWebSocketEventJSON +} + +// PrecomputeJSON precomputes and stores the serialized JSON for all fields other than Sequence. +// This makes ToJson much more efficient when sending the same event to multiple connections. +func (m *WebSocketEvent) PrecomputeJSON() { + event, _ := json.Marshal(m.Event) + data, _ := json.Marshal(m.Data) + broadcast, _ := json.Marshal(m.Broadcast) + m.precomputedJSON = &precomputedWebSocketEventJSON{ + Event: json.RawMessage(event), + Data: json.RawMessage(data), + Broadcast: json.RawMessage(broadcast), + } +} + +func (m *WebSocketEvent) Add(key string, value interface{}) { + m.Data[key] = value +} + +func NewWebSocketEvent(event, teamId, channelId, userId string, omitUsers map[string]bool) *WebSocketEvent { + return &WebSocketEvent{Event: event, Data: make(map[string]interface{}), + Broadcast: &WebsocketBroadcast{TeamId: teamId, ChannelId: channelId, UserId: userId, OmitUsers: omitUsers}} +} + +func (o *WebSocketEvent) IsValid() bool { + return o.Event != "" +} + +func (o *WebSocketEvent) EventType() string { + return o.Event +} + +func (o *WebSocketEvent) ToJson() string { + if o.precomputedJSON != nil { + return fmt.Sprintf(`{"event": %s, "data": %s, "broadcast": %s, "seq": %d}`, o.precomputedJSON.Event, o.precomputedJSON.Data, o.precomputedJSON.Broadcast, o.Sequence) + } + b, _ := json.Marshal(o) + return string(b) +} + +func WebSocketEventFromJson(data io.Reader) *WebSocketEvent { + var o *WebSocketEvent + json.NewDecoder(data).Decode(&o) + return o +} + +type WebSocketResponse struct { + Status string `json:"status"` + SeqReply int64 `json:"seq_reply,omitempty"` + Data map[string]interface{} `json:"data,omitempty"` + Error *AppError `json:"error,omitempty"` +} + +func (m *WebSocketResponse) Add(key string, value interface{}) { + m.Data[key] = value +} + +func NewWebSocketResponse(status string, seqReply int64, data map[string]interface{}) *WebSocketResponse { + return &WebSocketResponse{Status: status, SeqReply: seqReply, Data: data} +} + +func NewWebSocketError(seqReply int64, err *AppError) *WebSocketResponse { + return &WebSocketResponse{Status: STATUS_FAIL, SeqReply: seqReply, Error: err} +} + +func (o *WebSocketResponse) IsValid() bool { + return o.Status != "" +} + +func (o *WebSocketResponse) EventType() string { + return WEBSOCKET_EVENT_RESPONSE +} + +func (o *WebSocketResponse) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func WebSocketResponseFromJson(data io.Reader) *WebSocketResponse { + var o *WebSocketResponse + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/platform/model/websocket_request.go b/vendor/github.com/mattermost/mattermost-server/model/websocket_request.go similarity index 74% rename from vendor/github.com/mattermost/platform/model/websocket_request.go rename to vendor/github.com/mattermost/mattermost-server/model/websocket_request.go index 7dc0b433..4da626e2 100644 --- a/vendor/github.com/mattermost/platform/model/websocket_request.go +++ b/vendor/github.com/mattermost/mattermost-server/model/websocket_request.go @@ -23,21 +23,12 @@ type WebSocketRequest struct { } func (o *WebSocketRequest) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } + b, _ := json.Marshal(o) + return string(b) } func WebSocketRequestFromJson(data io.Reader) *WebSocketRequest { - decoder := json.NewDecoder(data) - var o WebSocketRequest - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } + var o *WebSocketRequest + json.NewDecoder(data).Decode(&o) + return o } diff --git a/vendor/github.com/mattermost/mattermost-server/utils/jsonutils/json.go b/vendor/github.com/mattermost/mattermost-server/utils/jsonutils/json.go new file mode 100644 index 00000000..da77a2b6 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/utils/jsonutils/json.go @@ -0,0 +1,56 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package jsonutils + +import ( + "bytes" + "encoding/json" + + "github.com/pkg/errors" +) + +type HumanizedJsonError struct { + Err error + Line int + Character int +} + +func (e *HumanizedJsonError) Error() string { + return e.Err.Error() +} + +// HumanizeJsonError extracts error offsets and annotates the error with useful context +func HumanizeJsonError(err error, data []byte) error { + if syntaxError, ok := err.(*json.SyntaxError); ok { + return NewHumanizedJsonError(syntaxError, data, syntaxError.Offset) + } else if unmarshalError, ok := err.(*json.UnmarshalTypeError); ok { + return NewHumanizedJsonError(unmarshalError, data, unmarshalError.Offset) + } else { + return err + } +} + +func NewHumanizedJsonError(err error, data []byte, offset int64) *HumanizedJsonError { + if err == nil { + return nil + } + + if offset < 0 || offset > int64(len(data)) { + return &HumanizedJsonError{ + Err: errors.Wrapf(err, "invalid offset %d", offset), + } + } + + lineSep := []byte{'\n'} + + line := bytes.Count(data[:offset], lineSep) + 1 + lastLineOffset := bytes.LastIndex(data[:offset], lineSep) + character := int(offset) - (lastLineOffset + 1) + 1 + + return &HumanizedJsonError{ + Line: line, + Character: character, + Err: errors.Wrapf(err, "parsing error at line %d, character %d", line, character), + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/utils/markdown/autolink.go b/vendor/github.com/mattermost/mattermost-server/utils/markdown/autolink.go new file mode 100644 index 00000000..7f7d1117 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/utils/markdown/autolink.go @@ -0,0 +1,253 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package markdown + +import ( + "regexp" + "strings" + "unicode" + "unicode/utf8" +) + +// Based off of extensions/autolink.c from https://github.com/github/cmark + +var ( + DefaultUrlSchemes = []string{"http", "https", "ftp", "mailto", "tel"} +) + +// Given a string with a w at the given position, tries to parse and return a range containing a www link. +// if one exists. If the text at the given position isn't a link, returns an empty string. Equivalent to +// www_match from the reference code. +func parseWWWAutolink(data string, position int) (Range, bool) { + // Check that this isn't part of another word + if position > 1 { + prevChar := data[position-1] + + if !isWhitespaceByte(prevChar) && !isAllowedBeforeWWWLink(prevChar) { + return Range{}, false + } + } + + // Check that this starts with www + if len(data)-position < 4 || !regexp.MustCompile(`^www\d{0,3}\.`).MatchString(data[position:]) { + return Range{}, false + } + + end := checkDomain(data[position:], false) + if end == 0 { + return Range{}, false + } + + end += position + + // Grab all text until the end of the string or the next whitespace character + for end < len(data) && !isWhitespaceByte(data[end]) { + end += 1 + } + + // Trim trailing punctuation + end = trimTrailingCharactersFromLink(data, position, end) + if position == end { + return Range{}, false + } + + return Range{position, end}, true +} + +func isAllowedBeforeWWWLink(c byte) bool { + switch c { + case '*', '_', '~', ')': + return true + default: + return false + } +} + +// Given a string with a : at the given position, tried to parse and return a range containing a URL scheme +// if one exists. If the text around the given position isn't a link, returns an empty string. Equivalent to +// url_match from the reference code. +func parseURLAutolink(data string, position int) (Range, bool) { + // Check that a :// exists. This doesn't match the clients that treat the slashes as optional. + if len(data)-position < 4 || data[position+1] != '/' || data[position+2] != '/' { + return Range{}, false + } + + start := position - 1 + for start > 0 && isAlphanumericByte(data[start-1]) { + start -= 1 + } + + // Ensure that the URL scheme is allowed and that at least one character after the scheme is valid. + scheme := data[start:position] + if !isSchemeAllowed(scheme) || !isValidHostCharacter(data[position+3:]) { + return Range{}, false + } + + end := checkDomain(data[position+3:], true) + if end == 0 { + return Range{}, false + } + + end += position + + // Grab all text until the end of the string or the next whitespace character + for end < len(data) && !isWhitespaceByte(data[end]) { + end += 1 + } + + // Trim trailing punctuation + end = trimTrailingCharactersFromLink(data, start, end) + if start == end { + return Range{}, false + } + + return Range{start, end}, true +} + +func isSchemeAllowed(scheme string) bool { + // Note that this doesn't support the custom URL schemes implemented by the client + for _, allowed := range DefaultUrlSchemes { + if strings.EqualFold(allowed, scheme) { + return true + } + } + + return false +} + +// Given a string starting with a URL, returns the number of valid characters that make up the URL's domain. +// Returns 0 if the string doesn't start with a domain name. allowShort determines whether or not the domain +// needs to contain a period to be considered valid. Equivalent to check_domain from the reference code. +func checkDomain(data string, allowShort bool) int { + foundUnderscore := false + foundPeriod := false + + i := 1 + for ; i < len(data)-1; i++ { + if data[i] == '_' { + foundUnderscore = true + break + } else if data[i] == '.' { + foundPeriod = true + } else if !isValidHostCharacter(data[i:]) && data[i] != '-' { + break + } + } + + if foundUnderscore { + return 0 + } + + if allowShort { + // If allowShort is set, accept any string of valid domain characters + return i + } + + // If allowShort isn't set, a valid domain just requires at least a single period. Note that this + // logic isn't entirely necessary because we already know the string starts with "www." when + // this is called from parseWWWAutolink + if foundPeriod { + return i + } else { + return 0 + } +} + +// Returns true if the provided link starts with a valid character for a domain name. Equivalent to +// is_valid_hostchar from the reference code. +func isValidHostCharacter(link string) bool { + c, _ := utf8.DecodeRuneInString(link) + if c == utf8.RuneError { + return false + } + + return !unicode.IsSpace(c) && !unicode.IsPunct(c) +} + +// Removes any trailing characters such as punctuation or stray brackets that shouldn't be part of the link. +// Returns a new end position for the link. Equivalent to autolink_delim from the reference code. +func trimTrailingCharactersFromLink(markdown string, start int, end int) int { + runes := []rune(markdown[start:end]) + linkEnd := len(runes) + + // Cut off the link before an open angle bracket if it contains one + for i, c := range runes { + if c == '<' { + linkEnd = i + break + } + } + + for linkEnd > 0 { + c := runes[linkEnd-1] + + if !canEndAutolink(c) { + // Trim trailing quotes, periods, etc + linkEnd = linkEnd - 1 + } else if c == ';' { + // Trim a trailing HTML entity + newEnd := linkEnd - 2 + + for newEnd > 0 && ((runes[newEnd] >= 'a' && runes[newEnd] <= 'z') || (runes[newEnd] >= 'A' && runes[newEnd] <= 'Z')) { + newEnd -= 1 + } + + if newEnd < linkEnd-2 && runes[newEnd] == '&' { + linkEnd = newEnd + } else { + // This isn't actually an HTML entity, so just trim the semicolon + linkEnd = linkEnd - 1 + } + } else if c == ')' { + // Only allow an autolink ending with a bracket if that bracket is part of a matching pair of brackets. + // If there are more closing brackets than opening ones, remove the extra bracket + + numClosing := 0 + numOpening := 0 + + // Examples (input text => output linked portion): + // + // http://www.pokemon.com/Pikachu_(Electric) + // => http://www.pokemon.com/Pikachu_(Electric) + // + // http://www.pokemon.com/Pikachu_((Electric) + // => http://www.pokemon.com/Pikachu_((Electric) + // + // http://www.pokemon.com/Pikachu_(Electric)) + // => http://www.pokemon.com/Pikachu_(Electric) + // + // http://www.pokemon.com/Pikachu_((Electric)) + // => http://www.pokemon.com/Pikachu_((Electric)) + + for i := 0; i < linkEnd; i++ { + if runes[i] == '(' { + numOpening += 1 + } else if runes[i] == ')' { + numClosing += 1 + } + } + + if numClosing <= numOpening { + // There's fewer or equal closing brackets, so we've found the end of the link + break + } + + linkEnd -= 1 + } else { + // There's no special characters at the end of the link, so we're at the end + break + } + } + + return start + len(string(runes[:linkEnd])) +} + +func canEndAutolink(c rune) bool { + switch c { + case '?', '!', '.', ',', ':', '*', '_', '~', '\'', '"': + return false + default: + return true + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/utils/markdown/block_quote.go b/vendor/github.com/mattermost/mattermost-server/utils/markdown/block_quote.go new file mode 100644 index 00000000..04a32461 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/utils/markdown/block_quote.go @@ -0,0 +1,62 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package markdown + +type BlockQuote struct { + blockBase + markdown string + + Children []Block +} + +func (b *BlockQuote) Continuation(indentation int, r Range) *continuation { + if indentation > 3 { + return nil + } + s := b.markdown[r.Position:r.End] + if s == "" || s[0] != '>' { + return nil + } + remaining := Range{r.Position + 1, r.End} + indentation, indentationBytes := countIndentation(b.markdown, remaining) + if indentation > 0 { + indentation-- + } + return &continuation{ + Indentation: indentation, + Remaining: Range{remaining.Position + indentationBytes, remaining.End}, + } +} + +func (b *BlockQuote) AddChild(openBlocks []Block) []Block { + b.Children = append(b.Children, openBlocks[0]) + return openBlocks +} + +func blockQuoteStart(markdown string, indent int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block { + if indent > 3 { + return nil + } + s := markdown[r.Position:r.End] + if s == "" || s[0] != '>' { + return nil + } + + block := &BlockQuote{ + markdown: markdown, + } + r.Position++ + if len(s) > 1 && s[1] == ' ' { + r.Position++ + } + + indent, bytes := countIndentation(markdown, r) + + ret := []Block{block} + if descendants := blockStartOrParagraph(markdown, indent, Range{r.Position + bytes, r.End}, nil, nil); descendants != nil { + block.Children = append(block.Children, descendants[0]) + ret = append(ret, descendants...) + } + return ret +} diff --git a/vendor/github.com/mattermost/mattermost-server/utils/markdown/blocks.go b/vendor/github.com/mattermost/mattermost-server/utils/markdown/blocks.go new file mode 100644 index 00000000..14972f94 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/utils/markdown/blocks.go @@ -0,0 +1,153 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package markdown + +import ( + "strings" +) + +type continuation struct { + Indentation int + Remaining Range +} + +type Block interface { + Continuation(indentation int, r Range) *continuation + AddLine(indentation int, r Range) bool + Close() + AllowsBlockStarts() bool + HasTrailingBlankLine() bool +} + +type blockBase struct{} + +func (*blockBase) AddLine(indentation int, r Range) bool { return false } +func (*blockBase) Close() {} +func (*blockBase) AllowsBlockStarts() bool { return true } +func (*blockBase) HasTrailingBlankLine() bool { return false } + +type ContainerBlock interface { + Block + AddChild(openBlocks []Block) []Block +} + +type Range struct { + Position int + End int +} + +func closeBlocks(blocks []Block, referenceDefinitions *[]*ReferenceDefinition) { + for _, block := range blocks { + block.Close() + if p, ok := block.(*Paragraph); ok && len(p.ReferenceDefinitions) > 0 { + *referenceDefinitions = append(*referenceDefinitions, p.ReferenceDefinitions...) + } + } +} + +func ParseBlocks(markdown string, lines []Line) (*Document, []*ReferenceDefinition) { + document := &Document{} + var referenceDefinitions []*ReferenceDefinition + + openBlocks := []Block{document} + + for _, line := range lines { + r := line.Range + lastMatchIndex := 0 + + indentation, indentationBytes := countIndentation(markdown, r) + r = Range{r.Position + indentationBytes, r.End} + + for i, block := range openBlocks { + if continuation := block.Continuation(indentation, r); continuation != nil { + indentation = continuation.Indentation + r = continuation.Remaining + additionalIndentation, additionalIndentationBytes := countIndentation(markdown, r) + r = Range{r.Position + additionalIndentationBytes, r.End} + indentation += additionalIndentation + lastMatchIndex = i + } else { + break + } + } + + if openBlocks[lastMatchIndex].AllowsBlockStarts() { + if newBlocks := blockStart(markdown, indentation, r, openBlocks[:lastMatchIndex+1], openBlocks[lastMatchIndex+1:]); newBlocks != nil { + didAdd := false + for i := lastMatchIndex; i >= 0; i-- { + if container, ok := openBlocks[i].(ContainerBlock); ok { + if newBlocks := container.AddChild(newBlocks); newBlocks != nil { + closeBlocks(openBlocks[i+1:], &referenceDefinitions) + openBlocks = openBlocks[:i+1] + openBlocks = append(openBlocks, newBlocks...) + didAdd = true + break + } + } + } + if didAdd { + continue + } + } + } + + isBlank := strings.TrimSpace(markdown[r.Position:r.End]) == "" + if paragraph, ok := openBlocks[len(openBlocks)-1].(*Paragraph); ok && !isBlank { + paragraph.Text = append(paragraph.Text, r) + continue + } + + closeBlocks(openBlocks[lastMatchIndex+1:], &referenceDefinitions) + openBlocks = openBlocks[:lastMatchIndex+1] + + if openBlocks[lastMatchIndex].AddLine(indentation, r) { + continue + } + + if paragraph := newParagraph(markdown, r); paragraph != nil { + for i := lastMatchIndex; i >= 0; i-- { + if container, ok := openBlocks[i].(ContainerBlock); ok { + if newBlocks := container.AddChild([]Block{paragraph}); newBlocks != nil { + closeBlocks(openBlocks[i+1:], &referenceDefinitions) + openBlocks = openBlocks[:i+1] + openBlocks = append(openBlocks, newBlocks...) + break + } + } + } + } + } + + closeBlocks(openBlocks, &referenceDefinitions) + + return document, referenceDefinitions +} + +func blockStart(markdown string, indentation int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block { + if r.Position >= r.End { + return nil + } + + if start := blockQuoteStart(markdown, indentation, r, matchedBlocks, unmatchedBlocks); start != nil { + return start + } else if start := listStart(markdown, indentation, r, matchedBlocks, unmatchedBlocks); start != nil { + return start + } else if start := indentedCodeStart(markdown, indentation, r, matchedBlocks, unmatchedBlocks); start != nil { + return start + } else if start := fencedCodeStart(markdown, indentation, r, matchedBlocks, unmatchedBlocks); start != nil { + return start + } + + return nil +} + +func blockStartOrParagraph(markdown string, indentation int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block { + if start := blockStart(markdown, indentation, r, matchedBlocks, unmatchedBlocks); start != nil { + return start + } + if paragraph := newParagraph(markdown, r); paragraph != nil { + return []Block{paragraph} + } + return nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/utils/markdown/document.go b/vendor/github.com/mattermost/mattermost-server/utils/markdown/document.go new file mode 100644 index 00000000..224b5d21 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/utils/markdown/document.go @@ -0,0 +1,22 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package markdown + +type Document struct { + blockBase + + Children []Block +} + +func (b *Document) Continuation(indentation int, r Range) *continuation { + return &continuation{ + Indentation: indentation, + Remaining: r, + } +} + +func (b *Document) AddChild(openBlocks []Block) []Block { + b.Children = append(b.Children, openBlocks[0]) + return openBlocks +} diff --git a/vendor/github.com/mattermost/mattermost-server/utils/markdown/fenced_code.go b/vendor/github.com/mattermost/mattermost-server/utils/markdown/fenced_code.go new file mode 100644 index 00000000..8b2ebd4f --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/utils/markdown/fenced_code.go @@ -0,0 +1,112 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package markdown + +import ( + "strings" +) + +type FencedCodeLine struct { + Indentation int + Range Range +} + +type FencedCode struct { + blockBase + markdown string + didSeeClosingFence bool + + Indentation int + OpeningFence Range + RawInfo Range + RawCode []FencedCodeLine +} + +func (b *FencedCode) Code() (result string) { + for _, code := range b.RawCode { + result += strings.Repeat(" ", code.Indentation) + b.markdown[code.Range.Position:code.Range.End] + } + return +} + +func (b *FencedCode) Info() string { + return Unescape(b.markdown[b.RawInfo.Position:b.RawInfo.End]) +} + +func (b *FencedCode) Continuation(indentation int, r Range) *continuation { + if b.didSeeClosingFence { + return nil + } + return &continuation{ + Indentation: indentation, + Remaining: r, + } +} + +func (b *FencedCode) AddLine(indentation int, r Range) bool { + s := b.markdown[r.Position:r.End] + if indentation <= 3 && strings.HasPrefix(s, b.markdown[b.OpeningFence.Position:b.OpeningFence.End]) { + suffix := strings.TrimSpace(s[b.OpeningFence.End-b.OpeningFence.Position:]) + isClosingFence := true + for _, c := range suffix { + if c != rune(s[0]) { + isClosingFence = false + break + } + } + if isClosingFence { + b.didSeeClosingFence = true + return true + } + } + + if indentation >= b.Indentation { + indentation -= b.Indentation + } else { + indentation = 0 + } + + b.RawCode = append(b.RawCode, FencedCodeLine{ + Indentation: indentation, + Range: r, + }) + return true +} + +func (b *FencedCode) AllowsBlockStarts() bool { + return false +} + +func fencedCodeStart(markdown string, indentation int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block { + s := markdown[r.Position:r.End] + + if !strings.HasPrefix(s, "```") && !strings.HasPrefix(s, "~~~") { + return nil + } + + fenceCharacter := rune(s[0]) + fenceLength := 3 + for _, c := range s[3:] { + if c == fenceCharacter { + fenceLength++ + } else { + break + } + } + + for i := r.Position + fenceLength; i < r.End; i++ { + if markdown[i] == '`' { + return nil + } + } + + return []Block{ + &FencedCode{ + markdown: markdown, + Indentation: indentation, + RawInfo: trimRightSpace(markdown, Range{r.Position + fenceLength, r.End}), + OpeningFence: Range{r.Position, r.Position + fenceLength}, + }, + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/utils/markdown/html.go b/vendor/github.com/mattermost/mattermost-server/utils/markdown/html.go new file mode 100644 index 00000000..afb72bff --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/utils/markdown/html.go @@ -0,0 +1,192 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package markdown + +import ( + "fmt" + "strings" +) + +var htmlEscaper = strings.NewReplacer( + `&`, "&", + `<`, "<", + `>`, ">", + `"`, """, +) + +// RenderHTML produces HTML with the same behavior as the example renderer used in the CommonMark +// reference materials except for one slight difference: for brevity, no unnecessary whitespace is +// inserted between elements. The output is not defined by the CommonMark spec, and it exists +// primarily as an aid in testing. +func RenderHTML(markdown string) string { + return RenderBlockHTML(Parse(markdown)) +} + +func RenderBlockHTML(block Block, referenceDefinitions []*ReferenceDefinition) (result string) { + return renderBlockHTML(block, referenceDefinitions, false) +} + +func renderBlockHTML(block Block, referenceDefinitions []*ReferenceDefinition, isTightList bool) (result string) { + switch v := block.(type) { + case *Document: + for _, block := range v.Children { + result += RenderBlockHTML(block, referenceDefinitions) + } + case *Paragraph: + if len(v.Text) == 0 { + return + } + if !isTightList { + result += "

" + } + for _, inline := range v.ParseInlines(referenceDefinitions) { + result += RenderInlineHTML(inline) + } + if !isTightList { + result += "

" + } + case *List: + if v.IsOrdered { + if v.OrderedStart != 1 { + result += fmt.Sprintf(`
    `, v.OrderedStart) + } else { + result += "
      " + } + } else { + result += "
        " + } + for _, block := range v.Children { + result += renderBlockHTML(block, referenceDefinitions, !v.IsLoose) + } + if v.IsOrdered { + result += "
    " + } else { + result += "" + } + case *ListItem: + result += "
  1. " + for _, block := range v.Children { + result += renderBlockHTML(block, referenceDefinitions, isTightList) + } + result += "
  2. " + case *BlockQuote: + result += "
    " + for _, block := range v.Children { + result += RenderBlockHTML(block, referenceDefinitions) + } + result += "
    " + case *FencedCode: + if info := v.Info(); info != "" { + language := strings.Fields(info)[0] + result += `
    `
    +		} else {
    +			result += "
    "
    +		}
    +		result += htmlEscaper.Replace(v.Code()) + "
    " + case *IndentedCode: + result += "
    " + htmlEscaper.Replace(v.Code()) + "
    " + default: + panic(fmt.Sprintf("missing case for type %T", v)) + } + return +} + +func escapeURL(url string) (result string) { + for i := 0; i < len(url); { + switch b := url[i]; b { + case ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '-', '_', '.', '!', '~', '*', '\'', '(', ')', '#': + result += string(b) + i++ + default: + if b == '%' && i+2 < len(url) && isHexByte(url[i+1]) && isHexByte(url[i+2]) { + result += url[i : i+3] + i += 3 + } else if (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') { + result += string(b) + i++ + } else { + result += fmt.Sprintf("%%%0X", b) + i++ + } + } + } + return +} + +func RenderInlineHTML(inline Inline) (result string) { + switch v := inline.(type) { + case *Text: + return htmlEscaper.Replace(v.Text) + case *HardLineBreak: + return "
    " + case *SoftLineBreak: + return "\n" + case *CodeSpan: + return "" + htmlEscaper.Replace(v.Code) + "" + case *InlineImage: + result += `` + htmlEscaper.Replace(renderImageAltText(v.Children)) + `` + case *ReferenceImage: + result += `` + htmlEscaper.Replace(renderImageAltText(v.Children)) + `` + case *InlineLink: + result += `` + for _, inline := range v.Children { + result += RenderInlineHTML(inline) + } + result += "" + case *ReferenceLink: + result += `` + for _, inline := range v.Children { + result += RenderInlineHTML(inline) + } + result += "" + case *Autolink: + result += `` + for _, inline := range v.Children { + result += RenderInlineHTML(inline) + } + result += "" + default: + panic(fmt.Sprintf("missing case for type %T", v)) + } + return +} + +func renderImageAltText(children []Inline) (result string) { + for _, inline := range children { + result += renderImageChildAltText(inline) + } + return +} + +func renderImageChildAltText(inline Inline) (result string) { + switch v := inline.(type) { + case *Text: + return v.Text + case *InlineImage: + for _, inline := range v.Children { + result += renderImageChildAltText(inline) + } + case *InlineLink: + for _, inline := range v.Children { + result += renderImageChildAltText(inline) + } + } + return +} diff --git a/vendor/github.com/mattermost/mattermost-server/utils/markdown/html_entities.go b/vendor/github.com/mattermost/mattermost-server/utils/markdown/html_entities.go new file mode 100644 index 00000000..8fe24811 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/utils/markdown/html_entities.go @@ -0,0 +1,2132 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package markdown + +var htmlEntities = map[string]string{ + "AElig": "\u00C6", + "AMP": "\u0026", + "Aacute": "\u00C1", + "Abreve": "\u0102", + "Acirc": "\u00C2", + "Acy": "\u0410", + "Afr": "\U0001D504", + "Agrave": "\u00C0", + "Alpha": "\u0391", + "Amacr": "\u0100", + "And": "\u2A53", + "Aogon": "\u0104", + "Aopf": "\U0001D538", + "ApplyFunction": "\u2061", + "Aring": "\u00C5", + "Ascr": "\U0001D49C", + "Assign": "\u2254", + "Atilde": "\u00C3", + "Auml": "\u00C4", + "Backslash": "\u2216", + "Barv": "\u2AE7", + "Barwed": "\u2306", + "Bcy": "\u0411", + "Because": "\u2235", + "Bernoullis": "\u212C", + "Beta": "\u0392", + "Bfr": "\U0001D505", + "Bopf": "\U0001D539", + "Breve": "\u02D8", + "Bscr": "\u212C", + "Bumpeq": "\u224E", + "CHcy": "\u0427", + "COPY": "\u00A9", + "Cacute": "\u0106", + "Cap": "\u22D2", + "CapitalDifferentialD": "\u2145", + "Cayleys": "\u212D", + "Ccaron": "\u010C", + "Ccedil": "\u00C7", + "Ccirc": "\u0108", + "Cconint": "\u2230", + "Cdot": "\u010A", + "Cedilla": "\u00B8", + "CenterDot": "\u00B7", + "Cfr": "\u212D", + "Chi": "\u03A7", + "CircleDot": "\u2299", + "CircleMinus": "\u2296", + "CirclePlus": "\u2295", + "CircleTimes": "\u2297", + "ClockwiseContourIntegral": "\u2232", + "CloseCurlyDoubleQuote": "\u201D", + "CloseCurlyQuote": "\u2019", + "Colon": "\u2237", + "Colone": "\u2A74", + "Congruent": "\u2261", + "Conint": "\u222F", + "ContourIntegral": "\u222E", + "Copf": "\u2102", + "Coproduct": "\u2210", + "CounterClockwiseContourIntegral": "\u2233", + "Cross": "\u2A2F", + "Cscr": "\U0001D49E", + "Cup": "\u22D3", + "CupCap": "\u224D", + "DD": "\u2145", + "DDotrahd": "\u2911", + "DJcy": "\u0402", + "DScy": "\u0405", + "DZcy": "\u040F", + "Dagger": "\u2021", + "Darr": "\u21A1", + "Dashv": "\u2AE4", + "Dcaron": "\u010E", + "Dcy": "\u0414", + "Del": "\u2207", + "Delta": "\u0394", + "Dfr": "\U0001D507", + "DiacriticalAcute": "\u00B4", + "DiacriticalDot": "\u02D9", + "DiacriticalDoubleAcute": "\u02DD", + "DiacriticalGrave": "\u0060", + "DiacriticalTilde": "\u02DC", + "Diamond": "\u22C4", + "DifferentialD": "\u2146", + "Dopf": "\U0001D53B", + "Dot": "\u00A8", + "DotDot": "\u20DC", + "DotEqual": "\u2250", + "DoubleContourIntegral": "\u222F", + "DoubleDot": "\u00A8", + "DoubleDownArrow": "\u21D3", + "DoubleLeftArrow": "\u21D0", + "DoubleLeftRightArrow": "\u21D4", + "DoubleLeftTee": "\u2AE4", + "DoubleLongLeftArrow": "\u27F8", + "DoubleLongLeftRightArrow": "\u27FA", + "DoubleLongRightArrow": "\u27F9", + "DoubleRightArrow": "\u21D2", + "DoubleRightTee": "\u22A8", + "DoubleUpArrow": "\u21D1", + "DoubleUpDownArrow": "\u21D5", + "DoubleVerticalBar": "\u2225", + "DownArrow": "\u2193", + "DownArrowBar": "\u2913", + "DownArrowUpArrow": "\u21F5", + "DownBreve": "\u0311", + "DownLeftRightVector": "\u2950", + "DownLeftTeeVector": "\u295E", + "DownLeftVector": "\u21BD", + "DownLeftVectorBar": "\u2956", + "DownRightTeeVector": "\u295F", + "DownRightVector": "\u21C1", + "DownRightVectorBar": "\u2957", + "DownTee": "\u22A4", + "DownTeeArrow": "\u21A7", + "Downarrow": "\u21D3", + "Dscr": "\U0001D49F", + "Dstrok": "\u0110", + "ENG": "\u014A", + "ETH": "\u00D0", + "Eacute": "\u00C9", + "Ecaron": "\u011A", + "Ecirc": "\u00CA", + "Ecy": "\u042D", + "Edot": "\u0116", + "Efr": "\U0001D508", + "Egrave": "\u00C8", + "Element": "\u2208", + "Emacr": "\u0112", + "EmptySmallSquare": "\u25FB", + "EmptyVerySmallSquare": "\u25AB", + "Eogon": "\u0118", + "Eopf": "\U0001D53C", + "Epsilon": "\u0395", + "Equal": "\u2A75", + "EqualTilde": "\u2242", + "Equilibrium": "\u21CC", + "Escr": "\u2130", + "Esim": "\u2A73", + "Eta": "\u0397", + "Euml": "\u00CB", + "Exists": "\u2203", + "ExponentialE": "\u2147", + "Fcy": "\u0424", + "Ffr": "\U0001D509", + "FilledSmallSquare": "\u25FC", + "FilledVerySmallSquare": "\u25AA", + "Fopf": "\U0001D53D", + "ForAll": "\u2200", + "Fouriertrf": "\u2131", + "Fscr": "\u2131", + "GJcy": "\u0403", + "GT": "\u003E", + "Gamma": "\u0393", + "Gammad": "\u03DC", + "Gbreve": "\u011E", + "Gcedil": "\u0122", + "Gcirc": "\u011C", + "Gcy": "\u0413", + "Gdot": "\u0120", + "Gfr": "\U0001D50A", + "Gg": "\u22D9", + "Gopf": "\U0001D53E", + "GreaterEqual": "\u2265", + "GreaterEqualLess": "\u22DB", + "GreaterFullEqual": "\u2267", + "GreaterGreater": "\u2AA2", + "GreaterLess": "\u2277", + "GreaterSlantEqual": "\u2A7E", + "GreaterTilde": "\u2273", + "Gscr": "\U0001D4A2", + "Gt": "\u226B", + "HARDcy": "\u042A", + "Hacek": "\u02C7", + "Hat": "\u005E", + "Hcirc": "\u0124", + "Hfr": "\u210C", + "HilbertSpace": "\u210B", + "Hopf": "\u210D", + "HorizontalLine": "\u2500", + "Hscr": "\u210B", + "Hstrok": "\u0126", + "HumpDownHump": "\u224E", + "HumpEqual": "\u224F", + "IEcy": "\u0415", + "IJlig": "\u0132", + "IOcy": "\u0401", + "Iacute": "\u00CD", + "Icirc": "\u00CE", + "Icy": "\u0418", + "Idot": "\u0130", + "Ifr": "\u2111", + "Igrave": "\u00CC", + "Im": "\u2111", + "Imacr": "\u012A", + "ImaginaryI": "\u2148", + "Implies": "\u21D2", + "Int": "\u222C", + "Integral": "\u222B", + "Intersection": "\u22C2", + "InvisibleComma": "\u2063", + "InvisibleTimes": "\u2062", + "Iogon": "\u012E", + "Iopf": "\U0001D540", + "Iota": "\u0399", + "Iscr": "\u2110", + "Itilde": "\u0128", + "Iukcy": "\u0406", + "Iuml": "\u00CF", + "Jcirc": "\u0134", + "Jcy": "\u0419", + "Jfr": "\U0001D50D", + "Jopf": "\U0001D541", + "Jscr": "\U0001D4A5", + "Jsercy": "\u0408", + "Jukcy": "\u0404", + "KHcy": "\u0425", + "KJcy": "\u040C", + "Kappa": "\u039A", + "Kcedil": "\u0136", + "Kcy": "\u041A", + "Kfr": "\U0001D50E", + "Kopf": "\U0001D542", + "Kscr": "\U0001D4A6", + "LJcy": "\u0409", + "LT": "\u003C", + "Lacute": "\u0139", + "Lambda": "\u039B", + "Lang": "\u27EA", + "Laplacetrf": "\u2112", + "Larr": "\u219E", + "Lcaron": "\u013D", + "Lcedil": "\u013B", + "Lcy": "\u041B", + "LeftAngleBracket": "\u27E8", + "LeftArrow": "\u2190", + "LeftArrowBar": "\u21E4", + "LeftArrowRightArrow": "\u21C6", + "LeftCeiling": "\u2308", + "LeftDoubleBracket": "\u27E6", + "LeftDownTeeVector": "\u2961", + "LeftDownVector": "\u21C3", + "LeftDownVectorBar": "\u2959", + "LeftFloor": "\u230A", + "LeftRightArrow": "\u2194", + "LeftRightVector": "\u294E", + "LeftTee": "\u22A3", + "LeftTeeArrow": "\u21A4", + "LeftTeeVector": "\u295A", + "LeftTriangle": "\u22B2", + "LeftTriangleBar": "\u29CF", + "LeftTriangleEqual": "\u22B4", + "LeftUpDownVector": "\u2951", + "LeftUpTeeVector": "\u2960", + "LeftUpVector": "\u21BF", + "LeftUpVectorBar": "\u2958", + "LeftVector": "\u21BC", + "LeftVectorBar": "\u2952", + "Leftarrow": "\u21D0", + "Leftrightarrow": "\u21D4", + "LessEqualGreater": "\u22DA", + "LessFullEqual": "\u2266", + "LessGreater": "\u2276", + "LessLess": "\u2AA1", + "LessSlantEqual": "\u2A7D", + "LessTilde": "\u2272", + "Lfr": "\U0001D50F", + "Ll": "\u22D8", + "Lleftarrow": "\u21DA", + "Lmidot": "\u013F", + "LongLeftArrow": "\u27F5", + "LongLeftRightArrow": "\u27F7", + "LongRightArrow": "\u27F6", + "Longleftarrow": "\u27F8", + "Longleftrightarrow": "\u27FA", + "Longrightarrow": "\u27F9", + "Lopf": "\U0001D543", + "LowerLeftArrow": "\u2199", + "LowerRightArrow": "\u2198", + "Lscr": "\u2112", + "Lsh": "\u21B0", + "Lstrok": "\u0141", + "Lt": "\u226A", + "Map": "\u2905", + "Mcy": "\u041C", + "MediumSpace": "\u205F", + "Mellintrf": "\u2133", + "Mfr": "\U0001D510", + "MinusPlus": "\u2213", + "Mopf": "\U0001D544", + "Mscr": "\u2133", + "Mu": "\u039C", + "NJcy": "\u040A", + "Nacute": "\u0143", + "Ncaron": "\u0147", + "Ncedil": "\u0145", + "Ncy": "\u041D", + "NegativeMediumSpace": "\u200B", + "NegativeThickSpace": "\u200B", + "NegativeThinSpace": "\u200B", + "NegativeVeryThinSpace": "\u200B", + "NestedGreaterGreater": "\u226B", + "NestedLessLess": "\u226A", + "NewLine": "\u000A", + "Nfr": "\U0001D511", + "NoBreak": "\u2060", + "NonBreakingSpace": "\u00A0", + "Nopf": "\u2115", + "Not": "\u2AEC", + "NotCongruent": "\u2262", + "NotCupCap": "\u226D", + "NotDoubleVerticalBar": "\u2226", + "NotElement": "\u2209", + "NotEqual": "\u2260", + "NotEqualTilde": "\u2242\u0338", + "NotExists": "\u2204", + "NotGreater": "\u226F", + "NotGreaterEqual": "\u2271", + "NotGreaterFullEqual": "\u2267\u0338", + "NotGreaterGreater": "\u226B\u0338", + "NotGreaterLess": "\u2279", + "NotGreaterSlantEqual": "\u2A7E\u0338", + "NotGreaterTilde": "\u2275", + "NotHumpDownHump": "\u224E\u0338", + "NotHumpEqual": "\u224F\u0338", + "NotLeftTriangle": "\u22EA", + "NotLeftTriangleBar": "\u29CF\u0338", + "NotLeftTriangleEqual": "\u22EC", + "NotLess": "\u226E", + "NotLessEqual": "\u2270", + "NotLessGreater": "\u2278", + "NotLessLess": "\u226A\u0338", + "NotLessSlantEqual": "\u2A7D\u0338", + "NotLessTilde": "\u2274", + "NotNestedGreaterGreater": "\u2AA2\u0338", + "NotNestedLessLess": "\u2AA1\u0338", + "NotPrecedes": "\u2280", + "NotPrecedesEqual": "\u2AAF\u0338", + "NotPrecedesSlantEqual": "\u22E0", + "NotReverseElement": "\u220C", + "NotRightTriangle": "\u22EB", + "NotRightTriangleBar": "\u29D0\u0338", + "NotRightTriangleEqual": "\u22ED", + "NotSquareSubset": "\u228F\u0338", + "NotSquareSubsetEqual": "\u22E2", + "NotSquareSuperset": "\u2290\u0338", + "NotSquareSupersetEqual": "\u22E3", + "NotSubset": "\u2282\u20D2", + "NotSubsetEqual": "\u2288", + "NotSucceeds": "\u2281", + "NotSucceedsEqual": "\u2AB0\u0338", + "NotSucceedsSlantEqual": "\u22E1", + "NotSucceedsTilde": "\u227F\u0338", + "NotSuperset": "\u2283\u20D2", + "NotSupersetEqual": "\u2289", + "NotTilde": "\u2241", + "NotTildeEqual": "\u2244", + "NotTildeFullEqual": "\u2247", + "NotTildeTilde": "\u2249", + "NotVerticalBar": "\u2224", + "Nscr": "\U0001D4A9", + "Ntilde": "\u00D1", + "Nu": "\u039D", + "OElig": "\u0152", + "Oacute": "\u00D3", + "Ocirc": "\u00D4", + "Ocy": "\u041E", + "Odblac": "\u0150", + "Ofr": "\U0001D512", + "Ograve": "\u00D2", + "Omacr": "\u014C", + "Omega": "\u03A9", + "Omicron": "\u039F", + "Oopf": "\U0001D546", + "OpenCurlyDoubleQuote": "\u201C", + "OpenCurlyQuote": "\u2018", + "Or": "\u2A54", + "Oscr": "\U0001D4AA", + "Oslash": "\u00D8", + "Otilde": "\u00D5", + "Otimes": "\u2A37", + "Ouml": "\u00D6", + "OverBar": "\u203E", + "OverBrace": "\u23DE", + "OverBracket": "\u23B4", + "OverParenthesis": "\u23DC", + "PartialD": "\u2202", + "Pcy": "\u041F", + "Pfr": "\U0001D513", + "Phi": "\u03A6", + "Pi": "\u03A0", + "PlusMinus": "\u00B1", + "Poincareplane": "\u210C", + "Popf": "\u2119", + "Pr": "\u2ABB", + "Precedes": "\u227A", + "PrecedesEqual": "\u2AAF", + "PrecedesSlantEqual": "\u227C", + "PrecedesTilde": "\u227E", + "Prime": "\u2033", + "Product": "\u220F", + "Proportion": "\u2237", + "Proportional": "\u221D", + "Pscr": "\U0001D4AB", + "Psi": "\u03A8", + "QUOT": "\u0022", + "Qfr": "\U0001D514", + "Qopf": "\u211A", + "Qscr": "\U0001D4AC", + "RBarr": "\u2910", + "REG": "\u00AE", + "Racute": "\u0154", + "Rang": "\u27EB", + "Rarr": "\u21A0", + "Rarrtl": "\u2916", + "Rcaron": "\u0158", + "Rcedil": "\u0156", + "Rcy": "\u0420", + "Re": "\u211C", + "ReverseElement": "\u220B", + "ReverseEquilibrium": "\u21CB", + "ReverseUpEquilibrium": "\u296F", + "Rfr": "\u211C", + "Rho": "\u03A1", + "RightAngleBracket": "\u27E9", + "RightArrow": "\u2192", + "RightArrowBar": "\u21E5", + "RightArrowLeftArrow": "\u21C4", + "RightCeiling": "\u2309", + "RightDoubleBracket": "\u27E7", + "RightDownTeeVector": "\u295D", + "RightDownVector": "\u21C2", + "RightDownVectorBar": "\u2955", + "RightFloor": "\u230B", + "RightTee": "\u22A2", + "RightTeeArrow": "\u21A6", + "RightTeeVector": "\u295B", + "RightTriangle": "\u22B3", + "RightTriangleBar": "\u29D0", + "RightTriangleEqual": "\u22B5", + "RightUpDownVector": "\u294F", + "RightUpTeeVector": "\u295C", + "RightUpVector": "\u21BE", + "RightUpVectorBar": "\u2954", + "RightVector": "\u21C0", + "RightVectorBar": "\u2953", + "Rightarrow": "\u21D2", + "Ropf": "\u211D", + "RoundImplies": "\u2970", + "Rrightarrow": "\u21DB", + "Rscr": "\u211B", + "Rsh": "\u21B1", + "RuleDelayed": "\u29F4", + "SHCHcy": "\u0429", + "SHcy": "\u0428", + "SOFTcy": "\u042C", + "Sacute": "\u015A", + "Sc": "\u2ABC", + "Scaron": "\u0160", + "Scedil": "\u015E", + "Scirc": "\u015C", + "Scy": "\u0421", + "Sfr": "\U0001D516", + "ShortDownArrow": "\u2193", + "ShortLeftArrow": "\u2190", + "ShortRightArrow": "\u2192", + "ShortUpArrow": "\u2191", + "Sigma": "\u03A3", + "SmallCircle": "\u2218", + "Sopf": "\U0001D54A", + "Sqrt": "\u221A", + "Square": "\u25A1", + "SquareIntersection": "\u2293", + "SquareSubset": "\u228F", + "SquareSubsetEqual": "\u2291", + "SquareSuperset": "\u2290", + "SquareSupersetEqual": "\u2292", + "SquareUnion": "\u2294", + "Sscr": "\U0001D4AE", + "Star": "\u22C6", + "Sub": "\u22D0", + "Subset": "\u22D0", + "SubsetEqual": "\u2286", + "Succeeds": "\u227B", + "SucceedsEqual": "\u2AB0", + "SucceedsSlantEqual": "\u227D", + "SucceedsTilde": "\u227F", + "SuchThat": "\u220B", + "Sum": "\u2211", + "Sup": "\u22D1", + "Superset": "\u2283", + "SupersetEqual": "\u2287", + "Supset": "\u22D1", + "THORN": "\u00DE", + "TRADE": "\u2122", + "TSHcy": "\u040B", + "TScy": "\u0426", + "Tab": "\u0009", + "Tau": "\u03A4", + "Tcaron": "\u0164", + "Tcedil": "\u0162", + "Tcy": "\u0422", + "Tfr": "\U0001D517", + "Therefore": "\u2234", + "Theta": "\u0398", + "ThickSpace": "\u205F\u200A", + "ThinSpace": "\u2009", + "Tilde": "\u223C", + "TildeEqual": "\u2243", + "TildeFullEqual": "\u2245", + "TildeTilde": "\u2248", + "Topf": "\U0001D54B", + "TripleDot": "\u20DB", + "Tscr": "\U0001D4AF", + "Tstrok": "\u0166", + "Uacute": "\u00DA", + "Uarr": "\u219F", + "Uarrocir": "\u2949", + "Ubrcy": "\u040E", + "Ubreve": "\u016C", + "Ucirc": "\u00DB", + "Ucy": "\u0423", + "Udblac": "\u0170", + "Ufr": "\U0001D518", + "Ugrave": "\u00D9", + "Umacr": "\u016A", + "UnderBar": "\u005F", + "UnderBrace": "\u23DF", + "UnderBracket": "\u23B5", + "UnderParenthesis": "\u23DD", + "Union": "\u22C3", + "UnionPlus": "\u228E", + "Uogon": "\u0172", + "Uopf": "\U0001D54C", + "UpArrow": "\u2191", + "UpArrowBar": "\u2912", + "UpArrowDownArrow": "\u21C5", + "UpDownArrow": "\u2195", + "UpEquilibrium": "\u296E", + "UpTee": "\u22A5", + "UpTeeArrow": "\u21A5", + "Uparrow": "\u21D1", + "Updownarrow": "\u21D5", + "UpperLeftArrow": "\u2196", + "UpperRightArrow": "\u2197", + "Upsi": "\u03D2", + "Upsilon": "\u03A5", + "Uring": "\u016E", + "Uscr": "\U0001D4B0", + "Utilde": "\u0168", + "Uuml": "\u00DC", + "VDash": "\u22AB", + "Vbar": "\u2AEB", + "Vcy": "\u0412", + "Vdash": "\u22A9", + "Vdashl": "\u2AE6", + "Vee": "\u22C1", + "Verbar": "\u2016", + "Vert": "\u2016", + "VerticalBar": "\u2223", + "VerticalLine": "\u007C", + "VerticalSeparator": "\u2758", + "VerticalTilde": "\u2240", + "VeryThinSpace": "\u200A", + "Vfr": "\U0001D519", + "Vopf": "\U0001D54D", + "Vscr": "\U0001D4B1", + "Vvdash": "\u22AA", + "Wcirc": "\u0174", + "Wedge": "\u22C0", + "Wfr": "\U0001D51A", + "Wopf": "\U0001D54E", + "Wscr": "\U0001D4B2", + "Xfr": "\U0001D51B", + "Xi": "\u039E", + "Xopf": "\U0001D54F", + "Xscr": "\U0001D4B3", + "YAcy": "\u042F", + "YIcy": "\u0407", + "YUcy": "\u042E", + "Yacute": "\u00DD", + "Ycirc": "\u0176", + "Ycy": "\u042B", + "Yfr": "\U0001D51C", + "Yopf": "\U0001D550", + "Yscr": "\U0001D4B4", + "Yuml": "\u0178", + "ZHcy": "\u0416", + "Zacute": "\u0179", + "Zcaron": "\u017D", + "Zcy": "\u0417", + "Zdot": "\u017B", + "ZeroWidthSpace": "\u200B", + "Zeta": "\u0396", + "Zfr": "\u2128", + "Zopf": "\u2124", + "Zscr": "\U0001D4B5", + "aacute": "\u00E1", + "abreve": "\u0103", + "ac": "\u223E", + "acE": "\u223E\u0333", + "acd": "\u223F", + "acirc": "\u00E2", + "acute": "\u00B4", + "acy": "\u0430", + "aelig": "\u00E6", + "af": "\u2061", + "afr": "\U0001D51E", + "agrave": "\u00E0", + "alefsym": "\u2135", + "aleph": "\u2135", + "alpha": "\u03B1", + "amacr": "\u0101", + "amalg": "\u2A3F", + "amp": "\u0026", + "and": "\u2227", + "andand": "\u2A55", + "andd": "\u2A5C", + "andslope": "\u2A58", + "andv": "\u2A5A", + "ang": "\u2220", + "ange": "\u29A4", + "angle": "\u2220", + "angmsd": "\u2221", + "angmsdaa": "\u29A8", + "angmsdab": "\u29A9", + "angmsdac": "\u29AA", + "angmsdad": "\u29AB", + "angmsdae": "\u29AC", + "angmsdaf": "\u29AD", + "angmsdag": "\u29AE", + "angmsdah": "\u29AF", + "angrt": "\u221F", + "angrtvb": "\u22BE", + "angrtvbd": "\u299D", + "angsph": "\u2222", + "angst": "\u00C5", + "angzarr": "\u237C", + "aogon": "\u0105", + "aopf": "\U0001D552", + "ap": "\u2248", + "apE": "\u2A70", + "apacir": "\u2A6F", + "ape": "\u224A", + "apid": "\u224B", + "apos": "\u0027", + "approx": "\u2248", + "approxeq": "\u224A", + "aring": "\u00E5", + "ascr": "\U0001D4B6", + "ast": "\u002A", + "asymp": "\u2248", + "asympeq": "\u224D", + "atilde": "\u00E3", + "auml": "\u00E4", + "awconint": "\u2233", + "awint": "\u2A11", + "bNot": "\u2AED", + "backcong": "\u224C", + "backepsilon": "\u03F6", + "backprime": "\u2035", + "backsim": "\u223D", + "backsimeq": "\u22CD", + "barvee": "\u22BD", + "barwed": "\u2305", + "barwedge": "\u2305", + "bbrk": "\u23B5", + "bbrktbrk": "\u23B6", + "bcong": "\u224C", + "bcy": "\u0431", + "bdquo": "\u201E", + "becaus": "\u2235", + "because": "\u2235", + "bemptyv": "\u29B0", + "bepsi": "\u03F6", + "bernou": "\u212C", + "beta": "\u03B2", + "beth": "\u2136", + "between": "\u226C", + "bfr": "\U0001D51F", + "bigcap": "\u22C2", + "bigcirc": "\u25EF", + "bigcup": "\u22C3", + "bigodot": "\u2A00", + "bigoplus": "\u2A01", + "bigotimes": "\u2A02", + "bigsqcup": "\u2A06", + "bigstar": "\u2605", + "bigtriangledown": "\u25BD", + "bigtriangleup": "\u25B3", + "biguplus": "\u2A04", + "bigvee": "\u22C1", + "bigwedge": "\u22C0", + "bkarow": "\u290D", + "blacklozenge": "\u29EB", + "blacksquare": "\u25AA", + "blacktriangle": "\u25B4", + "blacktriangledown": "\u25BE", + "blacktriangleleft": "\u25C2", + "blacktriangleright": "\u25B8", + "blank": "\u2423", + "blk12": "\u2592", + "blk14": "\u2591", + "blk34": "\u2593", + "block": "\u2588", + "bne": "\u003D\u20E5", + "bnequiv": "\u2261\u20E5", + "bnot": "\u2310", + "bopf": "\U0001D553", + "bot": "\u22A5", + "bottom": "\u22A5", + "bowtie": "\u22C8", + "boxDL": "\u2557", + "boxDR": "\u2554", + "boxDl": "\u2556", + "boxDr": "\u2553", + "boxH": "\u2550", + "boxHD": "\u2566", + "boxHU": "\u2569", + "boxHd": "\u2564", + "boxHu": "\u2567", + "boxUL": "\u255D", + "boxUR": "\u255A", + "boxUl": "\u255C", + "boxUr": "\u2559", + "boxV": "\u2551", + "boxVH": "\u256C", + "boxVL": "\u2563", + "boxVR": "\u2560", + "boxVh": "\u256B", + "boxVl": "\u2562", + "boxVr": "\u255F", + "boxbox": "\u29C9", + "boxdL": "\u2555", + "boxdR": "\u2552", + "boxdl": "\u2510", + "boxdr": "\u250C", + "boxh": "\u2500", + "boxhD": "\u2565", + "boxhU": "\u2568", + "boxhd": "\u252C", + "boxhu": "\u2534", + "boxminus": "\u229F", + "boxplus": "\u229E", + "boxtimes": "\u22A0", + "boxuL": "\u255B", + "boxuR": "\u2558", + "boxul": "\u2518", + "boxur": "\u2514", + "boxv": "\u2502", + "boxvH": "\u256A", + "boxvL": "\u2561", + "boxvR": "\u255E", + "boxvh": "\u253C", + "boxvl": "\u2524", + "boxvr": "\u251C", + "bprime": "\u2035", + "breve": "\u02D8", + "brvbar": "\u00A6", + "bscr": "\U0001D4B7", + "bsemi": "\u204F", + "bsim": "\u223D", + "bsime": "\u22CD", + "bsol": "\u005C", + "bsolb": "\u29C5", + "bsolhsub": "\u27C8", + "bull": "\u2022", + "bullet": "\u2022", + "bump": "\u224E", + "bumpE": "\u2AAE", + "bumpe": "\u224F", + "bumpeq": "\u224F", + "cacute": "\u0107", + "cap": "\u2229", + "capand": "\u2A44", + "capbrcup": "\u2A49", + "capcap": "\u2A4B", + "capcup": "\u2A47", + "capdot": "\u2A40", + "caps": "\u2229\uFE00", + "caret": "\u2041", + "caron": "\u02C7", + "ccaps": "\u2A4D", + "ccaron": "\u010D", + "ccedil": "\u00E7", + "ccirc": "\u0109", + "ccups": "\u2A4C", + "ccupssm": "\u2A50", + "cdot": "\u010B", + "cedil": "\u00B8", + "cemptyv": "\u29B2", + "cent": "\u00A2", + "centerdot": "\u00B7", + "cfr": "\U0001D520", + "chcy": "\u0447", + "check": "\u2713", + "checkmark": "\u2713", + "chi": "\u03C7", + "cir": "\u25CB", + "cirE": "\u29C3", + "circ": "\u02C6", + "circeq": "\u2257", + "circlearrowleft": "\u21BA", + "circlearrowright": "\u21BB", + "circledR": "\u00AE", + "circledS": "\u24C8", + "circledast": "\u229B", + "circledcirc": "\u229A", + "circleddash": "\u229D", + "cire": "\u2257", + "cirfnint": "\u2A10", + "cirmid": "\u2AEF", + "cirscir": "\u29C2", + "clubs": "\u2663", + "clubsuit": "\u2663", + "colon": "\u003A", + "colone": "\u2254", + "coloneq": "\u2254", + "comma": "\u002C", + "commat": "\u0040", + "comp": "\u2201", + "compfn": "\u2218", + "complement": "\u2201", + "complexes": "\u2102", + "cong": "\u2245", + "congdot": "\u2A6D", + "conint": "\u222E", + "copf": "\U0001D554", + "coprod": "\u2210", + "copy": "\u00A9", + "copysr": "\u2117", + "crarr": "\u21B5", + "cross": "\u2717", + "cscr": "\U0001D4B8", + "csub": "\u2ACF", + "csube": "\u2AD1", + "csup": "\u2AD0", + "csupe": "\u2AD2", + "ctdot": "\u22EF", + "cudarrl": "\u2938", + "cudarrr": "\u2935", + "cuepr": "\u22DE", + "cuesc": "\u22DF", + "cularr": "\u21B6", + "cularrp": "\u293D", + "cup": "\u222A", + "cupbrcap": "\u2A48", + "cupcap": "\u2A46", + "cupcup": "\u2A4A", + "cupdot": "\u228D", + "cupor": "\u2A45", + "cups": "\u222A\uFE00", + "curarr": "\u21B7", + "curarrm": "\u293C", + "curlyeqprec": "\u22DE", + "curlyeqsucc": "\u22DF", + "curlyvee": "\u22CE", + "curlywedge": "\u22CF", + "curren": "\u00A4", + "curvearrowleft": "\u21B6", + "curvearrowright": "\u21B7", + "cuvee": "\u22CE", + "cuwed": "\u22CF", + "cwconint": "\u2232", + "cwint": "\u2231", + "cylcty": "\u232D", + "dArr": "\u21D3", + "dHar": "\u2965", + "dagger": "\u2020", + "daleth": "\u2138", + "darr": "\u2193", + "dash": "\u2010", + "dashv": "\u22A3", + "dbkarow": "\u290F", + "dblac": "\u02DD", + "dcaron": "\u010F", + "dcy": "\u0434", + "dd": "\u2146", + "ddagger": "\u2021", + "ddarr": "\u21CA", + "ddotseq": "\u2A77", + "deg": "\u00B0", + "delta": "\u03B4", + "demptyv": "\u29B1", + "dfisht": "\u297F", + "dfr": "\U0001D521", + "dharl": "\u21C3", + "dharr": "\u21C2", + "diam": "\u22C4", + "diamond": "\u22C4", + "diamondsuit": "\u2666", + "diams": "\u2666", + "die": "\u00A8", + "digamma": "\u03DD", + "disin": "\u22F2", + "div": "\u00F7", + "divide": "\u00F7", + "divideontimes": "\u22C7", + "divonx": "\u22C7", + "djcy": "\u0452", + "dlcorn": "\u231E", + "dlcrop": "\u230D", + "dollar": "\u0024", + "dopf": "\U0001D555", + "dot": "\u02D9", + "doteq": "\u2250", + "doteqdot": "\u2251", + "dotminus": "\u2238", + "dotplus": "\u2214", + "dotsquare": "\u22A1", + "doublebarwedge": "\u2306", + "downarrow": "\u2193", + "downdownarrows": "\u21CA", + "downharpoonleft": "\u21C3", + "downharpoonright": "\u21C2", + "drbkarow": "\u2910", + "drcorn": "\u231F", + "drcrop": "\u230C", + "dscr": "\U0001D4B9", + "dscy": "\u0455", + "dsol": "\u29F6", + "dstrok": "\u0111", + "dtdot": "\u22F1", + "dtri": "\u25BF", + "dtrif": "\u25BE", + "duarr": "\u21F5", + "duhar": "\u296F", + "dwangle": "\u29A6", + "dzcy": "\u045F", + "dzigrarr": "\u27FF", + "eDDot": "\u2A77", + "eDot": "\u2251", + "eacute": "\u00E9", + "easter": "\u2A6E", + "ecaron": "\u011B", + "ecir": "\u2256", + "ecirc": "\u00EA", + "ecolon": "\u2255", + "ecy": "\u044D", + "edot": "\u0117", + "ee": "\u2147", + "efDot": "\u2252", + "efr": "\U0001D522", + "eg": "\u2A9A", + "egrave": "\u00E8", + "egs": "\u2A96", + "egsdot": "\u2A98", + "el": "\u2A99", + "elinters": "\u23E7", + "ell": "\u2113", + "els": "\u2A95", + "elsdot": "\u2A97", + "emacr": "\u0113", + "empty": "\u2205", + "emptyset": "\u2205", + "emptyv": "\u2205", + "emsp": "\u2003", + "emsp13": "\u2004", + "emsp14": "\u2005", + "eng": "\u014B", + "ensp": "\u2002", + "eogon": "\u0119", + "eopf": "\U0001D556", + "epar": "\u22D5", + "eparsl": "\u29E3", + "eplus": "\u2A71", + "epsi": "\u03B5", + "epsilon": "\u03B5", + "epsiv": "\u03F5", + "eqcirc": "\u2256", + "eqcolon": "\u2255", + "eqsim": "\u2242", + "eqslantgtr": "\u2A96", + "eqslantless": "\u2A95", + "equals": "\u003D", + "equest": "\u225F", + "equiv": "\u2261", + "equivDD": "\u2A78", + "eqvparsl": "\u29E5", + "erDot": "\u2253", + "erarr": "\u2971", + "escr": "\u212F", + "esdot": "\u2250", + "esim": "\u2242", + "eta": "\u03B7", + "eth": "\u00F0", + "euml": "\u00EB", + "euro": "\u20AC", + "excl": "\u0021", + "exist": "\u2203", + "expectation": "\u2130", + "exponentiale": "\u2147", + "fallingdotseq": "\u2252", + "fcy": "\u0444", + "female": "\u2640", + "ffilig": "\uFB03", + "fflig": "\uFB00", + "ffllig": "\uFB04", + "ffr": "\U0001D523", + "filig": "\uFB01", + "fjlig": "\u0066\u006A", + "flat": "\u266D", + "fllig": "\uFB02", + "fltns": "\u25B1", + "fnof": "\u0192", + "fopf": "\U0001D557", + "forall": "\u2200", + "fork": "\u22D4", + "forkv": "\u2AD9", + "fpartint": "\u2A0D", + "frac12": "\u00BD", + "frac13": "\u2153", + "frac14": "\u00BC", + "frac15": "\u2155", + "frac16": "\u2159", + "frac18": "\u215B", + "frac23": "\u2154", + "frac25": "\u2156", + "frac34": "\u00BE", + "frac35": "\u2157", + "frac38": "\u215C", + "frac45": "\u2158", + "frac56": "\u215A", + "frac58": "\u215D", + "frac78": "\u215E", + "frasl": "\u2044", + "frown": "\u2322", + "fscr": "\U0001D4BB", + "gE": "\u2267", + "gEl": "\u2A8C", + "gacute": "\u01F5", + "gamma": "\u03B3", + "gammad": "\u03DD", + "gap": "\u2A86", + "gbreve": "\u011F", + "gcirc": "\u011D", + "gcy": "\u0433", + "gdot": "\u0121", + "ge": "\u2265", + "gel": "\u22DB", + "geq": "\u2265", + "geqq": "\u2267", + "geqslant": "\u2A7E", + "ges": "\u2A7E", + "gescc": "\u2AA9", + "gesdot": "\u2A80", + "gesdoto": "\u2A82", + "gesdotol": "\u2A84", + "gesl": "\u22DB\uFE00", + "gesles": "\u2A94", + "gfr": "\U0001D524", + "gg": "\u226B", + "ggg": "\u22D9", + "gimel": "\u2137", + "gjcy": "\u0453", + "gl": "\u2277", + "glE": "\u2A92", + "gla": "\u2AA5", + "glj": "\u2AA4", + "gnE": "\u2269", + "gnap": "\u2A8A", + "gnapprox": "\u2A8A", + "gne": "\u2A88", + "gneq": "\u2A88", + "gneqq": "\u2269", + "gnsim": "\u22E7", + "gopf": "\U0001D558", + "grave": "\u0060", + "gscr": "\u210A", + "gsim": "\u2273", + "gsime": "\u2A8E", + "gsiml": "\u2A90", + "gt": "\u003E", + "gtcc": "\u2AA7", + "gtcir": "\u2A7A", + "gtdot": "\u22D7", + "gtlPar": "\u2995", + "gtquest": "\u2A7C", + "gtrapprox": "\u2A86", + "gtrarr": "\u2978", + "gtrdot": "\u22D7", + "gtreqless": "\u22DB", + "gtreqqless": "\u2A8C", + "gtrless": "\u2277", + "gtrsim": "\u2273", + "gvertneqq": "\u2269\uFE00", + "gvnE": "\u2269\uFE00", + "hArr": "\u21D4", + "hairsp": "\u200A", + "half": "\u00BD", + "hamilt": "\u210B", + "hardcy": "\u044A", + "harr": "\u2194", + "harrcir": "\u2948", + "harrw": "\u21AD", + "hbar": "\u210F", + "hcirc": "\u0125", + "hearts": "\u2665", + "heartsuit": "\u2665", + "hellip": "\u2026", + "hercon": "\u22B9", + "hfr": "\U0001D525", + "hksearow": "\u2925", + "hkswarow": "\u2926", + "hoarr": "\u21FF", + "homtht": "\u223B", + "hookleftarrow": "\u21A9", + "hookrightarrow": "\u21AA", + "hopf": "\U0001D559", + "horbar": "\u2015", + "hscr": "\U0001D4BD", + "hslash": "\u210F", + "hstrok": "\u0127", + "hybull": "\u2043", + "hyphen": "\u2010", + "iacute": "\u00ED", + "ic": "\u2063", + "icirc": "\u00EE", + "icy": "\u0438", + "iecy": "\u0435", + "iexcl": "\u00A1", + "iff": "\u21D4", + "ifr": "\U0001D526", + "igrave": "\u00EC", + "ii": "\u2148", + "iiiint": "\u2A0C", + "iiint": "\u222D", + "iinfin": "\u29DC", + "iiota": "\u2129", + "ijlig": "\u0133", + "imacr": "\u012B", + "image": "\u2111", + "imagline": "\u2110", + "imagpart": "\u2111", + "imath": "\u0131", + "imof": "\u22B7", + "imped": "\u01B5", + "in": "\u2208", + "incare": "\u2105", + "infin": "\u221E", + "infintie": "\u29DD", + "inodot": "\u0131", + "int": "\u222B", + "intcal": "\u22BA", + "integers": "\u2124", + "intercal": "\u22BA", + "intlarhk": "\u2A17", + "intprod": "\u2A3C", + "iocy": "\u0451", + "iogon": "\u012F", + "iopf": "\U0001D55A", + "iota": "\u03B9", + "iprod": "\u2A3C", + "iquest": "\u00BF", + "iscr": "\U0001D4BE", + "isin": "\u2208", + "isinE": "\u22F9", + "isindot": "\u22F5", + "isins": "\u22F4", + "isinsv": "\u22F3", + "isinv": "\u2208", + "it": "\u2062", + "itilde": "\u0129", + "iukcy": "\u0456", + "iuml": "\u00EF", + "jcirc": "\u0135", + "jcy": "\u0439", + "jfr": "\U0001D527", + "jmath": "\u0237", + "jopf": "\U0001D55B", + "jscr": "\U0001D4BF", + "jsercy": "\u0458", + "jukcy": "\u0454", + "kappa": "\u03BA", + "kappav": "\u03F0", + "kcedil": "\u0137", + "kcy": "\u043A", + "kfr": "\U0001D528", + "kgreen": "\u0138", + "khcy": "\u0445", + "kjcy": "\u045C", + "kopf": "\U0001D55C", + "kscr": "\U0001D4C0", + "lAarr": "\u21DA", + "lArr": "\u21D0", + "lAtail": "\u291B", + "lBarr": "\u290E", + "lE": "\u2266", + "lEg": "\u2A8B", + "lHar": "\u2962", + "lacute": "\u013A", + "laemptyv": "\u29B4", + "lagran": "\u2112", + "lambda": "\u03BB", + "lang": "\u27E8", + "langd": "\u2991", + "langle": "\u27E8", + "lap": "\u2A85", + "laquo": "\u00AB", + "larr": "\u2190", + "larrb": "\u21E4", + "larrbfs": "\u291F", + "larrfs": "\u291D", + "larrhk": "\u21A9", + "larrlp": "\u21AB", + "larrpl": "\u2939", + "larrsim": "\u2973", + "larrtl": "\u21A2", + "lat": "\u2AAB", + "latail": "\u2919", + "late": "\u2AAD", + "lates": "\u2AAD\uFE00", + "lbarr": "\u290C", + "lbbrk": "\u2772", + "lbrace": "\u007B", + "lbrack": "\u005B", + "lbrke": "\u298B", + "lbrksld": "\u298F", + "lbrkslu": "\u298D", + "lcaron": "\u013E", + "lcedil": "\u013C", + "lceil": "\u2308", + "lcub": "\u007B", + "lcy": "\u043B", + "ldca": "\u2936", + "ldquo": "\u201C", + "ldquor": "\u201E", + "ldrdhar": "\u2967", + "ldrushar": "\u294B", + "ldsh": "\u21B2", + "le": "\u2264", + "leftarrow": "\u2190", + "leftarrowtail": "\u21A2", + "leftharpoondown": "\u21BD", + "leftharpoonup": "\u21BC", + "leftleftarrows": "\u21C7", + "leftrightarrow": "\u2194", + "leftrightarrows": "\u21C6", + "leftrightharpoons": "\u21CB", + "leftrightsquigarrow": "\u21AD", + "leftthreetimes": "\u22CB", + "leg": "\u22DA", + "leq": "\u2264", + "leqq": "\u2266", + "leqslant": "\u2A7D", + "les": "\u2A7D", + "lescc": "\u2AA8", + "lesdot": "\u2A7F", + "lesdoto": "\u2A81", + "lesdotor": "\u2A83", + "lesg": "\u22DA\uFE00", + "lesges": "\u2A93", + "lessapprox": "\u2A85", + "lessdot": "\u22D6", + "lesseqgtr": "\u22DA", + "lesseqqgtr": "\u2A8B", + "lessgtr": "\u2276", + "lesssim": "\u2272", + "lfisht": "\u297C", + "lfloor": "\u230A", + "lfr": "\U0001D529", + "lg": "\u2276", + "lgE": "\u2A91", + "lhard": "\u21BD", + "lharu": "\u21BC", + "lharul": "\u296A", + "lhblk": "\u2584", + "ljcy": "\u0459", + "ll": "\u226A", + "llarr": "\u21C7", + "llcorner": "\u231E", + "llhard": "\u296B", + "lltri": "\u25FA", + "lmidot": "\u0140", + "lmoust": "\u23B0", + "lmoustache": "\u23B0", + "lnE": "\u2268", + "lnap": "\u2A89", + "lnapprox": "\u2A89", + "lne": "\u2A87", + "lneq": "\u2A87", + "lneqq": "\u2268", + "lnsim": "\u22E6", + "loang": "\u27EC", + "loarr": "\u21FD", + "lobrk": "\u27E6", + "longleftarrow": "\u27F5", + "longleftrightarrow": "\u27F7", + "longmapsto": "\u27FC", + "longrightarrow": "\u27F6", + "looparrowleft": "\u21AB", + "looparrowright": "\u21AC", + "lopar": "\u2985", + "lopf": "\U0001D55D", + "loplus": "\u2A2D", + "lotimes": "\u2A34", + "lowast": "\u2217", + "lowbar": "\u005F", + "loz": "\u25CA", + "lozenge": "\u25CA", + "lozf": "\u29EB", + "lpar": "\u0028", + "lparlt": "\u2993", + "lrarr": "\u21C6", + "lrcorner": "\u231F", + "lrhar": "\u21CB", + "lrhard": "\u296D", + "lrm": "\u200E", + "lrtri": "\u22BF", + "lsaquo": "\u2039", + "lscr": "\U0001D4C1", + "lsh": "\u21B0", + "lsim": "\u2272", + "lsime": "\u2A8D", + "lsimg": "\u2A8F", + "lsqb": "\u005B", + "lsquo": "\u2018", + "lsquor": "\u201A", + "lstrok": "\u0142", + "lt": "\u003C", + "ltcc": "\u2AA6", + "ltcir": "\u2A79", + "ltdot": "\u22D6", + "lthree": "\u22CB", + "ltimes": "\u22C9", + "ltlarr": "\u2976", + "ltquest": "\u2A7B", + "ltrPar": "\u2996", + "ltri": "\u25C3", + "ltrie": "\u22B4", + "ltrif": "\u25C2", + "lurdshar": "\u294A", + "luruhar": "\u2966", + "lvertneqq": "\u2268\uFE00", + "lvnE": "\u2268\uFE00", + "mDDot": "\u223A", + "macr": "\u00AF", + "male": "\u2642", + "malt": "\u2720", + "maltese": "\u2720", + "map": "\u21A6", + "mapsto": "\u21A6", + "mapstodown": "\u21A7", + "mapstoleft": "\u21A4", + "mapstoup": "\u21A5", + "marker": "\u25AE", + "mcomma": "\u2A29", + "mcy": "\u043C", + "mdash": "\u2014", + "measuredangle": "\u2221", + "mfr": "\U0001D52A", + "mho": "\u2127", + "micro": "\u00B5", + "mid": "\u2223", + "midast": "\u002A", + "midcir": "\u2AF0", + "middot": "\u00B7", + "minus": "\u2212", + "minusb": "\u229F", + "minusd": "\u2238", + "minusdu": "\u2A2A", + "mlcp": "\u2ADB", + "mldr": "\u2026", + "mnplus": "\u2213", + "models": "\u22A7", + "mopf": "\U0001D55E", + "mp": "\u2213", + "mscr": "\U0001D4C2", + "mstpos": "\u223E", + "mu": "\u03BC", + "multimap": "\u22B8", + "mumap": "\u22B8", + "nGg": "\u22D9\u0338", + "nGt": "\u226B\u20D2", + "nGtv": "\u226B\u0338", + "nLeftarrow": "\u21CD", + "nLeftrightarrow": "\u21CE", + "nLl": "\u22D8\u0338", + "nLt": "\u226A\u20D2", + "nLtv": "\u226A\u0338", + "nRightarrow": "\u21CF", + "nVDash": "\u22AF", + "nVdash": "\u22AE", + "nabla": "\u2207", + "nacute": "\u0144", + "nang": "\u2220\u20D2", + "nap": "\u2249", + "napE": "\u2A70\u0338", + "napid": "\u224B\u0338", + "napos": "\u0149", + "napprox": "\u2249", + "natur": "\u266E", + "natural": "\u266E", + "naturals": "\u2115", + "nbsp": "\u00A0", + "nbump": "\u224E\u0338", + "nbumpe": "\u224F\u0338", + "ncap": "\u2A43", + "ncaron": "\u0148", + "ncedil": "\u0146", + "ncong": "\u2247", + "ncongdot": "\u2A6D\u0338", + "ncup": "\u2A42", + "ncy": "\u043D", + "ndash": "\u2013", + "ne": "\u2260", + "neArr": "\u21D7", + "nearhk": "\u2924", + "nearr": "\u2197", + "nearrow": "\u2197", + "nedot": "\u2250\u0338", + "nequiv": "\u2262", + "nesear": "\u2928", + "nesim": "\u2242\u0338", + "nexist": "\u2204", + "nexists": "\u2204", + "nfr": "\U0001D52B", + "ngE": "\u2267\u0338", + "nge": "\u2271", + "ngeq": "\u2271", + "ngeqq": "\u2267\u0338", + "ngeqslant": "\u2A7E\u0338", + "nges": "\u2A7E\u0338", + "ngsim": "\u2275", + "ngt": "\u226F", + "ngtr": "\u226F", + "nhArr": "\u21CE", + "nharr": "\u21AE", + "nhpar": "\u2AF2", + "ni": "\u220B", + "nis": "\u22FC", + "nisd": "\u22FA", + "niv": "\u220B", + "njcy": "\u045A", + "nlArr": "\u21CD", + "nlE": "\u2266\u0338", + "nlarr": "\u219A", + "nldr": "\u2025", + "nle": "\u2270", + "nleftarrow": "\u219A", + "nleftrightarrow": "\u21AE", + "nleq": "\u2270", + "nleqq": "\u2266\u0338", + "nleqslant": "\u2A7D\u0338", + "nles": "\u2A7D\u0338", + "nless": "\u226E", + "nlsim": "\u2274", + "nlt": "\u226E", + "nltri": "\u22EA", + "nltrie": "\u22EC", + "nmid": "\u2224", + "nopf": "\U0001D55F", + "not": "\u00AC", + "notin": "\u2209", + "notinE": "\u22F9\u0338", + "notindot": "\u22F5\u0338", + "notinva": "\u2209", + "notinvb": "\u22F7", + "notinvc": "\u22F6", + "notni": "\u220C", + "notniva": "\u220C", + "notnivb": "\u22FE", + "notnivc": "\u22FD", + "npar": "\u2226", + "nparallel": "\u2226", + "nparsl": "\u2AFD\u20E5", + "npart": "\u2202\u0338", + "npolint": "\u2A14", + "npr": "\u2280", + "nprcue": "\u22E0", + "npre": "\u2AAF\u0338", + "nprec": "\u2280", + "npreceq": "\u2AAF\u0338", + "nrArr": "\u21CF", + "nrarr": "\u219B", + "nrarrc": "\u2933\u0338", + "nrarrw": "\u219D\u0338", + "nrightarrow": "\u219B", + "nrtri": "\u22EB", + "nrtrie": "\u22ED", + "nsc": "\u2281", + "nsccue": "\u22E1", + "nsce": "\u2AB0\u0338", + "nscr": "\U0001D4C3", + "nshortmid": "\u2224", + "nshortparallel": "\u2226", + "nsim": "\u2241", + "nsime": "\u2244", + "nsimeq": "\u2244", + "nsmid": "\u2224", + "nspar": "\u2226", + "nsqsube": "\u22E2", + "nsqsupe": "\u22E3", + "nsub": "\u2284", + "nsubE": "\u2AC5\u0338", + "nsube": "\u2288", + "nsubset": "\u2282\u20D2", + "nsubseteq": "\u2288", + "nsubseteqq": "\u2AC5\u0338", + "nsucc": "\u2281", + "nsucceq": "\u2AB0\u0338", + "nsup": "\u2285", + "nsupE": "\u2AC6\u0338", + "nsupe": "\u2289", + "nsupset": "\u2283\u20D2", + "nsupseteq": "\u2289", + "nsupseteqq": "\u2AC6\u0338", + "ntgl": "\u2279", + "ntilde": "\u00F1", + "ntlg": "\u2278", + "ntriangleleft": "\u22EA", + "ntrianglelefteq": "\u22EC", + "ntriangleright": "\u22EB", + "ntrianglerighteq": "\u22ED", + "nu": "\u03BD", + "num": "\u0023", + "numero": "\u2116", + "numsp": "\u2007", + "nvDash": "\u22AD", + "nvHarr": "\u2904", + "nvap": "\u224D\u20D2", + "nvdash": "\u22AC", + "nvge": "\u2265\u20D2", + "nvgt": "\u003E\u20D2", + "nvinfin": "\u29DE", + "nvlArr": "\u2902", + "nvle": "\u2264\u20D2", + "nvlt": "\u003C\u20D2", + "nvltrie": "\u22B4\u20D2", + "nvrArr": "\u2903", + "nvrtrie": "\u22B5\u20D2", + "nvsim": "\u223C\u20D2", + "nwArr": "\u21D6", + "nwarhk": "\u2923", + "nwarr": "\u2196", + "nwarrow": "\u2196", + "nwnear": "\u2927", + "oS": "\u24C8", + "oacute": "\u00F3", + "oast": "\u229B", + "ocir": "\u229A", + "ocirc": "\u00F4", + "ocy": "\u043E", + "odash": "\u229D", + "odblac": "\u0151", + "odiv": "\u2A38", + "odot": "\u2299", + "odsold": "\u29BC", + "oelig": "\u0153", + "ofcir": "\u29BF", + "ofr": "\U0001D52C", + "ogon": "\u02DB", + "ograve": "\u00F2", + "ogt": "\u29C1", + "ohbar": "\u29B5", + "ohm": "\u03A9", + "oint": "\u222E", + "olarr": "\u21BA", + "olcir": "\u29BE", + "olcross": "\u29BB", + "oline": "\u203E", + "olt": "\u29C0", + "omacr": "\u014D", + "omega": "\u03C9", + "omicron": "\u03BF", + "omid": "\u29B6", + "ominus": "\u2296", + "oopf": "\U0001D560", + "opar": "\u29B7", + "operp": "\u29B9", + "oplus": "\u2295", + "or": "\u2228", + "orarr": "\u21BB", + "ord": "\u2A5D", + "order": "\u2134", + "orderof": "\u2134", + "ordf": "\u00AA", + "ordm": "\u00BA", + "origof": "\u22B6", + "oror": "\u2A56", + "orslope": "\u2A57", + "orv": "\u2A5B", + "oscr": "\u2134", + "oslash": "\u00F8", + "osol": "\u2298", + "otilde": "\u00F5", + "otimes": "\u2297", + "otimesas": "\u2A36", + "ouml": "\u00F6", + "ovbar": "\u233D", + "par": "\u2225", + "para": "\u00B6", + "parallel": "\u2225", + "parsim": "\u2AF3", + "parsl": "\u2AFD", + "part": "\u2202", + "pcy": "\u043F", + "percnt": "\u0025", + "period": "\u002E", + "permil": "\u2030", + "perp": "\u22A5", + "pertenk": "\u2031", + "pfr": "\U0001D52D", + "phi": "\u03C6", + "phiv": "\u03D5", + "phmmat": "\u2133", + "phone": "\u260E", + "pi": "\u03C0", + "pitchfork": "\u22D4", + "piv": "\u03D6", + "planck": "\u210F", + "planckh": "\u210E", + "plankv": "\u210F", + "plus": "\u002B", + "plusacir": "\u2A23", + "plusb": "\u229E", + "pluscir": "\u2A22", + "plusdo": "\u2214", + "plusdu": "\u2A25", + "pluse": "\u2A72", + "plusmn": "\u00B1", + "plussim": "\u2A26", + "plustwo": "\u2A27", + "pm": "\u00B1", + "pointint": "\u2A15", + "popf": "\U0001D561", + "pound": "\u00A3", + "pr": "\u227A", + "prE": "\u2AB3", + "prap": "\u2AB7", + "prcue": "\u227C", + "pre": "\u2AAF", + "prec": "\u227A", + "precapprox": "\u2AB7", + "preccurlyeq": "\u227C", + "preceq": "\u2AAF", + "precnapprox": "\u2AB9", + "precneqq": "\u2AB5", + "precnsim": "\u22E8", + "precsim": "\u227E", + "prime": "\u2032", + "primes": "\u2119", + "prnE": "\u2AB5", + "prnap": "\u2AB9", + "prnsim": "\u22E8", + "prod": "\u220F", + "profalar": "\u232E", + "profline": "\u2312", + "profsurf": "\u2313", + "prop": "\u221D", + "propto": "\u221D", + "prsim": "\u227E", + "prurel": "\u22B0", + "pscr": "\U0001D4C5", + "psi": "\u03C8", + "puncsp": "\u2008", + "qfr": "\U0001D52E", + "qint": "\u2A0C", + "qopf": "\U0001D562", + "qprime": "\u2057", + "qscr": "\U0001D4C6", + "quaternions": "\u210D", + "quatint": "\u2A16", + "quest": "\u003F", + "questeq": "\u225F", + "quot": "\u0022", + "rAarr": "\u21DB", + "rArr": "\u21D2", + "rAtail": "\u291C", + "rBarr": "\u290F", + "rHar": "\u2964", + "race": "\u223D\u0331", + "racute": "\u0155", + "radic": "\u221A", + "raemptyv": "\u29B3", + "rang": "\u27E9", + "rangd": "\u2992", + "range": "\u29A5", + "rangle": "\u27E9", + "raquo": "\u00BB", + "rarr": "\u2192", + "rarrap": "\u2975", + "rarrb": "\u21E5", + "rarrbfs": "\u2920", + "rarrc": "\u2933", + "rarrfs": "\u291E", + "rarrhk": "\u21AA", + "rarrlp": "\u21AC", + "rarrpl": "\u2945", + "rarrsim": "\u2974", + "rarrtl": "\u21A3", + "rarrw": "\u219D", + "ratail": "\u291A", + "ratio": "\u2236", + "rationals": "\u211A", + "rbarr": "\u290D", + "rbbrk": "\u2773", + "rbrace": "\u007D", + "rbrack": "\u005D", + "rbrke": "\u298C", + "rbrksld": "\u298E", + "rbrkslu": "\u2990", + "rcaron": "\u0159", + "rcedil": "\u0157", + "rceil": "\u2309", + "rcub": "\u007D", + "rcy": "\u0440", + "rdca": "\u2937", + "rdldhar": "\u2969", + "rdquo": "\u201D", + "rdquor": "\u201D", + "rdsh": "\u21B3", + "real": "\u211C", + "realine": "\u211B", + "realpart": "\u211C", + "reals": "\u211D", + "rect": "\u25AD", + "reg": "\u00AE", + "rfisht": "\u297D", + "rfloor": "\u230B", + "rfr": "\U0001D52F", + "rhard": "\u21C1", + "rharu": "\u21C0", + "rharul": "\u296C", + "rho": "\u03C1", + "rhov": "\u03F1", + "rightarrow": "\u2192", + "rightarrowtail": "\u21A3", + "rightharpoondown": "\u21C1", + "rightharpoonup": "\u21C0", + "rightleftarrows": "\u21C4", + "rightleftharpoons": "\u21CC", + "rightrightarrows": "\u21C9", + "rightsquigarrow": "\u219D", + "rightthreetimes": "\u22CC", + "ring": "\u02DA", + "risingdotseq": "\u2253", + "rlarr": "\u21C4", + "rlhar": "\u21CC", + "rlm": "\u200F", + "rmoust": "\u23B1", + "rmoustache": "\u23B1", + "rnmid": "\u2AEE", + "roang": "\u27ED", + "roarr": "\u21FE", + "robrk": "\u27E7", + "ropar": "\u2986", + "ropf": "\U0001D563", + "roplus": "\u2A2E", + "rotimes": "\u2A35", + "rpar": "\u0029", + "rpargt": "\u2994", + "rppolint": "\u2A12", + "rrarr": "\u21C9", + "rsaquo": "\u203A", + "rscr": "\U0001D4C7", + "rsh": "\u21B1", + "rsqb": "\u005D", + "rsquo": "\u2019", + "rsquor": "\u2019", + "rthree": "\u22CC", + "rtimes": "\u22CA", + "rtri": "\u25B9", + "rtrie": "\u22B5", + "rtrif": "\u25B8", + "rtriltri": "\u29CE", + "ruluhar": "\u2968", + "rx": "\u211E", + "sacute": "\u015B", + "sbquo": "\u201A", + "sc": "\u227B", + "scE": "\u2AB4", + "scap": "\u2AB8", + "scaron": "\u0161", + "sccue": "\u227D", + "sce": "\u2AB0", + "scedil": "\u015F", + "scirc": "\u015D", + "scnE": "\u2AB6", + "scnap": "\u2ABA", + "scnsim": "\u22E9", + "scpolint": "\u2A13", + "scsim": "\u227F", + "scy": "\u0441", + "sdot": "\u22C5", + "sdotb": "\u22A1", + "sdote": "\u2A66", + "seArr": "\u21D8", + "searhk": "\u2925", + "searr": "\u2198", + "searrow": "\u2198", + "sect": "\u00A7", + "semi": "\u003B", + "seswar": "\u2929", + "setminus": "\u2216", + "setmn": "\u2216", + "sext": "\u2736", + "sfr": "\U0001D530", + "sfrown": "\u2322", + "sharp": "\u266F", + "shchcy": "\u0449", + "shcy": "\u0448", + "shortmid": "\u2223", + "shortparallel": "\u2225", + "shy": "\u00AD", + "sigma": "\u03C3", + "sigmaf": "\u03C2", + "sigmav": "\u03C2", + "sim": "\u223C", + "simdot": "\u2A6A", + "sime": "\u2243", + "simeq": "\u2243", + "simg": "\u2A9E", + "simgE": "\u2AA0", + "siml": "\u2A9D", + "simlE": "\u2A9F", + "simne": "\u2246", + "simplus": "\u2A24", + "simrarr": "\u2972", + "slarr": "\u2190", + "smallsetminus": "\u2216", + "smashp": "\u2A33", + "smeparsl": "\u29E4", + "smid": "\u2223", + "smile": "\u2323", + "smt": "\u2AAA", + "smte": "\u2AAC", + "smtes": "\u2AAC\uFE00", + "softcy": "\u044C", + "sol": "\u002F", + "solb": "\u29C4", + "solbar": "\u233F", + "sopf": "\U0001D564", + "spades": "\u2660", + "spadesuit": "\u2660", + "spar": "\u2225", + "sqcap": "\u2293", + "sqcaps": "\u2293\uFE00", + "sqcup": "\u2294", + "sqcups": "\u2294\uFE00", + "sqsub": "\u228F", + "sqsube": "\u2291", + "sqsubset": "\u228F", + "sqsubseteq": "\u2291", + "sqsup": "\u2290", + "sqsupe": "\u2292", + "sqsupset": "\u2290", + "sqsupseteq": "\u2292", + "squ": "\u25A1", + "square": "\u25A1", + "squarf": "\u25AA", + "squf": "\u25AA", + "srarr": "\u2192", + "sscr": "\U0001D4C8", + "ssetmn": "\u2216", + "ssmile": "\u2323", + "sstarf": "\u22C6", + "star": "\u2606", + "starf": "\u2605", + "straightepsilon": "\u03F5", + "straightphi": "\u03D5", + "strns": "\u00AF", + "sub": "\u2282", + "subE": "\u2AC5", + "subdot": "\u2ABD", + "sube": "\u2286", + "subedot": "\u2AC3", + "submult": "\u2AC1", + "subnE": "\u2ACB", + "subne": "\u228A", + "subplus": "\u2ABF", + "subrarr": "\u2979", + "subset": "\u2282", + "subseteq": "\u2286", + "subseteqq": "\u2AC5", + "subsetneq": "\u228A", + "subsetneqq": "\u2ACB", + "subsim": "\u2AC7", + "subsub": "\u2AD5", + "subsup": "\u2AD3", + "succ": "\u227B", + "succapprox": "\u2AB8", + "succcurlyeq": "\u227D", + "succeq": "\u2AB0", + "succnapprox": "\u2ABA", + "succneqq": "\u2AB6", + "succnsim": "\u22E9", + "succsim": "\u227F", + "sum": "\u2211", + "sung": "\u266A", + "sup": "\u2283", + "sup1": "\u00B9", + "sup2": "\u00B2", + "sup3": "\u00B3", + "supE": "\u2AC6", + "supdot": "\u2ABE", + "supdsub": "\u2AD8", + "supe": "\u2287", + "supedot": "\u2AC4", + "suphsol": "\u27C9", + "suphsub": "\u2AD7", + "suplarr": "\u297B", + "supmult": "\u2AC2", + "supnE": "\u2ACC", + "supne": "\u228B", + "supplus": "\u2AC0", + "supset": "\u2283", + "supseteq": "\u2287", + "supseteqq": "\u2AC6", + "supsetneq": "\u228B", + "supsetneqq": "\u2ACC", + "supsim": "\u2AC8", + "supsub": "\u2AD4", + "supsup": "\u2AD6", + "swArr": "\u21D9", + "swarhk": "\u2926", + "swarr": "\u2199", + "swarrow": "\u2199", + "swnwar": "\u292A", + "szlig": "\u00DF", + "target": "\u2316", + "tau": "\u03C4", + "tbrk": "\u23B4", + "tcaron": "\u0165", + "tcedil": "\u0163", + "tcy": "\u0442", + "tdot": "\u20DB", + "telrec": "\u2315", + "tfr": "\U0001D531", + "there4": "\u2234", + "therefore": "\u2234", + "theta": "\u03B8", + "thetasym": "\u03D1", + "thetav": "\u03D1", + "thickapprox": "\u2248", + "thicksim": "\u223C", + "thinsp": "\u2009", + "thkap": "\u2248", + "thksim": "\u223C", + "thorn": "\u00FE", + "tilde": "\u02DC", + "times": "\u00D7", + "timesb": "\u22A0", + "timesbar": "\u2A31", + "timesd": "\u2A30", + "tint": "\u222D", + "toea": "\u2928", + "top": "\u22A4", + "topbot": "\u2336", + "topcir": "\u2AF1", + "topf": "\U0001D565", + "topfork": "\u2ADA", + "tosa": "\u2929", + "tprime": "\u2034", + "trade": "\u2122", + "triangle": "\u25B5", + "triangledown": "\u25BF", + "triangleleft": "\u25C3", + "trianglelefteq": "\u22B4", + "triangleq": "\u225C", + "triangleright": "\u25B9", + "trianglerighteq": "\u22B5", + "tridot": "\u25EC", + "trie": "\u225C", + "triminus": "\u2A3A", + "triplus": "\u2A39", + "trisb": "\u29CD", + "tritime": "\u2A3B", + "trpezium": "\u23E2", + "tscr": "\U0001D4C9", + "tscy": "\u0446", + "tshcy": "\u045B", + "tstrok": "\u0167", + "twixt": "\u226C", + "twoheadleftarrow": "\u219E", + "twoheadrightarrow": "\u21A0", + "uArr": "\u21D1", + "uHar": "\u2963", + "uacute": "\u00FA", + "uarr": "\u2191", + "ubrcy": "\u045E", + "ubreve": "\u016D", + "ucirc": "\u00FB", + "ucy": "\u0443", + "udarr": "\u21C5", + "udblac": "\u0171", + "udhar": "\u296E", + "ufisht": "\u297E", + "ufr": "\U0001D532", + "ugrave": "\u00F9", + "uharl": "\u21BF", + "uharr": "\u21BE", + "uhblk": "\u2580", + "ulcorn": "\u231C", + "ulcorner": "\u231C", + "ulcrop": "\u230F", + "ultri": "\u25F8", + "umacr": "\u016B", + "uml": "\u00A8", + "uogon": "\u0173", + "uopf": "\U0001D566", + "uparrow": "\u2191", + "updownarrow": "\u2195", + "upharpoonleft": "\u21BF", + "upharpoonright": "\u21BE", + "uplus": "\u228E", + "upsi": "\u03C5", + "upsih": "\u03D2", + "upsilon": "\u03C5", + "upuparrows": "\u21C8", + "urcorn": "\u231D", + "urcorner": "\u231D", + "urcrop": "\u230E", + "uring": "\u016F", + "urtri": "\u25F9", + "uscr": "\U0001D4CA", + "utdot": "\u22F0", + "utilde": "\u0169", + "utri": "\u25B5", + "utrif": "\u25B4", + "uuarr": "\u21C8", + "uuml": "\u00FC", + "uwangle": "\u29A7", + "vArr": "\u21D5", + "vBar": "\u2AE8", + "vBarv": "\u2AE9", + "vDash": "\u22A8", + "vangrt": "\u299C", + "varepsilon": "\u03F5", + "varkappa": "\u03F0", + "varnothing": "\u2205", + "varphi": "\u03D5", + "varpi": "\u03D6", + "varpropto": "\u221D", + "varr": "\u2195", + "varrho": "\u03F1", + "varsigma": "\u03C2", + "varsubsetneq": "\u228A\uFE00", + "varsubsetneqq": "\u2ACB\uFE00", + "varsupsetneq": "\u228B\uFE00", + "varsupsetneqq": "\u2ACC\uFE00", + "vartheta": "\u03D1", + "vartriangleleft": "\u22B2", + "vartriangleright": "\u22B3", + "vcy": "\u0432", + "vdash": "\u22A2", + "vee": "\u2228", + "veebar": "\u22BB", + "veeeq": "\u225A", + "vellip": "\u22EE", + "verbar": "\u007C", + "vert": "\u007C", + "vfr": "\U0001D533", + "vltri": "\u22B2", + "vnsub": "\u2282\u20D2", + "vnsup": "\u2283\u20D2", + "vopf": "\U0001D567", + "vprop": "\u221D", + "vrtri": "\u22B3", + "vscr": "\U0001D4CB", + "vsubnE": "\u2ACB\uFE00", + "vsubne": "\u228A\uFE00", + "vsupnE": "\u2ACC\uFE00", + "vsupne": "\u228B\uFE00", + "vzigzag": "\u299A", + "wcirc": "\u0175", + "wedbar": "\u2A5F", + "wedge": "\u2227", + "wedgeq": "\u2259", + "weierp": "\u2118", + "wfr": "\U0001D534", + "wopf": "\U0001D568", + "wp": "\u2118", + "wr": "\u2240", + "wreath": "\u2240", + "wscr": "\U0001D4CC", + "xcap": "\u22C2", + "xcirc": "\u25EF", + "xcup": "\u22C3", + "xdtri": "\u25BD", + "xfr": "\U0001D535", + "xhArr": "\u27FA", + "xharr": "\u27F7", + "xi": "\u03BE", + "xlArr": "\u27F8", + "xlarr": "\u27F5", + "xmap": "\u27FC", + "xnis": "\u22FB", + "xodot": "\u2A00", + "xopf": "\U0001D569", + "xoplus": "\u2A01", + "xotime": "\u2A02", + "xrArr": "\u27F9", + "xrarr": "\u27F6", + "xscr": "\U0001D4CD", + "xsqcup": "\u2A06", + "xuplus": "\u2A04", + "xutri": "\u25B3", + "xvee": "\u22C1", + "xwedge": "\u22C0", + "yacute": "\u00FD", + "yacy": "\u044F", + "ycirc": "\u0177", + "ycy": "\u044B", + "yen": "\u00A5", + "yfr": "\U0001D536", + "yicy": "\u0457", + "yopf": "\U0001D56A", + "yscr": "\U0001D4CE", + "yucy": "\u044E", + "yuml": "\u00FF", + "zacute": "\u017A", + "zcaron": "\u017E", + "zcy": "\u0437", + "zdot": "\u017C", + "zeetrf": "\u2128", + "zeta": "\u03B6", + "zfr": "\U0001D537", + "zhcy": "\u0436", + "zigrarr": "\u21DD", + "zopf": "\U0001D56B", + "zscr": "\U0001D4CF", + "zwj": "\u200D", + "zwnj": "\u200C", +} diff --git a/vendor/github.com/mattermost/mattermost-server/utils/markdown/indented_code.go b/vendor/github.com/mattermost/mattermost-server/utils/markdown/indented_code.go new file mode 100644 index 00000000..dc5dce1a --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/utils/markdown/indented_code.go @@ -0,0 +1,98 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package markdown + +import ( + "strings" +) + +type IndentedCodeLine struct { + Indentation int + Range Range +} + +type IndentedCode struct { + blockBase + markdown string + + RawCode []IndentedCodeLine +} + +func (b *IndentedCode) Code() (result string) { + for _, code := range b.RawCode { + result += strings.Repeat(" ", code.Indentation) + b.markdown[code.Range.Position:code.Range.End] + } + return +} + +func (b *IndentedCode) Continuation(indentation int, r Range) *continuation { + if indentation >= 4 { + return &continuation{ + Indentation: indentation - 4, + Remaining: r, + } + } + s := b.markdown[r.Position:r.End] + if strings.TrimSpace(s) == "" { + return &continuation{ + Remaining: r, + } + } + return nil +} + +func (b *IndentedCode) AddLine(indentation int, r Range) bool { + b.RawCode = append(b.RawCode, IndentedCodeLine{ + Indentation: indentation, + Range: r, + }) + return true +} + +func (b *IndentedCode) Close() { + for { + last := b.RawCode[len(b.RawCode)-1] + s := b.markdown[last.Range.Position:last.Range.End] + if strings.TrimRight(s, "\r\n") == "" { + b.RawCode = b.RawCode[:len(b.RawCode)-1] + } else { + break + } + } +} + +func (b *IndentedCode) AllowsBlockStarts() bool { + return false +} + +func indentedCodeStart(markdown string, indentation int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block { + if len(unmatchedBlocks) > 0 { + if _, ok := unmatchedBlocks[len(unmatchedBlocks)-1].(*Paragraph); ok { + return nil + } + } else if len(matchedBlocks) > 0 { + if _, ok := matchedBlocks[len(matchedBlocks)-1].(*Paragraph); ok { + return nil + } + } + + if indentation < 4 { + return nil + } + + s := markdown[r.Position:r.End] + if strings.TrimSpace(s) == "" { + return nil + } + + return []Block{ + &IndentedCode{ + markdown: markdown, + RawCode: []IndentedCodeLine{{ + Indentation: indentation - 4, + Range: r, + }}, + }, + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/utils/markdown/inlines.go b/vendor/github.com/mattermost/mattermost-server/utils/markdown/inlines.go new file mode 100644 index 00000000..a3abccef --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/utils/markdown/inlines.go @@ -0,0 +1,664 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package markdown + +import ( + "container/list" + "strings" + "unicode" + "unicode/utf8" +) + +type Inline interface { + IsInline() bool +} + +type inlineBase struct{} + +func (inlineBase) IsInline() bool { return true } + +type Text struct { + inlineBase + + Text string + Range Range +} + +type CodeSpan struct { + inlineBase + + Code string +} + +type HardLineBreak struct { + inlineBase +} + +type SoftLineBreak struct { + inlineBase +} + +type InlineLinkOrImage struct { + inlineBase + + Children []Inline + + RawDestination Range + + markdown string + rawTitle string +} + +func (i *InlineLinkOrImage) Destination() string { + return Unescape(i.markdown[i.RawDestination.Position:i.RawDestination.End]) +} + +func (i *InlineLinkOrImage) Title() string { + return Unescape(i.rawTitle) +} + +type InlineLink struct { + InlineLinkOrImage +} + +type InlineImage struct { + InlineLinkOrImage +} + +type ReferenceLinkOrImage struct { + inlineBase + *ReferenceDefinition + + Children []Inline +} + +type ReferenceLink struct { + ReferenceLinkOrImage +} + +type ReferenceImage struct { + ReferenceLinkOrImage +} + +type Autolink struct { + inlineBase + + Children []Inline + + RawDestination Range + + markdown string +} + +func (i *Autolink) Destination() string { + destination := Unescape(i.markdown[i.RawDestination.Position:i.RawDestination.End]) + + if strings.HasPrefix(destination, "www") { + destination = "http://" + destination + } + + return destination +} + +type delimiterType int + +const ( + linkOpeningDelimiter delimiterType = iota + imageOpeningDelimiter +) + +type delimiter struct { + Type delimiterType + IsInactive bool + TextNode int + Range Range +} + +type inlineParser struct { + markdown string + ranges []Range + referenceDefinitions []*ReferenceDefinition + + raw string + position int + inlines []Inline + delimiterStack *list.List +} + +func newInlineParser(markdown string, ranges []Range, referenceDefinitions []*ReferenceDefinition) *inlineParser { + return &inlineParser{ + markdown: markdown, + ranges: ranges, + referenceDefinitions: referenceDefinitions, + delimiterStack: list.New(), + } +} + +func (p *inlineParser) parseBackticks() { + count := 1 + for i := p.position + 1; i < len(p.raw) && p.raw[i] == '`'; i++ { + count++ + } + opening := p.raw[p.position : p.position+count] + search := p.position + count + for search < len(p.raw) { + end := strings.Index(p.raw[search:], opening) + if end == -1 { + break + } + if search+end+count < len(p.raw) && p.raw[search+end+count] == '`' { + search += end + count + for search < len(p.raw) && p.raw[search] == '`' { + search++ + } + continue + } + code := strings.Join(strings.Fields(p.raw[p.position+count:search+end]), " ") + p.position = search + end + count + p.inlines = append(p.inlines, &CodeSpan{ + Code: code, + }) + return + } + p.position += len(opening) + absPos := relativeToAbsolutePosition(p.ranges, p.position-len(opening)) + p.inlines = append(p.inlines, &Text{ + Text: opening, + Range: Range{absPos, absPos + len(opening)}, + }) +} + +func (p *inlineParser) parseLineEnding() { + if p.position >= 1 && p.raw[p.position-1] == '\t' { + p.inlines = append(p.inlines, &HardLineBreak{}) + } else if p.position >= 2 && p.raw[p.position-1] == ' ' && (p.raw[p.position-2] == '\t' || p.raw[p.position-1] == ' ') { + p.inlines = append(p.inlines, &HardLineBreak{}) + } else { + p.inlines = append(p.inlines, &SoftLineBreak{}) + } + p.position++ + if p.position < len(p.raw) && p.raw[p.position] == '\n' { + p.position++ + } +} + +func (p *inlineParser) parseEscapeCharacter() { + if p.position+1 < len(p.raw) && isEscapableByte(p.raw[p.position+1]) { + absPos := relativeToAbsolutePosition(p.ranges, p.position+1) + p.inlines = append(p.inlines, &Text{ + Text: string(p.raw[p.position+1]), + Range: Range{absPos, absPos + len(string(p.raw[p.position+1]))}, + }) + p.position += 2 + } else { + absPos := relativeToAbsolutePosition(p.ranges, p.position) + p.inlines = append(p.inlines, &Text{ + Text: `\`, + Range: Range{absPos, absPos + 1}, + }) + p.position++ + } +} + +func (p *inlineParser) parseText() { + if next := strings.IndexAny(p.raw[p.position:], "\r\n\\`&![]wW:"); next == -1 { + absPos := relativeToAbsolutePosition(p.ranges, p.position) + p.inlines = append(p.inlines, &Text{ + Text: strings.TrimRightFunc(p.raw[p.position:], isWhitespace), + Range: Range{absPos, absPos + len(p.raw[p.position:])}, + }) + p.position = len(p.raw) + } else { + absPos := relativeToAbsolutePosition(p.ranges, p.position) + if p.raw[p.position+next] == '\r' || p.raw[p.position+next] == '\n' { + s := strings.TrimRightFunc(p.raw[p.position:p.position+next], isWhitespace) + p.inlines = append(p.inlines, &Text{ + Text: s, + Range: Range{absPos, absPos + len(s)}, + }) + } else { + if next == 0 { + // Always read at least one character since 'w', 'W', and ':' may not actually match another + // type of node + next = 1 + } + + p.inlines = append(p.inlines, &Text{ + Text: p.raw[p.position : p.position+next], + Range: Range{absPos, absPos + next}, + }) + } + p.position += next + } +} + +func (p *inlineParser) parseLinkOrImageDelimiter() { + absPos := relativeToAbsolutePosition(p.ranges, p.position) + if p.raw[p.position] == '[' { + p.inlines = append(p.inlines, &Text{ + Text: "[", + Range: Range{absPos, absPos + 1}, + }) + p.delimiterStack.PushBack(&delimiter{ + Type: linkOpeningDelimiter, + TextNode: len(p.inlines) - 1, + Range: Range{p.position, p.position + 1}, + }) + p.position++ + } else if p.raw[p.position] == '!' && p.position+1 < len(p.raw) && p.raw[p.position+1] == '[' { + p.inlines = append(p.inlines, &Text{ + Text: "![", + Range: Range{absPos, absPos + 2}, + }) + p.delimiterStack.PushBack(&delimiter{ + Type: imageOpeningDelimiter, + TextNode: len(p.inlines) - 1, + Range: Range{p.position, p.position + 2}, + }) + p.position += 2 + } else { + p.inlines = append(p.inlines, &Text{ + Text: "!", + Range: Range{absPos, absPos + 1}, + }) + p.position++ + } +} + +func (p *inlineParser) peekAtInlineLinkDestinationAndTitle(position int, isImage bool) (destination, title Range, end int, ok bool) { + if position >= len(p.raw) || p.raw[position] != '(' { + return + } + position++ + + destinationStart := nextNonWhitespace(p.raw, position) + if destinationStart >= len(p.raw) { + return + } else if p.raw[destinationStart] == ')' { + return Range{destinationStart, destinationStart}, Range{destinationStart, destinationStart}, destinationStart + 1, true + } + + destination, end, ok = parseLinkDestination(p.raw, destinationStart) + if !ok { + return + } + position = end + + if isImage && position < len(p.raw) && isWhitespaceByte(p.raw[position]) { + dimensionsStart := nextNonWhitespace(p.raw, position) + if dimensionsStart >= len(p.raw) { + return + } + + if p.raw[dimensionsStart] == '=' { + // Read optional image dimensions even if we don't use them + _, end, ok = parseImageDimensions(p.raw, dimensionsStart) + if !ok { + return + } + + position = end + } + } + + if position < len(p.raw) && isWhitespaceByte(p.raw[position]) { + titleStart := nextNonWhitespace(p.raw, position) + if titleStart >= len(p.raw) { + return + } else if p.raw[titleStart] == ')' { + return destination, Range{titleStart, titleStart}, titleStart + 1, true + } + + if p.raw[titleStart] == '"' || p.raw[titleStart] == '\'' || p.raw[titleStart] == '(' { + title, end, ok = parseLinkTitle(p.raw, titleStart) + if !ok { + return + } + position = end + } + } + + closingPosition := nextNonWhitespace(p.raw, position) + if closingPosition >= len(p.raw) || p.raw[closingPosition] != ')' { + return Range{}, Range{}, 0, false + } + + return destination, title, closingPosition + 1, true +} + +func (p *inlineParser) referenceDefinition(label string) *ReferenceDefinition { + clean := strings.Join(strings.Fields(label), " ") + for _, d := range p.referenceDefinitions { + if strings.EqualFold(clean, strings.Join(strings.Fields(d.Label()), " ")) { + return d + } + } + return nil +} + +func (p *inlineParser) lookForLinkOrImage() { + for element := p.delimiterStack.Back(); element != nil; element = element.Prev() { + d := element.Value.(*delimiter) + if d.Type != imageOpeningDelimiter && d.Type != linkOpeningDelimiter { + continue + } + if d.IsInactive { + p.delimiterStack.Remove(element) + break + } + + isImage := d.Type == imageOpeningDelimiter + + var inline Inline + + if destination, title, next, ok := p.peekAtInlineLinkDestinationAndTitle(p.position+1, isImage); ok { + destinationMarkdownPosition := relativeToAbsolutePosition(p.ranges, destination.Position) + linkOrImage := InlineLinkOrImage{ + Children: append([]Inline(nil), p.inlines[d.TextNode+1:]...), + RawDestination: Range{destinationMarkdownPosition, destinationMarkdownPosition + destination.End - destination.Position}, + markdown: p.markdown, + rawTitle: p.raw[title.Position:title.End], + } + if d.Type == imageOpeningDelimiter { + inline = &InlineImage{linkOrImage} + } else { + inline = &InlineLink{linkOrImage} + } + p.position = next + } else { + referenceLabel := "" + label, next, hasLinkLabel := parseLinkLabel(p.raw, p.position+1) + if hasLinkLabel && label.End > label.Position { + referenceLabel = p.raw[label.Position:label.End] + } else { + referenceLabel = p.raw[d.Range.End:p.position] + if !hasLinkLabel { + next = p.position + 1 + } + } + if referenceLabel != "" { + if reference := p.referenceDefinition(referenceLabel); reference != nil { + linkOrImage := ReferenceLinkOrImage{ + ReferenceDefinition: reference, + Children: append([]Inline(nil), p.inlines[d.TextNode+1:]...), + } + if d.Type == imageOpeningDelimiter { + inline = &ReferenceImage{linkOrImage} + } else { + inline = &ReferenceLink{linkOrImage} + } + p.position = next + } + } + } + + if inline != nil { + if d.Type == imageOpeningDelimiter { + p.inlines = append(p.inlines[:d.TextNode], inline) + } else { + p.inlines = append(p.inlines[:d.TextNode], inline) + for element := element.Prev(); element != nil; element = element.Prev() { + if d := element.Value.(*delimiter); d.Type == linkOpeningDelimiter { + d.IsInactive = true + } + } + } + p.delimiterStack.Remove(element) + return + } else { + p.delimiterStack.Remove(element) + break + } + } + absPos := relativeToAbsolutePosition(p.ranges, p.position) + p.inlines = append(p.inlines, &Text{ + Text: "]", + Range: Range{absPos, absPos + 1}, + }) + p.position++ +} + +func CharacterReference(ref string) string { + if ref == "" { + return "" + } + if ref[0] == '#' { + if len(ref) < 2 { + return "" + } + n := 0 + if ref[1] == 'X' || ref[1] == 'x' { + if len(ref) < 3 { + return "" + } + for i := 2; i < len(ref); i++ { + if i > 9 { + return "" + } + d := ref[i] + switch { + case d >= '0' && d <= '9': + n = n*16 + int(d-'0') + case d >= 'a' && d <= 'f': + n = n*16 + 10 + int(d-'a') + case d >= 'A' && d <= 'F': + n = n*16 + 10 + int(d-'A') + default: + return "" + } + } + } else { + for i := 1; i < len(ref); i++ { + if i > 8 || ref[i] < '0' || ref[i] > '9' { + return "" + } + n = n*10 + int(ref[i]-'0') + } + } + c := rune(n) + if c == '\u0000' || !utf8.ValidRune(c) { + return string(unicode.ReplacementChar) + } + return string(c) + } + if entity, ok := htmlEntities[ref]; ok { + return entity + } + return "" +} + +func (p *inlineParser) parseCharacterReference() { + absPos := relativeToAbsolutePosition(p.ranges, p.position) + p.position++ + if semicolon := strings.IndexByte(p.raw[p.position:], ';'); semicolon == -1 { + p.inlines = append(p.inlines, &Text{ + Text: "&", + Range: Range{absPos, absPos + 1}, + }) + } else if s := CharacterReference(p.raw[p.position : p.position+semicolon]); s != "" { + p.position += semicolon + 1 + p.inlines = append(p.inlines, &Text{ + Text: s, + Range: Range{absPos, absPos + len(s)}, + }) + } else { + p.inlines = append(p.inlines, &Text{ + Text: "&", + Range: Range{absPos, absPos + 1}, + }) + } +} + +func (p *inlineParser) parseAutolink(c rune) bool { + for element := p.delimiterStack.Back(); element != nil; element = element.Prev() { + d := element.Value.(*delimiter) + if !d.IsInactive { + return false + } + } + + var link Range + if c == ':' { + var ok bool + link, ok = parseURLAutolink(p.raw, p.position) + + if !ok { + return false + } + + // Since the current position is at the colon, we have to rewind the parsing slightly so that + // we don't duplicate the URL scheme + rewind := strings.Index(p.raw[link.Position:link.End], ":") + if rewind != -1 { + lastInline := p.inlines[len(p.inlines)-1] + lastText, ok := lastInline.(*Text) + + if !ok { + // This should never occur since parseURLAutolink will only return a non-empty value + // when the previous text ends in a valid URL protocol which would mean that the previous + // node is a Text node + return false + } + + p.inlines = p.inlines[0 : len(p.inlines)-1] + p.inlines = append(p.inlines, &Text{ + Text: lastText.Text[:len(lastText.Text)-rewind], + Range: Range{lastText.Range.Position, lastText.Range.End - rewind}, + }) + p.position -= rewind + } + } else if c == 'w' || c == 'W' { + var ok bool + link, ok = parseWWWAutolink(p.raw, p.position) + + if !ok { + return false + } + } + + linkMarkdownPosition := relativeToAbsolutePosition(p.ranges, link.Position) + linkRange := Range{linkMarkdownPosition, linkMarkdownPosition + link.End - link.Position} + + p.inlines = append(p.inlines, &Autolink{ + Children: []Inline{ + &Text{ + Text: p.raw[link.Position:link.End], + Range: linkRange, + }, + }, + RawDestination: linkRange, + markdown: p.markdown, + }) + p.position += (link.End - link.Position) + + return true +} + +func (p *inlineParser) Parse() []Inline { + for _, r := range p.ranges { + p.raw += p.markdown[r.Position:r.End] + } + + for p.position < len(p.raw) { + c, _ := utf8.DecodeRuneInString(p.raw[p.position:]) + + switch c { + case '\r', '\n': + p.parseLineEnding() + case '\\': + p.parseEscapeCharacter() + case '`': + p.parseBackticks() + case '&': + p.parseCharacterReference() + case '!', '[': + p.parseLinkOrImageDelimiter() + case ']': + p.lookForLinkOrImage() + case 'w', 'W', ':': + matched := p.parseAutolink(c) + + if !matched { + p.parseText() + } + default: + p.parseText() + } + } + + return p.inlines +} + +func ParseInlines(markdown string, ranges []Range, referenceDefinitions []*ReferenceDefinition) (inlines []Inline) { + return newInlineParser(markdown, ranges, referenceDefinitions).Parse() +} + +func MergeInlineText(inlines []Inline) []Inline { + var ret []Inline + for i, v := range inlines { + // always add first node + if i == 0 { + ret = append(ret, v) + continue + } + // not a text node? nothing to merge + text, ok := v.(*Text) + if !ok { + ret = append(ret, v) + continue + } + // previous node is not a text node? nothing to merge + prevText, ok := ret[len(ret)-1].(*Text) + if !ok { + ret = append(ret, v) + continue + } + // previous node is not right before this one + if prevText.Range.End != text.Range.Position { + ret = append(ret, v) + continue + } + // we have two consecutive text nodes + ret[len(ret)-1] = &Text{ + Text: prevText.Text + text.Text, + Range: Range{prevText.Range.Position, text.Range.End}, + } + } + return ret +} + +func Unescape(markdown string) string { + ret := "" + + position := 0 + for position < len(markdown) { + c, cSize := utf8.DecodeRuneInString(markdown[position:]) + + switch c { + case '\\': + if position+1 < len(markdown) && isEscapableByte(markdown[position+1]) { + ret += string(markdown[position+1]) + position += 2 + } else { + ret += `\` + position++ + } + case '&': + position++ + if semicolon := strings.IndexByte(markdown[position:], ';'); semicolon == -1 { + ret += "&" + } else if s := CharacterReference(markdown[position : position+semicolon]); s != "" { + position += semicolon + 1 + ret += s + } else { + ret += "&" + } + default: + ret += string(c) + position += cSize + } + } + + return ret +} diff --git a/vendor/github.com/mattermost/mattermost-server/utils/markdown/inspect.go b/vendor/github.com/mattermost/mattermost-server/utils/markdown/inspect.go new file mode 100644 index 00000000..80b5bc24 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/utils/markdown/inspect.go @@ -0,0 +1,78 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package markdown + +// Inspect traverses the markdown tree in depth-first order. If f returns true, Inspect invokes f +// recursively for each child of the block or inline, followed by a call of f(nil). +func Inspect(markdown string, f func(interface{}) bool) { + document, referenceDefinitions := Parse(markdown) + InspectBlock(document, func(block Block) bool { + if !f(block) { + return false + } + switch v := block.(type) { + case *Paragraph: + for _, inline := range MergeInlineText(v.ParseInlines(referenceDefinitions)) { + InspectInline(inline, func(inline Inline) bool { + return f(inline) + }) + } + } + return true + }) +} + +// InspectBlock traverses the blocks in depth-first order, starting with block. If f returns true, +// InspectBlock invokes f recursively for each child of the block, followed by a call of f(nil). +func InspectBlock(block Block, f func(Block) bool) { + if !f(block) { + return + } + switch v := block.(type) { + case *Document: + for _, child := range v.Children { + InspectBlock(child, f) + } + case *List: + for _, child := range v.Children { + InspectBlock(child, f) + } + case *ListItem: + for _, child := range v.Children { + InspectBlock(child, f) + } + case *BlockQuote: + for _, child := range v.Children { + InspectBlock(child, f) + } + } + f(nil) +} + +// InspectInline traverses the blocks in depth-first order, starting with block. If f returns true, +// InspectInline invokes f recursively for each child of the block, followed by a call of f(nil). +func InspectInline(inline Inline, f func(Inline) bool) { + if !f(inline) { + return + } + switch v := inline.(type) { + case *InlineImage: + for _, child := range v.Children { + InspectInline(child, f) + } + case *InlineLink: + for _, child := range v.Children { + InspectInline(child, f) + } + case *ReferenceImage: + for _, child := range v.Children { + InspectInline(child, f) + } + case *ReferenceLink: + for _, child := range v.Children { + InspectInline(child, f) + } + } + f(nil) +} diff --git a/vendor/github.com/mattermost/mattermost-server/utils/markdown/lines.go b/vendor/github.com/mattermost/mattermost-server/utils/markdown/lines.go new file mode 100644 index 00000000..a38b5164 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/utils/markdown/lines.go @@ -0,0 +1,27 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package markdown + +type Line struct { + Range +} + +func ParseLines(markdown string) (lines []Line) { + lineStartPosition := 0 + isAfterCarriageReturn := false + for position, r := range markdown { + if r == '\n' { + lines = append(lines, Line{Range{lineStartPosition, position + 1}}) + lineStartPosition = position + 1 + } else if isAfterCarriageReturn { + lines = append(lines, Line{Range{lineStartPosition, position}}) + lineStartPosition = position + } + isAfterCarriageReturn = r == '\r' + } + if lineStartPosition < len(markdown) { + lines = append(lines, Line{Range{lineStartPosition, len(markdown)}}) + } + return +} diff --git a/vendor/github.com/mattermost/mattermost-server/utils/markdown/links.go b/vendor/github.com/mattermost/mattermost-server/utils/markdown/links.go new file mode 100644 index 00000000..9f3128c4 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/utils/markdown/links.go @@ -0,0 +1,184 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package markdown + +import ( + "unicode/utf8" +) + +func parseLinkDestination(markdown string, position int) (raw Range, next int, ok bool) { + if position >= len(markdown) { + return + } + + if markdown[position] == '<' { + isEscaped := false + + for offset, c := range []byte(markdown[position+1:]) { + if isEscaped { + isEscaped = false + if isEscapableByte(c) { + continue + } + } + + if c == '\\' { + isEscaped = true + } else if c == '<' { + break + } else if c == '>' { + return Range{position + 1, position + 1 + offset}, position + 1 + offset + 1, true + } else if isWhitespaceByte(c) { + break + } + } + } + + openCount := 0 + isEscaped := false + for offset, c := range []byte(markdown[position:]) { + if isEscaped { + isEscaped = false + if isEscapableByte(c) { + continue + } + } + + switch c { + case '\\': + isEscaped = true + case '(': + openCount++ + case ')': + if openCount < 1 { + return Range{position, position + offset}, position + offset, true + } + openCount-- + default: + if isWhitespaceByte(c) { + return Range{position, position + offset}, position + offset, true + } + } + } + return Range{position, len(markdown)}, len(markdown), true +} + +func parseLinkTitle(markdown string, position int) (raw Range, next int, ok bool) { + if position >= len(markdown) { + return + } + + originalPosition := position + + var closer byte + switch markdown[position] { + case '"', '\'': + closer = markdown[position] + case '(': + closer = ')' + default: + return + } + position++ + + for position < len(markdown) { + switch markdown[position] { + case '\\': + position++ + if position < len(markdown) && isEscapableByte(markdown[position]) { + position++ + } + case closer: + return Range{originalPosition + 1, position}, position + 1, true + default: + position++ + } + } + + return +} + +func parseLinkLabel(markdown string, position int) (raw Range, next int, ok bool) { + if position >= len(markdown) || markdown[position] != '[' { + return + } + + originalPosition := position + position++ + + for position < len(markdown) { + switch markdown[position] { + case '\\': + position++ + if position < len(markdown) && isEscapableByte(markdown[position]) { + position++ + } + case '[': + return + case ']': + if position-originalPosition >= 1000 && utf8.RuneCountInString(markdown[originalPosition:position]) >= 1000 { + return + } + return Range{originalPosition + 1, position}, position + 1, true + default: + position++ + } + } + + return +} + +// As a non-standard feature, we allow image links to specify dimensions of the image by adding "=WIDTHxHEIGHT" +// after the image destination but before the image title like ![alt](http://example.com/image.png =100x200 "title"). +// Both width and height are optional, but at least one of them must be specified. +func parseImageDimensions(markdown string, position int) (raw Range, next int, ok bool) { + if position >= len(markdown) { + return + } + + originalPosition := position + + // Read = + position += 1 + if position >= len(markdown) { + return + } + + // Read width + hasWidth := false + for isNumericByte(markdown[position]) { + hasWidth = true + position += 1 + } + + // Look for early end of dimensions + if isWhitespaceByte(markdown[position]) || markdown[position] == ')' { + return Range{originalPosition, position - 1}, position, true + } + + // Read the x + if markdown[position] != 'x' && markdown[position] != 'X' { + return + } + position += 1 + + // Read height + hasHeight := false + for isNumericByte(markdown[position]) { + hasHeight = true + position += 1 + } + + // Make sure the there's no trailing characters + if !isWhitespaceByte(markdown[position]) && markdown[position] != ')' { + return + } + + if !hasWidth && !hasHeight { + // At least one of width or height is required + return + } + + return Range{originalPosition, position - 1}, position, true +} diff --git a/vendor/github.com/mattermost/mattermost-server/utils/markdown/list.go b/vendor/github.com/mattermost/mattermost-server/utils/markdown/list.go new file mode 100644 index 00000000..aea71156 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/utils/markdown/list.go @@ -0,0 +1,220 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package markdown + +import ( + "strings" +) + +type ListItem struct { + blockBase + markdown string + hasTrailingBlankLine bool + hasBlankLineBetweenChildren bool + + Indentation int + Children []Block +} + +func (b *ListItem) Continuation(indentation int, r Range) *continuation { + s := b.markdown[r.Position:r.End] + if strings.TrimSpace(s) == "" { + if b.Children == nil { + return nil + } + return &continuation{ + Remaining: r, + } + } + if indentation < b.Indentation { + return nil + } + return &continuation{ + Indentation: indentation - b.Indentation, + Remaining: r, + } +} + +func (b *ListItem) AddChild(openBlocks []Block) []Block { + b.Children = append(b.Children, openBlocks[0]) + if b.hasTrailingBlankLine { + b.hasBlankLineBetweenChildren = true + } + b.hasTrailingBlankLine = false + return openBlocks +} + +func (b *ListItem) AddLine(indentation int, r Range) bool { + isBlank := strings.TrimSpace(b.markdown[r.Position:r.End]) == "" + if isBlank { + b.hasTrailingBlankLine = true + } + return false +} + +func (b *ListItem) HasTrailingBlankLine() bool { + return b.hasTrailingBlankLine || (len(b.Children) > 0 && b.Children[len(b.Children)-1].HasTrailingBlankLine()) +} + +func (b *ListItem) isLoose() bool { + if b.hasBlankLineBetweenChildren { + return true + } + for i, child := range b.Children { + if i < len(b.Children)-1 && child.HasTrailingBlankLine() { + return true + } + } + return false +} + +type List struct { + blockBase + markdown string + hasTrailingBlankLine bool + hasBlankLineBetweenChildren bool + + IsLoose bool + IsOrdered bool + OrderedStart int + BulletOrDelimiter byte + Children []*ListItem +} + +func (b *List) Continuation(indentation int, r Range) *continuation { + s := b.markdown[r.Position:r.End] + if strings.TrimSpace(s) == "" { + return &continuation{ + Remaining: r, + } + } + return &continuation{ + Indentation: indentation, + Remaining: r, + } +} + +func (b *List) AddChild(openBlocks []Block) []Block { + if item, ok := openBlocks[0].(*ListItem); ok { + b.Children = append(b.Children, item) + if b.hasTrailingBlankLine { + b.hasBlankLineBetweenChildren = true + } + b.hasTrailingBlankLine = false + return openBlocks + } else if list, ok := openBlocks[0].(*List); ok { + if len(list.Children) == 1 && list.IsOrdered == b.IsOrdered && list.BulletOrDelimiter == b.BulletOrDelimiter { + return b.AddChild(openBlocks[1:]) + } + } + return nil +} + +func (b *List) AddLine(indentation int, r Range) bool { + isBlank := strings.TrimSpace(b.markdown[r.Position:r.End]) == "" + if isBlank { + b.hasTrailingBlankLine = true + } + return false +} + +func (b *List) HasTrailingBlankLine() bool { + return b.hasTrailingBlankLine || (len(b.Children) > 0 && b.Children[len(b.Children)-1].HasTrailingBlankLine()) +} + +func (b *List) isLoose() bool { + if b.hasBlankLineBetweenChildren { + return true + } + for i, child := range b.Children { + if child.isLoose() || (i < len(b.Children)-1 && child.HasTrailingBlankLine()) { + return true + } + } + return false +} + +func (b *List) Close() { + b.IsLoose = b.isLoose() +} + +func parseListMarker(markdown string, r Range) (success, isOrdered bool, orderedStart int, bulletOrDelimiter byte, markerWidth int, remaining Range) { + digits := 0 + n := 0 + for i := r.Position; i < r.End && markdown[i] >= '0' && markdown[i] <= '9'; i++ { + digits++ + n = n*10 + int(markdown[i]-'0') + } + if digits > 0 { + if digits > 9 || r.Position+digits >= r.End { + return + } + next := markdown[r.Position+digits] + if next != '.' && next != ')' { + return + } + return true, true, n, next, digits + 1, Range{r.Position + digits + 1, r.End} + } + if r.Position >= r.End { + return + } + next := markdown[r.Position] + if next != '-' && next != '+' && next != '*' { + return + } + return true, false, 0, next, 1, Range{r.Position + 1, r.End} +} + +func listStart(markdown string, indent int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block { + afterList := false + if len(matchedBlocks) > 0 { + _, afterList = matchedBlocks[len(matchedBlocks)-1].(*List) + } + if !afterList && indent > 3 { + return nil + } + + success, isOrdered, orderedStart, bulletOrDelimiter, markerWidth, remaining := parseListMarker(markdown, r) + if !success { + return nil + } + + isBlank := strings.TrimSpace(markdown[remaining.Position:remaining.End]) == "" + if len(matchedBlocks) > 0 && len(unmatchedBlocks) == 0 { + if _, ok := matchedBlocks[len(matchedBlocks)-1].(*Paragraph); ok { + if isBlank || (isOrdered && orderedStart != 1) { + return nil + } + } + } + + indentAfterMarker, indentBytesAfterMarker := countIndentation(markdown, remaining) + if !isBlank && indentAfterMarker < 1 { + return nil + } + + remaining = Range{remaining.Position + indentBytesAfterMarker, remaining.End} + consumedIndentAfterMarker := indentAfterMarker + if isBlank || indentAfterMarker >= 5 { + consumedIndentAfterMarker = 1 + } + + listItem := &ListItem{ + markdown: markdown, + Indentation: indent + markerWidth + consumedIndentAfterMarker, + } + list := &List{ + markdown: markdown, + IsOrdered: isOrdered, + OrderedStart: orderedStart, + BulletOrDelimiter: bulletOrDelimiter, + Children: []*ListItem{listItem}, + } + ret := []Block{list, listItem} + if descendants := blockStartOrParagraph(markdown, indentAfterMarker-consumedIndentAfterMarker, remaining, nil, nil); descendants != nil { + listItem.Children = append(listItem.Children, descendants[0]) + ret = append(ret, descendants...) + } + return ret +} diff --git a/vendor/github.com/mattermost/mattermost-server/utils/markdown/markdown.go b/vendor/github.com/mattermost/mattermost-server/utils/markdown/markdown.go new file mode 100644 index 00000000..57b10f3f --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/utils/markdown/markdown.go @@ -0,0 +1,148 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +// This package implements a parser for the subset of the CommonMark spec necessary for us to do +// server-side processing. It is not a full implementation and lacks many features. But it is +// complete enough to efficiently and accurately allow us to do what we need to like rewrite image +// URLs for proxying. +package markdown + +import ( + "strings" +) + +func isEscapable(c rune) bool { + return c > ' ' && (c < '0' || (c > '9' && (c < 'A' || (c > 'Z' && (c < 'a' || (c > 'z' && c <= '~')))))) +} + +func isEscapableByte(c byte) bool { + return isEscapable(rune(c)) +} + +func isWhitespace(c rune) bool { + switch c { + case ' ', '\t', '\n', '\u000b', '\u000c', '\r': + return true + default: + return false + } +} + +func isWhitespaceByte(c byte) bool { + return isWhitespace(rune(c)) +} + +func isNumeric(c rune) bool { + return c >= '0' && c <= '9' +} + +func isNumericByte(c byte) bool { + return isNumeric(rune(c)) +} + +func isHex(c rune) bool { + return isNumeric(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') +} + +func isHexByte(c byte) bool { + return isHex(rune(c)) +} + +func isAlphanumeric(c rune) bool { + return isNumeric(c) || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') +} + +func isAlphanumericByte(c byte) bool { + return isAlphanumeric(rune(c)) +} + +func nextNonWhitespace(markdown string, position int) int { + for offset, c := range []byte(markdown[position:]) { + if !isWhitespaceByte(c) { + return position + offset + } + } + return len(markdown) +} + +func nextLine(markdown string, position int) (linePosition int, skippedNonWhitespace bool) { + for i := position; i < len(markdown); i++ { + c := markdown[i] + if c == '\r' { + if i+1 < len(markdown) && markdown[i+1] == '\n' { + return i + 2, skippedNonWhitespace + } + return i + 1, skippedNonWhitespace + } else if c == '\n' { + return i + 1, skippedNonWhitespace + } else if !isWhitespaceByte(c) { + skippedNonWhitespace = true + } + } + return len(markdown), skippedNonWhitespace +} + +func countIndentation(markdown string, r Range) (spaces, bytes int) { + for i := r.Position; i < r.End; i++ { + if markdown[i] == ' ' { + spaces++ + bytes++ + } else if markdown[i] == '\t' { + spaces += 4 + bytes++ + } else { + break + } + } + return +} + +func trimLeftSpace(markdown string, r Range) Range { + s := markdown[r.Position:r.End] + trimmed := strings.TrimLeftFunc(s, isWhitespace) + return Range{r.Position, r.End - (len(s) - len(trimmed))} +} + +func trimRightSpace(markdown string, r Range) Range { + s := markdown[r.Position:r.End] + trimmed := strings.TrimRightFunc(s, isWhitespace) + return Range{r.Position, r.End - (len(s) - len(trimmed))} +} + +func relativeToAbsolutePosition(ranges []Range, position int) int { + rem := position + for _, r := range ranges { + l := r.End - r.Position + if rem < l { + return r.Position + rem + } + rem -= l + } + if len(ranges) == 0 { + return 0 + } + return ranges[len(ranges)-1].End +} + +func trimBytesFromRanges(ranges []Range, bytes int) (result []Range) { + rem := bytes + for _, r := range ranges { + if rem == 0 { + result = append(result, r) + continue + } + l := r.End - r.Position + if rem < l { + result = append(result, Range{r.Position + rem, r.End}) + rem = 0 + continue + } + rem -= l + } + return +} + +func Parse(markdown string) (*Document, []*ReferenceDefinition) { + lines := ParseLines(markdown) + return ParseBlocks(markdown, lines) +} diff --git a/vendor/github.com/mattermost/mattermost-server/utils/markdown/paragraph.go b/vendor/github.com/mattermost/mattermost-server/utils/markdown/paragraph.go new file mode 100644 index 00000000..6a40fdf8 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/utils/markdown/paragraph.go @@ -0,0 +1,71 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package markdown + +import ( + "strings" +) + +type Paragraph struct { + blockBase + markdown string + + Text []Range + ReferenceDefinitions []*ReferenceDefinition +} + +func (b *Paragraph) ParseInlines(referenceDefinitions []*ReferenceDefinition) []Inline { + return ParseInlines(b.markdown, b.Text, referenceDefinitions) +} + +func (b *Paragraph) Continuation(indentation int, r Range) *continuation { + s := b.markdown[r.Position:r.End] + if strings.TrimSpace(s) == "" { + return nil + } + return &continuation{ + Indentation: indentation, + Remaining: r, + } +} + +func (b *Paragraph) Close() { + for { + for i := 0; i < len(b.Text); i++ { + b.Text[i] = trimLeftSpace(b.markdown, b.Text[i]) + if b.Text[i].Position < b.Text[i].End { + break + } + } + + if len(b.Text) == 0 || b.Text[0].Position < b.Text[0].End && b.markdown[b.Text[0].Position] != '[' { + break + } + + definition, remaining := parseReferenceDefinition(b.markdown, b.Text) + if definition == nil { + break + } + b.ReferenceDefinitions = append(b.ReferenceDefinitions, definition) + b.Text = remaining + } + + for i := len(b.Text) - 1; i >= 0; i-- { + b.Text[i] = trimRightSpace(b.markdown, b.Text[i]) + if b.Text[i].Position < b.Text[i].End { + break + } + } +} + +func newParagraph(markdown string, r Range) *Paragraph { + s := markdown[r.Position:r.End] + if strings.TrimSpace(s) == "" { + return nil + } + return &Paragraph{ + markdown: markdown, + Text: []Range{r}, + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/utils/markdown/reference_definition.go b/vendor/github.com/mattermost/mattermost-server/utils/markdown/reference_definition.go new file mode 100644 index 00000000..e2d0be35 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/utils/markdown/reference_definition.go @@ -0,0 +1,75 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package markdown + +type ReferenceDefinition struct { + RawDestination Range + + markdown string + rawLabel string + rawTitle string +} + +func (d *ReferenceDefinition) Destination() string { + return Unescape(d.markdown[d.RawDestination.Position:d.RawDestination.End]) +} + +func (d *ReferenceDefinition) Label() string { + return d.rawLabel +} + +func (d *ReferenceDefinition) Title() string { + return Unescape(d.rawTitle) +} + +func parseReferenceDefinition(markdown string, ranges []Range) (*ReferenceDefinition, []Range) { + raw := "" + for _, r := range ranges { + raw += markdown[r.Position:r.End] + } + + label, next, ok := parseLinkLabel(raw, 0) + if !ok { + return nil, nil + } + position := next + + if position >= len(raw) || raw[position] != ':' { + return nil, nil + } + position++ + + destination, next, ok := parseLinkDestination(raw, nextNonWhitespace(raw, position)) + if !ok { + return nil, nil + } + position = next + + absoluteDestination := relativeToAbsolutePosition(ranges, destination.Position) + ret := &ReferenceDefinition{ + RawDestination: Range{absoluteDestination, absoluteDestination + destination.End - destination.Position}, + markdown: markdown, + rawLabel: raw[label.Position:label.End], + } + + if position < len(raw) && isWhitespaceByte(raw[position]) { + title, next, ok := parseLinkTitle(raw, nextNonWhitespace(raw, position)) + if !ok { + if nextLine, skippedNonWhitespace := nextLine(raw, position); !skippedNonWhitespace { + return ret, trimBytesFromRanges(ranges, nextLine) + } + return nil, nil + } + if nextLine, skippedNonWhitespace := nextLine(raw, next); !skippedNonWhitespace { + ret.rawTitle = raw[title.Position:title.End] + return ret, trimBytesFromRanges(ranges, nextLine) + } + } + + if nextLine, skippedNonWhitespace := nextLine(raw, position); !skippedNonWhitespace { + return ret, trimBytesFromRanges(ranges, nextLine) + } + + return nil, nil +} diff --git a/vendor/github.com/mattermost/platform/NOTICE.txt b/vendor/github.com/mattermost/platform/NOTICE.txt deleted file mode 100644 index f68ef872..00000000 --- a/vendor/github.com/mattermost/platform/NOTICE.txt +++ /dev/null @@ -1,2935 +0,0 @@ -Mattermost Server -© 2015-present Mattermost, Inc. All Rights Reserved. See LICENSE.txt for license information. - -NOTICES: --------- - -This document includes a list of open source components used in Mattermost Server, including those that have been modified. - ---- - -This product contains a modified portion of 'jquery-dragster', a drag and drop listener -by Jan Martin. - -* HOMEPAGE: - * https://github.com/catmanjan/jquery-dragster - -* LICENSE: - -The MIT License (MIT) - -Copyright (c) 2015 Jan Martin - -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. - ---- - -This product contains a modified portion of 'golang-lru', a golang LRU cache by hashicorp, -based on Groupcache by Google Inc. - -* HOMEPAGE: - * https://github.com/hashicorp/golang-lru - -* LICENSE: - -This Source Code Form is subject to the terms of the Mozilla Public License, v.2.0. If a -copy of the MPL was not distributed with this file, You can obtain one at -http://mozilla.org/MPL/2.0/. - ---- - -This product contains a modified portion of 'golang-freetype', a port of the Freetype -font rasterizer (www.freetype.org) to the Go programming. - -Portions of this software are copyright © 2010 The FreeType Project (www.freetype.org). -All rights reserved. - -* HOMEPAGE: - * http://www.freetype.org - -* LICENSE: - - The FreeType Project LICENSE - ---------------------------- - - 2006-Jan-27 - - Copyright 1996-2002, 2006 by - David Turner, Robert Wilhelm, and Werner Lemberg - - - -Introduction -============ - - The FreeType Project is distributed in several archive packages; - some of them may contain, in addition to the FreeType font engine, - various tools and contributions which rely on, or relate to, the - FreeType Project. - - This license applies to all files found in such packages, and - which do not fall under their own explicit license. The license - affects thus the FreeType font engine, the test programs, - documentation and makefiles, at the very least. - - This license was inspired by the BSD, Artistic, and IJG - (Independent JPEG Group) licenses, which all encourage inclusion - and use of free software in commercial and freeware products - alike. As a consequence, its main points are that: - - o We don't promise that this software works. However, we will be - interested in any kind of bug reports. (`as is' distribution) - - o You can use this software for whatever you want, in parts or - full form, without having to pay us. (`royalty-free' usage) - - o You may not pretend that you wrote this software. If you use - it, or only parts of it, in a program, you must acknowledge - somewhere in your documentation that you have used the - FreeType code. (`credits') - - We specifically permit and encourage the inclusion of this - software, with or without modifications, in commercial products. - We disclaim all warranties covering The FreeType Project and - assume no liability related to The FreeType Project. - - - Finally, many people asked us for a preferred form for a - credit/disclaimer to use in compliance with this license. We thus - encourage you to use the following text: - - """ - Portions of this software are copyright � The FreeType - Project (www.freetype.org). All rights reserved. - """ - - Please replace with the value from the FreeType version you - actually use. - - -Legal Terms -=========== - -0. Definitions --------------- - - Throughout this license, the terms `package', `FreeType Project', - and `FreeType archive' refer to the set of files originally - distributed by the authors (David Turner, Robert Wilhelm, and - Werner Lemberg) as the `FreeType Project', be they named as alpha, - beta or final release. - - `You' refers to the licensee, or person using the project, where - `using' is a generic term including compiling the project's source - code as well as linking it to form a `program' or `executable'. - This program is referred to as `a program using the FreeType - engine'. - - This license applies to all files distributed in the original - FreeType Project, including all source code, binaries and - documentation, unless otherwise stated in the file in its - original, unmodified form as distributed in the original archive. - If you are unsure whether or not a particular file is covered by - this license, you must contact us to verify this. - - The FreeType Project is copyright (C) 1996-2000 by David Turner, - Robert Wilhelm, and Werner Lemberg. All rights reserved except as - specified below. - -1. No Warranty --------------- - - THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY - KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS - BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO - USE, OF THE FREETYPE PROJECT. - -2. Redistribution ------------------ - - This license grants a worldwide, royalty-free, perpetual and - irrevocable right and license to use, execute, perform, compile, - display, copy, create derivative works of, distribute and - sublicense the FreeType Project (in both source and object code - forms) and derivative works thereof for any purpose; and to - authorize others to exercise some or all of the rights granted - herein, subject to the following conditions: - - o Redistribution of source code must retain this license file - (`FTL.TXT') unaltered; any additions, deletions or changes to - the original files must be clearly indicated in accompanying - documentation. The copyright notices of the unaltered, - original files must be preserved in all copies of source - files. - - o Redistribution in binary form must provide a disclaimer that - states that the software is based in part of the work of the - FreeType Team, in the distribution documentation. We also - encourage you to put an URL to the FreeType web page in your - documentation, though this isn't mandatory. - - These conditions apply to any software derived from or based on - the FreeType Project, not just the unmodified files. If you use - our work, you must acknowledge us. However, no fee need be paid - to us. - -3. Advertising --------------- - - Neither the FreeType authors and contributors nor you shall use - the name of the other for commercial, advertising, or promotional - purposes without specific prior written permission. - - We suggest, but do not require, that you use one or more of the - following phrases to refer to this software in your documentation - or advertising materials: `FreeType Project', `FreeType Engine', - `FreeType library', or `FreeType Distribution'. - - As you have not signed this license, you are not required to - accept it. However, as the FreeType Project is copyrighted - material, only this license, or another one contracted with the - authors, grants you the right to use, distribute, and modify it. - Therefore, by using, distributing, or modifying the FreeType - Project, you indicate that you understand and accept all the terms - of this license. - -4. Contacts ------------ - - There are two mailing lists related to FreeType: - - o freetype@nongnu.org - - Discusses general use and applications of FreeType, as well as - future and wanted additions to the library and distribution. - If you are looking for support, start in this list if you - haven't found anything to help you in the documentation. - - o freetype-devel@nongnu.org - - Discusses bugs, as well as engine internals, design issues, - specific licenses, porting, etc. - - Our home page can be found at - - http://www.freetype.org - ---- end of FTL.TXT --- - ---- - -This product contains a modified portion of 'gemoji', a collection of emoji images and names by Apple Inc. and other contributors. - -* HOMEPAGE: - * https://github.com/github/gemoji/blob/master/LICENSE - -* LICENSE: - -octocat, squirrel, shipit -Copyright (c) 2013 GitHub Inc. All rights reserved. - -bowtie, neckbeard, fu -Copyright (c) 2013 37signals, LLC. All rights reserved. - -feelsgood, finnadie, goberserk, godmode, hurtrealbad, rage 1-4, suspect -Copyright (c) 2013 id Software. All rights reserved. - -trollface -Copyright (c) 2013 whynne@deviantart. All rights reserved. - -All other images -Copyright (c) 2013 Apple Inc. All rights reserved. - -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. - ---- - -This product contains a modified portion of 'goexif', which provides decoding of basic exif and tiff encoded data. - -by Robert Carlsen & Contributors - -* HOMEPAGE: - * https://github.com/rwcarlsen/goexif - -* LICENSE: - -Copyright (c) 2012, Robert Carlsen & Contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -This product contains a modified portion of 'graphics-go', an implementation of basic image manipulation operations in the Go programming language. - -by The Graphics-Go Authors - -* HOMEPAGE: - * https://code.google.com/p/graphics-go/ - -* LICENSE: - -Copyright (c) 2011 The Graphics-Go Authors. All rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: -Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -This product contains a modified portion of 'babel-es6-polyfill', a pure ES6 polyfill for Babel that only includes standardised features. - -by Jason Berry - -* HOMEPAGE: - * https://github.com/JasonBerry/babel-es6-polyfill - -* LICENSE: - -Copyright (c) 2015 Jason Berry - -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. - ---- - -This product contains a modified portion of 'go-ldap', basic LDAP v3 functionality for the GO programming language. - -by The Go Authors - -* HOMEPAGE: - * https://github.com/go-ldap/ldap - -* LICENSE: - -Copyright (c) 2012 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -This product contains a modified portion of 'Google Fonts', various Open Source fonts for use on the web. - -by Google Inc. - -* HOMEPAGE: - * https://www.google.com/fonts - -* LICENSE: - -# Code licensed under the Apache 2.0 License: - -Copyright 2011 Google Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -# Fonts licensed under CC-BY 3.0: - -CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE. -License - -THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. - -BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. - -1. Definitions - -"Adaptation" means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered an Adaptation for the purpose of this License. -"Collection" means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined above) for the purposes of this License. -"Distribute" means to make available to the public the original and copies of the Work or Adaptation, as appropriate, through sale or other transfer of ownership. -"Licensor" means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License. -"Original Author" means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast. -"Work" means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three-dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work. -"You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation. -"Publicly Perform" means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images. -"Reproduce" means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium. - -2. Fair Dealing Rights. Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws. - -3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below: - -to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections; -to create and Reproduce Adaptations provided that any such Adaptation, including any translation in any medium, takes reasonable steps to clearly label, demarcate or otherwise identify that changes were made to the original Work. For example, a translation could be marked "The original work was translated from English to Spanish," or a modification could indicate "The original work has been modified."; -to Distribute and Publicly Perform the Work including as incorporated in Collections; and, -to Distribute and Publicly Perform Adaptations. -For the avoidance of doubt: - -Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; -Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor waives the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; and, -Voluntary License Schemes. The Licensor waives the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License. -The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. Subject to Section 8(f), all rights not expressly granted by Licensor are hereby reserved. - -4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions: - -You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(b), as requested. If You create an Adaptation, upon notice from any Licensor You must, to the extent practicable, remove from the Adaptation any credit as required by Section 4(b), as requested. -If You Distribute, or Publicly Perform the Work or any Adaptations or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and (iv) , consistent with Section 3(b), in the case of an Adaptation, a credit identifying the use of the Work in the Adaptation (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). The credit required by this Section 4 (b) may be implemented in any reasonable manner; provided, however, that in the case of a Adaptation or Collection, at a minimum such credit will appear, if a credit for all contributing authors of the Adaptation or Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties. -Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Adaptations or Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author's honor or reputation. Licensor agrees that in those jurisdictions (e.g. Japan), in which any exercise of the right granted in Section 3(b) of this License (the right to make Adaptations) would be deemed to be a distortion, mutilation, modification or other derogatory action prejudicial to the Original Author's honor and reputation, the Licensor will waive or not assert, as appropriate, this Section, to the fullest extent permitted by the applicable national law, to enable You to reasonably exercise Your right under Section 3(b) of this License (right to make Adaptations) but not otherwise. - -5. Representations, Warranties and Disclaimer - -UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. - -6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -7. Termination - -This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Adaptations or Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License. -Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above. - -8. Miscellaneous - -Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License. -Each time You Distribute or Publicly Perform an Adaptation, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License. -If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. -No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent. -This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You. -The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law. - -Creative Commons Notice - -Creative Commons is not a party to this License, and makes no warranty whatsoever in connection with the Work. Creative Commons will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor. - -Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, Creative Commons does not authorize the use by either party of the trademark "Creative Commons" or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons' then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. For the avoidance of doubt, this trademark restriction does not form part of this License. - -Creative Commons may be contacted at https://creativecommons.org/. - ---- - -This product contains a modified portion of 'go-i18n', a Go package and a command that translates Go programs into multiple languages -by Nick Snyder. - -* HOMEPAGE: - * https://github.com/nicksnyder/go-i18n - -* LICENSE: - -Copyright (c) 2014 Nick Snyder https://github.com/nicksnyder - -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. - ---- - -This product contains a modified portion of 'dgoogauth', a go implementation of the Google Authenticator library by Damian Gryski. - -* HOMEPAGE: - * https://github.com/dgryski/dgoogauth - -* LICENSE: - -Copyright 2012 Damian Gryski - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - ---- - -This product contains a modified portion of 'fbjs', a collection of JavaScript utilities by Facebook, Inc. - -* HOMEPAGE: - * https://github.com/facebook/fbjs - -* LICENSE: - -BSD License - -For fbjs software - -Copyright (c) 2013-present, Facebook, Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - * Neither the name Facebook nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -This product contains a modified portion of 'marked', a full-featured markdown parser and compiler, written in JavaScript. Built for speed by Christopher Jeffrey. - -* HOMEPAGE: - * https://github.com/chjj/marked - -* LICENSE: - -The MIT License (MIT) - -Copyright (c) 2011-2014, Christopher Jeffrey. - -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. -No attribution is required by products that make use of this software. - ---- - -This product contains a modified portion of 'gziphandler', a tiny Go package which wraps HTTP handlers to transparently gzip the response body, for clients which support it by The New York Times Company. - -* HOMEPAGE: - * https://github.com/NYTimes/gziphandler - -* LICENSE: - -Apache License 2.0 - -Copyright (c) 2015 The New York Times Company - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this library except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - ---- - -This product contains a modified portion of 'handlers', a collection of handlers (aka "HTTP middleware") for use with Go's net/http package (or any framework supporting http.Handler) by The Gorilla Handlers Authors. - -* HOMEPAGE: - * https://github.com/gorilla/handlers - -* LICENSE: - -Copyright (c) 2013 The Gorilla Handlers Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -This product contains a modified portion of 'log4go', an unmaintained fork, left only so it doesn't break imports by Kyle Lemons. - -* HOMEPAGE: - * https://github.com/alecthomas/log4go - -* LICENSE: - -Copyright (c) 2010, Kyle Lemons . All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -This product contains a modified portion of 'manners', a package imaging providing basic image manipulation functions (resize, rotate, flip, crop, etc.) by Grigory Dryapak. - -* HOMEPAGE: - * https://github.com/disintegration/imaging - -* LICENSE: - -The MIT License (MIT) - -Copyright (c) 2012-2014 Grigory Dryapak - -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. - ---- - -This product contains a modified portion of 'gorp', to save time, minimize the drudgery of getting data in and out of the database, and help code focus on algorithms, not infrastructure by James Cooper. - -* HOMEPAGE: - * https://github.com/go-gorp/gorp - -* LICENSE: - -The MIT License (MIT) - -Copyright (c) 2012 James Cooper - -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. - ---- - -This product contains a modified portion of 'mysql', a lightweight and fast MySQL driver for Go's (golang) database/sql package by Mozilla Public. - -* HOMEPAGE: - * https://github.com/go-sql-driver/mysql - -* LICENSE: - -Mozilla Public License Version 2.0 -================================== - -1. Definitions --------------- - -1.1. "Contributor" - means each individual or legal entity that creates, contributes to - the creation of, or owns Covered Software. - -1.2. "Contributor Version" - means the combination of the Contributions of others (if any) used - by a Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" - means Covered Software of a particular Contributor. - -1.4. "Covered Software" - means Source Code Form to which the initial Contributor has attached - the notice in Exhibit A, the Executable Form of such Source Code - Form, and Modifications of such Source Code Form, in each case - including portions thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - (a) that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or - - (b) that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the - terms of a Secondary License. - -1.6. "Executable Form" - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - means a work that combines Covered Software with other material, in - a separate file or files, that is not Covered Software. - -1.8. "License" - means this document. - -1.9. "Licensable" - means having the right to grant, to the maximum extent possible, - whether at the time of the initial grant or subsequently, any and - all of the rights conveyed by this License. - -1.10. "Modifications" - means any of the following: - - (a) any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered - Software; or - - (b) any new file in Source Code Form that contains any Covered - Software. - -1.11. "Patent Claims" of a Contributor - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the - License, by the making, using, selling, offering for sale, having - made, import, or transfer of either its Contributions or its - Contributor Version. - -1.12. "Secondary License" - means either the GNU General Public License, Version 2.0, the GNU - Lesser General Public License, Version 2.1, the GNU Affero General - Public License, Version 3.0, or any later versions of those - licenses. - -1.13. "Source Code Form" - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - means an individual or a legal entity exercising rights under this - License. For legal entities, "You" includes any entity that - controls, is controlled by, or is under common control with You. For - purposes of this definition, "control" means (a) the power, direct - or indirect, to cause the direction or management of such entity, - whether by contract or otherwise, or (b) ownership of more than - fifty percent (50%) of the outstanding shares or beneficial - ownership of such entity. - -2. License Grants and Conditions --------------------------------- - -2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -(a) under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - -(b) under Patent Claims of such Contributor to make, use, sell, offer - for sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted under -this License. No additional rights or licenses will be implied from the -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -(a) for any code that a Contributor has removed from Covered Software; - or - -(b) for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - -(c) under Patent Claims infringed by Covered Software in the absence of - its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -3. Responsibilities -------------------- - -3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -(a) such Covered Software must also be made available in Source Code - Form, as described in Section 3.1, and You must inform recipients of - the Executable Form how they can obtain a copy of such Source Code - Form by reasonable means in a timely manner, at a charge no more - than the cost of distribution to the recipient; and - -(b) You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter - the recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -4. Inability to Comply Due to Statute or Regulation ---------------------------------------------------- - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -5. Termination --------------- - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - -************************************************************************ -* * -* 6. Disclaimer of Warranty * -* ------------------------- * -* * -* Covered Software is provided under this License on an "as is" * -* basis, without warranty of any kind, either expressed, implied, or * -* statutory, including, without limitation, warranties that the * -* Covered Software is free of defects, merchantable, fit for a * -* particular purpose or non-infringing. The entire risk as to the * -* quality and performance of the Covered Software is with You. * -* Should any Covered Software prove defective in any respect, You * -* (not any Contributor) assume the cost of any necessary servicing, * -* repair, or correction. This disclaimer of warranty constitutes an * -* essential part of this License. No use of any Covered Software is * -* authorized under this License except under this disclaimer. * -* * -************************************************************************ - -************************************************************************ -* * -* 7. Limitation of Liability * -* -------------------------- * -* * -* Under no circumstances and under no legal theory, whether tort * -* (including negligence), contract, or otherwise, shall any * -* Contributor, or anyone who distributes Covered Software as * -* permitted above, be liable to You for any direct, indirect, * -* special, incidental, or consequential damages of any character * -* including, without limitation, damages for lost profits, loss of * -* goodwill, work stoppage, computer failure or malfunction, or any * -* and all other commercial damages or losses, even if such party * -* shall have been informed of the possibility of such damages. This * -* limitation of liability shall not apply to liability for death or * -* personal injury resulting from such party's negligence to the * -* extent applicable law prohibits such limitation. Some * -* jurisdictions do not allow the exclusion or limitation of * -* incidental or consequential damages, so this exclusion and * -* limitation may not apply to You. * -* * -************************************************************************ - -8. Litigation -------------- - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -9. Miscellaneous ----------------- - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -10. Versions of the License ---------------------------- - -10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -Exhibit A - Source Code Form License Notice -------------------------------------------- - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------- - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. - ---- - -This product contains a modified portion of 'context', a golang registry for global request variables by Rodrigo Moraes. - -* HOMEPAGE: - * https://github.com/gorilla/context - -* LICENSE: - -Copyright (c) 2012 Rodrigo Moraes. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -This product contains a modified portion of 'mux', a powerful URL router and dispatcher for golang by Rodrigo Moraes. - -* HOMEPAGE: - * https://github.com/gorilla/mux - -* LICENSE: - -Copyright (c) 2012 Rodrigo Moraes. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -This product contains a modified portion of 'websocket', a WebSocket implementation for Go by The Gorilla WebSocket Authors. - -* HOMEPAGE: - * https://github.com/gorilla/websocket - -* LICENSE: - -Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -## pq - -This product contains a modified portion of 'pq', a Pure Go Postgres driver for database/sql by Blake Mizerany and Contributors. - -* HOMEPAGE: - * https://github.com/lib/pq - -* LICENSE: - -Copyright (c) 2011-2013, 'pq' Contributors -Portions Copyright (C) 2011 Blake Mizerany - -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. - ---- - -This product contains a modified portion of 'user_agent', a Go library that parses HTTP User Agents by Miquel Sabaté Solà. - -* HOMEPAGE: - * https://github.com/mssola/user_agent - -* LICENSE: - -Copyright (c) 2012-2016 Miquel Sabaté Solà - -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. - ---- - -This product contains a modified portion of 'uuid', a package generates and inspects UUIDs based on RFC 412 and DCE 1.1: Authentication and Security Services by Google Inc. - -* HOMEPAGE: - * https://github.com/pborman/uuid - -* LICENSE: - -Copyright (c) 2009,2014 Google Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -This product contains a modified portion of 'yaml', a YAML support for the Go language by Canonical Inc. - -* HOMEPAGE: - * https://gopkg.in/yaml.v2 - -* LICENSE: - - -Copyright (c) 2011-2014 - Canonical Inc. - -This software is licensed under the LGPLv3, included below. - -As a special exception to the GNU Lesser General Public License version 3 -("LGPL3"), the copyright holders of this Library give you permission to -convey to a third party a Combined Work that links statically or dynamically -to this Library without providing any Minimal Corresponding Source or -Minimal Application Code as set out in 4d or providing the installation -information set out in section 4e, provided that you comply with the other -provisions of LGPL3 and provided that you meet, for the Application the -terms and conditions of the license(s) which apply to the Application. - -Except as stated in this special exception, the provisions of LGPL3 will -continue to comply in full to this Library. If you modify this Library, you -may apply this exception to your version of this Library, but you are not -obliged to do so. If you do not wish to do so, delete this exception -statement from your version. This exception does not (and cannot) modify any -license terms which apply to the Application, with which you must still -comply. - - - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. - ---- - -This product contains a modified portion of 'throttled', a package store offering a memory-based and a Redis-based throttled by Martin Angers. - -* HOMEPAGE: - * http://gopkg.in/throttled/throttled.v1/store - -* LICENSE: - -Copyright (c) 2014, Martin Angers -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -This product contains a modified portion of 'throttled', a package throttled implementing different throttling strategies for controlling access to HTTP handlers by Martin Angers and Contributors. - -* HOMEPAGE: - * http://gopkg.in/throttled/throttled.v1 - -* LICENSE: - -Copyright (c) 2014, Martin Angers and Contributors. -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -This product contains a modified portion of 'fsnotify', to utilize golang.org/x/sys rather than syscall from the standard library by The Go Authors and fsnotify Authors. - -* HOMEPAGE: - * http://gopkg.in/fsnotify.v1 - -* LICENSE: - -Copyright (c) 2012 The Go Authors. All rights reserved. -Copyright (c) 2012 fsnotify Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -## asn1-ber - -This product contains a modified portion of 'asn1-ber', an ASN1 BER Encoding / Decoding Library for the GO programming language by The Go Authors. - -* HOMEPAGE: - * http://gopkg.in/asn1-ber.v1 - -* LICENSE: - -Copyright (c) 2012 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -This product contains a modified portion of 'asn1-ber', an ASN1 BER Encoding / Decoding Library for the GO programming language by The Go Authors. - -* HOMEPAGE: - * http://gopkg.in/asn1-ber.v1 - -* LICENSE: - -Copyright (c) 2012 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -## redigo - -This product contains a modified portion of 'redigo', a Go client for the Redis database. - -* HOMEPAGE: - * https://github.com/garyburd/redigo - -* LICENSE: - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - ---- - -## crypto - -This product contains a modified portion of 'crypto', a set of Go cryptographic libraries. - -* HOMEPAGE: - * https://github.com/golang/crypto - -* LICENSE: - -Copyright (c) 2009 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - ---- - -## image - -This product contains a modified portion of 'image', a set of Go image libraries. - -* HOMEPAGE: - * https://github.com/golang/image - -* LICENSE: - -Copyright (c) 2009 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - ---- - -## rsc - -This product contains a modified portion of 'rsc', a random source code library from Google - -* HOMEPAGE: - * https://code.google.com/archive/p/rsc/ - -* LICENSE: - -Copyright (c) , -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -## mattermost-driver-javascript - -This product contains a modified portion of 'mattermost-driver-javascript', a Javascript library for interacting with the Mattermost API. - -* HOMEPAGE: - * https://github.com/mattermost/mattermost-driver-javascript - -* LICENSE: - -Copyright 2016-present Mattermost - -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - ---- - -This product contains a modified portion of 'osext', providing a function that returns an absolute path to the current program executable, built by kardianos. - -* HOMEPAGE: - * https://github.com/kardianos/osext - -* LICENSE: - -Copyright (c) 2012 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -This product contains a modified portion of 'go-saml', a SAML client library for Go written by RobotsAndPencils. - -* HOMEPAGE: - * https://github.com/RobotsAndPencils/go-saml - -* LICENSE: - -The MIT License (MIT) - -Copyright (c) 2015 Robots and Pencils - -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. - ---- - -This product contains a modified portion of 'minio-go', a Golang Client SDK providing simple APIs to access any Amazon S3 compatible object storage server, built by Minio. - -* HOMEPAGE: - * https://github.com/minio/minio-go - -* LICENSE: - - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - ---- - -This product contains a modified portion of 'graceful', a Go package enabling graceful shutdown of an http.Handler server, built by Tyler Bunnell. - -* HOMEPAGE: - * https://github.com/tylerb/graceful - -* LICENSE: - -The MIT License (MIT) - -Copyright (c) 2014 Tyler Bunnell - -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. - ---- - -This product contains a modified portion of 'letsencrypt' to manage TLS certificates automatically via LetsEncrypt.org, built by Russ Cox. - -* HOMEPAGE: - * https://github.com/rsc/letsencrypt - -* LICENSE: - -Copyright (c) 2009 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -This product contains a modified portion of ‘manners’ a polite webserver for Go, built by Braintree, a division of PayPal, Inc. - -* HOMEPAGE: - * https://github.com/braintree/manners - -* LICENSE: - -Copyright (c) 2014 Braintree, a division of PayPal, Inc. - -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. - ---- - -This product contains a modified portion of ‘perks’ which contains the Go package quantile that computes approximate quantiles over an unbounded data stream within low memory and CPU bounds, built by Blake Mizerany. - -* HOMEPAGE: - * https://github.com/beorn7/perks - -* LICENSE: - -Copyright (C) 2013 Blake Mizerany - -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. - ---- - -This product contains a modified portion of ‘protobuf’ for Go support for Protocol Buffers, built by The Go Authors. - -* HOMEPAGE: - * https://github.com/golang/protobuf - -* LICENSE: - -Go support for Protocol Buffers - Google's data interchange format - -Copyright 2010 The Go Authors. All rights reserved. -https://github.com/golang/protobuf - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -This product contains a modified portion of ‘golang_protobuf_extensions’ which provides various Protocol Buffer extensions, built by matttproud. - -* HOMEPAGE: - * https://github.com/matttproud/golang_protobuf_extensions - -* LICENSE: - -# Code licensed under the Apache 2.0 License: - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - ---- - -This product contains a modified portion of ‘cobra’, a Commander for modern Go CLI interactions, built by spf13. - -* HOMEPAGE: - * https://github.com/spf13/cobra - -* LICENSE: - -# Code licensed under the Apache 2.0 License: - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - ---- - -This product contains a modified portion of ‘pflag’, a Commander for modern Go CLI interactions, built by Alex Ogier. - -* HOMEPAGE: - * https://github.com/spf13/pflag - -* LICENSE: - -Copyright (c) 2012 Alex Ogier. All rights reserved. -Copyright (c) 2012 The Go Authors. All rights reserved. - - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -This product contains modified portions of Prometheus ‘client_golang’, which is the Go library for Prometheus. Prometheus is a systems and service monitoring system, started by Matt T. Proud and Julius Volz in 2012. - -* HOMEPAGE: - * https://github.com/prometheus/client_golang - -* LICENSE: - -# Code licensed under the Apache 2.0 License: - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - ---- - -This product contains modified portions of Prometheus ‘client_model’, which provides data model artifacts for Prometheus. Prometheus is a systems and service monitoring system, started by Matt T. Proud and Julius Volz in 2012. - -* HOMEPAGE: - * https://github.com/prometheus/client_model - -* LICENSE: - -# Code licensed under the Apache 2.0 License: - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - ---- - -This product contains modified portions of Prometheus ‘common’, which is the part of the Prometheus project that contains Go libraries that are shared across Prometheus components and libraries. Prometheus is a systems and service monitoring system, started by Matt T. Proud and Julius Volz in 2012. - -* HOMEPAGE: - * https://github.com/prometheus/common - -* LICENSE: - -# Code licensed under the Apache 2.0 License: - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - ---- - -This product contains modified portions of Prometheus ‘procfs’, which is the part of the Prometheus project that provides functions to retrieve system, kernel and process metrics from the pseudo-filesystem proc. Prometheus is a systems and service monitoring system, started by Matt T. Proud and Julius Volz in 2012. - -* HOMEPAGE: - * https://github.com/prometheus/procfs - -* LICENSE: - -# Code licensed under the Apache 2.0 License: - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - ----- - -This product contains a modified portion of 'go-opengraph', for parsing OpenGraph data from HTML into regular structures by Vitaly Dyatlov. - -* HOMEPAGE: - * https://github.com/dyatlov/go-opengraph - -* LICENSE: - -The MIT License (MIT) - -Copyright (c) 2015 Vitaly Dyatlov - -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. - ----- - -This product contains a modified portion of `viper` for handling configuration in Go programs. Built by Steve Francia. - -* HOMEPAGE: - * https://github.com/spf13/viper - -* LICENSE: - -The MIT License (MIT) - -Copyright (c) 2014 Steve Francia - -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. - ----- - -This product contains a modified version of `gomail` for sending emails. Built by Alexandre Cesaro. - -* HOMEPAGE: - * https://github.com/go-gomail/gomail - -* LICENSE: - -The MIT License (MIT) - -Copyright (c) 2014 Alexandre Cesaro - -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. - ----- - -This product contains a modified version of `fsnotify` for file system notifications. Built by the Go Authors and the fsnotify Authors. - -* HOMEPAGE: - * https://github.com/fsnotify/fsnotify - -* LICENSE: - -Copyright (c) 2012 The Go Authors. All rights reserved. -Copyright (c) 2012 fsnotify Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- - -## analytics-go - -This product contains a modified portion of `analytics-go`, a segment analytics client for Go by Segment. - -* HOMEPAGE: - * https://github.com/segmentio/analytics-go - -* LICENSE: - -MIT license - ----- - -## html2text - -This product contains a modified portion of 'html2text', an HTML to raw text converter by Carlos Tadeu Panato Junior, based on `html2text` by Jay Taylor. - -* HOMEPAGE - * https://github.com/mattermost/html2text - -* LICENSE - -The MIT License (MIT) - -Copyright (c) 2015 Jay Taylor - -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. - ---- - -## elastic - -This product contains a modified portion of `elastic`, an Elasticsearch client for the Go programming language by Oliver Eilhard. - -* HOMEPAGE - * https://github.com/olivere/elastic - -* LICENSE - -The MIT License (MIT) -Copyright © 2012-2015 Oliver Eilhard - -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. - ---- - -## redis - -This product contains a modified portion of `redis`, a type-safe Redis client for Golang, by go-redis. - -* HOMEPAGE - * https://github.com/go-redis/redis - -* LICENSE - -Copyright (c) 2013 The github.com/go-redis/redis Authors. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - -## testify - -This product contains a modified portion of testify, a Golang toolkit providing tools for testifying that code behaves as intended. - -* HOMEPAGE - * https://github.com/stretchr/testify - -* LICENSE - -Copyright (c) 2012 - 2013 Mat Ryer and Tyler Bunnell - -Please consider promoting this project if you find it useful. - -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. - ---- - -## html2text - -This product contains a modified portion of `html2text`, a Golang library for HTML to plaintext conversion. - -* HOMEPAGE - * https://github.com/mattermost/html2text - -* LICENSE - -The MIT License (MIT) - -Copyright (c) 2015 Jay Taylor -Modified work: Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. - -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. diff --git a/vendor/github.com/mattermost/platform/model/client.go b/vendor/github.com/mattermost/platform/model/client.go deleted file mode 100644 index ef890b59..00000000 --- a/vendor/github.com/mattermost/platform/model/client.go +++ /dev/null @@ -1,2379 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package model - -import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "mime/multipart" - "net/http" - "net/url" - "strconv" - "strings" - "time" - - l4g "github.com/alecthomas/log4go" -) - -var UsedApiV3 *int32 = new(int32) - -const ( - HEADER_REQUEST_ID = "X-Request-ID" - HEADER_VERSION_ID = "X-Version-ID" - HEADER_CLUSTER_ID = "X-Cluster-ID" - HEADER_ETAG_SERVER = "ETag" - HEADER_ETAG_CLIENT = "If-None-Match" - HEADER_FORWARDED = "X-Forwarded-For" - HEADER_REAL_IP = "X-Real-IP" - HEADER_FORWARDED_PROTO = "X-Forwarded-Proto" - HEADER_TOKEN = "token" - HEADER_BEARER = "BEARER" - HEADER_AUTH = "Authorization" - HEADER_REQUESTED_WITH = "X-Requested-With" - HEADER_REQUESTED_WITH_XML = "XMLHttpRequest" - STATUS = "status" - STATUS_OK = "OK" - STATUS_FAIL = "FAIL" - STATUS_REMOVE = "REMOVE" - - CLIENT_DIR = "client" - - API_URL_SUFFIX_V1 = "/api/v1" - API_URL_SUFFIX_V3 = "/api/v3" - API_URL_SUFFIX_V4 = "/api/v4" - API_URL_SUFFIX = API_URL_SUFFIX_V4 -) - -type Result struct { - RequestId string - Etag string - Data interface{} -} - -type ResponseMetadata struct { - StatusCode int - Error *AppError - RequestId string - Etag string -} - -type Client struct { - Url string // The location of the server like "http://localhost:8065" - ApiUrl string // The api location of the server like "http://localhost:8065/api/v3" - HttpClient *http.Client // The http client - AuthToken string - AuthType string - TeamId string - RequestId string - Etag string - ServerVersion string -} - -// NewClient constructs a new client with convienence methods for talking to -// the server. -func NewClient(url string) *Client { - return &Client{url, url + API_URL_SUFFIX_V3, &http.Client{}, "", "", "", "", "", ""} -} - -func closeBody(r *http.Response) { - if r.Body != nil { - ioutil.ReadAll(r.Body) - r.Body.Close() - } -} - -func (c *Client) SetOAuthToken(token string) { - c.AuthToken = token - c.AuthType = HEADER_TOKEN -} - -func (c *Client) ClearOAuthToken() { - c.AuthToken = "" - c.AuthType = HEADER_BEARER -} - -func (c *Client) SetTeamId(teamId string) { - c.TeamId = teamId -} - -func (c *Client) GetTeamId() string { - if len(c.TeamId) == 0 { - println(`You are trying to use a route that requires a team_id, - but you have not called SetTeamId() in client.go`) - } - - return c.TeamId -} - -func (c *Client) ClearTeamId() { - c.TeamId = "" -} - -func (c *Client) GetTeamRoute() string { - return fmt.Sprintf("/teams/%v", c.GetTeamId()) -} - -func (c *Client) GetChannelRoute(channelId string) string { - return fmt.Sprintf("/teams/%v/channels/%v", c.GetTeamId(), channelId) -} - -func (c *Client) GetUserRequiredRoute(userId string) string { - return fmt.Sprintf("/users/%v", userId) -} - -func (c *Client) GetChannelNameRoute(channelName string) string { - return fmt.Sprintf("/teams/%v/channels/name/%v", c.GetTeamId(), channelName) -} - -func (c *Client) GetEmojiRoute() string { - return "/emoji" -} - -func (c *Client) GetGeneralRoute() string { - return "/general" -} - -func (c *Client) GetFileRoute(fileId string) string { - return fmt.Sprintf("/files/%v", fileId) -} - -func (c *Client) DoPost(url, data, contentType string) (*http.Response, *AppError) { - rq, _ := http.NewRequest("POST", c.Url+url, strings.NewReader(data)) - rq.Header.Set("Content-Type", contentType) - rq.Close = true - - if rp, err := c.HttpClient.Do(rq); err != nil { - return nil, NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0) - } else if rp.StatusCode >= 300 { - defer closeBody(rp) - return nil, AppErrorFromJson(rp.Body) - } else { - return rp, nil - } -} - -func (c *Client) DoApiPost(url string, data string) (*http.Response, *AppError) { - rq, _ := http.NewRequest("POST", c.ApiUrl+url, strings.NewReader(data)) - rq.Close = true - - if len(c.AuthToken) > 0 { - rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) - } - - if rp, err := c.HttpClient.Do(rq); err != nil { - return nil, NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0) - } else if rp.StatusCode >= 300 { - defer closeBody(rp) - return nil, AppErrorFromJson(rp.Body) - } else { - return rp, nil - } -} - -func (c *Client) DoApiGet(url string, data string, etag string) (*http.Response, *AppError) { - rq, _ := http.NewRequest("GET", c.ApiUrl+url, strings.NewReader(data)) - rq.Close = true - - if len(etag) > 0 { - rq.Header.Set(HEADER_ETAG_CLIENT, etag) - } - - if len(c.AuthToken) > 0 { - rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) - } - - if rp, err := c.HttpClient.Do(rq); err != nil { - return nil, NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0) - } else if rp.StatusCode == 304 { - return rp, nil - } else if rp.StatusCode >= 300 { - defer closeBody(rp) - return rp, AppErrorFromJson(rp.Body) - } else { - return rp, nil - } -} - -func getCookie(name string, resp *http.Response) *http.Cookie { - for _, cookie := range resp.Cookies() { - if cookie.Name == name { - return cookie - } - } - - return nil -} - -// Must is a convenience function used for testing. -func (c *Client) Must(result *Result, err *AppError) *Result { - if err != nil { - l4g.Close() - time.Sleep(time.Second) - panic(err) - } - - return result -} - -// MustGeneric is a convenience function used for testing. -func (c *Client) MustGeneric(result interface{}, err *AppError) interface{} { - if err != nil { - l4g.Close() - time.Sleep(time.Second) - panic(err) - } - - return result -} - -// CheckStatusOK is a convenience function for checking the return of Web Service -// call that return the a map of status=OK. -func (c *Client) CheckStatusOK(r *http.Response) bool { - m := MapFromJson(r.Body) - defer closeBody(r) - - if m != nil && m[STATUS] == STATUS_OK { - return true - } - - return false -} - -func (c *Client) fillInExtraProperties(r *http.Response) { - c.RequestId = r.Header.Get(HEADER_REQUEST_ID) - c.Etag = r.Header.Get(HEADER_ETAG_SERVER) - c.ServerVersion = r.Header.Get(HEADER_VERSION_ID) -} - -func (c *Client) clearExtraProperties() { - c.RequestId = "" - c.Etag = "" - c.ServerVersion = "" -} - -// General Routes Section - -// GetClientProperties returns properties needed by the client to show/hide -// certian features. It returns a map of strings. -func (c *Client) GetClientProperties() (map[string]string, *AppError) { - c.clearExtraProperties() - if r, err := c.DoApiGet(c.GetGeneralRoute()+"/client_props", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - c.fillInExtraProperties(r) - return MapFromJson(r.Body), nil - } -} - -// LogClient is a convenience Web Service call so clients can log messages into -// the server-side logs. For example we typically log javascript error messages -// into the server-side. It returns true if the logging was successful. -func (c *Client) LogClient(message string) (bool, *AppError) { - c.clearExtraProperties() - m := make(map[string]string) - m["level"] = "ERROR" - m["message"] = message - - if r, err := c.DoApiPost(c.GetGeneralRoute()+"/log_client", MapToJson(m)); err != nil { - return false, err - } else { - defer closeBody(r) - c.fillInExtraProperties(r) - return c.CheckStatusOK(r), nil - } -} - -// GetPing returns a map of strings with server time, server version, and node Id. -// Systems that want to check on health status of the server should check the -// url /api/v3/ping for a 200 status response. -func (c *Client) GetPing() (map[string]string, *AppError) { - c.clearExtraProperties() - if r, err := c.DoApiGet(c.GetGeneralRoute()+"/ping", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - c.fillInExtraProperties(r) - return MapFromJson(r.Body), nil - } -} - -// Team Routes Section - -// CreateTeam creates a team based on the provided Team struct. On success it returns -// the Team struct with the Id, CreateAt and other server-decided fields populated. -func (c *Client) CreateTeam(team *Team) (*Result, *AppError) { - if r, err := c.DoApiPost("/teams/create", team.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), TeamFromJson(r.Body)}, nil - } -} - -// GetAllTeams returns a map of all teams using team ids as the key. -func (c *Client) GetAllTeams() (*Result, *AppError) { - if r, err := c.DoApiGet("/teams/all", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil - } -} - -// GetAllTeamListings returns a map of all teams that are available to join -// using team ids as the key. Must be authenticated. -func (c *Client) GetAllTeamListings() (*Result, *AppError) { - if r, err := c.DoApiGet("/teams/all_team_listings", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil - } -} - -// FindTeamByName returns the strings "true" or "false" depending on if a team -// with the provided name was found. -func (c *Client) FindTeamByName(name string) (*Result, *AppError) { - m := make(map[string]string) - m["name"] = name - if r, err := c.DoApiPost("/teams/find_team_by_name", MapToJson(m)); err != nil { - return nil, err - } else { - val := false - if body, _ := ioutil.ReadAll(r.Body); string(body) == "true" { - val = true - } - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), val}, nil - } -} - -// Adds a user directly to the team without sending an invite. -// The teamId and userId are required. You must be a valid member of the team and/or -// have the correct role to add new users to the team. Returns a map of user_id=userId -// if successful, otherwise returns an AppError. -func (c *Client) AddUserToTeam(teamId string, userId string) (*Result, *AppError) { - if len(teamId) == 0 { - teamId = c.GetTeamId() - } - - data := make(map[string]string) - data["user_id"] = userId - if r, err := c.DoApiPost(fmt.Sprintf("/teams/%v", teamId)+"/add_user_to_team", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -// AddUserToTeamFromInvite adds a user to a team based off data provided in an invite link. -// Either hash and dataToHash are required or inviteId is required. -func (c *Client) AddUserToTeamFromInvite(hash, dataToHash, inviteId string) (*Result, *AppError) { - data := make(map[string]string) - data["hash"] = hash - data["data"] = dataToHash - data["invite_id"] = inviteId - if r, err := c.DoApiPost("/teams/add_user_to_team_from_invite", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), TeamFromJson(r.Body)}, nil - } -} - -// Removes a user directly from the team. -// The teamId and userId are required. You must be a valid member of the team and/or -// have the correct role to remove a user from the team. Returns a map of user_id=userId -// if successful, otherwise returns an AppError. -func (c *Client) RemoveUserFromTeam(teamId string, userId string) (*Result, *AppError) { - if len(teamId) == 0 { - teamId = c.GetTeamId() - } - - data := make(map[string]string) - data["user_id"] = userId - if r, err := c.DoApiPost(fmt.Sprintf("/teams/%v", teamId)+"/remove_user_from_team", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/invite_members", invites.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), InvitesFromJson(r.Body)}, nil - } -} - -// UpdateTeam updates a team based on the changes in the provided team struct. On success -// it returns a sanitized version of the updated team. Must be authenticated as a team admin -// for that team or a system admin. -func (c *Client) UpdateTeam(team *Team) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/update", team.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), TeamFromJson(r.Body)}, nil - } -} - -// User Routes Section - -// CreateUser creates a user in the system based on the provided user struct. -func (c *Client) CreateUser(user *User, hash string) (*Result, *AppError) { - if r, err := c.DoApiPost("/users/create", user.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil - } -} - -// CreateUserWithInvite creates a user based on the provided user struct. Either the hash and -// data strings or the inviteId is required from the invite. -func (c *Client) CreateUserWithInvite(user *User, hash string, data string, inviteId string) (*Result, *AppError) { - - url := "/users/create?d=" + url.QueryEscape(data) + "&h=" + url.QueryEscape(hash) + "&iid=" + url.QueryEscape(inviteId) - - if r, err := c.DoApiPost(url, user.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil - } -} - -func (c *Client) CreateUserFromSignup(user *User, data string, hash string) (*Result, *AppError) { - if r, err := c.DoApiPost("/users/create?d="+url.QueryEscape(data)+"&h="+hash, user.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil - } -} - -// GetUser returns a user based on a provided user id string. Must be authenticated. -func (c *Client) GetUser(id string, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet("/users/"+id+"/get", "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil - } -} - -// getByUsername returns a user based on a provided username string. Must be authenticated. -func (c *Client) GetByUsername(username string, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(fmt.Sprintf("/users/name/%v", username), "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil - } -} - -// getByEmail returns a user based on a provided username string. Must be authenticated. -func (c *Client) GetByEmail(email string, etag string) (*User, *ResponseMetadata) { - if r, err := c.DoApiGet(fmt.Sprintf("/users/email/%v", email), "", etag); err != nil { - return nil, &ResponseMetadata{StatusCode: r.StatusCode, Error: err} - } else { - defer closeBody(r) - return UserFromJson(r.Body), - &ResponseMetadata{ - StatusCode: r.StatusCode, - RequestId: r.Header.Get(HEADER_REQUEST_ID), - Etag: r.Header.Get(HEADER_ETAG_SERVER), - } - } -} - -// GetMe returns the current user. -func (c *Client) GetMe(etag string) (*Result, *AppError) { - if r, err := c.DoApiGet("/users/me", "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil - } -} - -// GetProfiles returns a map of users using user id as the key. Must be authenticated. -func (c *Client) GetProfiles(offset int, limit int, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(fmt.Sprintf("/users/%v/%v", offset, limit), "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil - } -} - -// GetProfilesInTeam returns a map of users for a team using user id as the key. Must -// be authenticated. -func (c *Client) GetProfilesInTeam(teamId string, offset int, limit int, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/users/%v/%v", teamId, offset, limit), "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil - } -} - -// GetProfilesInChannel returns a map of users for a channel using user id as the key. Must -// be authenticated. -func (c *Client) GetProfilesInChannel(channelId string, offset int, limit int, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(fmt.Sprintf(c.GetChannelRoute(channelId)+"/users/%v/%v", offset, limit), "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil - } -} - -// GetProfilesNotInChannel returns a map of users not in a channel but on the team using user id as the key. Must -// be authenticated. -func (c *Client) GetProfilesNotInChannel(channelId string, offset int, limit int, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(fmt.Sprintf(c.GetChannelRoute(channelId)+"/users/not_in_channel/%v/%v", offset, limit), "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil - } -} - -// GetProfilesByIds returns a map of users based on the user ids provided. Must -// be authenticated. -func (c *Client) GetProfilesByIds(userIds []string) (*Result, *AppError) { - if r, err := c.DoApiPost("/users/ids", ArrayToJson(userIds)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil - } -} - -// SearchUsers returns a list of users that have a username matching or similar to the search term. Must -// be authenticated. -func (c *Client) SearchUsers(params UserSearch) (*Result, *AppError) { - if r, err := c.DoApiPost("/users/search", params.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserListFromJson(r.Body)}, nil - } -} - -// AutocompleteUsersInChannel returns two lists for autocompletion of users in a channel. The first list "in_channel", -// specifies users in the channel. The second list "out_of_channel" specifies users outside of the -// channel. Term, the string to search against, is required, channel id is also required. Must be authenticated. -func (c *Client) AutocompleteUsersInChannel(term string, channelId string) (*Result, *AppError) { - url := fmt.Sprintf("%s/users/autocomplete?term=%s", c.GetChannelRoute(channelId), url.QueryEscape(term)) - if r, err := c.DoApiGet(url, "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserAutocompleteInChannelFromJson(r.Body)}, nil - } -} - -// AutocompleteUsersInTeam returns a list for autocompletion of users in a team. The list "in_team" specifies -// the users in the team that match the provided term, matching against username, full name and -// nickname. Must be authenticated. -func (c *Client) AutocompleteUsersInTeam(term string) (*Result, *AppError) { - url := fmt.Sprintf("%s/users/autocomplete?term=%s", c.GetTeamRoute(), url.QueryEscape(term)) - if r, err := c.DoApiGet(url, "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserAutocompleteInTeamFromJson(r.Body)}, nil - } -} - -// AutocompleteUsers returns a list for autocompletion of users on the system that match the provided term, -// matching against username, full name and nickname. Must be authenticated. -func (c *Client) AutocompleteUsers(term string) (*Result, *AppError) { - url := fmt.Sprintf("/users/autocomplete?term=%s", url.QueryEscape(term)) - if r, err := c.DoApiGet(url, "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserListFromJson(r.Body)}, nil - } -} - -// LoginById authenticates a user by user id and password. -func (c *Client) LoginById(id string, password string) (*Result, *AppError) { - m := make(map[string]string) - m["id"] = id - m["password"] = password - return c.login(m) -} - -// Login authenticates a user by login id, which can be username, email or some sort -// of SSO identifier based on configuration, and a password. -func (c *Client) Login(loginId string, password string) (*Result, *AppError) { - m := make(map[string]string) - m["login_id"] = loginId - m["password"] = password - return c.login(m) -} - -// LoginByLdap authenticates a user by LDAP id and password. -func (c *Client) LoginByLdap(loginId string, password string) (*Result, *AppError) { - m := make(map[string]string) - m["login_id"] = loginId - m["password"] = password - m["ldap_only"] = "true" - return c.login(m) -} - -// LoginWithDevice authenticates a user by login id (username, email or some sort -// of SSO identifier based on configuration), password and attaches a device id to -// the session. -func (c *Client) LoginWithDevice(loginId string, password string, deviceId string) (*Result, *AppError) { - m := make(map[string]string) - m["login_id"] = loginId - m["password"] = password - m["device_id"] = deviceId - return c.login(m) -} - -func (c *Client) login(m map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/users/login", MapToJson(m)); err != nil { - return nil, err - } else { - c.AuthToken = r.Header.Get(HEADER_TOKEN) - c.AuthType = HEADER_BEARER - sessionToken := getCookie(SESSION_COOKIE_TOKEN, r) - - if c.AuthToken != sessionToken.Value { - NewAppError("/users/login", "model.client.login.app_error", nil, "", 0) - } - - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil - } -} - -// Logout terminates the current user's session. -func (c *Client) Logout() (*Result, *AppError) { - if r, err := c.DoApiPost("/users/logout", ""); err != nil { - return nil, err - } else { - c.AuthToken = "" - c.AuthType = HEADER_BEARER - c.TeamId = "" - - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -// CheckMfa returns a map with key "mfa_required" with the string value "true" or "false", -// indicating whether MFA is required to log the user in, based on a provided login id -// (username, email or some sort of SSO identifier based on configuration). -func (c *Client) CheckMfa(loginId string) (*Result, *AppError) { - m := make(map[string]string) - m["login_id"] = loginId - - if r, err := c.DoApiPost("/users/mfa", MapToJson(m)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -// GenerateMfaSecret returns a QR code image containing the secret, to be scanned -// by a multi-factor authentication mobile application. It also returns the secret -// for manual entry. Must be authenticated. -func (c *Client) GenerateMfaSecret() (*Result, *AppError) { - if r, err := c.DoApiGet("/users/generate_mfa_secret", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -// UpdateMfa activates multi-factor authenticates for the current user if activate -// is true and a valid token is provided. If activate is false, then token is not -// required and multi-factor authentication is disabled for the current user. -func (c *Client) UpdateMfa(activate bool, token string) (*Result, *AppError) { - m := make(map[string]interface{}) - m["activate"] = activate - m["token"] = token - - if r, err := c.DoApiPost("/users/update_mfa", StringInterfaceToJson(m)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) AdminResetMfa(userId string) (*Result, *AppError) { - m := make(map[string]string) - m["user_id"] = userId - - if r, err := c.DoApiPost("/admin/reset_mfa", MapToJson(m)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) RevokeSession(sessionAltId string) (*Result, *AppError) { - m := make(map[string]string) - m["id"] = sessionAltId - - if r, err := c.DoApiPost("/users/revoke_session", MapToJson(m)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) GetSessions(id string) (*Result, *AppError) { - if r, err := c.DoApiGet("/users/"+id+"/sessions", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), SessionsFromJson(r.Body)}, nil - } -} - -func (c *Client) EmailToOAuth(m map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/users/claim/email_to_oauth", MapToJson(m)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) OAuthToEmail(m map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/users/claim/oauth_to_email", MapToJson(m)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) LDAPToEmail(m map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/users/claim/ldap_to_email", MapToJson(m)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) EmailToLDAP(m map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/users/claim/ldap_to_email", MapToJson(m)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) Command(channelId string, command string) (*Result, *AppError) { - args := &CommandArgs{ChannelId: channelId, Command: command} - if r, err := c.DoApiPost(c.GetTeamRoute()+"/commands/execute", args.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), CommandResponseFromJson(r.Body)}, nil - } -} - -func (c *Client) ListCommands() (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetTeamRoute()+"/commands/list", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), CommandListFromJson(r.Body)}, nil - } -} - -func (c *Client) ListTeamCommands() (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetTeamRoute()+"/commands/list_team_commands", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), CommandListFromJson(r.Body)}, nil - } -} - -func (c *Client) CreateCommand(cmd *Command) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/commands/create", cmd.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), CommandFromJson(r.Body)}, nil - } -} - -func (c *Client) UpdateCommand(cmd *Command) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/commands/update", cmd.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), CommandFromJson(r.Body)}, nil - } -} - -func (c *Client) RegenCommandToken(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/commands/regen_token", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), CommandFromJson(r.Body)}, nil - } -} - -func (c *Client) DeleteCommand(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/commands/delete", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) GetAudits(id string, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet("/users/"+id+"/audits", "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), AuditsFromJson(r.Body)}, nil - } -} - -func (c *Client) GetLogs() (*Result, *AppError) { - if r, err := c.DoApiGet("/admin/logs", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ArrayFromJson(r.Body)}, nil - } -} - -func (c *Client) GetClusterStatus() ([]*ClusterInfo, *AppError) { - if r, err := c.DoApiGet("/admin/cluster_status", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return ClusterInfosFromJson(r.Body), nil - } -} - -// GetRecentlyActiveUsers returns a map of users including lastActivityAt using user id as the key -func (c *Client) GetRecentlyActiveUsers(teamId string) (*Result, *AppError) { - if r, err := c.DoApiGet("/admin/recently_active_users/"+teamId, "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil - } -} - -func (c *Client) GetAllAudits() (*Result, *AppError) { - if r, err := c.DoApiGet("/admin/audits", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), AuditsFromJson(r.Body)}, nil - } -} - -func (c *Client) GetConfig() (*Result, *AppError) { - if r, err := c.DoApiGet("/admin/config", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ConfigFromJson(r.Body)}, nil - } -} - -// ReloadConfig will reload the config.json file from disk. Properties -// requiring a server restart will still need a server restart. You must -// have the system admin role to call this method. It will return status=OK -// if it's successfully reloaded the config file, otherwise check the returned error. -func (c *Client) ReloadConfig() (bool, *AppError) { - c.clearExtraProperties() - if r, err := c.DoApiGet("/admin/reload_config", "", ""); err != nil { - return false, err - } else { - c.fillInExtraProperties(r) - return c.CheckStatusOK(r), nil - } -} - -func (c *Client) InvalidateAllCaches() (bool, *AppError) { - c.clearExtraProperties() - if r, err := c.DoApiGet("/admin/invalidate_all_caches", "", ""); err != nil { - return false, err - } else { - c.fillInExtraProperties(r) - return c.CheckStatusOK(r), nil - } -} - -func (c *Client) SaveConfig(config *Config) (*Result, *AppError) { - if r, err := c.DoApiPost("/admin/save_config", config.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -// RecycleDatabaseConnection will attempt to recycle the database connections. -// You must have the system admin role to call this method. It will return status=OK -// if it's successfully recycled the connections, otherwise check the returned error. -func (c *Client) RecycleDatabaseConnection() (bool, *AppError) { - c.clearExtraProperties() - if r, err := c.DoApiGet("/admin/recycle_db_conn", "", ""); err != nil { - return false, err - } else { - c.fillInExtraProperties(r) - return c.CheckStatusOK(r), nil - } -} - -func (c *Client) TestEmail(config *Config) (*Result, *AppError) { - if r, err := c.DoApiPost("/admin/test_email", config.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -// TestLdap will run a connection test on the current LDAP settings. -// It will return the standard OK response if settings work. Otherwise -// it will return an appropriate error. -func (c *Client) TestLdap(config *Config) (*Result, *AppError) { - if r, err := c.DoApiPost("/admin/ldap_test", config.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) GetComplianceReports() (*Result, *AppError) { - if r, err := c.DoApiGet("/admin/compliance_reports", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), CompliancesFromJson(r.Body)}, nil - } -} - -func (c *Client) SaveComplianceReport(job *Compliance) (*Result, *AppError) { - if r, err := c.DoApiPost("/admin/save_compliance_report", job.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ComplianceFromJson(r.Body)}, nil - } -} - -func (c *Client) DownloadComplianceReport(id string) (*Result, *AppError) { - var rq *http.Request - rq, _ = http.NewRequest("GET", c.ApiUrl+"/admin/download_compliance_report/"+id, nil) - rq.Close = true - - if len(c.AuthToken) > 0 { - rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) - } - - if rp, err := c.HttpClient.Do(rq); err != nil { - return nil, NewAppError("/admin/download_compliance_report", "model.client.connecting.app_error", nil, err.Error(), 0) - } else if rp.StatusCode >= 300 { - defer rp.Body.Close() - return nil, AppErrorFromJson(rp.Body) - } else { - defer closeBody(rp) - return &Result{rp.Header.Get(HEADER_REQUEST_ID), - rp.Header.Get(HEADER_ETAG_SERVER), rp.Body}, nil - } -} - -func (c *Client) GetTeamAnalytics(teamId, name string) (*Result, *AppError) { - if r, err := c.DoApiGet("/admin/analytics/"+teamId+"/"+name, "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), AnalyticsRowsFromJson(r.Body)}, nil - } -} - -func (c *Client) GetSystemAnalytics(name string) (*Result, *AppError) { - if r, err := c.DoApiGet("/admin/analytics/"+name, "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), AnalyticsRowsFromJson(r.Body)}, nil - } -} - -// Initiate immediate synchronization of LDAP users. -// The synchronization will be performed asynchronously and this function will -// always return OK unless you don't have permissions. -// You must be the system administrator to use this function. -func (c *Client) LdapSyncNow() (*Result, *AppError) { - if r, err := c.DoApiPost("/admin/ldap_sync_now", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/create", channel.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil - } -} - -func (c *Client) CreateDirectChannel(userId string) (*Result, *AppError) { - data := make(map[string]string) - data["user_id"] = userId - if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/create_direct", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil - } -} - -func (c *Client) CreateGroupChannel(userIds []string) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/create_group", ArrayToJson(userIds)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil - } -} - -func (c *Client) UpdateChannel(channel *Channel) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/update", channel.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil - } -} - -func (c *Client) UpdateChannelHeader(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/update_header", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil - } -} - -func (c *Client) UpdateChannelPurpose(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/update_purpose", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil - } -} - -func (c *Client) UpdateNotifyProps(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/update_notify_props", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) GetMyChannelMembers() (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/members", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelMembersFromJson(r.Body)}, nil - } -} - -func (c *Client) GetChannel(id, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetChannelRoute(id)+"/", "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelDataFromJson(r.Body)}, nil - } -} - -// GetMoreChannelsPage will return a page of open channels the user is not in based on -// the provided offset and limit. Must be authenticated. -func (c *Client) GetMoreChannelsPage(offset int, limit int) (*Result, *AppError) { - if r, err := c.DoApiGet(fmt.Sprintf(c.GetTeamRoute()+"/channels/more/%v/%v", offset, limit), "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelListFromJson(r.Body)}, nil - } -} - -// SearchMoreChannels will return a list of open channels the user is not in, that matches -// the search criteria provided. Must be authenticated. -func (c *Client) SearchMoreChannels(channelSearch ChannelSearch) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/more/search", channelSearch.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelListFromJson(r.Body)}, nil - } -} - -// AutocompleteChannels will return a list of open channels that match the provided -// string. Must be authenticated. -func (c *Client) AutocompleteChannels(term string) (*Result, *AppError) { - url := fmt.Sprintf("%s/channels/autocomplete?term=%s", c.GetTeamRoute(), url.QueryEscape(term)) - if r, err := c.DoApiGet(url, "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelListFromJson(r.Body)}, nil - } -} - -func (c *Client) GetChannelCounts(etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/counts", "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelCountsFromJson(r.Body)}, nil - } -} - -func (c *Client) GetChannels(etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetTeamRoute()+"/channels/", "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelListFromJson(r.Body)}, nil - } -} - -func (c *Client) GetChannelByName(channelName string) (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetChannelNameRoute(channelName), "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil - } -} - -func (c *Client) JoinChannel(id string) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/join", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), nil}, nil - } -} - -func (c *Client) JoinChannelByName(name string) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetChannelNameRoute(name)+"/join", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), nil}, nil - } -} - -func (c *Client) LeaveChannel(id string) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/leave", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), nil}, nil - } -} - -func (c *Client) DeleteChannel(id string) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/delete", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), nil}, nil - } -} - -func (c *Client) AddChannelMember(id, user_id string) (*Result, *AppError) { - data := make(map[string]string) - data["user_id"] = user_id - if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/add", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), nil}, nil - } -} - -func (c *Client) RemoveChannelMember(id, user_id string) (*Result, *AppError) { - data := make(map[string]string) - data["user_id"] = user_id - if r, err := c.DoApiPost(c.GetChannelRoute(id)+"/remove", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), nil}, nil - } -} - -// ViewChannel performs all the actions related to viewing a channel. This includes marking -// the channel and the previous one as read, and marking the channel as being actively viewed. -// ChannelId is required but may be blank to indicate no channel is being viewed. -// PrevChannelId is optional, populate to indicate a channel switch occurred. -func (c *Client) ViewChannel(params ChannelView) (bool, *ResponseMetadata) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/view", params.ToJson()); err != nil { - return false, &ResponseMetadata{StatusCode: r.StatusCode, Error: err} - } else { - return c.CheckStatusOK(r), - &ResponseMetadata{ - StatusCode: r.StatusCode, - RequestId: r.Header.Get(HEADER_REQUEST_ID), - Etag: r.Header.Get(HEADER_ETAG_SERVER), - } - } -} - -func (c *Client) GetChannelStats(id string, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetChannelRoute(id)+"/stats", "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelStatsFromJson(r.Body)}, nil - } -} - -func (c *Client) GetChannelMember(channelId string, userId string) (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/members/"+userId, "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelMemberFromJson(r.Body)}, nil - } -} - -// GetChannelMembersByIds will return channel member objects as an array based on the -// channel id and a list of user ids provided. Must be authenticated. -func (c *Client) GetChannelMembersByIds(channelId string, userIds []string) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/members/ids", ArrayToJson(userIds)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ChannelMembersFromJson(r.Body)}, nil - } -} - -func (c *Client) CreatePost(post *Post) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetChannelRoute(post.ChannelId)+"/posts/create", post.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), PostFromJson(r.Body)}, nil - } -} - -func (c *Client) UpdatePost(post *Post) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetChannelRoute(post.ChannelId)+"/posts/update", post.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), PostFromJson(r.Body)}, nil - } -} - -func (c *Client) GetPosts(channelId string, offset int, limit int, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/page/%v/%v", offset, limit), "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil - } -} - -func (c *Client) GetPostsSince(channelId string, time int64) (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/since/%v", time), "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil - } -} - -func (c *Client) GetPostsBefore(channelId string, postid string, offset int, limit int, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/before/%v/%v", postid, offset, limit), "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil - } -} - -func (c *Client) GetPostsAfter(channelId string, postid string, offset int, limit int, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(fmt.Sprintf(c.GetChannelRoute(channelId)+"/posts/%v/after/%v/%v", postid, offset, limit), "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil - } -} - -func (c *Client) GetPost(channelId string, postId string, etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/get", postId), "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil - } -} - -// GetPostById returns a post and any posts in the same thread by post id -func (c *Client) GetPostById(postId string, etag string) (*PostList, *ResponseMetadata) { - if r, err := c.DoApiGet(c.GetTeamRoute()+fmt.Sprintf("/posts/%v", postId), "", etag); err != nil { - return nil, &ResponseMetadata{StatusCode: r.StatusCode, Error: err} - } else { - defer closeBody(r) - return PostListFromJson(r.Body), - &ResponseMetadata{ - StatusCode: r.StatusCode, - RequestId: r.Header.Get(HEADER_REQUEST_ID), - Etag: r.Header.Get(HEADER_ETAG_SERVER), - } - } -} - -// GetPermalink returns a post list, based on the provided channel and post ID. -func (c *Client) GetPermalink(channelId string, postId string, etag string) (*PostList, *ResponseMetadata) { - if r, err := c.DoApiGet(c.GetTeamRoute()+fmt.Sprintf("/pltmp/%v", postId), "", etag); err != nil { - return nil, &ResponseMetadata{StatusCode: r.StatusCode, Error: err} - } else { - defer closeBody(r) - return PostListFromJson(r.Body), - &ResponseMetadata{ - StatusCode: r.StatusCode, - RequestId: r.Header.Get(HEADER_REQUEST_ID), - Etag: r.Header.Get(HEADER_ETAG_SERVER), - } - } -} - -func (c *Client) DeletePost(channelId string, postId string) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/delete", postId), ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) SearchPosts(terms string, isOrSearch bool) (*Result, *AppError) { - data := map[string]interface{}{} - data["terms"] = terms - data["is_or_search"] = isOrSearch - if r, err := c.DoApiPost(c.GetTeamRoute()+"/posts/search", StringInterfaceToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil - } -} - -// GetFlaggedPosts will return a post list of posts that have been flagged by the user. -// The page is set by the integer parameters offset and limit. -func (c *Client) GetFlaggedPosts(offset int, limit int) (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetTeamRoute()+fmt.Sprintf("/posts/flagged/%v/%v", offset, limit), "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil - } -} - -func (c *Client) GetPinnedPosts(channelId string) (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/pinned", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil - } -} - -func (c *Client) UploadProfileFile(data []byte, contentType string) (*Result, *AppError) { - return c.uploadFile(c.ApiUrl+"/users/newimage", data, contentType) -} - -func (c *Client) UploadPostAttachment(data []byte, channelId string, filename string) (*FileUploadResponse, *AppError) { - c.clearExtraProperties() - - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - if part, err := writer.CreateFormFile("files", filename); err != nil { - return nil, NewAppError("UploadPostAttachment", "model.client.upload_post_attachment.file.app_error", nil, err.Error(), 0) - } else if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil { - return nil, NewAppError("UploadPostAttachment", "model.client.upload_post_attachment.file.app_error", nil, err.Error(), 0) - } - - if part, err := writer.CreateFormField("channel_id"); err != nil { - return nil, NewAppError("UploadPostAttachment", "model.client.upload_post_attachment.channel_id.app_error", nil, err.Error(), 0) - } else if _, err = io.Copy(part, strings.NewReader(channelId)); err != nil { - return nil, NewAppError("UploadPostAttachment", "model.client.upload_post_attachment.channel_id.app_error", nil, err.Error(), 0) - } - - if err := writer.Close(); err != nil { - return nil, NewAppError("UploadPostAttachment", "model.client.upload_post_attachment.writer.app_error", nil, err.Error(), 0) - } - - if result, err := c.uploadFile(c.ApiUrl+c.GetTeamRoute()+"/files/upload", body.Bytes(), writer.FormDataContentType()); err != nil { - return nil, err - } else { - return result.Data.(*FileUploadResponse), nil - } -} - -func (c *Client) uploadFile(url string, data []byte, contentType string) (*Result, *AppError) { - rq, _ := http.NewRequest("POST", url, bytes.NewReader(data)) - rq.Header.Set("Content-Type", contentType) - rq.Close = true - - if len(c.AuthToken) > 0 { - rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) - } - - if rp, err := c.HttpClient.Do(rq); err != nil { - return nil, NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0) - } else if rp.StatusCode >= 300 { - return nil, AppErrorFromJson(rp.Body) - } else { - defer closeBody(rp) - return &Result{rp.Header.Get(HEADER_REQUEST_ID), - rp.Header.Get(HEADER_ETAG_SERVER), FileUploadResponseFromJson(rp.Body)}, nil - } -} - -func (c *Client) GetFile(fileId string) (io.ReadCloser, *AppError) { - if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get", "", ""); err != nil { - return nil, err - } else { - c.fillInExtraProperties(r) - return r.Body, nil - } -} - -func (c *Client) GetFileThumbnail(fileId string) (io.ReadCloser, *AppError) { - if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get_thumbnail", "", ""); err != nil { - return nil, err - } else { - c.fillInExtraProperties(r) - return r.Body, nil - } -} - -func (c *Client) GetFilePreview(fileId string) (io.ReadCloser, *AppError) { - if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get_preview", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - c.fillInExtraProperties(r) - return r.Body, nil - } -} - -func (c *Client) GetFileInfo(fileId string) (*FileInfo, *AppError) { - if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get_info", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - c.fillInExtraProperties(r) - return FileInfoFromJson(r.Body), nil - } -} - -func (c *Client) GetPublicLink(fileId string) (string, *AppError) { - if r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/get_public_link", "", ""); err != nil { - return "", err - } else { - defer closeBody(r) - c.fillInExtraProperties(r) - return StringFromJson(r.Body), nil - } -} - -func (c *Client) UpdateUser(user *User) (*Result, *AppError) { - if r, err := c.DoApiPost("/users/update", user.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil - } -} - -func (c *Client) UpdateUserRoles(userId string, roles string) (*Result, *AppError) { - data := make(map[string]string) - data["new_roles"] = roles - - if r, err := c.DoApiPost(c.GetUserRequiredRoute(userId)+"/update_roles", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) UpdateTeamRoles(userId string, roles string) (*Result, *AppError) { - data := make(map[string]string) - data["new_roles"] = roles - data["user_id"] = userId - - if r, err := c.DoApiPost(c.GetTeamRoute()+"/update_member_roles", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) AttachDeviceId(deviceId string) (*Result, *AppError) { - data := make(map[string]string) - data["device_id"] = deviceId - if r, err := c.DoApiPost("/users/attach_device", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil - } -} - -func (c *Client) UpdateActive(userId string, active bool) (*Result, *AppError) { - data := make(map[string]string) - data["user_id"] = userId - data["active"] = strconv.FormatBool(active) - if r, err := c.DoApiPost("/users/update_active", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil - } -} - -func (c *Client) UpdateUserNotify(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/users/update_notify", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil - } -} - -func (c *Client) UpdateUserPassword(userId, currentPassword, newPassword string) (*Result, *AppError) { - data := make(map[string]string) - data["current_password"] = currentPassword - data["new_password"] = newPassword - data["user_id"] = userId - - if r, err := c.DoApiPost("/users/newpassword", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) SendPasswordReset(email string) (*Result, *AppError) { - data := map[string]string{} - data["email"] = email - if r, err := c.DoApiPost("/users/send_password_reset", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) ResetPassword(code, newPassword string) (*Result, *AppError) { - data := map[string]string{} - data["code"] = code - data["new_password"] = newPassword - if r, err := c.DoApiPost("/users/reset_password", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) AdminResetPassword(userId, newPassword string) (*Result, *AppError) { - data := map[string]string{} - data["user_id"] = userId - data["new_password"] = newPassword - if r, err := c.DoApiPost("/admin/reset_password", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -// GetStatuses returns a map of string statuses using user id as the key -func (c *Client) GetStatuses() (*Result, *AppError) { - if r, err := c.DoApiGet("/users/status", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -// GetStatusesByIds returns a map of string statuses using user id as the key, -// based on the provided user ids -func (c *Client) GetStatusesByIds(userIds []string) (*Result, *AppError) { - if r, err := c.DoApiPost("/users/status/ids", ArrayToJson(userIds)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) GetMyTeam(etag string) (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetTeamRoute()+"/me", "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), TeamFromJson(r.Body)}, nil - } -} - -// GetTeamMembers will return a page of team member objects as an array paged based on the -// team id, offset and limit provided. Must be authenticated. -func (c *Client) GetTeamMembers(teamId string, offset int, limit int) (*Result, *AppError) { - if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/members/%v/%v", teamId, offset, limit), "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), TeamMembersFromJson(r.Body)}, nil - } -} - -// GetMyTeamMembers will return an array with team member objects that the current user -// is a member of. Must be authenticated. -func (c *Client) GetMyTeamMembers() (*Result, *AppError) { - if r, err := c.DoApiGet("/teams/members", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), TeamMembersFromJson(r.Body)}, nil - } -} - -// GetMyTeamsUnread will return an array with TeamUnread objects that contain the amount of -// unread messages and mentions the current user has for the teams it belongs to. -// An optional team ID can be set to exclude that team from the results. Must be authenticated. -func (c *Client) GetMyTeamsUnread(teamId string) (*Result, *AppError) { - endpoint := "/teams/unread" - - if teamId != "" { - endpoint += fmt.Sprintf("?id=%s", url.QueryEscape(teamId)) - } - if r, err := c.DoApiGet(endpoint, "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), TeamsUnreadFromJson(r.Body)}, nil - } -} - -// GetTeamMember will return a team member object based on the team id and user id provided. -// Must be authenticated. -func (c *Client) GetTeamMember(teamId string, userId string) (*Result, *AppError) { - if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/members/%v", teamId, userId), "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), TeamMemberFromJson(r.Body)}, nil - } -} - -// GetTeamStats will return a team stats object containing the number of users on the team -// based on the team id provided. Must be authenticated. -func (c *Client) GetTeamStats(teamId string) (*Result, *AppError) { - if r, err := c.DoApiGet(fmt.Sprintf("/teams/%v/stats", teamId), "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), TeamStatsFromJson(r.Body)}, nil - } -} - -// GetTeamByName will return a team object based on the team name provided. Must be authenticated. -func (c *Client) GetTeamByName(teamName string) (*Result, *AppError) { - if r, err := c.DoApiGet(fmt.Sprintf("/teams/name/%v", teamName), "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), TeamFromJson(r.Body)}, nil - } -} - -// GetTeamMembersByIds will return team member objects as an array based on the -// team id and a list of user ids provided. Must be authenticated. -func (c *Client) GetTeamMembersByIds(teamId string, userIds []string) (*Result, *AppError) { - if r, err := c.DoApiPost(fmt.Sprintf("/teams/%v/members/ids", teamId), ArrayToJson(userIds)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), TeamMembersFromJson(r.Body)}, nil - } -} - -// RegisterApp creates a new OAuth2 app to be used with the OAuth2 Provider. On success -// it returns the created app. Must be authenticated as a user. -func (c *Client) RegisterApp(app *OAuthApp) (*Result, *AppError) { - if r, err := c.DoApiPost("/oauth/register", app.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), OAuthAppFromJson(r.Body)}, nil - } -} - -// AllowOAuth allows a new session by an OAuth2 App. On success -// it returns the url to be redirected back to the app which initiated the oauth2 flow. -// Must be authenticated as a user. -func (c *Client) AllowOAuth(rspType, clientId, redirect, scope, state string) (*Result, *AppError) { - if r, err := c.DoApiGet("/oauth/allow?response_type="+rspType+"&client_id="+clientId+"&redirect_uri="+url.QueryEscape(redirect)+"&scope="+scope+"&state="+url.QueryEscape(state), "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -// GetOAuthAppsByUser returns the OAuth2 Apps registered by the user. On success -// it returns a list of OAuth2 Apps from the same user or all the registered apps if the user -// is a System Administrator. Must be authenticated as a user. -func (c *Client) GetOAuthAppsByUser() (*Result, *AppError) { - if r, err := c.DoApiGet("/oauth/list", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), OAuthAppListFromJson(r.Body)}, nil - } -} - -// GetOAuthAppInfo lookup an OAuth2 App using the client_id. On success -// it returns a Sanitized OAuth2 App. Must be authenticated as a user. -func (c *Client) GetOAuthAppInfo(clientId string) (*Result, *AppError) { - if r, err := c.DoApiGet("/oauth/app/"+clientId, "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), OAuthAppFromJson(r.Body)}, nil - } -} - -// DeleteOAuthApp deletes an OAuth2 app, the app must be deleted by the same user who created it or -// a System Administrator. On success returs Status OK. Must be authenticated as a user. -func (c *Client) DeleteOAuthApp(id string) (*Result, *AppError) { - data := make(map[string]string) - data["id"] = id - if r, err := c.DoApiPost("/oauth/delete", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -// GetOAuthAuthorizedApps returns the OAuth2 Apps authorized by the user. On success -// it returns a list of sanitized OAuth2 Authorized Apps by the user. -func (c *Client) GetOAuthAuthorizedApps() (*Result, *AppError) { - if r, err := c.DoApiGet("/oauth/authorized", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), OAuthAppListFromJson(r.Body)}, nil - } -} - -// OAuthDeauthorizeApp deauthorize a user an OAuth 2.0 app. On success -// it returns status OK or an AppError on fail. -func (c *Client) OAuthDeauthorizeApp(clientId string) *AppError { - if r, err := c.DoApiPost("/oauth/"+clientId+"/deauthorize", ""); err != nil { - return err - } else { - defer closeBody(r) - return nil - } -} - -// RegenerateOAuthAppSecret generates a new OAuth App Client Secret. On success -// it returns an OAuth2 App. Must be authenticated as a user and the same user who -// registered the app or a System Admin. -func (c *Client) RegenerateOAuthAppSecret(clientId string) (*Result, *AppError) { - if r, err := c.DoApiPost("/oauth/"+clientId+"/regen_secret", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), OAuthAppFromJson(r.Body)}, nil - } -} - -func (c *Client) GetAccessToken(data url.Values) (*Result, *AppError) { - if r, err := c.DoPost("/oauth/access_token", data.Encode(), "application/x-www-form-urlencoded"); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), AccessResponseFromJson(r.Body)}, nil - } -} - -func (c *Client) CreateIncomingWebhook(hook *IncomingWebhook) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/hooks/incoming/create", hook.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), IncomingWebhookFromJson(r.Body)}, nil - } -} - -func (c *Client) UpdateIncomingWebhook(hook *IncomingWebhook) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/hooks/incoming/update", hook.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), IncomingWebhookFromJson(r.Body)}, nil - } -} - -func (c *Client) PostToWebhook(id, payload string) (*Result, *AppError) { - if r, err := c.DoPost("/hooks/"+id, payload, "application/x-www-form-urlencoded"); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), nil}, nil - } -} - -func (c *Client) DeleteIncomingWebhook(id string) (*Result, *AppError) { - data := make(map[string]string) - data["id"] = id - if r, err := c.DoApiPost(c.GetTeamRoute()+"/hooks/incoming/delete", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) ListIncomingWebhooks() (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetTeamRoute()+"/hooks/incoming/list", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), IncomingWebhookListFromJson(r.Body)}, nil - } -} - -func (c *Client) GetAllPreferences() (*Result, *AppError) { - if r, err := c.DoApiGet("/preferences/", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - preferences, _ := PreferencesFromJson(r.Body) - return &Result{r.Header.Get(HEADER_REQUEST_ID), r.Header.Get(HEADER_ETAG_SERVER), preferences}, nil - } -} - -func (c *Client) SetPreferences(preferences *Preferences) (*Result, *AppError) { - if r, err := c.DoApiPost("/preferences/save", preferences.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), preferences}, nil - } -} - -func (c *Client) GetPreference(category string, name string) (*Result, *AppError) { - if r, err := c.DoApiGet("/preferences/"+category+"/"+name, "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), r.Header.Get(HEADER_ETAG_SERVER), PreferenceFromJson(r.Body)}, nil - } -} - -func (c *Client) GetPreferenceCategory(category string) (*Result, *AppError) { - if r, err := c.DoApiGet("/preferences/"+category, "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - preferences, _ := PreferencesFromJson(r.Body) - return &Result{r.Header.Get(HEADER_REQUEST_ID), r.Header.Get(HEADER_ETAG_SERVER), preferences}, nil - } -} - -// DeletePreferences deletes a list of preferences owned by the current user. If successful, -// it will return status=ok. Otherwise, an error will be returned. -func (c *Client) DeletePreferences(preferences *Preferences) (bool, *AppError) { - if r, err := c.DoApiPost("/preferences/delete", preferences.ToJson()); err != nil { - return false, err - } else { - return c.CheckStatusOK(r), nil - } -} - -func (c *Client) CreateOutgoingWebhook(hook *OutgoingWebhook) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/hooks/outgoing/create", hook.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), OutgoingWebhookFromJson(r.Body)}, nil - } -} - -func (c *Client) UpdateOutgoingWebhook(hook *OutgoingWebhook) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetTeamRoute()+"/hooks/outgoing/update", hook.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), OutgoingWebhookFromJson(r.Body)}, nil - } -} - -func (c *Client) DeleteOutgoingWebhook(id string) (*Result, *AppError) { - data := make(map[string]string) - data["id"] = id - if r, err := c.DoApiPost(c.GetTeamRoute()+"/hooks/outgoing/delete", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) ListOutgoingWebhooks() (*Result, *AppError) { - if r, err := c.DoApiGet(c.GetTeamRoute()+"/hooks/outgoing/list", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), OutgoingWebhookListFromJson(r.Body)}, nil - } -} - -func (c *Client) RegenOutgoingWebhookToken(id string) (*Result, *AppError) { - data := make(map[string]string) - data["id"] = id - if r, err := c.DoApiPost(c.GetTeamRoute()+"/hooks/outgoing/regen_token", MapToJson(data)); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), OutgoingWebhookFromJson(r.Body)}, nil - } -} - -func (c *Client) MockSession(sessionToken string) { - c.AuthToken = sessionToken - c.AuthType = HEADER_BEARER -} - -func (c *Client) GetClientLicenceConfig(etag string) (*Result, *AppError) { - if r, err := c.DoApiGet("/license/client_config", "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil - } -} - -func (c *Client) GetInitialLoad() (*Result, *AppError) { - if r, err := c.DoApiGet("/users/initial_load", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), InitialLoadFromJson(r.Body)}, nil - } -} - -// ListEmoji returns a list of all user-created emoji for the server. -func (c *Client) ListEmoji() ([]*Emoji, *AppError) { - if r, err := c.DoApiGet(c.GetEmojiRoute()+"/list", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - c.fillInExtraProperties(r) - return EmojiListFromJson(r.Body), nil - } -} - -// CreateEmoji will save an emoji to the server if the current user has permission -// to do so. If successful, the provided emoji will be returned with its Id field -// filled in. Otherwise, an error will be returned. -func (c *Client) CreateEmoji(emoji *Emoji, image []byte, filename string) (*Emoji, *AppError) { - c.clearExtraProperties() - - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - if part, err := writer.CreateFormFile("image", filename); err != nil { - return nil, NewAppError("CreateEmoji", "model.client.create_emoji.image.app_error", nil, err.Error(), 0) - } else if _, err = io.Copy(part, bytes.NewBuffer(image)); err != nil { - return nil, NewAppError("CreateEmoji", "model.client.create_emoji.image.app_error", nil, err.Error(), 0) - } - - if err := writer.WriteField("emoji", emoji.ToJson()); err != nil { - return nil, NewAppError("CreateEmoji", "model.client.create_emoji.emoji.app_error", nil, err.Error(), 0) - } - - if err := writer.Close(); err != nil { - return nil, NewAppError("CreateEmoji", "model.client.create_emoji.writer.app_error", nil, err.Error(), 0) - } - - rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetEmojiRoute()+"/create", body) - rq.Header.Set("Content-Type", writer.FormDataContentType()) - rq.Close = true - - if len(c.AuthToken) > 0 { - rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) - } - - if r, err := c.HttpClient.Do(rq); err != nil { - return nil, NewAppError("CreateEmoji", "model.client.connecting.app_error", nil, err.Error(), 0) - } else if r.StatusCode >= 300 { - return nil, AppErrorFromJson(r.Body) - } else { - defer closeBody(r) - c.fillInExtraProperties(r) - return EmojiFromJson(r.Body), nil - } -} - -// DeleteEmoji will delete an emoji from the server if the current user has permission -// to do so. If successful, it will return status=ok. Otherwise, an error will be returned. -func (c *Client) DeleteEmoji(id string) (bool, *AppError) { - data := map[string]string{"id": id} - - if r, err := c.DoApiPost(c.GetEmojiRoute()+"/delete", MapToJson(data)); err != nil { - return false, err - } else { - defer closeBody(r) - c.fillInExtraProperties(r) - return c.CheckStatusOK(r), nil - } -} - -// GetCustomEmojiImageUrl returns the API route that can be used to get the image used by -// the given emoji. -func (c *Client) GetCustomEmojiImageUrl(id string) string { - return c.GetEmojiRoute() + "/" + id -} - -// Uploads a x509 base64 Certificate or Private Key file to be used with SAML. -// data byte array is required and needs to be a Multi-Part with 'certificate' as the field name -// contentType is also required. Returns nil if succesful, otherwise returns an AppError -func (c *Client) UploadCertificateFile(data []byte, contentType string) *AppError { - url := c.ApiUrl + "/admin/add_certificate" - rq, _ := http.NewRequest("POST", url, bytes.NewReader(data)) - rq.Header.Set("Content-Type", contentType) - rq.Close = true - - if len(c.AuthToken) > 0 { - rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) - } - - if rp, err := c.HttpClient.Do(rq); err != nil { - return NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0) - } else if rp.StatusCode >= 300 { - return AppErrorFromJson(rp.Body) - } else { - defer closeBody(rp) - c.fillInExtraProperties(rp) - return nil - } -} - -// Removes a x509 base64 Certificate or Private Key file used with SAML. -// filename is required. Returns nil if successful, otherwise returns an AppError -func (c *Client) RemoveCertificateFile(filename string) *AppError { - if r, err := c.DoApiPost("/admin/remove_certificate", MapToJson(map[string]string{"filename": filename})); err != nil { - return err - } else { - defer closeBody(r) - c.fillInExtraProperties(r) - return nil - } -} - -// Checks if the x509 base64 Certificates and Private Key files used with SAML exists on the file system. -// Returns a map[string]interface{} if successful, otherwise returns an AppError. Must be System Admin authenticated. -func (c *Client) SamlCertificateStatus(filename string) (map[string]interface{}, *AppError) { - if r, err := c.DoApiGet("/admin/remove_certificate", "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - c.fillInExtraProperties(r) - return StringInterfaceFromJson(r.Body), nil - } -} - -// GetWebrtcToken if Successful returns a map with a valid token, stun server and turn server with credentials to use with -// the Mattermost WebRTC service, otherwise returns an AppError. Must be authenticated user. -func (c *Client) GetWebrtcToken() (map[string]string, *AppError) { - if r, err := c.DoApiPost("/webrtc/token", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return MapFromJson(r.Body), nil - } -} - -// GetFileInfosForPost returns a list of FileInfo objects for a given post id, if successful. -// Otherwise, it returns an error. -func (c *Client) GetFileInfosForPost(channelId string, postId string, etag string) ([]*FileInfo, *AppError) { - c.clearExtraProperties() - - if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/get_file_infos", postId), "", etag); err != nil { - return nil, err - } else { - defer closeBody(r) - c.fillInExtraProperties(r) - return FileInfosFromJson(r.Body), nil - } -} - -// Saves an emoji reaction for a post in the given channel. Returns the saved reaction if successful, otherwise returns an AppError. -func (c *Client) SaveReaction(channelId string, reaction *Reaction) (*Reaction, *AppError) { - if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/reactions/save", reaction.PostId), reaction.ToJson()); err != nil { - return nil, err - } else { - defer closeBody(r) - c.fillInExtraProperties(r) - return ReactionFromJson(r.Body), nil - } -} - -// Removes an emoji reaction for a post in the given channel. Returns nil if successful, otherwise returns an AppError. -func (c *Client) DeleteReaction(channelId string, reaction *Reaction) *AppError { - if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/reactions/delete", reaction.PostId), reaction.ToJson()); err != nil { - return err - } else { - defer closeBody(r) - c.fillInExtraProperties(r) - return nil - } -} - -// Lists all emoji reactions made for the given post in the given channel. Returns a list of Reactions if successful, otherwise returns an AppError. -func (c *Client) ListReactions(channelId string, postId string) ([]*Reaction, *AppError) { - if r, err := c.DoApiGet(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/reactions", postId), "", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - c.fillInExtraProperties(r) - return ReactionsFromJson(r.Body), nil - } -} - -// Updates the user's roles in the channel by replacing them with the roles provided. -func (c *Client) UpdateChannelRoles(channelId string, userId string, roles string) (map[string]string, *ResponseMetadata) { - data := make(map[string]string) - data["new_roles"] = roles - data["user_id"] = userId - - if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/update_member_roles", MapToJson(data)); err != nil { - metadata := ResponseMetadata{Error: err} - if r != nil { - metadata.StatusCode = r.StatusCode - } - return nil, &metadata - } else { - defer closeBody(r) - return MapFromJson(r.Body), - &ResponseMetadata{ - StatusCode: r.StatusCode, - RequestId: r.Header.Get(HEADER_REQUEST_ID), - Etag: r.Header.Get(HEADER_ETAG_SERVER), - } - } -} - -func (c *Client) PinPost(channelId string, postId string) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/posts/"+postId+"/pin", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), PostFromJson(r.Body)}, nil - } -} - -func (c *Client) UnpinPost(channelId string, postId string) (*Result, *AppError) { - if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/posts/"+postId+"/unpin", ""); err != nil { - return nil, err - } else { - defer closeBody(r) - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), PostFromJson(r.Body)}, nil - } -} diff --git a/vendor/github.com/mattermost/platform/model/message_export.go b/vendor/github.com/mattermost/platform/model/message_export.go deleted file mode 100644 index b59b114d..00000000 --- a/vendor/github.com/mattermost/platform/model/message_export.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package model - -type MessageExport struct { - ChannelId *string - ChannelDisplayName *string - - UserId *string - UserEmail *string - - PostId *string - PostCreateAt *int64 - PostMessage *string - PostType *string - PostFileIds StringArray -} diff --git a/vendor/github.com/mattermost/platform/model/post.go b/vendor/github.com/mattermost/platform/model/post.go deleted file mode 100644 index 6b282fbf..00000000 --- a/vendor/github.com/mattermost/platform/model/post.go +++ /dev/null @@ -1,394 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package model - -import ( - "encoding/json" - "io" - "net/http" - "regexp" - "strings" - "unicode/utf8" -) - -const ( - POST_SYSTEM_MESSAGE_PREFIX = "system_" - POST_DEFAULT = "" - POST_SLACK_ATTACHMENT = "slack_attachment" - POST_SYSTEM_GENERIC = "system_generic" - POST_JOIN_LEAVE = "system_join_leave" // Deprecated, use POST_JOIN_CHANNEL or POST_LEAVE_CHANNEL instead - POST_JOIN_CHANNEL = "system_join_channel" - POST_LEAVE_CHANNEL = "system_leave_channel" - POST_JOIN_TEAM = "system_join_team" - POST_LEAVE_TEAM = "system_leave_team" - POST_ADD_REMOVE = "system_add_remove" // Deprecated, use POST_ADD_TO_CHANNEL or POST_REMOVE_FROM_CHANNEL instead - POST_ADD_TO_CHANNEL = "system_add_to_channel" - POST_REMOVE_FROM_CHANNEL = "system_remove_from_channel" - POST_ADD_TO_TEAM = "system_add_to_team" - POST_REMOVE_FROM_TEAM = "system_remove_from_team" - POST_HEADER_CHANGE = "system_header_change" - POST_DISPLAYNAME_CHANGE = "system_displayname_change" - POST_PURPOSE_CHANGE = "system_purpose_change" - POST_CHANNEL_DELETED = "system_channel_deleted" - POST_EPHEMERAL = "system_ephemeral" - POST_FILEIDS_MAX_RUNES = 150 - POST_FILENAMES_MAX_RUNES = 4000 - POST_HASHTAGS_MAX_RUNES = 1000 - POST_MESSAGE_MAX_RUNES = 4000 - POST_PROPS_MAX_RUNES = 8000 - POST_PROPS_MAX_USER_RUNES = POST_PROPS_MAX_RUNES - 400 // Leave some room for system / pre-save modifications - POST_CUSTOM_TYPE_PREFIX = "custom_" - PROPS_ADD_CHANNEL_MEMBER = "add_channel_member" -) - -type Post struct { - Id string `json:"id"` - CreateAt int64 `json:"create_at"` - UpdateAt int64 `json:"update_at"` - EditAt int64 `json:"edit_at"` - DeleteAt int64 `json:"delete_at"` - IsPinned bool `json:"is_pinned"` - UserId string `json:"user_id"` - ChannelId string `json:"channel_id"` - RootId string `json:"root_id"` - ParentId string `json:"parent_id"` - OriginalId string `json:"original_id"` - Message string `json:"message"` - Type string `json:"type"` - Props StringInterface `json:"props"` - Hashtags string `json:"hashtags"` - Filenames StringArray `json:"filenames,omitempty"` // Deprecated, do not use this field any more - FileIds StringArray `json:"file_ids,omitempty"` - PendingPostId string `json:"pending_post_id" db:"-"` - HasReactions bool `json:"has_reactions,omitempty"` -} - -type PostPatch struct { - IsPinned *bool `json:"is_pinned"` - Message *string `json:"message"` - Props *StringInterface `json:"props"` - FileIds *StringArray `json:"file_ids"` - HasReactions *bool `json:"has_reactions"` -} - -type PostForIndexing struct { - Post - TeamId string `json:"team_id"` - ParentCreateAt *int64 `json:"parent_create_at"` -} - -type PostAction struct { - Id string `json:"id"` - Name string `json:"name"` - Integration *PostActionIntegration `json:"integration,omitempty"` -} - -type PostActionIntegration struct { - URL string `json:"url,omitempty"` - Context StringInterface `json:"context,omitempty"` -} - -type PostActionIntegrationRequest struct { - UserId string `json:"user_id"` - Context StringInterface `json:"context,omitempty"` -} - -type PostActionIntegrationResponse struct { - Update *Post `json:"update"` - EphemeralText string `json:"ephemeral_text"` -} - -func (o *Post) ToJson() string { - copy := *o - copy.StripActionIntegrations() - b, err := json.Marshal(©) - if err != nil { - return "" - } else { - return string(b) - } -} - -func PostFromJson(data io.Reader) *Post { - decoder := json.NewDecoder(data) - var o Post - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } -} - -func (o *Post) Etag() string { - return Etag(o.Id, o.UpdateAt) -} - -func (o *Post) IsValid() *AppError { - - if len(o.Id) != 26 { - return NewAppError("Post.IsValid", "model.post.is_valid.id.app_error", nil, "", http.StatusBadRequest) - } - - if o.CreateAt == 0 { - return NewAppError("Post.IsValid", "model.post.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) - } - - if o.UpdateAt == 0 { - return NewAppError("Post.IsValid", "model.post.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) - } - - if len(o.UserId) != 26 { - return NewAppError("Post.IsValid", "model.post.is_valid.user_id.app_error", nil, "", http.StatusBadRequest) - } - - if len(o.ChannelId) != 26 { - return NewAppError("Post.IsValid", "model.post.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest) - } - - if !(len(o.RootId) == 26 || len(o.RootId) == 0) { - return NewAppError("Post.IsValid", "model.post.is_valid.root_id.app_error", nil, "", http.StatusBadRequest) - } - - if !(len(o.ParentId) == 26 || len(o.ParentId) == 0) { - return NewAppError("Post.IsValid", "model.post.is_valid.parent_id.app_error", nil, "", http.StatusBadRequest) - } - - if len(o.ParentId) == 26 && len(o.RootId) == 0 { - return NewAppError("Post.IsValid", "model.post.is_valid.root_parent.app_error", nil, "", http.StatusBadRequest) - } - - if !(len(o.OriginalId) == 26 || len(o.OriginalId) == 0) { - return NewAppError("Post.IsValid", "model.post.is_valid.original_id.app_error", nil, "", http.StatusBadRequest) - } - - if utf8.RuneCountInString(o.Message) > POST_MESSAGE_MAX_RUNES { - return NewAppError("Post.IsValid", "model.post.is_valid.msg.app_error", nil, "id="+o.Id, http.StatusBadRequest) - } - - if utf8.RuneCountInString(o.Hashtags) > POST_HASHTAGS_MAX_RUNES { - return NewAppError("Post.IsValid", "model.post.is_valid.hashtags.app_error", nil, "id="+o.Id, http.StatusBadRequest) - } - - switch o.Type { - case - POST_DEFAULT, - POST_JOIN_LEAVE, - POST_ADD_REMOVE, - POST_JOIN_CHANNEL, - POST_LEAVE_CHANNEL, - POST_JOIN_TEAM, - POST_LEAVE_TEAM, - POST_ADD_TO_CHANNEL, - POST_REMOVE_FROM_CHANNEL, - POST_ADD_TO_TEAM, - POST_REMOVE_FROM_TEAM, - POST_SLACK_ATTACHMENT, - POST_HEADER_CHANGE, - POST_PURPOSE_CHANGE, - POST_DISPLAYNAME_CHANGE, - POST_CHANNEL_DELETED: - default: - if !strings.HasPrefix(o.Type, POST_CUSTOM_TYPE_PREFIX) { - return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest) - } - } - - if utf8.RuneCountInString(ArrayToJson(o.Filenames)) > POST_FILENAMES_MAX_RUNES { - return NewAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id, http.StatusBadRequest) - } - - if utf8.RuneCountInString(ArrayToJson(o.FileIds)) > POST_FILEIDS_MAX_RUNES { - return NewAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id, http.StatusBadRequest) - } - - if utf8.RuneCountInString(StringInterfaceToJson(o.Props)) > POST_PROPS_MAX_RUNES { - return NewAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id, http.StatusBadRequest) - } - - return nil -} - -func (o *Post) SanitizeProps() { - membersToSanitize := []string{ - PROPS_ADD_CHANNEL_MEMBER, - } - - for _, member := range membersToSanitize { - if _, ok := o.Props[member]; ok { - delete(o.Props, member) - } - } -} - -func (o *Post) PreSave() { - if o.Id == "" { - o.Id = NewId() - } - - o.OriginalId = "" - - if o.CreateAt == 0 { - o.CreateAt = GetMillis() - } - - o.UpdateAt = o.CreateAt - o.PreCommit() -} - -func (o *Post) PreCommit() { - if o.Props == nil { - o.Props = make(map[string]interface{}) - } - - if o.Filenames == nil { - o.Filenames = []string{} - } - - if o.FileIds == nil { - o.FileIds = []string{} - } - - o.GenerateActionIds() -} - -func (o *Post) MakeNonNil() { - if o.Props == nil { - o.Props = make(map[string]interface{}) - } -} - -func (o *Post) AddProp(key string, value interface{}) { - - o.MakeNonNil() - - o.Props[key] = value -} - -func (o *Post) IsSystemMessage() bool { - return len(o.Type) >= len(POST_SYSTEM_MESSAGE_PREFIX) && o.Type[:len(POST_SYSTEM_MESSAGE_PREFIX)] == POST_SYSTEM_MESSAGE_PREFIX -} - -func (p *Post) Patch(patch *PostPatch) { - if patch.IsPinned != nil { - p.IsPinned = *patch.IsPinned - } - - if patch.Message != nil { - p.Message = *patch.Message - } - - if patch.Props != nil { - p.Props = *patch.Props - } - - if patch.FileIds != nil { - p.FileIds = *patch.FileIds - } - - if patch.HasReactions != nil { - p.HasReactions = *patch.HasReactions - } -} - -func (o *PostPatch) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } - - return string(b) -} - -func PostPatchFromJson(data io.Reader) *PostPatch { - decoder := json.NewDecoder(data) - var post PostPatch - err := decoder.Decode(&post) - if err != nil { - return nil - } - - return &post -} - -var channelMentionRegexp = regexp.MustCompile(`\B~[a-zA-Z0-9\-_]+`) - -func (o *Post) ChannelMentions() (names []string) { - if strings.Contains(o.Message, "~") { - alreadyMentioned := make(map[string]bool) - for _, match := range channelMentionRegexp.FindAllString(o.Message, -1) { - name := match[1:] - if !alreadyMentioned[name] { - names = append(names, name) - alreadyMentioned[name] = true - } - } - } - return -} - -func (r *PostActionIntegrationRequest) ToJson() string { - b, err := json.Marshal(r) - if err != nil { - return "" - } else { - return string(b) - } -} - -func (o *Post) Attachments() []*SlackAttachment { - if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok { - return attachments - } - var ret []*SlackAttachment - if attachments, ok := o.Props["attachments"].([]interface{}); ok { - for _, attachment := range attachments { - if enc, err := json.Marshal(attachment); err == nil { - var decoded SlackAttachment - if json.Unmarshal(enc, &decoded) == nil { - ret = append(ret, &decoded) - } - } - } - } - return ret -} - -func (o *Post) StripActionIntegrations() { - attachments := o.Attachments() - if o.Props["attachments"] != nil { - o.Props["attachments"] = attachments - } - for _, attachment := range attachments { - for _, action := range attachment.Actions { - action.Integration = nil - } - } -} - -func (o *Post) GetAction(id string) *PostAction { - for _, attachment := range o.Attachments() { - for _, action := range attachment.Actions { - if action.Id == id { - return action - } - } - } - return nil -} - -func (o *Post) GenerateActionIds() { - if o.Props["attachments"] != nil { - o.Props["attachments"] = o.Attachments() - } - if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok { - for _, attachment := range attachments { - for _, action := range attachment.Actions { - if action.Id == "" { - action.Id = NewId() - } - } - } - } -} diff --git a/vendor/github.com/mattermost/platform/model/scheduled_task.go b/vendor/github.com/mattermost/platform/model/scheduled_task.go deleted file mode 100644 index 453828bd..00000000 --- a/vendor/github.com/mattermost/platform/model/scheduled_task.go +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package model - -import ( - "fmt" - "sync" - "time" -) - -type TaskFunc func() - -type ScheduledTask struct { - Name string `json:"name"` - Interval time.Duration `json:"interval"` - Recurring bool `json:"recurring"` - function TaskFunc - timer *time.Timer -} - -var taskMutex = sync.Mutex{} -var tasks = make(map[string]*ScheduledTask) - -func addTask(task *ScheduledTask) { - taskMutex.Lock() - defer taskMutex.Unlock() - tasks[task.Name] = task -} - -func removeTaskByName(name string) { - taskMutex.Lock() - defer taskMutex.Unlock() - delete(tasks, name) -} - -func GetTaskByName(name string) *ScheduledTask { - taskMutex.Lock() - defer taskMutex.Unlock() - if task, ok := tasks[name]; ok { - return task - } - return nil -} - -func GetAllTasks() *map[string]*ScheduledTask { - taskMutex.Lock() - defer taskMutex.Unlock() - return &tasks -} - -func CreateTask(name string, function TaskFunc, timeToExecution time.Duration) *ScheduledTask { - task := &ScheduledTask{ - Name: name, - Interval: timeToExecution, - Recurring: false, - function: function, - } - - taskRunner := func() { - go task.function() - removeTaskByName(task.Name) - } - - task.timer = time.AfterFunc(timeToExecution, taskRunner) - - addTask(task) - - return task -} - -func CreateRecurringTask(name string, function TaskFunc, interval time.Duration) *ScheduledTask { - task := &ScheduledTask{ - Name: name, - Interval: interval, - Recurring: true, - function: function, - } - - taskRecurer := func() { - go task.function() - task.timer.Reset(task.Interval) - } - - task.timer = time.AfterFunc(interval, taskRecurer) - - addTask(task) - - return task -} - -func (task *ScheduledTask) Cancel() { - task.timer.Stop() - removeTaskByName(task.Name) -} - -// Executes the task immediatly. A recurring task will be run regularally after interval. -func (task *ScheduledTask) Execute() { - task.function() - task.timer.Reset(task.Interval) -} - -func (task *ScheduledTask) String() string { - return fmt.Sprintf( - "%s\nInterval: %s\nRecurring: %t\n", - task.Name, - task.Interval.String(), - task.Recurring, - ) -} diff --git a/vendor/github.com/mattermost/platform/model/system.go b/vendor/github.com/mattermost/platform/model/system.go deleted file mode 100644 index e2f4283a..00000000 --- a/vendor/github.com/mattermost/platform/model/system.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package model - -import ( - "encoding/json" - "io" -) - -const ( - SYSTEM_DIAGNOSTIC_ID = "DiagnosticId" - SYSTEM_RAN_UNIT_TESTS = "RanUnitTests" - SYSTEM_LAST_SECURITY_TIME = "LastSecurityTime" - SYSTEM_ACTIVE_LICENSE_ID = "ActiveLicenseId" - SYSTEM_LAST_COMPLIANCE_TIME = "LastComplianceTime" -) - -type System struct { - Name string `json:"name"` - Value string `json:"value"` -} - -func (o *System) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } -} - -func SystemFromJson(data io.Reader) *System { - decoder := json.NewDecoder(data) - var o System - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } -} diff --git a/vendor/github.com/mattermost/platform/model/websocket_message.go b/vendor/github.com/mattermost/platform/model/websocket_message.go deleted file mode 100644 index bf2535dc..00000000 --- a/vendor/github.com/mattermost/platform/model/websocket_message.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package model - -import ( - "encoding/json" - "io" -) - -const ( - WEBSOCKET_EVENT_TYPING = "typing" - WEBSOCKET_EVENT_POSTED = "posted" - WEBSOCKET_EVENT_POST_EDITED = "post_edited" - WEBSOCKET_EVENT_POST_DELETED = "post_deleted" - WEBSOCKET_EVENT_CHANNEL_DELETED = "channel_deleted" - WEBSOCKET_EVENT_CHANNEL_CREATED = "channel_created" - WEBSOCKET_EVENT_CHANNEL_UPDATED = "channel_updated" - WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added" - WEBSOCKET_EVENT_GROUP_ADDED = "group_added" - WEBSOCKET_EVENT_NEW_USER = "new_user" - WEBSOCKET_EVENT_ADDED_TO_TEAM = "added_to_team" - WEBSOCKET_EVENT_LEAVE_TEAM = "leave_team" - WEBSOCKET_EVENT_UPDATE_TEAM = "update_team" - WEBSOCKET_EVENT_USER_ADDED = "user_added" - WEBSOCKET_EVENT_USER_UPDATED = "user_updated" - WEBSOCKET_EVENT_USER_ROLE_UPDATED = "user_role_updated" - WEBSOCKET_EVENT_MEMBERROLE_UPDATED = "memberrole_updated" - WEBSOCKET_EVENT_USER_REMOVED = "user_removed" - WEBSOCKET_EVENT_PREFERENCE_CHANGED = "preference_changed" - WEBSOCKET_EVENT_PREFERENCES_CHANGED = "preferences_changed" - WEBSOCKET_EVENT_PREFERENCES_DELETED = "preferences_deleted" - WEBSOCKET_EVENT_EPHEMERAL_MESSAGE = "ephemeral_message" - WEBSOCKET_EVENT_STATUS_CHANGE = "status_change" - WEBSOCKET_EVENT_HELLO = "hello" - WEBSOCKET_EVENT_WEBRTC = "webrtc" - WEBSOCKET_AUTHENTICATION_CHALLENGE = "authentication_challenge" - WEBSOCKET_EVENT_REACTION_ADDED = "reaction_added" - WEBSOCKET_EVENT_REACTION_REMOVED = "reaction_removed" - WEBSOCKET_EVENT_RESPONSE = "response" - WEBSOCKET_EVENT_EMOJI_ADDED = "emoji_added" - WEBSOCKET_EVENT_CHANNEL_VIEWED = "channel_viewed" - WEBSOCKET_EVENT_PLUGIN_ACTIVATED = "plugin_activated" // EXPERIMENTAL - SUBJECT TO CHANGE - WEBSOCKET_EVENT_PLUGIN_DEACTIVATED = "plugin_deactivated" // EXPERIMENTAL - SUBJECT TO CHANGE -) - -type WebSocketMessage interface { - ToJson() string - IsValid() bool - EventType() string -} - -type WebsocketBroadcast struct { - OmitUsers map[string]bool `json:"omit_users"` // broadcast is omitted for users listed here - UserId string `json:"user_id"` // broadcast only occurs for this user - ChannelId string `json:"channel_id"` // broadcast only occurs for users in this channel - TeamId string `json:"team_id"` // broadcast only occurs for users in this team -} - -type WebSocketEvent struct { - Event string `json:"event"` - Data map[string]interface{} `json:"data"` - Broadcast *WebsocketBroadcast `json:"broadcast"` - Sequence int64 `json:"seq"` -} - -func (m *WebSocketEvent) Add(key string, value interface{}) { - m.Data[key] = value -} - -func NewWebSocketEvent(event, teamId, channelId, userId string, omitUsers map[string]bool) *WebSocketEvent { - return &WebSocketEvent{Event: event, Data: make(map[string]interface{}), - Broadcast: &WebsocketBroadcast{TeamId: teamId, ChannelId: channelId, UserId: userId, OmitUsers: omitUsers}} -} - -func (o *WebSocketEvent) IsValid() bool { - return o.Event != "" -} - -func (o *WebSocketEvent) EventType() string { - return o.Event -} - -func (o *WebSocketEvent) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } -} - -func WebSocketEventFromJson(data io.Reader) *WebSocketEvent { - decoder := json.NewDecoder(data) - var o WebSocketEvent - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } -} - -type WebSocketResponse struct { - Status string `json:"status"` - SeqReply int64 `json:"seq_reply,omitempty"` - Data map[string]interface{} `json:"data,omitempty"` - Error *AppError `json:"error,omitempty"` -} - -func (m *WebSocketResponse) Add(key string, value interface{}) { - m.Data[key] = value -} - -func NewWebSocketResponse(status string, seqReply int64, data map[string]interface{}) *WebSocketResponse { - return &WebSocketResponse{Status: status, SeqReply: seqReply, Data: data} -} - -func NewWebSocketError(seqReply int64, err *AppError) *WebSocketResponse { - return &WebSocketResponse{Status: STATUS_FAIL, SeqReply: seqReply, Error: err} -} - -func (o *WebSocketResponse) IsValid() bool { - return o.Status != "" -} - -func (o *WebSocketResponse) EventType() string { - return WEBSOCKET_EVENT_RESPONSE -} - -func (o *WebSocketResponse) ToJson() string { - b, err := json.Marshal(o) - if err != nil { - return "" - } else { - return string(b) - } -} - -func WebSocketResponseFromJson(data io.Reader) *WebSocketResponse { - decoder := json.NewDecoder(data) - var o WebSocketResponse - err := decoder.Decode(&o) - if err == nil { - return &o - } else { - return nil - } -} diff --git a/vendor/github.com/mitchellh/mapstructure/.travis.yml b/vendor/github.com/mitchellh/mapstructure/.travis.yml index d9deadb8..1689c7d7 100644 --- a/vendor/github.com/mitchellh/mapstructure/.travis.yml +++ b/vendor/github.com/mitchellh/mapstructure/.travis.yml @@ -1,7 +1,7 @@ language: go go: - - 1.9.x + - "1.11.x" - tip script: diff --git a/vendor/github.com/mitchellh/mapstructure/CHANGELOG.md b/vendor/github.com/mitchellh/mapstructure/CHANGELOG.md new file mode 100644 index 00000000..3b3cb723 --- /dev/null +++ b/vendor/github.com/mitchellh/mapstructure/CHANGELOG.md @@ -0,0 +1,21 @@ +## 1.1.2 + +* Fix error when decode hook decodes interface implementation into interface + type. [GH-140] + +## 1.1.1 + +* Fix panic that can happen in `decodePtr` + +## 1.1.0 + +* Added `StringToIPHookFunc` to convert `string` to `net.IP` and `net.IPNet` [GH-133] +* Support struct to struct decoding [GH-137] +* If source map value is nil, then destination map value is nil (instead of empty) +* If source slice value is nil, then destination slice value is nil (instead of empty) +* If source pointer is nil, then destination pointer is set to nil (instead of + allocated zero value of type) + +## 1.0.0 + +* Initial tagged stable release. diff --git a/vendor/github.com/mitchellh/mapstructure/decode_hooks.go b/vendor/github.com/mitchellh/mapstructure/decode_hooks.go index 2a727575..1f0abc65 100644 --- a/vendor/github.com/mitchellh/mapstructure/decode_hooks.go +++ b/vendor/github.com/mitchellh/mapstructure/decode_hooks.go @@ -2,6 +2,8 @@ package mapstructure import ( "errors" + "fmt" + "net" "reflect" "strconv" "strings" @@ -115,6 +117,50 @@ func StringToTimeDurationHookFunc() DecodeHookFunc { } } +// StringToIPHookFunc returns a DecodeHookFunc that converts +// strings to net.IP +func StringToIPHookFunc() DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + if f.Kind() != reflect.String { + return data, nil + } + if t != reflect.TypeOf(net.IP{}) { + return data, nil + } + + // Convert it by parsing + ip := net.ParseIP(data.(string)) + if ip == nil { + return net.IP{}, fmt.Errorf("failed parsing ip %v", data) + } + + return ip, nil + } +} + +// StringToIPNetHookFunc returns a DecodeHookFunc that converts +// strings to net.IPNet +func StringToIPNetHookFunc() DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + if f.Kind() != reflect.String { + return data, nil + } + if t != reflect.TypeOf(net.IPNet{}) { + return data, nil + } + + // Convert it by parsing + _, net, err := net.ParseCIDR(data.(string)) + return net, err + } +} + // StringToTimeHookFunc returns a DecodeHookFunc that converts // strings to time.Time. func StringToTimeHookFunc(layout string) DecodeHookFunc { diff --git a/vendor/github.com/mitchellh/mapstructure/go.mod b/vendor/github.com/mitchellh/mapstructure/go.mod new file mode 100644 index 00000000..d2a71256 --- /dev/null +++ b/vendor/github.com/mitchellh/mapstructure/go.mod @@ -0,0 +1 @@ +module github.com/mitchellh/mapstructure diff --git a/vendor/github.com/mitchellh/mapstructure/mapstructure.go b/vendor/github.com/mitchellh/mapstructure/mapstructure.go index aaf12a29..256ee63f 100644 --- a/vendor/github.com/mitchellh/mapstructure/mapstructure.go +++ b/vendor/github.com/mitchellh/mapstructure/mapstructure.go @@ -224,6 +224,17 @@ func (d *Decoder) Decode(input interface{}) error { // Decodes an unknown data type into a specific reflection value. func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) error { + var inputVal reflect.Value + if input != nil { + inputVal = reflect.ValueOf(input) + + // We need to check here if input is a typed nil. Typed nils won't + // match the "input == nil" below so we check that here. + if inputVal.Kind() == reflect.Ptr && inputVal.IsNil() { + input = nil + } + } + if input == nil { // If the data is nil, then we don't set anything, unless ZeroFields is set // to true. @@ -237,7 +248,6 @@ func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) e return nil } - inputVal := reflect.ValueOf(input) if !inputVal.IsValid() { // If the input value is invalid, then we just set the value // to be the zero value. @@ -260,8 +270,8 @@ func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) e } var err error - inputKind := getKind(outVal) - switch inputKind { + outputKind := getKind(outVal) + switch outputKind { case reflect.Bool: err = d.decodeBool(name, input, outVal) case reflect.Interface: @@ -288,7 +298,7 @@ func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) e err = d.decodeFunc(name, input, outVal) default: // If we reached this point then we weren't able to decode it - return fmt.Errorf("%s: unsupported type: %s", name, inputKind) + return fmt.Errorf("%s: unsupported type: %s", name, outputKind) } // If we reached here, then we successfully decoded SOMETHING, so @@ -306,7 +316,16 @@ func (d *Decoder) decodeBasic(name string, data interface{}, val reflect.Value) if val.IsValid() && val.Elem().IsValid() { return d.decode(name, data, val.Elem()) } + dataVal := reflect.ValueOf(data) + + // If the input data is a pointer, and the assigned type is the dereference + // of that exact pointer, then indirect it so that we can assign it. + // Example: *string to string + if dataVal.Kind() == reflect.Ptr && dataVal.Type().Elem() == val.Type() { + dataVal = reflect.Indirect(dataVal) + } + if !dataVal.IsValid() { dataVal = reflect.Zero(val.Type()) } @@ -323,7 +342,7 @@ func (d *Decoder) decodeBasic(name string, data interface{}, val reflect.Value) } func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) error { - dataVal := reflect.ValueOf(data) + dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) converted := true @@ -375,7 +394,7 @@ func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) } func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) error { - dataVal := reflect.ValueOf(data) + dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) dataType := dataVal.Type() @@ -417,7 +436,7 @@ func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) er } func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) error { - dataVal := reflect.ValueOf(data) + dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) switch { @@ -460,7 +479,7 @@ func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) e } func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) error { - dataVal := reflect.ValueOf(data) + dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) switch { @@ -491,7 +510,7 @@ func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) e } func (d *Decoder) decodeFloat(name string, data interface{}, val reflect.Value) error { - dataVal := reflect.ValueOf(data) + dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) dataType := dataVal.Type() @@ -595,6 +614,20 @@ func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val refle // Accumulate errors errors := make([]string, 0) + // If the input data is empty, then we just match what the input data is. + if dataVal.Len() == 0 { + if dataVal.IsNil() { + if !val.IsNil() { + val.Set(dataVal) + } + } else { + // Set to empty allocated value + val.Set(valMap) + } + + return nil + } + for _, k := range dataVal.MapKeys() { fieldName := fmt.Sprintf("%s[%s]", name, k) @@ -644,16 +677,28 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re return fmt.Errorf("cannot assign type '%s' to map value field of type '%s'", v.Type(), valMap.Type().Elem()) } + tagValue := f.Tag.Get(d.config.TagName) + tagParts := strings.Split(tagValue, ",") + // Determine the name of the key in the map keyName := f.Name - tagValue := f.Tag.Get(d.config.TagName) - tagValue = strings.SplitN(tagValue, ",", 2)[0] - if tagValue != "" { - if tagValue == "-" { + if tagParts[0] != "" { + if tagParts[0] == "-" { continue } + keyName = tagParts[0] + } - keyName = tagValue + // If "squash" is specified in the tag, we squash the field down. + squash := false + for _, tag := range tagParts[1:] { + if tag == "squash" { + squash = true + break + } + } + if squash && v.Kind() != reflect.Struct { + return fmt.Errorf("cannot squash non-struct type '%s'", v.Type()) } switch v.Kind() { @@ -673,7 +718,13 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re return err } - valMap.SetMapIndex(reflect.ValueOf(keyName), vMap) + if squash { + for _, k := range vMap.MapKeys() { + valMap.SetMapIndex(k, vMap.MapIndex(k)) + } + } else { + valMap.SetMapIndex(reflect.ValueOf(keyName), vMap) + } default: valMap.SetMapIndex(reflect.ValueOf(keyName), v) @@ -688,11 +739,33 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re } func (d *Decoder) decodePtr(name string, data interface{}, val reflect.Value) error { + // If the input data is nil, then we want to just set the output + // pointer to be nil as well. + isNil := data == nil + if !isNil { + switch v := reflect.Indirect(reflect.ValueOf(data)); v.Kind() { + case reflect.Chan, + reflect.Func, + reflect.Interface, + reflect.Map, + reflect.Ptr, + reflect.Slice: + isNil = v.IsNil() + } + } + if isNil { + if !val.IsNil() && val.CanSet() { + nilValue := reflect.New(val.Type()).Elem() + val.Set(nilValue) + } + + return nil + } + // Create an element of the concrete (non pointer) type and decode // into that. Then set the value of the pointer to this type. valType := val.Type() valElemType := valType.Elem() - if val.CanSet() { realVal := val if realVal.IsNil() || d.config.ZeroFields { @@ -734,30 +807,44 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) valSlice := val if valSlice.IsNil() || d.config.ZeroFields { + if d.config.WeaklyTypedInput { + switch { + // Slice and array we use the normal logic + case dataValKind == reflect.Slice, dataValKind == reflect.Array: + break + + // Empty maps turn into empty slices + case dataValKind == reflect.Map: + if dataVal.Len() == 0 { + val.Set(reflect.MakeSlice(sliceType, 0, 0)) + return nil + } + // Create slice of maps of other sizes + return d.decodeSlice(name, []interface{}{data}, val) + + case dataValKind == reflect.String && valElemType.Kind() == reflect.Uint8: + return d.decodeSlice(name, []byte(dataVal.String()), val) + + // All other types we try to convert to the slice type + // and "lift" it into it. i.e. a string becomes a string slice. + default: + // Just re-try this function with data as a slice. + return d.decodeSlice(name, []interface{}{data}, val) + } + } + // Check input type if dataValKind != reflect.Array && dataValKind != reflect.Slice { - if d.config.WeaklyTypedInput { - switch { - // Empty maps turn into empty slices - case dataValKind == reflect.Map: - if dataVal.Len() == 0 { - val.Set(reflect.MakeSlice(sliceType, 0, 0)) - return nil - } - case dataValKind == reflect.String && valElemType.Kind() == reflect.Uint8: - return d.decodeSlice(name, []byte(dataVal.String()), val) - // All other types we try to convert to the slice type - // and "lift" it into it. i.e. a string becomes a string slice. - default: - // Just re-try this function with data as a slice. - return d.decodeSlice(name, []interface{}{data}, val) - } - } return fmt.Errorf( "'%s': source data must be an array or slice, got %s", name, dataValKind) } + // If the input value is empty, then don't allocate since non-nil != nil + if dataVal.Len() == 0 { + return nil + } + // Make a new slice to hold our result, same size as the original data. valSlice = reflect.MakeSlice(sliceType, dataVal.Len(), dataVal.Len()) } @@ -867,10 +954,29 @@ func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value) } dataValKind := dataVal.Kind() - if dataValKind != reflect.Map { - return fmt.Errorf("'%s' expected a map, got '%s'", name, dataValKind) - } + switch dataValKind { + case reflect.Map: + return d.decodeStructFromMap(name, dataVal, val) + case reflect.Struct: + // Not the most efficient way to do this but we can optimize later if + // we want to. To convert from struct to struct we go to map first + // as an intermediary. + m := make(map[string]interface{}) + mval := reflect.Indirect(reflect.ValueOf(&m)) + if err := d.decodeMapFromStruct(name, dataVal, mval, mval); err != nil { + return err + } + + result := d.decodeStructFromMap(name, mval, val) + return result + + default: + return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind()) + } +} + +func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) error { dataValType := dataVal.Type() if kind := dataValType.Key().Kind(); kind != reflect.String && kind != reflect.Interface { return fmt.Errorf( diff --git a/vendor/github.com/nlopes/slack/.travis.yml b/vendor/github.com/nlopes/slack/.travis.yml index bd0539e0..b0615288 100644 --- a/vendor/github.com/nlopes/slack/.travis.yml +++ b/vendor/github.com/nlopes/slack/.travis.yml @@ -4,6 +4,8 @@ go: - 1.7.x - 1.8.x - 1.9.x + - 1.10.x + - 1.11.x - tip before_install: diff --git a/vendor/github.com/nlopes/slack/CHANGELOG.md b/vendor/github.com/nlopes/slack/CHANGELOG.md index a79ea50c..cf0fc2cc 100644 --- a/vendor/github.com/nlopes/slack/CHANGELOG.md +++ b/vendor/github.com/nlopes/slack/CHANGELOG.md @@ -1,3 +1,15 @@ +### v0.4.0 - October 06, 2018 +full differences can be viewed using `git log --oneline --decorate --color v0.3.0..v0.4.0` +- Breaking Change: renamed ApplyMessageOption, to mark it as unsafe, +this means it may break without warning in the future. +- Breaking: Msg structure files field changed to an array. +- General: implementation for new security headers. +- RTM: deadlock fix between connect/disconnect. +- Events: various new fields added. +- Web: various fixes, new fields exposed, new methods added. +- Interactions: minor additions expect breaking changes in next release for dialogs/button clicks. +- Utils: new methods added. + ### v0.3.0 - July 30, 2018 full differences can be viewed using `git log --oneline --decorate --color v0.2.0..v0.3.0` - slack events initial support added. (still considered experimental and undergoing changes, stability not promised) diff --git a/vendor/github.com/nlopes/slack/Gopkg.lock b/vendor/github.com/nlopes/slack/Gopkg.lock index 5cc0520e..9c33d0dc 100644 --- a/vendor/github.com/nlopes/slack/Gopkg.lock +++ b/vendor/github.com/nlopes/slack/Gopkg.lock @@ -13,6 +13,12 @@ revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" version = "v1.2.0" +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + [[projects]] name = "github.com/pmezard/go-difflib" packages = ["difflib"] @@ -28,6 +34,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "888307bf47ee004aaaa4c45e6139929b4984f2253e48e382246bfb8c66f3cd65" + inputs-digest = "596fa546322c2a1e9708a10c9f39aca2e04792b477fab86fb2899fbaab776070" solver-name = "gps-cdcl" solver-version = 1 diff --git a/vendor/github.com/nlopes/slack/Gopkg.toml b/vendor/github.com/nlopes/slack/Gopkg.toml new file mode 100644 index 00000000..257870d6 --- /dev/null +++ b/vendor/github.com/nlopes/slack/Gopkg.toml @@ -0,0 +1,17 @@ +ignored = ["github.com/lusis/slack-test"] + +[[constraint]] + name = "github.com/gorilla/websocket" + version = "1.2.0" + +[[constraint]] + name = "github.com/stretchr/testify" + version = "1.2.1" + +[[constraint]] + name = "github.com/pkg/errors" + version = "0.8.0" + +[prune] + go-tests = true + unused-packages = true diff --git a/vendor/github.com/nlopes/slack/admin.go b/vendor/github.com/nlopes/slack/admin.go index a2aa7e5c..db44aa38 100644 --- a/vendor/github.com/nlopes/slack/admin.go +++ b/vendor/github.com/nlopes/slack/admin.go @@ -12,9 +12,9 @@ type adminResponse struct { Error string `json:"error"` } -func adminRequest(ctx context.Context, client HTTPRequester, method string, teamName string, values url.Values, debug bool) (*adminResponse, error) { +func adminRequest(ctx context.Context, client httpClient, method string, teamName string, values url.Values, d debug) (*adminResponse, error) { adminResponse := &adminResponse{} - err := parseAdminResponse(ctx, client, method, teamName, values, adminResponse, debug) + err := parseAdminResponse(ctx, client, method, teamName, values, adminResponse, d) if err != nil { return nil, err } @@ -40,7 +40,7 @@ func (api *Client) DisableUserContext(ctx context.Context, teamName string, uid "_attempts": {"1"}, } - _, err := adminRequest(ctx, api.httpclient, "setInactive", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "setInactive", teamName, values, api) if err != nil { return fmt.Errorf("Failed to disable user with id '%s': %s", uid, err) } @@ -67,7 +67,7 @@ func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, fi "_attempts": {"1"}, } - _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api) if err != nil { return fmt.Errorf("Failed to invite single-channel guest: %s", err) } @@ -94,7 +94,7 @@ func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channe "_attempts": {"1"}, } - _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api) if err != nil { return fmt.Errorf("Failed to restricted account: %s", err) } @@ -118,7 +118,7 @@ func (api *Client) InviteToTeamContext(ctx context.Context, teamName, firstName, "_attempts": {"1"}, } - _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "invite", teamName, values, api) if err != nil { return fmt.Errorf("Failed to invite to team: %s", err) } @@ -140,7 +140,7 @@ func (api *Client) SetRegularContext(ctx context.Context, teamName, user string) "_attempts": {"1"}, } - _, err := adminRequest(ctx, api.httpclient, "setRegular", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "setRegular", teamName, values, api) if err != nil { return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err) } @@ -162,7 +162,7 @@ func (api *Client) SendSSOBindingEmailContext(ctx context.Context, teamName, use "_attempts": {"1"}, } - _, err := adminRequest(ctx, api.httpclient, "sendSSOBind", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "sendSSOBind", teamName, values, api) if err != nil { return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err) } @@ -185,7 +185,7 @@ func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid, "_attempts": {"1"}, } - _, err := adminRequest(ctx, api.httpclient, "setUltraRestricted", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "setUltraRestricted", teamName, values, api) if err != nil { return fmt.Errorf("Failed to ultra-restrict account: %s", err) } @@ -207,7 +207,7 @@ func (api *Client) SetRestrictedContext(ctx context.Context, teamName, uid strin "_attempts": {"1"}, } - _, err := adminRequest(ctx, api.httpclient, "setRestricted", teamName, values, api.debug) + _, err := adminRequest(ctx, api.httpclient, "setRestricted", teamName, values, api) if err != nil { return fmt.Errorf("Failed to restrict account: %s", err) } diff --git a/vendor/github.com/nlopes/slack/attachments.go b/vendor/github.com/nlopes/slack/attachments.go index 326fc010..06f59fa3 100644 --- a/vendor/github.com/nlopes/slack/attachments.go +++ b/vendor/github.com/nlopes/slack/attachments.go @@ -42,24 +42,14 @@ type AttachmentActionOptionGroup struct { } // AttachmentActionCallback is sent from Slack when a user clicks a button in an interactive message (aka AttachmentAction) -type AttachmentActionCallback struct { - Actions []AttachmentAction `json:"actions"` - CallbackID string `json:"callback_id"` - Team Team `json:"team"` - Channel Channel `json:"channel"` - User User `json:"user"` +// DEPRECATED: use InteractionCallback +type AttachmentActionCallback InteractionCallback - Name string `json:"name"` - Value string `json:"value"` - - OriginalMessage Message `json:"original_message"` - - ActionTs string `json:"action_ts"` - MessageTs string `json:"message_ts"` - AttachmentID string `json:"attachment_id"` - Token string `json:"token"` - ResponseURL string `json:"response_url"` - TriggerID string `json:"trigger_id"` +// ActionCallback specific fields for the action callback. +type ActionCallback struct { + MessageTs string `json:"message_ts"` + AttachmentID string `json:"attachment_id"` + Actions []AttachmentAction `json:"actions"` } // ConfirmationField are used to ask users to confirm actions diff --git a/vendor/github.com/nlopes/slack/auth.go b/vendor/github.com/nlopes/slack/auth.go new file mode 100644 index 00000000..f8fe1f9d --- /dev/null +++ b/vendor/github.com/nlopes/slack/auth.go @@ -0,0 +1,40 @@ +package slack + +import ( + "context" + "net/url" +) + +// AuthRevokeResponse contains our Auth response from the auth.revoke endpoint +type AuthRevokeResponse struct { + SlackResponse // Contains the "ok", and "Error", if any + Revoked bool `json:"revoked,omitempty"` +} + +// authRequest sends the actual request, and unmarshals the response +func authRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*AuthRevokeResponse, error) { + response := &AuthRevokeResponse{} + err := postSlackMethod(ctx, client, path, values, response, d) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// SendAuthRevoke will send a revocation for our token +func (api *Client) SendAuthRevoke(token string) (*AuthRevokeResponse, error) { + return api.SendAuthRevokeContext(context.Background(), token) +} + +// SendAuthRevokeContext will retrieve the satus from api.test +func (api *Client) SendAuthRevokeContext(ctx context.Context, token string) (*AuthRevokeResponse, error) { + if token == "" { + token = api.token + } + values := url.Values{ + "token": {token}, + } + + return authRequest(ctx, api.httpclient, "auth.revoke", values, api) +} diff --git a/vendor/github.com/nlopes/slack/bots.go b/vendor/github.com/nlopes/slack/bots.go index 92570a04..e27e76ab 100644 --- a/vendor/github.com/nlopes/slack/bots.go +++ b/vendor/github.com/nlopes/slack/bots.go @@ -19,9 +19,9 @@ type botResponseFull struct { SlackResponse } -func botRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*botResponseFull, error) { +func botRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*botResponseFull, error) { response := &botResponseFull{} - err := postSlackMethod(ctx, client, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, d) if err != nil { return nil, err } @@ -43,7 +43,7 @@ func (api *Client) GetBotInfoContext(ctx context.Context, bot string) (*Bot, err "bot": {bot}, } - response, err := botRequest(ctx, api.httpclient, "bots.info", values, api.debug) + response, err := botRequest(ctx, api.httpclient, "bots.info", values, api) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/channels.go b/vendor/github.com/nlopes/slack/channels.go index 6204315a..007985bc 100644 --- a/vendor/github.com/nlopes/slack/channels.go +++ b/vendor/github.com/nlopes/slack/channels.go @@ -26,9 +26,9 @@ type Channel struct { Locale string `json:"locale"` } -func channelRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*channelResponseFull, error) { +func channelRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*channelResponseFull, error) { response := &channelResponseFull{} - err := postForm(ctx, client, SLACK_API+path, values, response, debug) + err := postForm(ctx, client, APIURL+path, values, response, d) if err != nil { return nil, err } @@ -52,7 +52,7 @@ func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string) "channel": {channelID}, } - _, err = channelRequest(ctx, api.httpclient, "channels.archive", values, api.debug) + _, err = channelRequest(ctx, api.httpclient, "channels.archive", values, api) return err } @@ -70,7 +70,7 @@ func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string "channel": {channelID}, } - _, err = channelRequest(ctx, api.httpclient, "channels.unarchive", values, api.debug) + _, err = channelRequest(ctx, api.httpclient, "channels.unarchive", values, api) return err } @@ -88,7 +88,7 @@ func (api *Client) CreateChannelContext(ctx context.Context, channelName string) "name": {channelName}, } - response, err := channelRequest(ctx, api.httpclient, "channels.create", values, api.debug) + response, err := channelRequest(ctx, api.httpclient, "channels.create", values, api) if err != nil { return nil, err } @@ -133,7 +133,7 @@ func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID strin } } - response, err := channelRequest(ctx, api.httpclient, "channels.history", values, api.debug) + response, err := channelRequest(ctx, api.httpclient, "channels.history", values, api) if err != nil { return nil, err } @@ -154,7 +154,7 @@ func (api *Client) GetChannelInfoContext(ctx context.Context, channelID string) "channel": {channelID}, } - response, err := channelRequest(ctx, api.httpclient, "channels.info", values, api.debug) + response, err := channelRequest(ctx, api.httpclient, "channels.info", values, api) if err != nil { return nil, err } @@ -167,7 +167,7 @@ func (api *Client) InviteUserToChannel(channelID, user string) (*Channel, error) return api.InviteUserToChannelContext(context.Background(), channelID, user) } -// InviteUserToChannelCustom invites a user to a given channel and returns a *Channel with a custom context +// InviteUserToChannelContext invites a user to a given channel and returns a *Channel with a custom context // see https://api.slack.com/methods/channels.invite func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, user string) (*Channel, error) { values := url.Values{ @@ -176,7 +176,7 @@ func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, us "user": {user}, } - response, err := channelRequest(ctx, api.httpclient, "channels.invite", values, api.debug) + response, err := channelRequest(ctx, api.httpclient, "channels.invite", values, api) if err != nil { return nil, err } @@ -197,7 +197,7 @@ func (api *Client) JoinChannelContext(ctx context.Context, channelName string) ( "name": {channelName}, } - response, err := channelRequest(ctx, api.httpclient, "channels.join", values, api.debug) + response, err := channelRequest(ctx, api.httpclient, "channels.join", values, api) if err != nil { return nil, err } @@ -218,7 +218,7 @@ func (api *Client) LeaveChannelContext(ctx context.Context, channelID string) (b "channel": {channelID}, } - response, err := channelRequest(ctx, api.httpclient, "channels.leave", values, api.debug) + response, err := channelRequest(ctx, api.httpclient, "channels.leave", values, api) if err != nil { return false, err } @@ -241,7 +241,7 @@ func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, us "user": {user}, } - _, err = channelRequest(ctx, api.httpclient, "channels.kick", values, api.debug) + _, err = channelRequest(ctx, api.httpclient, "channels.kick", values, api) return err } @@ -261,7 +261,7 @@ func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool) values.Add("exclude_archived", "1") } - response, err := channelRequest(ctx, api.httpclient, "channels.list", values, api.debug) + response, err := channelRequest(ctx, api.httpclient, "channels.list", values, api) if err != nil { return nil, err } @@ -288,7 +288,7 @@ func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts "ts": {ts}, } - _, err = channelRequest(ctx, api.httpclient, "channels.mark", values, api.debug) + _, err = channelRequest(ctx, api.httpclient, "channels.mark", values, api) return err } @@ -309,7 +309,7 @@ func (api *Client) RenameChannelContext(ctx context.Context, channelID, name str // XXX: the created entry in this call returns a string instead of a number // so I may have to do some workaround to solve it. - response, err := channelRequest(ctx, api.httpclient, "channels.rename", values, api.debug) + response, err := channelRequest(ctx, api.httpclient, "channels.rename", values, api) if err != nil { return nil, err } @@ -331,7 +331,7 @@ func (api *Client) SetChannelPurposeContext(ctx context.Context, channelID, purp "purpose": {purpose}, } - response, err := channelRequest(ctx, api.httpclient, "channels.setPurpose", values, api.debug) + response, err := channelRequest(ctx, api.httpclient, "channels.setPurpose", values, api) if err != nil { return "", err } @@ -353,7 +353,7 @@ func (api *Client) SetChannelTopicContext(ctx context.Context, channelID, topic "topic": {topic}, } - response, err := channelRequest(ctx, api.httpclient, "channels.setTopic", values, api.debug) + response, err := channelRequest(ctx, api.httpclient, "channels.setTopic", values, api) if err != nil { return "", err } @@ -374,7 +374,7 @@ func (api *Client) GetChannelRepliesContext(ctx context.Context, channelID, thre "channel": {channelID}, "thread_ts": {thread_ts}, } - response, err := channelRequest(ctx, api.httpclient, "channels.replies", values, api.debug) + response, err := channelRequest(ctx, api.httpclient, "channels.replies", values, api) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/chat.go b/vendor/github.com/nlopes/slack/chat.go index 2b89a44c..eb8fd6c8 100644 --- a/vendor/github.com/nlopes/slack/chat.go +++ b/vendor/github.com/nlopes/slack/chat.go @@ -4,7 +4,8 @@ import ( "context" "encoding/json" "net/url" - "strings" + + "github.com/nlopes/slack/slackutilsx" ) const ( @@ -95,26 +96,24 @@ func (api *Client) DeleteMessageContext(ctx context.Context, channel, messageTim // PostMessage sends a message to a channel. // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. -func (api *Client) PostMessage(channel, text string, params PostMessageParameters) (string, string, error) { +func (api *Client) PostMessage(channelID string, options ...MsgOption) (string, string, error) { respChannel, respTimestamp, _, err := api.SendMessageContext( context.Background(), - channel, - MsgOptionText(text, params.EscapeText), - MsgOptionAttachments(params.Attachments...), - MsgOptionPostMessageParameters(params), + channelID, + MsgOptionPost(), + MsgOptionCompose(options...), ) return respChannel, respTimestamp, err } // PostMessageContext sends a message to a channel with a custom context -// For more details, see PostMessage documentation -func (api *Client) PostMessageContext(ctx context.Context, channel, text string, params PostMessageParameters) (string, string, error) { +// For more details, see PostMessage documentation. +func (api *Client) PostMessageContext(ctx context.Context, channelID string, options ...MsgOption) (string, string, error) { respChannel, respTimestamp, _, err := api.SendMessageContext( ctx, - channel, - MsgOptionText(text, params.EscapeText), - MsgOptionAttachments(params.Attachments...), - MsgOptionPostMessageParameters(params), + channelID, + MsgOptionPost(), + MsgOptionCompose(options...), ) return respChannel, respTimestamp, err } @@ -134,18 +133,23 @@ func (api *Client) PostEphemeral(channelID, userID string, options ...MsgOption) // PostEphemeralContext sends an ephemeal message to a user in a channel with a custom context // For more details, see PostEphemeral documentation func (api *Client) PostEphemeralContext(ctx context.Context, channelID, userID string, options ...MsgOption) (timestamp string, err error) { - _, timestamp, _, err = api.SendMessageContext(ctx, channelID, append(options, MsgOptionPostEphemeral2(userID))...) + _, timestamp, _, err = api.SendMessageContext(ctx, channelID, MsgOptionPostEphemeral(userID), MsgOptionCompose(options...)) return timestamp, err } // UpdateMessage updates a message in a channel -func (api *Client) UpdateMessage(channelID, timestamp, text string) (string, string, string, error) { - return api.UpdateMessageContext(context.Background(), channelID, timestamp, text) +func (api *Client) UpdateMessage(channelID, timestamp string, options ...MsgOption) (string, string, string, error) { + return api.SendMessageContext(context.Background(), channelID, MsgOptionUpdate(timestamp), MsgOptionCompose(options...)) } // UpdateMessageContext updates a message in a channel -func (api *Client) UpdateMessageContext(ctx context.Context, channelID, timestamp, text string) (string, string, string, error) { - return api.SendMessageContext(ctx, channelID, MsgOptionUpdate(timestamp), MsgOptionText(text, true)) +func (api *Client) UpdateMessageContext(ctx context.Context, channelID, timestamp string, options ...MsgOption) (string, string, string, error) { + return api.SendMessageContext(ctx, channelID, MsgOptionUpdate(timestamp), MsgOptionCompose(options...)) +} + +// UnfurlMessage unfurls a message in a channel +func (api *Client) UnfurlMessage(channelID, timestamp string, unfurls map[string]Attachment, options ...MsgOption) (string, string, string, error) { + return api.SendMessageContext(context.Background(), channelID, MsgOptionUnfurl(timestamp, unfurls), MsgOptionCompose(options...)) } // SendMessage more flexible method for configuring messages. @@ -164,22 +168,24 @@ func (api *Client) SendMessageContext(ctx context.Context, channelID string, opt return "", "", "", err } - if err = postSlackMethod(ctx, api.httpclient, string(config.mode), config.values, &response, api.debug); err != nil { + if err = postForm(ctx, api.httpclient, config.endpoint, config.values, &response, api); err != nil { return "", "", "", err } return response.Channel, response.getMessageTimestamp(), response.Text, response.Err() } -// ApplyMsgOptions utility function for debugging/testing chat requests. -func ApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.Values, error) { +// UnsafeApplyMsgOptions utility function for debugging/testing chat requests. +// NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this function +// will be supported by the library. +func UnsafeApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.Values, error) { config, err := applyMsgOptions(token, channel, options...) - return string(config.mode), config.values, err + return config.endpoint, config.values, err } func applyMsgOptions(token, channel string, options ...MsgOption) (sendConfig, error) { config := sendConfig{ - mode: chatPostMessage, + endpoint: APIURL + string(chatPostMessage), values: url.Values{ "token": {token}, "channel": {channel}, @@ -195,11 +201,6 @@ func applyMsgOptions(token, channel string, options ...MsgOption) (sendConfig, e return config, nil } -func escapeMessage(message string) string { - replacer := strings.NewReplacer("&", "&", "<", "<", ">", ">") - return replacer.Replace(message) -} - type sendMode string const ( @@ -208,11 +209,12 @@ const ( chatDelete sendMode = "chat.delete" chatPostEphemeral sendMode = "chat.postEphemeral" chatMeMessage sendMode = "chat.meMessage" + chatUnfurl sendMode = "chat.unfurl" ) type sendConfig struct { - mode sendMode - values url.Values + endpoint string + values url.Values } // MsgOption option provided when sending a message. @@ -221,26 +223,16 @@ type MsgOption func(*sendConfig) error // MsgOptionPost posts a messages, this is the default. func MsgOptionPost() MsgOption { return func(config *sendConfig) error { - config.mode = chatPostMessage + config.endpoint = APIURL + string(chatPostMessage) config.values.Del("ts") return nil } } -// MsgOptionPostEphemeral - DEPRECATED: use MsgOptionPostEphemeral2 -// posts an ephemeral message. -func MsgOptionPostEphemeral() MsgOption { +// MsgOptionPostEphemeral - posts an ephemeral message to the provided user. +func MsgOptionPostEphemeral(userID string) MsgOption { return func(config *sendConfig) error { - config.mode = chatPostEphemeral - config.values.Del("ts") - return nil - } -} - -// MsgOptionPostEphemeral2 - posts an ephemeral message to the provided user. -func MsgOptionPostEphemeral2(userID string) MsgOption { - return func(config *sendConfig) error { - config.mode = chatPostEphemeral + config.endpoint = APIURL + string(chatPostEphemeral) MsgOptionUser(userID)(config) config.values.Del("ts") @@ -251,7 +243,7 @@ func MsgOptionPostEphemeral2(userID string) MsgOption { // MsgOptionMeMessage posts a "me message" type from the calling user func MsgOptionMeMessage() MsgOption { return func(config *sendConfig) error { - config.mode = chatMeMessage + config.endpoint = APIURL + string(chatMeMessage) return nil } } @@ -259,7 +251,7 @@ func MsgOptionMeMessage() MsgOption { // MsgOptionUpdate updates a message based on the timestamp. func MsgOptionUpdate(timestamp string) MsgOption { return func(config *sendConfig) error { - config.mode = chatUpdate + config.endpoint = APIURL + string(chatUpdate) config.values.Add("ts", timestamp) return nil } @@ -268,12 +260,25 @@ func MsgOptionUpdate(timestamp string) MsgOption { // MsgOptionDelete deletes a message based on the timestamp. func MsgOptionDelete(timestamp string) MsgOption { return func(config *sendConfig) error { - config.mode = chatDelete + config.endpoint = APIURL + string(chatDelete) config.values.Add("ts", timestamp) return nil } } +// MsgOptionUnfurl unfurls a message based on the timestamp. +func MsgOptionUnfurl(timestamp string, unfurls map[string]Attachment) MsgOption { + return func(config *sendConfig) error { + config.endpoint = APIURL + string(chatUnfurl) + config.values.Add("ts", timestamp) + unfurlsStr, err := json.Marshal(unfurls) + if err == nil { + config.values.Add("unfurls", string(unfurlsStr)) + } + return err + } +} + // MsgOptionAsUser whether or not to send the message as the user. func MsgOptionAsUser(b bool) MsgOption { return func(config *sendConfig) error { @@ -292,12 +297,20 @@ func MsgOptionUser(userID string) MsgOption { } } +// MsgOptionUsername set the username for the message. +func MsgOptionUsername(username string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("username", username) + return nil + } +} + // MsgOptionText provide the text for the message, optionally escape the provided // text. func MsgOptionText(text string, escape bool) MsgOption { return func(config *sendConfig) error { if escape { - text = escapeMessage(text) + text = slackutilsx.EscapeMessage(text) } config.values.Add("text", text) return nil @@ -367,7 +380,7 @@ func MsgOptionBroadcast() MsgOption { } } -// this function combines multiple options into a single option. +// MsgOptionCompose combines multiple options into a single option. func MsgOptionCompose(options ...MsgOption) MsgOption { return func(c *sendConfig) error { for _, opt := range options { @@ -379,6 +392,7 @@ func MsgOptionCompose(options ...MsgOption) MsgOption { } } +// MsgOptionParse set parse option. func MsgOptionParse(b bool) MsgOption { return func(c *sendConfig) error { var v string @@ -392,6 +406,18 @@ func MsgOptionParse(b bool) MsgOption { } } +// UnsafeMsgOptionEndpoint deliver the message to the specified endpoint. +// NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this Option +// will be supported by the library, it is subject to change without notice that +// may result in compilation errors or runtime behaviour changes. +func UnsafeMsgOptionEndpoint(endpoint string, update func(url.Values)) MsgOption { + return func(config *sendConfig) error { + config.endpoint = endpoint + update(config.values) + return nil + } +} + // MsgOptionPostMessageParameters maintain backwards compatibility. func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { return func(config *sendConfig) error { @@ -446,3 +472,38 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { return nil } } + +// PermalinkParameters are the parameters required to get a permalink to a +// message. Slack documentation can be found here: +// https://api.slack.com/methods/chat.getPermalink +type PermalinkParameters struct { + Channel string + Ts string +} + +// GetPermalink returns the permalink for a message. It takes +// PermalinkParameters and returns a string containing the permalink. It +// returns an error if unable to retrieve the permalink. +func (api *Client) GetPermalink(params *PermalinkParameters) (string, error) { + return api.GetPermalinkContext(context.Background(), params) +} + +// GetPermalinkContext returns the permalink for a message using a custom context. +func (api *Client) GetPermalinkContext(ctx context.Context, params *PermalinkParameters) (string, error) { + values := url.Values{ + "token": {api.token}, + "channel": {params.Channel}, + "message_ts": {params.Ts}, + } + + response := struct { + Channel string `json:"channel"` + Permalink string `json:"permalink"` + SlackResponse + }{} + err := getSlackMethod(ctx, api.httpclient, "chat.getPermalink", values, &response, api) + if err != nil { + return "", err + } + return response.Permalink, response.Err() +} diff --git a/vendor/github.com/nlopes/slack/conversation.go b/vendor/github.com/nlopes/slack/conversation.go index edde87a2..ccd38f88 100644 --- a/vendor/github.com/nlopes/slack/conversation.go +++ b/vendor/github.com/nlopes/slack/conversation.go @@ -29,6 +29,8 @@ type conversation struct { NameNormalized string `json:"name_normalized"` NumMembers int `json:"num_members"` Priority float64 `json:"priority"` + User string `json:"user"` + // TODO support pending_shared // TODO support previous_names } @@ -64,6 +66,13 @@ type GetUsersInConversationParameters struct { Limit int } +type GetConversationsForUserParameters struct { + UserID string + Cursor string + Types []string + Limit int +} + type responseMetaData struct { NextCursor string `json:"next_cursor"` } @@ -90,7 +99,7 @@ func (api *Client) GetUsersInConversationContext(ctx context.Context, params *Ge ResponseMetaData responseMetaData `json:"response_metadata"` SlackResponse }{} - err := postSlackMethod(ctx, api.httpclient, "conversations.members", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.members", values, &response, api) if err != nil { return nil, "", err } @@ -100,6 +109,41 @@ func (api *Client) GetUsersInConversationContext(ctx context.Context, params *Ge return response.Members, response.ResponseMetaData.NextCursor, nil } +// GetConversationsForUser returns the list conversations for a given user +func (api *Client) GetConversationsForUser(params *GetConversationsForUserParameters) (channels []Channel, nextCursor string, err error) { + return api.GetConversationsForUserContext(context.Background(), params) +} + +// GetConversationsForUserContext returns the list conversations for a given user with a custom context +func (api *Client) GetConversationsForUserContext(ctx context.Context, params *GetConversationsForUserParameters) (channels []Channel, nextCursor string, err error) { + values := url.Values{ + "token": {api.token}, + } + if params.UserID != "" { + values.Add("user", params.UserID) + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + if params.Types != nil { + values.Add("types", strings.Join(params.Types, ",")) + } + response := struct { + Channels []Channel `json:"channels"` + ResponseMetaData responseMetaData `json:"response_metadata"` + SlackResponse + }{} + err = postSlackMethod(ctx, api.httpclient, "users.conversations", values, &response, api) + if err != nil { + return nil, "", err + } + + return response.Channels, response.ResponseMetaData.NextCursor, response.Err() +} + // ArchiveConversation archives a conversation func (api *Client) ArchiveConversation(channelID string) error { return api.ArchiveConversationContext(context.Background(), channelID) @@ -112,7 +156,7 @@ func (api *Client) ArchiveConversationContext(ctx context.Context, channelID str "channel": {channelID}, } response := SlackResponse{} - err := postSlackMethod(ctx, api.httpclient, "conversations.archive", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.archive", values, &response, api) if err != nil { return err } @@ -132,7 +176,7 @@ func (api *Client) UnArchiveConversationContext(ctx context.Context, channelID s "channel": {channelID}, } response := SlackResponse{} - err := postSlackMethod(ctx, api.httpclient, "conversations.unarchive", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.unarchive", values, &response, api) if err != nil { return err } @@ -156,7 +200,7 @@ func (api *Client) SetTopicOfConversationContext(ctx context.Context, channelID, SlackResponse Channel *Channel `json:"channel"` }{} - err := postSlackMethod(ctx, api.httpclient, "conversations.setTopic", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.setTopic", values, &response, api) if err != nil { return nil, err } @@ -180,7 +224,7 @@ func (api *Client) SetPurposeOfConversationContext(ctx context.Context, channelI SlackResponse Channel *Channel `json:"channel"` }{} - err := postSlackMethod(ctx, api.httpclient, "conversations.setPurpose", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.setPurpose", values, &response, api) if err != nil { return nil, err } @@ -204,7 +248,7 @@ func (api *Client) RenameConversationContext(ctx context.Context, channelID, cha SlackResponse Channel *Channel `json:"channel"` }{} - err := postSlackMethod(ctx, api.httpclient, "conversations.rename", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.rename", values, &response, api) if err != nil { return nil, err } @@ -228,7 +272,7 @@ func (api *Client) InviteUsersToConversationContext(ctx context.Context, channel SlackResponse Channel *Channel `json:"channel"` }{} - err := postSlackMethod(ctx, api.httpclient, "conversations.invite", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.invite", values, &response, api) if err != nil { return nil, err } @@ -249,7 +293,7 @@ func (api *Client) KickUserFromConversationContext(ctx context.Context, channelI "user": {user}, } response := SlackResponse{} - err := postSlackMethod(ctx, api.httpclient, "conversations.kick", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.kick", values, &response, api) if err != nil { return err } @@ -274,7 +318,7 @@ func (api *Client) CloseConversationContext(ctx context.Context, channelID strin AlreadyClosed bool `json:"already_closed"` }{} - err = postSlackMethod(ctx, api.httpclient, "conversations.close", values, &response, api.debug) + err = postSlackMethod(ctx, api.httpclient, "conversations.close", values, &response, api) if err != nil { return false, false, err } @@ -295,7 +339,7 @@ func (api *Client) CreateConversationContext(ctx context.Context, channelName st "is_private": {strconv.FormatBool(isPrivate)}, } response, err := channelRequest( - ctx, api.httpclient, "conversations.create", values, api.debug) + ctx, api.httpclient, "conversations.create", values, api) if err != nil { return nil, err } @@ -316,7 +360,7 @@ func (api *Client) GetConversationInfoContext(ctx context.Context, channelID str "include_locale": {strconv.FormatBool(includeLocale)}, } response, err := channelRequest( - ctx, api.httpclient, "conversations.info", values, api.debug) + ctx, api.httpclient, "conversations.info", values, api) if err != nil { return nil, err } @@ -336,7 +380,7 @@ func (api *Client) LeaveConversationContext(ctx context.Context, channelID strin "channel": {channelID}, } - response, err := channelRequest(ctx, api.httpclient, "conversations.leave", values, api.debug) + response, err := channelRequest(ctx, api.httpclient, "conversations.leave", values, api) if err != nil { return false, err } @@ -392,7 +436,7 @@ func (api *Client) GetConversationRepliesContext(ctx context.Context, params *Ge Messages []Message `json:"messages"` }{} - err = postSlackMethod(ctx, api.httpclient, "conversations.replies", values, &response, api.debug) + err = postSlackMethod(ctx, api.httpclient, "conversations.replies", values, &response, api) if err != nil { return nil, false, "", err } @@ -432,7 +476,7 @@ func (api *Client) GetConversationsContext(ctx context.Context, params *GetConve ResponseMetaData responseMetaData `json:"response_metadata"` SlackResponse }{} - err = postSlackMethod(ctx, api.httpclient, "conversations.list", values, &response, api.debug) + err = postSlackMethod(ctx, api.httpclient, "conversations.list", values, &response, api) if err != nil { return nil, "", err } @@ -469,7 +513,7 @@ func (api *Client) OpenConversationContext(ctx context.Context, params *OpenConv AlreadyOpen bool `json:"already_open"` SlackResponse }{} - err := postSlackMethod(ctx, api.httpclient, "conversations.open", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.open", values, &response, api) if err != nil { return nil, false, false, err } @@ -493,7 +537,7 @@ func (api *Client) JoinConversationContext(ctx context.Context, channelID string } `json:"response_metadata"` SlackResponse }{} - err := postSlackMethod(ctx, api.httpclient, "conversations.join", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.join", values, &response, api) if err != nil { return nil, "", nil, err } @@ -555,12 +599,10 @@ func (api *Client) GetConversationHistoryContext(ctx context.Context, params *Ge response := GetConversationHistoryResponse{} - err := postSlackMethod(ctx, api.httpclient, "conversations.history", values, &response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "conversations.history", values, &response, api) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return &response, nil + + return &response, response.Err() } diff --git a/vendor/github.com/nlopes/slack/dialog.go b/vendor/github.com/nlopes/slack/dialog.go index a13e53da..2e45a06c 100644 --- a/vendor/github.com/nlopes/slack/dialog.go +++ b/vendor/github.com/nlopes/slack/dialog.go @@ -6,100 +6,94 @@ import ( "errors" ) +// InputType is the type of the dialog input type +type InputType string + +const ( + // InputTypeText textfield input + InputTypeText InputType = "text" + // InputTypeTextArea textarea input + InputTypeTextArea InputType = "textarea" + // InputTypeSelect textfield input + InputTypeSelect InputType = "select" +) + +// DialogInput for dialogs input type text or menu +type DialogInput struct { + Type InputType `json:"type"` + Label string `json:"label"` + Name string `json:"name"` + Placeholder string `json:"placeholder"` + Optional bool `json:"optional"` +} + +// DialogTrigger ... type DialogTrigger struct { - TriggerId string `json:"trigger_id"` //Required. Must respond within 3 seconds. + TriggerID string `json:"trigger_id"` //Required. Must respond within 3 seconds. Dialog Dialog `json:"dialog"` //Required. } +// Dialog as in Slack dialogs +// https://api.slack.com/dialogs#option_element_attributes#top-level_dialog_attributes type Dialog struct { - CallbackId string `json:"callback_id"` //Required. - Title string `json:"title"` //Required. - SubmitLabel string `json:"submit_label,omitempty"` //Optional. Default value is 'Submit' - NotifyOnCancel bool `json:"notify_on_cancel,omitempty"` //Optional. Default value is false - Elements []DialogElement `json:"elements"` //Required. + TriggerID string `json:"trigger_id"` // Required + CallbackID string `json:"callback_id"` // Required + State string `json:"state,omitempty"` // Optional + Title string `json:"title"` + SubmitLabel string `json:"submit_label,omitempty"` + NotifyOnCancel bool `json:"notify_on_cancel"` + Elements []DialogElement `json:"elements"` } +// DialogElement abstract type for dialogs. type DialogElement interface{} -type DialogTextElement struct { - Label string `json:"label"` //Required. - Name string `json:"name"` //Required. - Type string `json:"type"` //Required. Allowed values: "text", "textarea", "select". - Placeholder string `json:"placeholder,omitempty"` //Optional. - Optional bool `json:"optional,omitempty"` //Optional. Default value is false - Value string `json:"value,omitempty"` //Optional. - MaxLength int `json:"max_length,omitempty"` //Optional. - MinLength int `json:"min_length,omitempty"` //Optional,. Default value is 0 - Hint string `json:"hint,omitempty"` //Optional. - Subtype string `json:"subtype,omitempty"` //Optional. Allowed values: "email", "number", "tel", "url". +// DialogCallback DEPRECATED use InteractionCallback +type DialogCallback InteractionCallback + +// DialogSubmissionCallback is sent from Slack when a user submits a form from within a dialog +type DialogSubmissionCallback struct { + State string `json:"state,omitempty"` + Submission map[string]string `json:"submission"` } -type DialogSelectElement struct { - Label string `json:"label"` //Required. - Name string `json:"name"` //Required. - Type string `json:"type"` //Required. Allowed values: "text", "textarea", "select". - Placeholder string `json:"placeholder,omitempty"` //Optional. - Optional bool `json:"optional,omitempty"` //Optional. Default value is false - Value string `json:"value,omitempty"` //Optional. - DataSource string `json:"data_source,omitempty"` //Optional. Allowed values: "users", "channels", "conversations", "external". - SelectedOptions string `json:"selected_options,omitempty"` //Optional. Default value for "external" only - Options []DialogElementOption `json:"options,omitempty"` //One of options or option_groups is required. - OptionGroups []DialogElementOption `json:"option_groups,omitempty"` //Provide up to 100 options. +// DialogOpenResponse response from `dialog.open` +type DialogOpenResponse struct { + SlackResponse + DialogResponseMetadata DialogResponseMetadata `json:"response_metadata"` } -type DialogElementOption struct { - Label string `json:"label"` //Required. - Value string `json:"value"` //Required. +// DialogResponseMetadata lists the error messages +type DialogResponseMetadata struct { + Messages []string `json:"messages"` } -// DialogCallback is sent from Slack when a user submits a form from within a dialog -type DialogCallback struct { - Type string `json:"type"` - CallbackID string `json:"callback_id"` - Team Team `json:"team"` - Channel Channel `json:"channel"` - User User `json:"user"` - ActionTs string `json:"action_ts"` - Token string `json:"token"` - ResponseURL string `json:"response_url"` - Submission map[string]string `json:"submission"` -} - -// DialogSuggestionCallback is sent from Slack when a user types in a select field with an external data source -type DialogSuggestionCallback struct { - Type string `json:"type"` - Token string `json:"token"` - ActionTs string `json:"action_ts"` - Team Team `json:"team"` - User User `json:"user"` - Channel Channel `json:"channel"` - ElementName string `json:"name"` - Value string `json:"value"` - CallbackID string `json:"callback_id"` -} - -// OpenDialog opens a dialog window where the triggerId originated from -func (api *Client) OpenDialog(triggerId string, dialog Dialog) (err error) { - return api.OpenDialogContext(context.Background(), triggerId, dialog) +// OpenDialog opens a dialog window where the triggerID originated from. +// EXPERIMENTAL: dialog functionality is currently experimental, api is not considered stable. +func (api *Client) OpenDialog(triggerID string, dialog Dialog) (err error) { + return api.OpenDialogContext(context.Background(), triggerID, dialog) } // OpenDialogContext opens a dialog window where the triggerId originated from with a custom context -func (api *Client) OpenDialogContext(ctx context.Context, triggerId string, dialog Dialog) (err error) { - if triggerId == "" { +// EXPERIMENTAL: dialog functionality is currently experimental, api is not considered stable. +func (api *Client) OpenDialogContext(ctx context.Context, triggerID string, dialog Dialog) (err error) { + if triggerID == "" { return errors.New("received empty parameters") } - resp := DialogTrigger{ - TriggerId: triggerId, + req := DialogTrigger{ + TriggerID: triggerID, Dialog: dialog, } - jsonResp, err := json.Marshal(resp) + + encoded, err := json.Marshal(req) if err != nil { return err } - response := &SlackResponse{} - endpoint := SLACK_API + "dialog.open" - if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonResp, response, api.debug); err != nil { + + response := &DialogOpenResponse{} + endpoint := APIURL + "dialog.open" + if err := postJSON(ctx, api.httpclient, endpoint, api.token, encoded, response, api); err != nil { return err } diff --git a/vendor/github.com/nlopes/slack/dialog_select.go b/vendor/github.com/nlopes/slack/dialog_select.go new file mode 100644 index 00000000..ea95ccfa --- /dev/null +++ b/vendor/github.com/nlopes/slack/dialog_select.go @@ -0,0 +1,126 @@ +package slack + +// SelectDataSource types of select datasource +type SelectDataSource string + +const ( + // DialogDataSourceStatic menu with static Options/OptionGroups + DialogDataSourceStatic SelectDataSource = "static" + // DialogDataSourceExternal dynamic datasource + DialogDataSourceExternal SelectDataSource = "external" + // DialogDataSourceConversations provides a list of conversations + DialogDataSourceConversations SelectDataSource = "conversations" + // DialogDataSourceChannels provides a list of channels + DialogDataSourceChannels SelectDataSource = "channels" + // DialogDataSourceUsers provides a list of users + DialogDataSourceUsers SelectDataSource = "users" +) + +// DialogInputSelect dialog support for select boxes. +type DialogInputSelect struct { + DialogInput + Value string `json:"value,omitempty"` //Optional. + DataSource SelectDataSource `json:"data_source,omitempty"` //Optional. Allowed values: "users", "channels", "conversations", "external". + SelectedOptions string `json:"selected_options,omitempty"` //Optional. Default value for "external" only + Options []DialogSelectOption `json:"options,omitempty"` //One of options or option_groups is required. + OptionGroups []DialogOptionGroup `json:"option_groups,omitempty"` //Provide up to 100 options. + MinQueryLength int `json:"min_query_length,omitempty"` //Optional. minimum characters before query is sent. +} + +// DialogSelectOption is an option for the user to select from the menu +type DialogSelectOption struct { + Label string `json:"label"` + Value string `json:"value"` +} + +// DialogOptionGroup is a collection of options for creating a segmented table +type DialogOptionGroup struct { + Label string `json:"label"` + Options []DialogSelectOption `json:"options"` +} + +// NewStaticSelectDialogInput constructor for a `static` datasource menu input +func NewStaticSelectDialogInput(name, label string, options []DialogSelectOption) *DialogInputSelect { + return &DialogInputSelect{ + DialogInput: DialogInput{ + Type: InputTypeSelect, + Name: name, + Label: label, + Optional: true, + }, + DataSource: DialogDataSourceStatic, + Options: options, + } +} + +// NewGroupedSelectDialogInput creates grouped options select input for Dialogs. +func NewGroupedSelectDialogInput(name, label string, groups map[string]map[string]string) *DialogInputSelect { + optionGroups := []DialogOptionGroup{} + for groupName, options := range groups { + optionGroups = append(optionGroups, DialogOptionGroup{ + Label: groupName, + Options: optionsFromMap(options), + }) + } + return &DialogInputSelect{ + DialogInput: DialogInput{ + Type: InputTypeSelect, + Name: name, + Label: label, + }, + DataSource: DialogDataSourceStatic, + OptionGroups: optionGroups, + } +} + +func optionsFromArray(options []string) []DialogSelectOption { + selectOptions := make([]DialogSelectOption, len(options)) + for idx, value := range options { + selectOptions[idx] = DialogSelectOption{ + Label: value, + Value: value, + } + } + return selectOptions +} + +func optionsFromMap(options map[string]string) []DialogSelectOption { + selectOptions := make([]DialogSelectOption, len(options)) + idx := 0 + var option DialogSelectOption + for key, value := range options { + option = DialogSelectOption{ + Label: key, + Value: value, + } + selectOptions[idx] = option + idx++ + } + return selectOptions +} + +// NewConversationsSelect returns a `Conversations` select +func NewConversationsSelect(name, label string) *DialogInputSelect { + return newPresetSelect(name, label, DialogDataSourceConversations) +} + +// NewChannelsSelect returns a `Channels` select +func NewChannelsSelect(name, label string) *DialogInputSelect { + return newPresetSelect(name, label, DialogDataSourceChannels) +} + +// NewUsersSelect returns a `Users` select +func NewUsersSelect(name, label string) *DialogInputSelect { + return newPresetSelect(name, label, DialogDataSourceUsers) +} + +func newPresetSelect(name, label string, dataSourceType SelectDataSource) *DialogInputSelect { + return &DialogInputSelect{ + DialogInput: DialogInput{ + Type: InputTypeSelect, + Label: label, + Name: name, + }, + DataSource: dataSourceType, + } +} diff --git a/vendor/github.com/nlopes/slack/dialog_text.go b/vendor/github.com/nlopes/slack/dialog_text.go new file mode 100644 index 00000000..bf9602cc --- /dev/null +++ b/vendor/github.com/nlopes/slack/dialog_text.go @@ -0,0 +1,50 @@ +package slack + +// TextInputSubtype Accepts email, number, tel, or url. In some form factors, optimized input is provided for this subtype. +type TextInputSubtype string + +const ( + // InputSubtypeEmail email keyboard + InputSubtypeEmail TextInputSubtype = "email" + // InputSubtypeNumber numeric keyboard + InputSubtypeNumber TextInputSubtype = "number" + // InputSubtypeTel Phone keyboard + InputSubtypeTel TextInputSubtype = "tel" + // InputSubtypeURL Phone keyboard + InputSubtypeURL TextInputSubtype = "url" +) + +// TextInputElement subtype of DialogInput +// https://api.slack.com/dialogs#option_element_attributes#text_element_attributes +type TextInputElement struct { + DialogInput + MaxLength int `json:"max_length,omitempty"` + MinLength int `json:"min_length,omitempty"` + Hint string `json:"hint,omitempty"` + Subtype TextInputSubtype `json:"subtype"` + Value string `json:"value"` +} + +// NewTextInput constructor for a `text` input +func NewTextInput(name, label, text string) *TextInputElement { + return &TextInputElement{ + DialogInput: DialogInput{ + Type: InputTypeText, + Name: name, + Label: label, + }, + Value: text, + } +} + +// NewTextAreaInput constructor for a `textarea` input +func NewTextAreaInput(name, label, text string) *TextInputElement { + return &TextInputElement{ + DialogInput: DialogInput{ + Type: InputTypeTextArea, + Name: name, + Label: label, + }, + Value: text, + } +} diff --git a/vendor/github.com/nlopes/slack/dnd.go b/vendor/github.com/nlopes/slack/dnd.go index 26d36d6a..da6e4a16 100644 --- a/vendor/github.com/nlopes/slack/dnd.go +++ b/vendor/github.com/nlopes/slack/dnd.go @@ -36,9 +36,9 @@ type dndTeamInfoResponse struct { SlackResponse } -func dndRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*dndResponseFull, error) { +func dndRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*dndResponseFull, error) { response := &dndResponseFull{} - err := postSlackMethod(ctx, client, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, d) if err != nil { return nil, err } @@ -61,7 +61,7 @@ func (api *Client) EndDNDContext(ctx context.Context) error { response := &SlackResponse{} - if err := postSlackMethod(ctx, api.httpclient, "dnd.endDnd", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "dnd.endDnd", values, response, api); err != nil { return err } @@ -79,7 +79,7 @@ func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) { "token": {api.token}, } - response, err := dndRequest(ctx, api.httpclient, "dnd.endSnooze", values, api.debug) + response, err := dndRequest(ctx, api.httpclient, "dnd.endSnooze", values, api) if err != nil { return nil, err } @@ -100,7 +100,7 @@ func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDSta values.Set("user", *user) } - response, err := dndRequest(ctx, api.httpclient, "dnd.info", values, api.debug) + response, err := dndRequest(ctx, api.httpclient, "dnd.info", values, api) if err != nil { return nil, err } @@ -120,11 +120,12 @@ func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (m } response := &dndTeamInfoResponse{} - if err := postSlackMethod(ctx, api.httpclient, "dnd.teamInfo", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "dnd.teamInfo", values, response, api); err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) + + if response.Err() != nil { + return nil, response.Err() } return response.Users, nil } @@ -144,7 +145,7 @@ func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatu "num_minutes": {strconv.Itoa(minutes)}, } - response, err := dndRequest(ctx, api.httpclient, "dnd.setSnooze", values, api.debug) + response, err := dndRequest(ctx, api.httpclient, "dnd.setSnooze", values, api) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/emoji.go b/vendor/github.com/nlopes/slack/emoji.go index fe2945c4..aed2129f 100644 --- a/vendor/github.com/nlopes/slack/emoji.go +++ b/vendor/github.com/nlopes/slack/emoji.go @@ -23,7 +23,7 @@ func (api *Client) GetEmojiContext(ctx context.Context) (map[string]string, erro } response := &emojiResponseFull{} - err := postSlackMethod(ctx, api.httpclient, "emoji.list", values, response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "emoji.list", values, response, api) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/files.go b/vendor/github.com/nlopes/slack/files.go index 2381ec3c..13158582 100644 --- a/vendor/github.com/nlopes/slack/files.go +++ b/vendor/github.com/nlopes/slack/files.go @@ -93,14 +93,15 @@ type File struct { // There are three ways to upload a file. You can either set Content if file is small, set Reader if file is large, // or provide a local file path in File to upload it from your filesystem. type FileUploadParameters struct { - File string - Content string - Reader io.Reader - Filetype string - Filename string - Title string - InitialComment string - Channels []string + File string + Content string + Reader io.Reader + Filetype string + Filename string + Title string + InitialComment string + Channels []string + ThreadTimestamp string } // GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request @@ -136,16 +137,14 @@ func NewGetFilesParameters() GetFilesParameters { } } -func fileRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*fileResponseFull, error) { +func fileRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*fileResponseFull, error) { response := &fileResponseFull{} - err := postForm(ctx, client, SLACK_API+path, values, response, debug) + err := postForm(ctx, client, APIURL+path, values, response, d) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil + + return response, response.Err() } // GetFileInfo retrieves a file and related comments @@ -162,7 +161,7 @@ func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count, "page": {strconv.Itoa(page)}, } - response, err := fileRequest(ctx, api.httpclient, "files.info", values, api.debug) + response, err := fileRequest(ctx, api.httpclient, "files.info", values, api) if err != nil { return nil, nil, nil, err } @@ -201,7 +200,7 @@ func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameter values.Add("page", strconv.Itoa(params.Page)) } - response, err := fileRequest(ctx, api.httpclient, "files.list", values, api.debug) + response, err := fileRequest(ctx, api.httpclient, "files.list", values, api) if err != nil { return nil, nil, err } @@ -237,24 +236,25 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam if params.InitialComment != "" { values.Add("initial_comment", params.InitialComment) } + if params.ThreadTimestamp != "" { + values.Add("thread_ts", params.ThreadTimestamp) + } if len(params.Channels) != 0 { values.Add("channels", strings.Join(params.Channels, ",")) } if params.Content != "" { values.Add("content", params.Content) - err = postForm(ctx, api.httpclient, SLACK_API+"files.upload", values, response, api.debug) + err = postForm(ctx, api.httpclient, APIURL+"files.upload", values, response, api) } else if params.File != "" { - err = postLocalWithMultipartResponse(ctx, api.httpclient, "files.upload", params.File, "file", values, response, api.debug) + err = postLocalWithMultipartResponse(ctx, api.httpclient, "files.upload", params.File, "file", values, response, api) } else if params.Reader != nil { - err = postWithMultipartResponse(ctx, api.httpclient, "files.upload", params.Filename, "file", values, params.Reader, response, api.debug) + err = postWithMultipartResponse(ctx, api.httpclient, "files.upload", params.Filename, "file", values, params.Reader, response, api) } if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return &response.File, nil + + return &response.File, response.Err() } // DeleteFileComment deletes a file's comment @@ -273,7 +273,7 @@ func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, comment "file": {fileID}, "id": {commentID}, } - _, err = fileRequest(ctx, api.httpclient, "files.comments.delete", values, api.debug) + _, err = fileRequest(ctx, api.httpclient, "files.comments.delete", values, api) return err } @@ -289,7 +289,7 @@ func (api *Client) DeleteFileContext(ctx context.Context, fileID string) (err er "file": {fileID}, } - _, err = fileRequest(ctx, api.httpclient, "files.delete", values, api.debug) + _, err = fileRequest(ctx, api.httpclient, "files.delete", values, api) return err } @@ -305,7 +305,7 @@ func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string "file": {fileID}, } - response, err := fileRequest(ctx, api.httpclient, "files.revokePublicURL", values, api.debug) + response, err := fileRequest(ctx, api.httpclient, "files.revokePublicURL", values, api) if err != nil { return nil, err } @@ -324,7 +324,7 @@ func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string) "file": {fileID}, } - response, err := fileRequest(ctx, api.httpclient, "files.sharedPublicURL", values, api.debug) + response, err := fileRequest(ctx, api.httpclient, "files.sharedPublicURL", values, api) if err != nil { return nil, nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/groups.go b/vendor/github.com/nlopes/slack/groups.go index 67e78e99..a248f6fd 100644 --- a/vendor/github.com/nlopes/slack/groups.go +++ b/vendor/github.com/nlopes/slack/groups.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "net/url" "strconv" ) @@ -28,16 +27,14 @@ type groupResponseFull struct { SlackResponse } -func groupRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*groupResponseFull, error) { +func groupRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*groupResponseFull, error) { response := &groupResponseFull{} - err := postForm(ctx, client, SLACK_API+path, values, response, debug) + err := postForm(ctx, client, APIURL+path, values, response, d) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil + + return response, response.Err() } // ArchiveGroup archives a private group @@ -52,7 +49,7 @@ func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error "channel": {group}, } - _, err := groupRequest(ctx, api.httpclient, "groups.archive", values, api.debug) + _, err := groupRequest(ctx, api.httpclient, "groups.archive", values, api) return err } @@ -68,7 +65,7 @@ func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) erro "channel": {group}, } - _, err := groupRequest(ctx, api.httpclient, "groups.unarchive", values, api.debug) + _, err := groupRequest(ctx, api.httpclient, "groups.unarchive", values, api) return err } @@ -84,7 +81,7 @@ func (api *Client) CreateGroupContext(ctx context.Context, group string) (*Group "name": {group}, } - response, err := groupRequest(ctx, api.httpclient, "groups.create", values, api.debug) + response, err := groupRequest(ctx, api.httpclient, "groups.create", values, api) if err != nil { return nil, err } @@ -109,7 +106,7 @@ func (api *Client) CreateChildGroupContext(ctx context.Context, group string) (* "channel": {group}, } - response, err := groupRequest(ctx, api.httpclient, "groups.createChild", values, api.debug) + response, err := groupRequest(ctx, api.httpclient, "groups.createChild", values, api) if err != nil { return nil, err } @@ -128,7 +125,7 @@ func (api *Client) CloseGroupContext(ctx context.Context, group string) (bool, b "channel": {group}, } - response, err := imRequest(ctx, api.httpclient, "groups.close", values, api.debug) + response, err := imRequest(ctx, api.httpclient, "groups.close", values, api) if err != nil { return false, false, err } @@ -170,7 +167,7 @@ func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, par } } - response, err := groupRequest(ctx, api.httpclient, "groups.history", values, api.debug) + response, err := groupRequest(ctx, api.httpclient, "groups.history", values, api) if err != nil { return nil, err } @@ -190,7 +187,7 @@ func (api *Client) InviteUserToGroupContext(ctx context.Context, group, user str "user": {user}, } - response, err := groupRequest(ctx, api.httpclient, "groups.invite", values, api.debug) + response, err := groupRequest(ctx, api.httpclient, "groups.invite", values, api) if err != nil { return nil, false, err } @@ -209,7 +206,7 @@ func (api *Client) LeaveGroupContext(ctx context.Context, group string) (err err "channel": {group}, } - _, err = groupRequest(ctx, api.httpclient, "groups.leave", values, api.debug) + _, err = groupRequest(ctx, api.httpclient, "groups.leave", values, api) return err } @@ -226,7 +223,7 @@ func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user str "user": {user}, } - _, err = groupRequest(ctx, api.httpclient, "groups.kick", values, api.debug) + _, err = groupRequest(ctx, api.httpclient, "groups.kick", values, api) return err } @@ -244,7 +241,7 @@ func (api *Client) GetGroupsContext(ctx context.Context, excludeArchived bool) ( values.Add("exclude_archived", "1") } - response, err := groupRequest(ctx, api.httpclient, "groups.list", values, api.debug) + response, err := groupRequest(ctx, api.httpclient, "groups.list", values, api) if err != nil { return nil, err } @@ -263,7 +260,7 @@ func (api *Client) GetGroupInfoContext(ctx context.Context, group string) (*Grou "channel": {group}, } - response, err := groupRequest(ctx, api.httpclient, "groups.info", values, api.debug) + response, err := groupRequest(ctx, api.httpclient, "groups.info", values, api) if err != nil { return nil, err } @@ -288,7 +285,7 @@ func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string "ts": {ts}, } - _, err = groupRequest(ctx, api.httpclient, "groups.mark", values, api.debug) + _, err = groupRequest(ctx, api.httpclient, "groups.mark", values, api) return err } @@ -304,7 +301,7 @@ func (api *Client) OpenGroupContext(ctx context.Context, group string) (bool, bo "channel": {group}, } - response, err := groupRequest(ctx, api.httpclient, "groups.open", values, api.debug) + response, err := groupRequest(ctx, api.httpclient, "groups.open", values, api) if err != nil { return false, false, err } @@ -328,7 +325,7 @@ func (api *Client) RenameGroupContext(ctx context.Context, group, name string) ( // XXX: the created entry in this call returns a string instead of a number // so I may have to do some workaround to solve it. - response, err := groupRequest(ctx, api.httpclient, "groups.rename", values, api.debug) + response, err := groupRequest(ctx, api.httpclient, "groups.rename", values, api) if err != nil { return nil, err } @@ -348,7 +345,7 @@ func (api *Client) SetGroupPurposeContext(ctx context.Context, group, purpose st "purpose": {purpose}, } - response, err := groupRequest(ctx, api.httpclient, "groups.setPurpose", values, api.debug) + response, err := groupRequest(ctx, api.httpclient, "groups.setPurpose", values, api) if err != nil { return "", err } @@ -368,7 +365,7 @@ func (api *Client) SetGroupTopicContext(ctx context.Context, group, topic string "topic": {topic}, } - response, err := groupRequest(ctx, api.httpclient, "groups.setTopic", values, api.debug) + response, err := groupRequest(ctx, api.httpclient, "groups.setTopic", values, api) if err != nil { return "", err } diff --git a/vendor/github.com/nlopes/slack/im.go b/vendor/github.com/nlopes/slack/im.go index fa8b0959..10563d91 100644 --- a/vendor/github.com/nlopes/slack/im.go +++ b/vendor/github.com/nlopes/slack/im.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "net/url" "strconv" ) @@ -29,16 +28,14 @@ type IM struct { IsUserDeleted bool `json:"is_user_deleted"` } -func imRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*imResponseFull, error) { +func imRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*imResponseFull, error) { response := &imResponseFull{} - err := postSlackMethod(ctx, client, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, d) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil + + return response, response.Err() } // CloseIMChannel closes the direct message channel @@ -53,7 +50,7 @@ func (api *Client) CloseIMChannelContext(ctx context.Context, channel string) (b "channel": {channel}, } - response, err := imRequest(ctx, api.httpclient, "im.close", values, api.debug) + response, err := imRequest(ctx, api.httpclient, "im.close", values, api) if err != nil { return false, false, err } @@ -74,7 +71,7 @@ func (api *Client) OpenIMChannelContext(ctx context.Context, user string) (bool, "user": {user}, } - response, err := imRequest(ctx, api.httpclient, "im.open", values, api.debug) + response, err := imRequest(ctx, api.httpclient, "im.open", values, api) if err != nil { return false, false, "", err } @@ -94,7 +91,7 @@ func (api *Client) MarkIMChannelContext(ctx context.Context, channel, ts string) "ts": {ts}, } - _, err := imRequest(ctx, api.httpclient, "im.mark", values, api.debug) + _, err := imRequest(ctx, api.httpclient, "im.mark", values, api) return err } @@ -133,7 +130,7 @@ func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, para } } - response, err := imRequest(ctx, api.httpclient, "im.history", values, api.debug) + response, err := imRequest(ctx, api.httpclient, "im.history", values, api) if err != nil { return nil, err } @@ -151,7 +148,7 @@ func (api *Client) GetIMChannelsContext(ctx context.Context) ([]IM, error) { "token": {api.token}, } - response, err := imRequest(ctx, api.httpclient, "im.list", values, api.debug) + response, err := imRequest(ctx, api.httpclient, "im.list", values, api) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/interactions.go b/vendor/github.com/nlopes/slack/interactions.go new file mode 100644 index 00000000..97e31a49 --- /dev/null +++ b/vendor/github.com/nlopes/slack/interactions.go @@ -0,0 +1,31 @@ +package slack + +// InteractionType type of interactions +type InteractionType string + +// Types of interactions that can be received. +const ( + InteractionTypeDialogSubmission = InteractionType("dialog_submission") + InteractionTypeDialogSuggestion = InteractionType("dialog_suggestion") + InteractionTypeInteractionMessage = InteractionType("interactive_message") + InteractionTypeMessageAction = InteractionType("message_action") +) + +// InteractionCallback is sent from slack when a user interactions with a button or dialog. +type InteractionCallback struct { + Type InteractionType `json:"type"` + Token string `json:"token"` + CallbackID string `json:"callback_id"` + ResponseURL string `json:"response_url"` + TriggerID string `json:"trigger_id"` + ActionTs string `json:"action_ts"` + Team Team `json:"team"` + Channel Channel `json:"channel"` + User User `json:"user"` + OriginalMessage Message `json:"original_message"` + Message Message `json:"message"` + Name string `json:"name"` + Value string `json:"value"` + ActionCallback + DialogSubmissionCallback +} diff --git a/vendor/github.com/nlopes/slack/logger.go b/vendor/github.com/nlopes/slack/logger.go index 501d1672..6a3533a9 100644 --- a/vendor/github.com/nlopes/slack/logger.go +++ b/vendor/github.com/nlopes/slack/logger.go @@ -2,52 +2,59 @@ package slack import ( "fmt" - "sync" ) -// SetLogger let's library users supply a logger, so that api debugging -// can be logged along with the application's debugging info. -func SetLogger(l logProvider) { - loggerMutex.Lock() - logger = ilogger{logProvider: l} - loggerMutex.Unlock() -} - -var ( - loggerMutex = new(sync.Mutex) - logger logInternal // A logger that can be set by consumers -) - -// logProvider is a logger interface compatible with both stdlib and some -// 3rd party loggers such as logrus. -type logProvider interface { +// logger is a logger interface compatible with both stdlib and some +// 3rd party loggers. +type logger interface { Output(int, string) error } -// logInternal represents the internal logging api we use. -type logInternal interface { +// ilogger represents the internal logging api we use. +type ilogger interface { + logger Print(...interface{}) Printf(string, ...interface{}) Println(...interface{}) - Output(int, string) error } -// ilogger implements the additional methods used by our internal logging. -type ilogger struct { - logProvider +type debug interface { + Debug() bool + + // Debugf print a formatted debug line. + Debugf(format string, v ...interface{}) + // Debugln print a debug line. + Debugln(v ...interface{}) +} + +// internalLog implements the additional methods used by our internal logging. +type internalLog struct { + logger } // Println replicates the behaviour of the standard logger. -func (t ilogger) Println(v ...interface{}) { +func (t internalLog) Println(v ...interface{}) { t.Output(2, fmt.Sprintln(v...)) } // Printf replicates the behaviour of the standard logger. -func (t ilogger) Printf(format string, v ...interface{}) { +func (t internalLog) Printf(format string, v ...interface{}) { t.Output(2, fmt.Sprintf(format, v...)) } // Print replicates the behaviour of the standard logger. -func (t ilogger) Print(v ...interface{}) { +func (t internalLog) Print(v ...interface{}) { t.Output(2, fmt.Sprint(v...)) } + +type discard struct{} + +func (t discard) Debug() bool { + return false +} + +// Debugf print a formatted debug line. +func (t discard) Debugf(format string, v ...interface{}) {} + +// Debugln print a debug line. +func (t discard) Debugln(v ...interface{}) {} diff --git a/vendor/github.com/nlopes/slack/messages.go b/vendor/github.com/nlopes/slack/messages.go index 6551dd4f..bde9a37e 100644 --- a/vendor/github.com/nlopes/slack/messages.go +++ b/vendor/github.com/nlopes/slack/messages.go @@ -4,11 +4,12 @@ package slack type OutgoingMessage struct { ID int `json:"id"` // channel ID - Channel string `json:"channel,omitempty"` - Text string `json:"text,omitempty"` - Type string `json:"type,omitempty"` - ThreadTimestamp string `json:"thread_ts,omitempty"` - ThreadBroadcast bool `json:"reply_broadcast,omitempty"` + Channel string `json:"channel,omitempty"` + Text string `json:"text,omitempty"` + Type string `json:"type,omitempty"` + ThreadTimestamp string `json:"thread_ts,omitempty"` + ThreadBroadcast bool `json:"reply_broadcast,omitempty"` + IDs []string `json:"ids,omitempty"` } // Message is an auxiliary type to allow us to have a message containing sub messages @@ -147,6 +148,15 @@ func (rtm *RTM) NewOutgoingMessage(text string, channelID string, options ...RTM return &msg } +// NewSubscribeUserPresence prepares an OutgoingMessage that the user can +// use to subscribe presence events for the specified users. +func (rtm *RTM) NewSubscribeUserPresence(ids []string) *OutgoingMessage { + return &OutgoingMessage{ + Type: "presence_sub", + IDs: ids, + } +} + // NewTypingMessage prepares an OutgoingMessage that the user can // use to send as a typing indicator. Use this function to properly set the // messageID. @@ -174,5 +184,4 @@ func RTMsgOptionBroadcast() RTMsgOption { return func(msg *OutgoingMessage) { msg.ThreadBroadcast = true } - } diff --git a/vendor/github.com/nlopes/slack/misc.go b/vendor/github.com/nlopes/slack/misc.go index 69103384..30ae4628 100644 --- a/vendor/github.com/nlopes/slack/misc.go +++ b/vendor/github.com/nlopes/slack/misc.go @@ -19,6 +19,7 @@ import ( "time" ) +// SlackResponse handles parsing out errors from the web api. type SlackResponse struct { Ok bool `json:"ok"` Error string `json:"error"` @@ -64,47 +65,31 @@ func (e *RateLimitedError) Error() string { return fmt.Sprintf("Slack rate limit exceeded, retry after %s", e.RetryAfter) } -func fileUploadReq(ctx context.Context, path, fieldname, filename string, values url.Values, r io.Reader) (*http.Request, error) { - body := &bytes.Buffer{} - wr := multipart.NewWriter(body) +func fileUploadReq(ctx context.Context, path string, values url.Values, r io.Reader) (*http.Request, error) { + req, err := http.NewRequest("POST", path, r) - ioWriter, err := wr.CreateFormFile(fieldname, filename) - if err != nil { - wr.Close() - return nil, err - } - _, err = io.Copy(ioWriter, r) - if err != nil { - wr.Close() - return nil, err - } - // Close the multipart writer or the footer won't be written - wr.Close() - req, err := http.NewRequest("POST", path, body) req = req.WithContext(ctx) if err != nil { return nil, err } - req.Header.Add("Content-Type", wr.FormDataContentType()) req.URL.RawQuery = (values).Encode() return req, nil } -func parseResponseBody(body io.ReadCloser, intf interface{}, debug bool) error { +func parseResponseBody(body io.ReadCloser, intf interface{}, d debug) error { response, err := ioutil.ReadAll(body) if err != nil { return err } - // FIXME: will be api.Debugf - if debug { - logger.Printf("parseResponseBody: %s\n", string(response)) + if d.Debug() { + d.Debugln("parseResponseBody", string(response)) } return json.Unmarshal(response, intf) } -func postLocalWithMultipartResponse(ctx context.Context, client HTTPRequester, path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error { +func postLocalWithMultipartResponse(ctx context.Context, client httpClient, path, fpath, fieldname string, values url.Values, intf interface{}, d debug) error { fullpath, err := filepath.Abs(fpath) if err != nil { return err @@ -114,14 +99,65 @@ func postLocalWithMultipartResponse(ctx context.Context, client HTTPRequester, p return err } defer file.Close() - return postWithMultipartResponse(ctx, client, path, filepath.Base(fpath), fieldname, values, file, intf, debug) + return postWithMultipartResponse(ctx, client, path, filepath.Base(fpath), fieldname, values, file, intf, d) } -func postWithMultipartResponse(ctx context.Context, client HTTPRequester, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, debug bool) error { - req, err := fileUploadReq(ctx, SLACK_API+path, fieldname, name, values, r) +func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, d debug) error { + pipeReader, pipeWriter := io.Pipe() + wr := multipart.NewWriter(pipeWriter) + errc := make(chan error) + go func() { + defer pipeWriter.Close() + ioWriter, err := wr.CreateFormFile(fieldname, name) + if err != nil { + errc <- err + return + } + _, err = io.Copy(ioWriter, r) + if err != nil { + errc <- err + return + } + if err = wr.Close(); err != nil { + errc <- err + return + } + }() + req, err := fileUploadReq(ctx, APIURL+path, values, pipeReader) if err != nil { return err } + req.Header.Add("Content-Type", wr.FormDataContentType()) + req = req.WithContext(ctx) + resp, err := client.Do(req) + + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusTooManyRequests { + retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64) + if err != nil { + return err + } + return &RateLimitedError{time.Duration(retry) * time.Second} + } + + // Slack seems to send an HTML body along with 5xx error codes. Don't parse it. + if resp.StatusCode != http.StatusOK { + logResponse(resp, d) + return statusCodeError{Code: resp.StatusCode, Status: resp.Status} + } + select { + case err = <-errc: + return err + default: + return parseResponseBody(resp.Body, intf, d) + } +} + +func doPost(ctx context.Context, client httpClient, req *http.Request, intf interface{}, d debug) error { req = req.WithContext(ctx) resp, err := client.Do(req) if err != nil { @@ -139,40 +175,15 @@ func postWithMultipartResponse(ctx context.Context, client HTTPRequester, path, // Slack seems to send an HTML body along with 5xx error codes. Don't parse it. if resp.StatusCode != http.StatusOK { - logResponse(resp, debug) + logResponse(resp, d) return statusCodeError{Code: resp.StatusCode, Status: resp.Status} } - return parseResponseBody(resp.Body, intf, debug) -} - -func doPost(ctx context.Context, client HTTPRequester, req *http.Request, intf interface{}, debug bool) error { - req = req.WithContext(ctx) - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode == http.StatusTooManyRequests { - retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64) - if err != nil { - return err - } - return &RateLimitedError{time.Duration(retry) * time.Second} - } - - // Slack seems to send an HTML body along with 5xx error codes. Don't parse it. - if resp.StatusCode != http.StatusOK { - logResponse(resp, debug) - return statusCodeError{Code: resp.StatusCode, Status: resp.Status} - } - - return parseResponseBody(resp.Body, intf, debug) + return parseResponseBody(resp.Body, intf, d) } // post JSON. -func postJSON(ctx context.Context, client HTTPRequester, endpoint, token string, json []byte, intf interface{}, debug bool) error { +func postJSON(ctx context.Context, client httpClient, endpoint, token string, json []byte, intf interface{}, d debug) error { reqBody := bytes.NewBuffer(json) req, err := http.NewRequest("POST", endpoint, reqBody) if err != nil { @@ -180,38 +191,53 @@ func postJSON(ctx context.Context, client HTTPRequester, endpoint, token string, } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - return doPost(ctx, client, req, intf, debug) + return doPost(ctx, client, req, intf, d) } // post a url encoded form. -func postForm(ctx context.Context, client HTTPRequester, endpoint string, values url.Values, intf interface{}, debug bool) error { +func postForm(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d debug) error { reqBody := strings.NewReader(values.Encode()) req, err := http.NewRequest("POST", endpoint, reqBody) if err != nil { return err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - return doPost(ctx, client, req, intf, debug) + return doPost(ctx, client, req, intf, d) } // post to a slack web method. -func postSlackMethod(ctx context.Context, client HTTPRequester, path string, values url.Values, intf interface{}, debug bool) error { - return postForm(ctx, client, SLACK_API+path, values, intf, debug) +func postSlackMethod(ctx context.Context, client httpClient, path string, values url.Values, intf interface{}, d debug) error { + return postForm(ctx, client, APIURL+path, values, intf, d) } -func parseAdminResponse(ctx context.Context, client HTTPRequester, method string, teamName string, values url.Values, intf interface{}, debug bool) error { - endpoint := fmt.Sprintf(SLACK_WEB_API_FORMAT, teamName, method, time.Now().Unix()) - return postForm(ctx, client, endpoint, values, intf, debug) +// get a slack web method. +func getSlackMethod(ctx context.Context, client httpClient, path string, values url.Values, intf interface{}, d debug) error { + return getResource(ctx, client, APIURL+path, values, intf, d) } -func logResponse(resp *http.Response, debug bool) error { - if debug { +func getResource(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d debug) error { + req, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.URL.RawQuery = values.Encode() + + return doPost(ctx, client, req, intf, d) +} + +func parseAdminResponse(ctx context.Context, client httpClient, method string, teamName string, values url.Values, intf interface{}, d debug) error { + endpoint := fmt.Sprintf(WEBAPIURLFormat, teamName, method, time.Now().Unix()) + return postForm(ctx, client, endpoint, values, intf, d) +} + +func logResponse(resp *http.Response, d debug) error { + if d.Debug() { text, err := httputil.DumpResponse(resp, true) if err != nil { return err } - - logger.Print(string(text)) + d.Debugln(string(text)) } return nil diff --git a/vendor/github.com/nlopes/slack/oauth.go b/vendor/github.com/nlopes/slack/oauth.go index 378af4a5..8a8194cb 100644 --- a/vendor/github.com/nlopes/slack/oauth.go +++ b/vendor/github.com/nlopes/slack/oauth.go @@ -2,10 +2,10 @@ package slack import ( "context" - "errors" "net/url" ) +// OAuthResponseIncomingWebhook ... type OAuthResponseIncomingWebhook struct { URL string `json:"url"` Channel string `json:"channel"` @@ -13,11 +13,13 @@ type OAuthResponseIncomingWebhook struct { ConfigurationURL string `json:"configuration_url"` } +// OAuthResponseBot ... type OAuthResponseBot struct { BotUserID string `json:"bot_user_id"` BotAccessToken string `json:"bot_access_token"` } +// OAuthResponse ... type OAuthResponse struct { AccessToken string `json:"access_token"` Scope string `json:"scope"` @@ -30,24 +32,24 @@ type OAuthResponse struct { } // GetOAuthToken retrieves an AccessToken -func GetOAuthToken(clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) { - return GetOAuthTokenContext(context.Background(), clientID, clientSecret, code, redirectURI, debug) +func GetOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, err error) { + return GetOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI) } // GetOAuthTokenContext retrieves an AccessToken with a custom context -func GetOAuthTokenContext(ctx context.Context, clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) { - response, err := GetOAuthResponseContext(ctx, clientID, clientSecret, code, redirectURI, debug) +func GetOAuthTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, err error) { + response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI) if err != nil { return "", "", err } return response.AccessToken, response.Scope, nil } -func GetOAuthResponse(clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) { - return GetOAuthResponseContext(context.Background(), clientID, clientSecret, code, redirectURI, debug) +func GetOAuthResponse(client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) { + return GetOAuthResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI) } -func GetOAuthResponseContext(ctx context.Context, clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) { +func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) { values := url.Values{ "client_id": {clientID}, "client_secret": {clientSecret}, @@ -55,12 +57,8 @@ func GetOAuthResponseContext(ctx context.Context, clientID, clientSecret, code, "redirect_uri": {redirectURI}, } response := &OAuthResponse{} - err = postSlackMethod(ctx, customHTTPClient, "oauth.access", values, response, debug) - if err != nil { + if err = postSlackMethod(ctx, client, "oauth.access", values, response, discard{}); err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil + return response, response.Err() } diff --git a/vendor/github.com/nlopes/slack/pins.go b/vendor/github.com/nlopes/slack/pins.go index 34863f17..c1d525df 100644 --- a/vendor/github.com/nlopes/slack/pins.go +++ b/vendor/github.com/nlopes/slack/pins.go @@ -34,7 +34,7 @@ func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemR } response := &SlackResponse{} - if err := postSlackMethod(ctx, api.httpclient, "pins.add", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "pins.add", values, response, api); err != nil { return err } @@ -63,7 +63,7 @@ func (api *Client) RemovePinContext(ctx context.Context, channel string, item It } response := &SlackResponse{} - if err := postSlackMethod(ctx, api.httpclient, "pins.remove", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "pins.remove", values, response, api); err != nil { return err } @@ -83,7 +83,7 @@ func (api *Client) ListPinsContext(ctx context.Context, channel string) ([]Item, } response := &listPinsResponseFull{} - err := postSlackMethod(ctx, api.httpclient, "pins.list", values, response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "pins.list", values, response, api) if err != nil { return nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/reactions.go b/vendor/github.com/nlopes/slack/reactions.go index 5eabde63..abe1e72a 100644 --- a/vendor/github.com/nlopes/slack/reactions.go +++ b/vendor/github.com/nlopes/slack/reactions.go @@ -155,7 +155,7 @@ func (api *Client) AddReactionContext(ctx context.Context, name string, item Ite } response := &SlackResponse{} - if err := postSlackMethod(ctx, api.httpclient, "reactions.add", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "reactions.add", values, response, api); err != nil { return err } @@ -189,7 +189,7 @@ func (api *Client) RemoveReactionContext(ctx context.Context, name string, item } response := &SlackResponse{} - if err := postSlackMethod(ctx, api.httpclient, "reactions.remove", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "reactions.remove", values, response, api); err != nil { return err } @@ -223,7 +223,7 @@ func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params } response := &getReactionsResponseFull{} - if err := postSlackMethod(ctx, api.httpclient, "reactions.get", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "reactions.get", values, response, api); err != nil { return nil, err } if !response.Ok { @@ -256,7 +256,7 @@ func (api *Client) ListReactionsContext(ctx context.Context, params ListReaction } response := &listReactionsResponseFull{} - err := postSlackMethod(ctx, api.httpclient, "reactions.list", values, response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "reactions.list", values, response, api) if err != nil { return nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/rtm.go b/vendor/github.com/nlopes/slack/rtm.go index 41a136eb..e7fa83f7 100644 --- a/vendor/github.com/nlopes/slack/rtm.go +++ b/vendor/github.com/nlopes/slack/rtm.go @@ -38,7 +38,7 @@ func (api *Client) StartRTM() (info *Info, websocketURL string, err error) { // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. func (api *Client) StartRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) { response := &infoResponseFull{} - err = postSlackMethod(ctx, api.httpclient, "rtm.start", url.Values{"token": {api.token}}, response, api.debug) + err = postSlackMethod(ctx, api.httpclient, "rtm.start", url.Values{"token": {api.token}}, response, api) if err != nil { return nil, "", err } @@ -63,7 +63,7 @@ func (api *Client) ConnectRTM() (info *Info, websocketURL string, err error) { // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. func (api *Client) ConnectRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) { response := &infoResponseFull{} - err = postSlackMethod(ctx, api.httpclient, "rtm.connect", url.Values{"token": {api.token}}, response, api.debug) + err = postSlackMethod(ctx, api.httpclient, "rtm.connect", url.Values{"token": {api.token}}, response, api) if err != nil { api.Debugf("Failed to connect to RTM: %s", err) return nil, "", err @@ -100,17 +100,24 @@ func RTMOptionPingInterval(d time.Duration) RTMOption { } } +// RTMOptionConnParams installs parameters to embed into the connection URL. +func RTMOptionConnParams(connParams url.Values) RTMOption { + return func(rtm *RTM) { + rtm.connParams = connParams + } +} + // NewRTM returns a RTM, which provides a fully managed connection to // Slack's websocket-based Real-Time Messaging protocol. func (api *Client) NewRTM(options ...RTMOption) *RTM { result := &RTM{ Client: *api, + wasIntentional: true, + isConnected: false, IncomingEvents: make(chan RTMEvent, 50), outgoingMessages: make(chan OutgoingMessage, 20), pingInterval: defaultPingInterval, pingDeadman: time.NewTimer(deadmanDuration(defaultPingInterval)), - isConnected: false, - wasIntentional: true, killChannel: make(chan bool), disconnected: make(chan struct{}, 1), forcePing: make(chan bool), @@ -125,14 +132,3 @@ func (api *Client) NewRTM(options ...RTMOption) *RTM { return result } - -// NewRTMWithOptions Deprecated just use NewRTM(RTMOptionsUseStart(true)) -// returns a RTM, which provides a fully managed connection to -// Slack's websocket-based Real-Time Messaging protocol. -// This also allows to configure various options available for RTM API. -func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM { - if options != nil { - return api.NewRTM(RTMOptionUseStart(options.UseRTMStart)) - } - return api.NewRTM() -} diff --git a/vendor/github.com/nlopes/slack/search.go b/vendor/github.com/nlopes/slack/search.go index e858952f..2d018fcc 100644 --- a/vendor/github.com/nlopes/slack/search.go +++ b/vendor/github.com/nlopes/slack/search.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "net/url" "strconv" ) @@ -104,14 +103,12 @@ func (api *Client) _search(ctx context.Context, path, query string, params Searc } response = &searchResponseFull{} - err := postSlackMethod(ctx, api.httpclient, path, values, response, api.debug) + err := postSlackMethod(ctx, api.httpclient, path, values, response, api) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil + + return response, response.Err() } diff --git a/vendor/github.com/nlopes/slack/security.go b/vendor/github.com/nlopes/slack/security.go new file mode 100644 index 00000000..6ab3c698 --- /dev/null +++ b/vendor/github.com/nlopes/slack/security.go @@ -0,0 +1,99 @@ +package slack + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "hash" + "net/http" + "strconv" + "strings" + "time" +) + +// Signature headers +const ( + hSignature = "X-Slack-Signature" + hTimestamp = "X-Slack-Request-Timestamp" +) + +// SecretsVerifier contains the information needed to verify that the request comes from Slack +type SecretsVerifier struct { + signature []byte + hmac hash.Hash +} + +func unsafeSignatureVerifier(header http.Header, secret string) (_ SecretsVerifier, err error) { + var ( + bsignature []byte + ) + + signature := header.Get(hSignature) + stimestamp := header.Get(hTimestamp) + + if signature == "" || stimestamp == "" { + return SecretsVerifier{}, errors.New("missing headers") + } + + if bsignature, err = hex.DecodeString(strings.TrimPrefix(signature, "v0=")); err != nil { + return SecretsVerifier{}, err + } + + hash := hmac.New(sha256.New, []byte(secret)) + hash.Write([]byte(fmt.Sprintf("v0:%s:", stimestamp))) + + return SecretsVerifier{ + signature: bsignature, + hmac: hash, + }, nil +} + +// NewSecretsVerifier returns a SecretsVerifier object in exchange for an http.Header object and signing secret +func NewSecretsVerifier(header http.Header, secret string) (sv SecretsVerifier, err error) { + var ( + timestamp int64 + ) + + stimestamp := header.Get(hTimestamp) + + if sv, err = unsafeSignatureVerifier(header, secret); err != nil { + return SecretsVerifier{}, err + } + + if timestamp, err = strconv.ParseInt(stimestamp, 10, 64); err != nil { + return SecretsVerifier{}, err + } + + diff := absDuration(time.Now().Sub(time.Unix(timestamp, 0))) + if diff > 5*time.Minute { + return SecretsVerifier{}, fmt.Errorf("timestamp is too old") + } + + return sv, err +} + +func (v *SecretsVerifier) Write(body []byte) (n int, err error) { + return v.hmac.Write(body) +} + +// Ensure compares the signature sent from Slack with the actual computed hash to judge validity +func (v SecretsVerifier) Ensure() error { + computed := v.hmac.Sum(nil) + // use hmac.Equal prevent leaking timing information. + if hmac.Equal(computed, v.signature) { + return nil + } + + return fmt.Errorf("Expected signing signature: %s, but computed: %s", v.signature, computed) +} + +func abs64(n int64) int64 { + y := n >> 63 + return (n ^ y) - y +} + +func absDuration(n time.Duration) time.Duration { + return time.Duration(abs64(int64(n))) +} diff --git a/vendor/github.com/nlopes/slack/slack.go b/vendor/github.com/nlopes/slack/slack.go index 6d1e7de9..1e75142d 100644 --- a/vendor/github.com/nlopes/slack/slack.go +++ b/vendor/github.com/nlopes/slack/slack.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "fmt" "log" "net/http" @@ -10,31 +9,17 @@ import ( "os" ) -// Added as a var so that we can change this for testing purposes -var SLACK_API string = "https://slack.com/api/" -var SLACK_WEB_API_FORMAT string = "https://%s.slack.com/api/users.admin.%s?t=%s" +// APIURL a dded as a var so that we can change this for testing purposes +var APIURL = "https://slack.com/api/" -// HTTPClient sets a custom http.Client -// deprecated: in favor of SetHTTPClient() -var HTTPClient = &http.Client{} +// WEBAPIURLFormat ... +const WEBAPIURLFormat = "https://%s.slack.com/api/users.admin.%s?t=%d" -var customHTTPClient HTTPRequester = HTTPClient - -// HTTPRequester defines the minimal interface needed for an http.Client to be implemented. -// -// Use it in conjunction with the SetHTTPClient function to allow for other capabilities -// like a tracing http.Client -type HTTPRequester interface { +// httpClient defines the minimal interface needed for an http.Client to be implemented. +type httpClient interface { Do(*http.Request) (*http.Response, error) } -// SetHTTPClient allows you to specify a custom http.Client -// Use this instead of the package level HTTPClient variable if you want to use a custom client like the -// Stackdriver Trace HTTPClient https://godoc.org/cloud.google.com/go/trace#HTTPClient -func SetHTTPClient(client HTTPRequester) { - customHTTPClient = client -} - // ResponseMetadata holds pagination metadata type ResponseMetadata struct { Cursor string `json:"next_cursor"` @@ -48,6 +33,7 @@ func (t *ResponseMetadata) initialize() *ResponseMetadata { return &ResponseMetadata{} } +// AuthTestResponse ... type AuthTestResponse struct { URL string `json:"url"` Team string `json:"team"` @@ -61,20 +47,36 @@ type authTestResponseFull struct { AuthTestResponse } +// Client for the slack api. type Client struct { token string info Info debug bool - httpclient HTTPRequester + log ilogger + httpclient httpClient } // Option defines an option for a Client type Option func(*Client) // OptionHTTPClient - provide a custom http client to the slack client. -func OptionHTTPClient(c HTTPRequester) func(*Client) { - return func(s *Client) { - s.httpclient = c +func OptionHTTPClient(client httpClient) func(*Client) { + return func(c *Client) { + c.httpclient = client + } +} + +// OptionDebug enable debugging for the client +func OptionDebug(b bool) func(*Client) { + return func(c *Client) { + c.debug = b + } +} + +// OptionLog set logging for client. +func OptionLog(l logger) func(*Client) { + return func(c *Client) { + c.log = internalLog{logger: l} } } @@ -82,7 +84,8 @@ func OptionHTTPClient(c HTTPRequester) func(*Client) { func New(token string, options ...Option) *Client { s := &Client{ token: token, - httpclient: customHTTPClient, + httpclient: &http.Client{}, + log: log.New(os.Stderr, "nlopes/slack", log.LstdFlags|log.Lshortfile), } for _, opt := range options { @@ -98,43 +101,32 @@ func (api *Client) AuthTest() (response *AuthTestResponse, error error) { } // AuthTestContext tests if the user is able to do authenticated requests or not with a custom context -func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestResponse, error error) { +func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestResponse, err error) { api.Debugf("Challenging auth...") responseFull := &authTestResponseFull{} - err := postSlackMethod(ctx, api.httpclient, "auth.test", url.Values{"token": {api.token}}, responseFull, api.debug) + err = postSlackMethod(ctx, api.httpclient, "auth.test", url.Values{"token": {api.token}}, responseFull, api) if err != nil { - api.Debugf("failed to test for auth: %s", err) return nil, err } - if !responseFull.Ok { - api.Debugf("auth response was not Ok: %s", responseFull.Error) - return nil, errors.New(responseFull.Error) - } - api.Debugf("Auth challenge was successful with response %+v", responseFull.AuthTestResponse) - return &responseFull.AuthTestResponse, nil -} - -// SetDebug switches the api into debug mode -// When in debug mode, it logs various info about what its doing -// If you ever use this in production, don't call SetDebug(true) -func (api *Client) SetDebug(debug bool) { - api.debug = debug - if debug && logger == nil { - SetLogger(log.New(os.Stdout, "nlopes/slack", log.LstdFlags|log.Lshortfile)) - } + return &responseFull.AuthTestResponse, responseFull.Err() } // Debugf print a formatted debug line. func (api *Client) Debugf(format string, v ...interface{}) { if api.debug { - logger.Output(2, fmt.Sprintf(format, v...)) + api.log.Output(2, fmt.Sprintf(format, v...)) } } // Debugln print a debug line. func (api *Client) Debugln(v ...interface{}) { if api.debug { - logger.Output(2, fmt.Sprintln(v...)) + api.log.Output(2, fmt.Sprintln(v...)) } } + +// Debug returns if debug is enabled. +func (api *Client) Debug() bool { + return api.debug +} diff --git a/vendor/github.com/nlopes/slack/slackutilsx/slackutilsx.go b/vendor/github.com/nlopes/slack/slackutilsx/slackutilsx.go new file mode 100644 index 00000000..ccf5372b --- /dev/null +++ b/vendor/github.com/nlopes/slack/slackutilsx/slackutilsx.go @@ -0,0 +1,57 @@ +// Package slackutilsx is a utility package that doesn't promise API stability. +// its for experimental functionality and utilities. +package slackutilsx + +import ( + "strings" + "unicode/utf8" +) + +// ChannelType the type of channel based on the channelID +type ChannelType int + +func (t ChannelType) String() string { + switch t { + case CTypeDM: + return "Direct" + case CTypeGroup: + return "Group" + case CTypeChannel: + return "Channel" + default: + return "Unknown" + } +} + +const ( + // CTypeUnknown represents channels we cannot properly detect. + CTypeUnknown ChannelType = iota + // CTypeDM is a private channel between two slack users. + CTypeDM + // CTypeGroup is a group channel. + CTypeGroup + // CTypeChannel is a public channel. + CTypeChannel +) + +// DetectChannelType converts a channelID to a ChannelType. +// channelID must not be empty. However, if it is empty, the channel type will default to Unknown. +func DetectChannelType(channelID string) ChannelType { + // intentionally ignore the error and just default to CTypeUnknown + switch r, _ := utf8.DecodeRuneInString(channelID); r { + case 'C': + return CTypeChannel + case 'G': + return CTypeGroup + case 'D': + return CTypeDM + default: + return CTypeUnknown + } +} + +// EscapeMessage text +func EscapeMessage(message string) string { + replacer := strings.NewReplacer("&", "&", "<", "<", ">", ">") + return replacer.Replace(message) +} diff --git a/vendor/github.com/nlopes/slack/stars.go b/vendor/github.com/nlopes/slack/stars.go index c1e2f6cb..7e1e621d 100644 --- a/vendor/github.com/nlopes/slack/stars.go +++ b/vendor/github.com/nlopes/slack/stars.go @@ -58,7 +58,7 @@ func (api *Client) AddStarContext(ctx context.Context, channel string, item Item } response := &SlackResponse{} - if err := postSlackMethod(ctx, api.httpclient, "stars.add", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "stars.add", values, response, api); err != nil { return err } @@ -87,7 +87,7 @@ func (api *Client) RemoveStarContext(ctx context.Context, channel string, item I } response := &SlackResponse{} - if err := postSlackMethod(ctx, api.httpclient, "stars.remove", values, response, api.debug); err != nil { + if err := postSlackMethod(ctx, api.httpclient, "stars.remove", values, response, api); err != nil { return err } @@ -115,7 +115,7 @@ func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) } response := &listResponseFull{} - err := postSlackMethod(ctx, api.httpclient, "stars.list", values, response, api.debug) + err := postSlackMethod(ctx, api.httpclient, "stars.list", values, response, api) if err != nil { return nil, nil, err } diff --git a/vendor/github.com/nlopes/slack/team.go b/vendor/github.com/nlopes/slack/team.go index b6e341eb..1892cf5f 100644 --- a/vendor/github.com/nlopes/slack/team.go +++ b/vendor/github.com/nlopes/slack/team.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "net/url" "strconv" ) @@ -67,44 +66,33 @@ func NewAccessLogParameters() AccessLogParameters { } } -func teamRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*TeamResponse, error) { +func teamRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*TeamResponse, error) { response := &TeamResponse{} - err := postSlackMethod(ctx, client, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, d) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - - return response, nil + return response, response.Err() } -func billableInfoRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (map[string]BillingActive, error) { +func billableInfoRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (map[string]BillingActive, error) { response := &BillableInfoResponse{} - err := postSlackMethod(ctx, client, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, d) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - - return response.BillableInfo, nil + return response.BillableInfo, response.Err() } -func accessLogsRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*LoginResponse, error) { +func accessLogsRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*LoginResponse, error) { response := &LoginResponse{} - err := postSlackMethod(ctx, client, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, d) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil + return response, response.Err() } // GetTeamInfo gets the Team Information of the user @@ -118,7 +106,7 @@ func (api *Client) GetTeamInfoContext(ctx context.Context) (*TeamInfo, error) { "token": {api.token}, } - response, err := teamRequest(ctx, api.httpclient, "team.info", values, api.debug) + response, err := teamRequest(ctx, api.httpclient, "team.info", values, api) if err != nil { return nil, err } @@ -142,7 +130,7 @@ func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogPar values.Add("page", strconv.Itoa(params.Page)) } - response, err := accessLogsRequest(ctx, api.httpclient, "team.accessLogs", values, api.debug) + response, err := accessLogsRequest(ctx, api.httpclient, "team.accessLogs", values, api) if err != nil { return nil, nil, err } @@ -159,7 +147,7 @@ func (api *Client) GetBillableInfoContext(ctx context.Context, user string) (map "user": {user}, } - return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api.debug) + return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api) } // GetBillableInfoForTeam returns the billing_active status of all users on the team. @@ -173,5 +161,5 @@ func (api *Client) GetBillableInfoForTeamContext(ctx context.Context) (map[strin "token": {api.token}, } - return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api.debug) + return billableInfoRequest(ctx, api.httpclient, "team.billableInfo", values, api) } diff --git a/vendor/github.com/nlopes/slack/usergroups.go b/vendor/github.com/nlopes/slack/usergroups.go index 1e2b6442..9e145272 100644 --- a/vendor/github.com/nlopes/slack/usergroups.go +++ b/vendor/github.com/nlopes/slack/usergroups.go @@ -2,7 +2,6 @@ package slack import ( "context" - "errors" "net/url" "strings" ) @@ -25,6 +24,7 @@ type UserGroup struct { DeletedBy string `json:"deleted_by"` Prefs UserGroupPrefs `json:"prefs"` UserCount int `json:"user_count"` + Users []string `json:"users"` } // UserGroupPrefs contains default channels and groups (private channels) @@ -40,16 +40,14 @@ type userGroupResponseFull struct { SlackResponse } -func userGroupRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*userGroupResponseFull, error) { +func userGroupRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*userGroupResponseFull, error) { response := &userGroupResponseFull{} - err := postSlackMethod(ctx, client, path, values, response, debug) + err := postSlackMethod(ctx, client, path, values, response, d) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil + + return response, response.Err() } // CreateUserGroup creates a new user group @@ -76,7 +74,7 @@ func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGro values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")} } - response, err := userGroupRequest(ctx, api.httpclient, "usergroups.create", values, api.debug) + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.create", values, api) if err != nil { return UserGroup{}, err } @@ -95,7 +93,7 @@ func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string "usergroup": {userGroup}, } - response, err := userGroupRequest(ctx, api.httpclient, "usergroups.disable", values, api.debug) + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.disable", values, api) if err != nil { return UserGroup{}, err } @@ -114,25 +112,71 @@ func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string) "usergroup": {userGroup}, } - response, err := userGroupRequest(ctx, api.httpclient, "usergroups.enable", values, api.debug) + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.enable", values, api) if err != nil { return UserGroup{}, err } return response.UserGroup, nil } +// GetUserGroupsOption options for the GetUserGroups method call. +type GetUserGroupsOption func(*GetUserGroupsParams) + +// GetUserGroupsOptionIncludeCount include the number of users in each User Group (default: false) +func GetUserGroupsOptionIncludeCount(b bool) GetUserGroupsOption { + return func(params *GetUserGroupsParams) { + params.IncludeCount = b + } +} + +// GetUserGroupsOptionIncludeDisabled include disabled User Groups (default: false) +func GetUserGroupsOptionIncludeDisabled(b bool) GetUserGroupsOption { + return func(params *GetUserGroupsParams) { + params.IncludeDisabled = b + } +} + +// GetUserGroupsOptionIncludeUsers include the list of users for each User Group (default: false) +func GetUserGroupsOptionIncludeUsers(b bool) GetUserGroupsOption { + return func(params *GetUserGroupsParams) { + params.IncludeUsers = b + } +} + +// GetUserGroupsParams contains arguments for GetUserGroups method call +type GetUserGroupsParams struct { + IncludeCount bool + IncludeDisabled bool + IncludeUsers bool +} + // GetUserGroups returns a list of user groups for the team -func (api *Client) GetUserGroups() ([]UserGroup, error) { - return api.GetUserGroupsContext(context.Background()) +func (api *Client) GetUserGroups(options ...GetUserGroupsOption) ([]UserGroup, error) { + return api.GetUserGroupsContext(context.Background(), options...) } // GetUserGroupsContext returns a list of user groups for the team with a custom context -func (api *Client) GetUserGroupsContext(ctx context.Context) ([]UserGroup, error) { +func (api *Client) GetUserGroupsContext(ctx context.Context, options ...GetUserGroupsOption) ([]UserGroup, error) { + params := GetUserGroupsParams{} + + for _, opt := range options { + opt(¶ms) + } + values := url.Values{ "token": {api.token}, } + if params.IncludeCount { + values.Add("include_count", "true") + } + if params.IncludeDisabled { + values.Add("include_disabled", "true") + } + if params.IncludeUsers { + values.Add("include_users", "true") + } - response, err := userGroupRequest(ctx, api.httpclient, "usergroups.list", values, api.debug) + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.list", values, api) if err != nil { return nil, err } @@ -163,7 +207,7 @@ func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroup UserGro values["description"] = []string{userGroup.Description} } - response, err := userGroupRequest(ctx, api.httpclient, "usergroups.update", values, api.debug) + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.update", values, api) if err != nil { return UserGroup{}, err } @@ -182,7 +226,7 @@ func (api *Client) GetUserGroupMembersContext(ctx context.Context, userGroup str "usergroup": {userGroup}, } - response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.list", values, api.debug) + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.list", values, api) if err != nil { return []string{}, err } @@ -202,7 +246,7 @@ func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup "users": {members}, } - response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.update", values, api.debug) + response, err := userGroupRequest(ctx, api.httpclient, "usergroups.users.update", values, api) if err != nil { return UserGroup{}, err } diff --git a/vendor/github.com/nlopes/slack/users.go b/vendor/github.com/nlopes/slack/users.go index 0dd20db5..aa941a74 100644 --- a/vendor/github.com/nlopes/slack/users.go +++ b/vendor/github.com/nlopes/slack/users.go @@ -189,16 +189,14 @@ func NewUserSetPhotoParams() UserSetPhotoParams { } } -func userRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*userResponseFull, error) { +func userRequest(ctx context.Context, client httpClient, path string, values url.Values, d debug) (*userResponseFull, error) { response := &userResponseFull{} - err := postForm(ctx, client, SLACK_API+path, values, response, debug) + err := postForm(ctx, client, APIURL+path, values, response, d) if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil + + return response, response.Err() } // GetUserPresence will retrieve the current presence status of given user. @@ -213,7 +211,7 @@ func (api *Client) GetUserPresenceContext(ctx context.Context, user string) (*Us "user": {user}, } - response, err := userRequest(ctx, api.httpclient, "users.getPresence", values, api.debug) + response, err := userRequest(ctx, api.httpclient, "users.getPresence", values, api) if err != nil { return nil, err } @@ -232,7 +230,7 @@ func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, "user": {user}, } - response, err := userRequest(ctx, api.httpclient, "users.info", values, api.debug) + response, err := userRequest(ctx, api.httpclient, "users.info", values, api) if err != nil { return nil, err } @@ -310,7 +308,7 @@ func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error) "cursor": {t.previousResp.Cursor}, } - if resp, err = userRequest(ctx, t.c.httpclient, "users.list", values, t.c.debug); err != nil { + if resp, err = userRequest(ctx, t.c.httpclient, "users.list", values, t.c); err != nil { return t, err } @@ -355,7 +353,7 @@ func (api *Client) GetUserByEmailContext(ctx context.Context, email string) (*Us "token": {api.token}, "email": {email}, } - response, err := userRequest(ctx, api.httpclient, "users.lookupByEmail", values, api.debug) + response, err := userRequest(ctx, api.httpclient, "users.lookupByEmail", values, api) if err != nil { return nil, err } @@ -373,7 +371,7 @@ func (api *Client) SetUserAsActiveContext(ctx context.Context) (err error) { "token": {api.token}, } - _, err = userRequest(ctx, api.httpclient, "users.setActive", values, api.debug) + _, err = userRequest(ctx, api.httpclient, "users.setActive", values, api) return err } @@ -389,7 +387,7 @@ func (api *Client) SetUserPresenceContext(ctx context.Context, presence string) "presence": {presence}, } - _, err := userRequest(ctx, api.httpclient, "users.setPresence", values, api.debug) + _, err := userRequest(ctx, api.httpclient, "users.setPresence", values, api) return err } @@ -405,7 +403,7 @@ func (api *Client) GetUserIdentityContext(ctx context.Context) (*UserIdentityRes } response := &UserIdentityResponse{} - err := postForm(ctx, api.httpclient, SLACK_API+"users.identity", values, response, api.debug) + err := postForm(ctx, api.httpclient, APIURL+"users.identity", values, response, api) if err != nil { return nil, err } @@ -436,7 +434,7 @@ func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params values.Add("crop_w", strconv.Itoa(params.CropW)) } - err := postLocalWithMultipartResponse(ctx, api.httpclient, "users.setPhoto", image, "image", values, response, api.debug) + err := postLocalWithMultipartResponse(ctx, api.httpclient, "users.setPhoto", image, "image", values, response, api) if err != nil { return err } @@ -456,7 +454,7 @@ func (api *Client) DeleteUserPhotoContext(ctx context.Context) error { "token": {api.token}, } - err := postForm(ctx, api.httpclient, SLACK_API+"users.deletePhoto", values, response, api.debug) + err := postForm(ctx, api.httpclient, APIURL+"users.deletePhoto", values, response, api) if err != nil { return err } @@ -506,7 +504,7 @@ func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, s } response := &userResponseFull{} - if err = postForm(ctx, api.httpclient, SLACK_API+"users.profile.set", values, response, api.debug); err != nil { + if err = postForm(ctx, api.httpclient, APIURL+"users.profile.set", values, response, api); err != nil { return err } @@ -547,7 +545,7 @@ func (api *Client) GetUserProfileContext(ctx context.Context, userID string, inc } resp := &getUserProfileResponse{} - err := postSlackMethod(ctx, api.httpclient, "users.profile.get", values, &resp, api.debug) + err := postSlackMethod(ctx, api.httpclient, "users.profile.get", values, &resp, api) if err != nil { return nil, err } diff --git a/vendor/github.com/nlopes/slack/webhooks.go b/vendor/github.com/nlopes/slack/webhooks.go index 870a8d8b..3ea69ffe 100644 --- a/vendor/github.com/nlopes/slack/webhooks.go +++ b/vendor/github.com/nlopes/slack/webhooks.go @@ -1,15 +1,16 @@ package slack import ( - "github.com/pkg/errors" - "net/http" "bytes" "encoding/json" + "net/http" + + "github.com/pkg/errors" ) type WebhookMessage struct { - Text string `json:"text,omitempty"` - Attachments []Attachment `json:"attachments,omitempty"` + Text string `json:"text,omitempty"` + Attachments []Attachment `json:"attachments,omitempty"` } func PostWebhook(url string, msg *WebhookMessage) error { @@ -19,7 +20,7 @@ func PostWebhook(url string, msg *WebhookMessage) error { return errors.Wrap(err, "marshal failed") } - response, err := http.Post(url, "application/json", bytes.NewReader(raw)); + response, err := http.Post(url, "application/json", bytes.NewReader(raw)) if err != nil { return errors.Wrap(err, "failed to post webhook") diff --git a/vendor/github.com/nlopes/slack/websocket.go b/vendor/github.com/nlopes/slack/websocket.go index 242acf40..ec810a9b 100644 --- a/vendor/github.com/nlopes/slack/websocket.go +++ b/vendor/github.com/nlopes/slack/websocket.go @@ -3,6 +3,7 @@ package slack import ( "encoding/json" "errors" + "net/url" "sync" "time" @@ -20,6 +21,9 @@ const ( // // Create this element with Client's NewRTM() or NewRTMWithOptions(*RTMOptions) type RTM struct { + // Client is the main API, embedded + Client + idGen IDGenerator pingInterval time.Duration pingDeadman *time.Timer @@ -35,8 +39,6 @@ type RTM struct { wasIntentional bool isConnected bool - // Client is the main API, embedded - Client websocketURL string // UserDetails upon connection @@ -53,18 +55,9 @@ type RTM struct { // mu is mutex used to prevent RTM connection race conditions mu *sync.Mutex -} -// RTMOptions allows configuration of various options available for RTM messaging -// -// This structure will evolve in time so please make sure you are always using the -// named keys for every entry available as per Go 1 compatibility promise adding fields -// to this structure should not be considered a breaking change. -type RTMOptions struct { - // UseRTMStart set to true in order to use rtm.start or false to use rtm.connect - // As of 11th July 2017 you should prefer setting this to false, see: - // https://api.slack.com/changelog/2017-04-start-using-rtm-connect-and-stop-using-rtm-start - UseRTMStart bool + // connParams is a map of flags for connection parameters. + connParams url.Values } // Disconnect and wait, blocking until a successful disconnection. diff --git a/vendor/github.com/nlopes/slack/websocket_managed_conn.go b/vendor/github.com/nlopes/slack/websocket_managed_conn.go index b6d1bfc8..62157910 100644 --- a/vendor/github.com/nlopes/slack/websocket_managed_conn.go +++ b/vendor/github.com/nlopes/slack/websocket_managed_conn.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + stdurl "net/url" "reflect" "time" @@ -157,6 +158,14 @@ func (rtm *RTM) startRTMAndDial(useRTMStart bool) (info *Info, _ *websocket.Conn return nil, nil, err } + // install connection parameters + u, err := stdurl.Parse(url) + if err != nil { + return nil, nil, err + } + u.RawQuery = rtm.connParams.Encode() + url = u.String() + rtm.Debugf("Dialing to websocket on url %s", url) // Only use HTTPS for connections to prevent MITM attacks on the connection. upgradeHeader := http.Header{} @@ -274,7 +283,7 @@ func (rtm *RTM) sendWithDeadline(msg interface{}) error { // and instead lets a future failed 'PING' detect the failed connection. func (rtm *RTM) sendOutgoingMessage(msg OutgoingMessage) { rtm.Debugln("Sending message:", msg) - if len(msg.Text) > MaxMessageTextLength { + if len([]rune(msg.Text)) > MaxMessageTextLength { rtm.IncomingEvents <- RTMEvent{"outgoing_error", &MessageTooLongEvent{ Message: msg, MaxLength: MaxMessageTextLength, @@ -405,8 +414,7 @@ func (rtm *RTM) handlePong(event json.RawMessage) { rtm.resetDeadman() if err := json.Unmarshal(event, &p); err != nil { - logger.Println("RTM Error unmarshalling 'pong' event:", err) - rtm.Debugln(" -> Erroneous 'ping' event:", string(event)) + rtm.Client.log.Println("RTM Error unmarshalling 'pong' event:", err) return } @@ -423,8 +431,8 @@ func (rtm *RTM) handlePong(event json.RawMessage) { func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) { v, exists := EventMapping[typeStr] if !exists { - rtm.Debugf("RTM Error, received unmapped event %q: %s\n", typeStr, string(event)) - err := fmt.Errorf("RTM Error: Received unmapped event %q: %s\n", typeStr, string(event)) + rtm.Debugf("RTM Error - received unmapped event %q: %s\n", typeStr, string(event)) + err := fmt.Errorf("RTM Error: Received unmapped event %q: %s", typeStr, string(event)) rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}} return } @@ -433,7 +441,7 @@ func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) { err := json.Unmarshal(event, recvEvent) if err != nil { rtm.Debugf("RTM Error, could not unmarshall event %q: %s\n", typeStr, string(event)) - err := fmt.Errorf("RTM Error: Could not unmarshall event %q: %s\n", typeStr, string(event)) + err := fmt.Errorf("RTM Error: Could not unmarshall event %q: %s", typeStr, string(event)) rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}} return } @@ -524,4 +532,9 @@ var EventMapping = map[string]interface{}{ "member_joined_channel": MemberJoinedChannelEvent{}, "member_left_channel": MemberLeftChannelEvent{}, + + "subteam_created": SubteamCreatedEvent{}, + "subteam_self_added": SubteamSelfAddedEvent{}, + "subteam_self_removed": SubteamSelfRemovedEvent{}, + "subteam_updated": SubteamUpdatedEvent{}, } diff --git a/vendor/github.com/nlopes/slack/websocket_misc.go b/vendor/github.com/nlopes/slack/websocket_misc.go index 16f48c74..bfcc805e 100644 --- a/vendor/github.com/nlopes/slack/websocket_misc.go +++ b/vendor/github.com/nlopes/slack/websocket_misc.go @@ -43,9 +43,10 @@ type HelloEvent struct{} // PresenceChangeEvent represents the presence change event type PresenceChangeEvent struct { - Type string `json:"type"` - Presence string `json:"presence"` - User string `json:"user"` + Type string `json:"type"` + Presence string `json:"presence"` + User string `json:"user"` + Users []string `json:"users"` } // UserTypingEvent represents the user typing event diff --git a/vendor/github.com/nlopes/slack/websocket_subteam.go b/vendor/github.com/nlopes/slack/websocket_subteam.go new file mode 100644 index 00000000..a23b274c --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_subteam.go @@ -0,0 +1,35 @@ +package slack + +// SubteamCreatedEvent represents the Subteam created event +type SubteamCreatedEvent struct { + Type string `json:"type"` + Subteam UserGroup `json:"subteam"` +} + +// SubteamCreatedEvent represents the membership of an existing User Group has changed event +type SubteamMembersChangedEvent struct { + Type string `json:"type"` + SubteamID string `json:"subteam_id"` + TeamID string `json:"team_id"` + DatePreviousUpdate JSONTime `json:"date_previous_update"` + DateUpdate JSONTime `json:"date_update"` + AddedUsers []string `json:"added_users"` + AddedUsersCount string `json:"added_users_count"` + RemovedUsers []string `json:"removed_users"` + RemovedUsersCount string `json:"removed_users_count"` +} + +// SubteamSelfAddedEvent represents an event of you have been added to a User Group +type SubteamSelfAddedEvent struct { + Type string `json:"type"` + SubteamID string `json:"subteam_id"` +} + +// SubteamSelfRemovedEvent represents an event of you have been removed from a User Group +type SubteamSelfRemovedEvent SubteamSelfAddedEvent + +// SubteamUpdatedEvent represents an event of an existing User Group has been updated or its members changed +type SubteamUpdatedEvent struct { + Type string `json:"type"` + Subteam UserGroup `json:"subteam"` +} diff --git a/vendor/github.com/pelletier/go-toml/.travis.yml b/vendor/github.com/pelletier/go-toml/.travis.yml index ab2775d7..c9fbf304 100644 --- a/vendor/github.com/pelletier/go-toml/.travis.yml +++ b/vendor/github.com/pelletier/go-toml/.travis.yml @@ -1,8 +1,9 @@ sudo: false language: go go: - - 1.8.5 - - 1.9.2 + - 1.8.x + - 1.9.x + - 1.10.x - tip matrix: allow_failures: diff --git a/vendor/github.com/pelletier/go-toml/benchmark.toml b/vendor/github.com/pelletier/go-toml/benchmark.toml new file mode 100644 index 00000000..dfd77e09 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/benchmark.toml @@ -0,0 +1,244 @@ +################################################################################ +## Comment + +# Speak your mind with the hash symbol. They go from the symbol to the end of +# the line. + + +################################################################################ +## Table + +# Tables (also known as hash tables or dictionaries) are collections of +# key/value pairs. They appear in square brackets on a line by themselves. + +[table] + +key = "value" # Yeah, you can do this. + +# Nested tables are denoted by table names with dots in them. Name your tables +# whatever crap you please, just don't use #, ., [ or ]. + +[table.subtable] + +key = "another value" + +# You don't need to specify all the super-tables if you don't want to. TOML +# knows how to do it for you. + +# [x] you +# [x.y] don't +# [x.y.z] need these +[x.y.z.w] # for this to work + + +################################################################################ +## Inline Table + +# Inline tables provide a more compact syntax for expressing tables. They are +# especially useful for grouped data that can otherwise quickly become verbose. +# Inline tables are enclosed in curly braces `{` and `}`. No newlines are +# allowed between the curly braces unless they are valid within a value. + +[table.inline] + +name = { first = "Tom", last = "Preston-Werner" } +point = { x = 1, y = 2 } + + +################################################################################ +## String + +# There are four ways to express strings: basic, multi-line basic, literal, and +# multi-line literal. All strings must contain only valid UTF-8 characters. + +[string.basic] + +basic = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF." + +[string.multiline] + +# The following strings are byte-for-byte equivalent: +key1 = "One\nTwo" +key2 = """One\nTwo""" +key3 = """ +One +Two""" + +[string.multiline.continued] + +# The following strings are byte-for-byte equivalent: +key1 = "The quick brown fox jumps over the lazy dog." + +key2 = """ +The quick brown \ + + + fox jumps over \ + the lazy dog.""" + +key3 = """\ + The quick brown \ + fox jumps over \ + the lazy dog.\ + """ + +[string.literal] + +# What you see is what you get. +winpath = 'C:\Users\nodejs\templates' +winpath2 = '\\ServerX\admin$\system32\' +quoted = 'Tom "Dubs" Preston-Werner' +regex = '<\i\c*\s*>' + + +[string.literal.multiline] + +regex2 = '''I [dw]on't need \d{2} apples''' +lines = ''' +The first newline is +trimmed in raw strings. + All other whitespace + is preserved. +''' + + +################################################################################ +## Integer + +# Integers are whole numbers. Positive numbers may be prefixed with a plus sign. +# Negative numbers are prefixed with a minus sign. + +[integer] + +key1 = +99 +key2 = 42 +key3 = 0 +key4 = -17 + +[integer.underscores] + +# For large numbers, you may use underscores to enhance readability. Each +# underscore must be surrounded by at least one digit. +key1 = 1_000 +key2 = 5_349_221 +key3 = 1_2_3_4_5 # valid but inadvisable + + +################################################################################ +## Float + +# A float consists of an integer part (which may be prefixed with a plus or +# minus sign) followed by a fractional part and/or an exponent part. + +[float.fractional] + +key1 = +1.0 +key2 = 3.1415 +key3 = -0.01 + +[float.exponent] + +key1 = 5e+22 +key2 = 1e6 +key3 = -2E-2 + +[float.both] + +key = 6.626e-34 + +[float.underscores] + +key1 = 9_224_617.445_991_228_313 +key2 = 1e1_00 + + +################################################################################ +## Boolean + +# Booleans are just the tokens you're used to. Always lowercase. + +[boolean] + +True = true +False = false + + +################################################################################ +## Datetime + +# Datetimes are RFC 3339 dates. + +[datetime] + +key1 = 1979-05-27T07:32:00Z +key2 = 1979-05-27T00:32:00-07:00 +key3 = 1979-05-27T00:32:00.999999-07:00 + + +################################################################################ +## Array + +# Arrays are square brackets with other primitives inside. Whitespace is +# ignored. Elements are separated by commas. Data types may not be mixed. + +[array] + +key1 = [ 1, 2, 3 ] +key2 = [ "red", "yellow", "green" ] +key3 = [ [ 1, 2 ], [3, 4, 5] ] +#key4 = [ [ 1, 2 ], ["a", "b", "c"] ] # this is ok + +# Arrays can also be multiline. So in addition to ignoring whitespace, arrays +# also ignore newlines between the brackets. Terminating commas are ok before +# the closing bracket. + +key5 = [ + 1, 2, 3 +] +key6 = [ + 1, + 2, # this is ok +] + + +################################################################################ +## Array of Tables + +# These can be expressed by using a table name in double brackets. Each table +# with the same double bracketed name will be an element in the array. The +# tables are inserted in the order encountered. + +[[products]] + +name = "Hammer" +sku = 738594937 + +[[products]] + +[[products]] + +name = "Nail" +sku = 284758393 +color = "gray" + + +# You can create nested arrays of tables as well. + +[[fruit]] + name = "apple" + + [fruit.physical] + color = "red" + shape = "round" + + [[fruit.variety]] + name = "red delicious" + + [[fruit.variety]] + name = "granny smith" + +[[fruit]] + name = "banana" + + [[fruit.variety]] + name = "plantain" diff --git a/vendor/github.com/pelletier/go-toml/example-crlf.toml b/vendor/github.com/pelletier/go-toml/example-crlf.toml new file mode 100644 index 00000000..12950a16 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/example-crlf.toml @@ -0,0 +1,29 @@ +# This is a TOML document. Boom. + +title = "TOML Example" + +[owner] +name = "Tom Preston-Werner" +organization = "GitHub" +bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." +dob = 1979-05-27T07:32:00Z # First class dates? Why not? + +[database] +server = "192.168.1.1" +ports = [ 8001, 8001, 8002 ] +connection_max = 5000 +enabled = true + +[servers] + + # You can indent as you please. Tabs or spaces. TOML don't care. + [servers.alpha] + ip = "10.0.0.1" + dc = "eqdc10" + + [servers.beta] + ip = "10.0.0.2" + dc = "eqdc10" + +[clients] +data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it diff --git a/vendor/github.com/pelletier/go-toml/example.toml b/vendor/github.com/pelletier/go-toml/example.toml new file mode 100644 index 00000000..3d902f28 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/example.toml @@ -0,0 +1,29 @@ +# This is a TOML document. Boom. + +title = "TOML Example" + +[owner] +name = "Tom Preston-Werner" +organization = "GitHub" +bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." +dob = 1979-05-27T07:32:00Z # First class dates? Why not? + +[database] +server = "192.168.1.1" +ports = [ 8001, 8001, 8002 ] +connection_max = 5000 +enabled = true + +[servers] + + # You can indent as you please. Tabs or spaces. TOML don't care. + [servers.alpha] + ip = "10.0.0.1" + dc = "eqdc10" + + [servers.beta] + ip = "10.0.0.2" + dc = "eqdc10" + +[clients] +data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it diff --git a/vendor/github.com/pelletier/go-toml/marshal.go b/vendor/github.com/pelletier/go-toml/marshal.go index f584ba4e..671da556 100644 --- a/vendor/github.com/pelletier/go-toml/marshal.go +++ b/vendor/github.com/pelletier/go-toml/marshal.go @@ -11,10 +11,13 @@ import ( "time" ) +const tagKeyMultiline = "multiline" + type tomlOpts struct { name string comment string commented bool + multiline bool include bool omitempty bool } @@ -230,7 +233,12 @@ func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, er if err != nil { return nil, err } - tval.SetWithComment(opts.name, opts.comment, opts.commented, val) + + tval.SetWithOptions(opts.name, SetOptions{ + Comment: opts.comment, + Commented: opts.commented, + Multiline: opts.multiline, + }, val) } } case reflect.Map: @@ -559,7 +567,8 @@ func tomlOptions(vf reflect.StructField) tomlOpts { comment = c } commented, _ := strconv.ParseBool(vf.Tag.Get("commented")) - result := tomlOpts{name: vf.Name, comment: comment, commented: commented, include: true, omitempty: false} + multiline, _ := strconv.ParseBool(vf.Tag.Get(tagKeyMultiline)) + result := tomlOpts{name: vf.Name, comment: comment, commented: commented, multiline: multiline, include: true, omitempty: false} if parse[0] != "" { if parse[0] == "-" && len(parse) == 1 { result.include = false diff --git a/vendor/github.com/pelletier/go-toml/marshal_test.toml b/vendor/github.com/pelletier/go-toml/marshal_test.toml new file mode 100644 index 00000000..1c5f98e7 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/marshal_test.toml @@ -0,0 +1,38 @@ +title = "TOML Marshal Testing" + +[basic] + bool = true + date = 1979-05-27T07:32:00Z + float = 123.4 + int = 5000 + string = "Bite me" + uint = 5001 + +[basic_lists] + bools = [true,false,true] + dates = [1979-05-27T07:32:00Z,1980-05-27T07:32:00Z] + floats = [12.3,45.6,78.9] + ints = [8001,8001,8002] + strings = ["One","Two","Three"] + uints = [5002,5003] + +[basic_map] + one = "one" + two = "two" + +[subdoc] + + [subdoc.first] + name = "First" + + [subdoc.second] + name = "Second" + +[[subdoclist]] + name = "List.First" + +[[subdoclist]] + name = "List.Second" + +[[subdocptrs]] + name = "Second" diff --git a/vendor/github.com/pelletier/go-toml/test.sh b/vendor/github.com/pelletier/go-toml/test.sh index a70a8b02..ba6adf3f 100644 --- a/vendor/github.com/pelletier/go-toml/test.sh +++ b/vendor/github.com/pelletier/go-toml/test.sh @@ -23,9 +23,6 @@ function git_clone() { # Remove potential previous runs rm -rf src test_program_bin toml-test -# Run go vet -go vet ./... - go get github.com/pelletier/go-buffruneio go get github.com/davecgh/go-spew/spew go get gopkg.in/yaml.v2 diff --git a/vendor/github.com/pelletier/go-toml/toml.go b/vendor/github.com/pelletier/go-toml/toml.go index 05493a44..98c185ad 100644 --- a/vendor/github.com/pelletier/go-toml/toml.go +++ b/vendor/github.com/pelletier/go-toml/toml.go @@ -14,6 +14,7 @@ type tomlValue struct { value interface{} // string, int64, uint64, float64, bool, time.Time, [] of any of this list comment string commented bool + multiline bool position Position } @@ -175,6 +176,63 @@ func (t *Tree) GetDefault(key string, def interface{}) interface{} { return val } +// SetOptions arguments are supplied to the SetWithOptions and SetPathWithOptions functions to modify marshalling behaviour. +// The default values within the struct are valid default options. +type SetOptions struct { + Comment string + Commented bool + Multiline bool +} + +// SetWithOptions is the same as Set, but allows you to provide formatting +// instructions to the key, that will be used by Marshal(). +func (t *Tree) SetWithOptions(key string, opts SetOptions, value interface{}) { + t.SetPathWithOptions(strings.Split(key, "."), opts, value) +} + +// SetPathWithOptions is the same as SetPath, but allows you to provide +// formatting instructions to the key, that will be reused by Marshal(). +func (t *Tree) SetPathWithOptions(keys []string, opts SetOptions, value interface{}) { + subtree := t + for _, intermediateKey := range keys[:len(keys)-1] { + nextTree, exists := subtree.values[intermediateKey] + if !exists { + nextTree = newTree() + subtree.values[intermediateKey] = nextTree // add new element here + } + switch node := nextTree.(type) { + case *Tree: + subtree = node + case []*Tree: + // go to most recent element + if len(node) == 0 { + // create element if it does not exist + subtree.values[intermediateKey] = append(node, newTree()) + } + subtree = node[len(node)-1] + } + } + + var toInsert interface{} + + switch value.(type) { + case *Tree: + tt := value.(*Tree) + tt.comment = opts.Comment + toInsert = value + case []*Tree: + toInsert = value + case *tomlValue: + tt := value.(*tomlValue) + tt.comment = opts.Comment + toInsert = tt + default: + toInsert = &tomlValue{value: value, comment: opts.Comment, commented: opts.Commented, multiline: opts.Multiline} + } + + subtree.values[keys[len(keys)-1]] = toInsert +} + // Set an element in the tree. // Key is a dot-separated path (e.g. a.b.c). // Creates all necessary intermediate trees, if needed. diff --git a/vendor/github.com/pelletier/go-toml/tomltree_write.go b/vendor/github.com/pelletier/go-toml/tomltree_write.go index b5600a58..e4049e29 100644 --- a/vendor/github.com/pelletier/go-toml/tomltree_write.go +++ b/vendor/github.com/pelletier/go-toml/tomltree_write.go @@ -12,7 +12,41 @@ import ( "time" ) -// encodes a string to a TOML-compliant string value +// Encodes a string to a TOML-compliant multi-line string value +// This function is a clone of the existing encodeTomlString function, except that whitespace characters +// are preserved. Quotation marks and backslashes are also not escaped. +func encodeMultilineTomlString(value string) string { + var b bytes.Buffer + + for _, rr := range value { + switch rr { + case '\b': + b.WriteString(`\b`) + case '\t': + b.WriteString("\t") + case '\n': + b.WriteString("\n") + case '\f': + b.WriteString(`\f`) + case '\r': + b.WriteString("\r") + case '"': + b.WriteString(`"`) + case '\\': + b.WriteString(`\`) + default: + intRr := uint16(rr) + if intRr < 0x001F { + b.WriteString(fmt.Sprintf("\\u%0.4X", intRr)) + } else { + b.WriteRune(rr) + } + } + } + return b.String() +} + +// Encodes a string to a TOML-compliant string value func encodeTomlString(value string) string { var b bytes.Buffer @@ -45,6 +79,15 @@ func encodeTomlString(value string) string { } func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElementPerLine bool) (string, error) { + // this interface check is added to dereference the change made in the writeTo function. + // That change was made to allow this function to see formatting options. + tv, ok := v.(*tomlValue) + if ok { + v = tv.value + } else { + tv = &tomlValue{} + } + switch value := v.(type) { case uint64: return strconv.FormatUint(value, 10), nil @@ -58,6 +101,9 @@ func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElemen } return strings.ToLower(strconv.FormatFloat(value, 'f', -1, 32)), nil case string: + if tv.multiline { + return "\"\"\"\n" + encodeMultilineTomlString(value) + "\"\"\"", nil + } return "\"" + encodeTomlString(value) + "\"", nil case []byte: b, _ := v.([]byte) @@ -130,7 +176,7 @@ func (t *Tree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64, a return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k]) } - repr, err := tomlValueStringRepresentation(v.value, indent, arraysOneElementPerLine) + repr, err := tomlValueStringRepresentation(v, indent, arraysOneElementPerLine) if err != nil { return bytesCount, err } diff --git a/vendor/github.com/peterhellberg/emojilib/.travis.yml b/vendor/github.com/peterhellberg/emojilib/.travis.yml index 15cfa8b6..99c59c46 100644 --- a/vendor/github.com/peterhellberg/emojilib/.travis.yml +++ b/vendor/github.com/peterhellberg/emojilib/.travis.yml @@ -1,8 +1,11 @@ language: go go: - - 1.8 - - 1.7.5 - - 1.6.4 + - "1.10.3" + - "1.9.7" sudo: false + +script: + - go vet ./... + - go test ./... diff --git a/vendor/github.com/peterhellberg/emojilib/LICENSE b/vendor/github.com/peterhellberg/emojilib/LICENSE index 782fe09a..e1df4843 100644 --- a/vendor/github.com/peterhellberg/emojilib/LICENSE +++ b/vendor/github.com/peterhellberg/emojilib/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015-2017 Peter Hellberg https://c7.se/ +Copyright (c) 2015-2018 Peter Hellberg https://c7.se/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), diff --git a/vendor/github.com/peterhellberg/emojilib/README.md b/vendor/github.com/peterhellberg/emojilib/README.md index 3175a43d..2a8ab690 100644 --- a/vendor/github.com/peterhellberg/emojilib/README.md +++ b/vendor/github.com/peterhellberg/emojilib/README.md @@ -39,7 +39,7 @@ _You’ll need to have the [golang.org/x/tools/imports](https://golang.org/x/too ## License (MIT) -Copyright (c) 2015-2017 [Peter Hellberg](http://c7.se/) +Copyright (c) 2015-2018 [Peter Hellberg](http://c7.se/) > Permission is hereby granted, free of charge, to any person obtaining > a copy of this software and associated documentation files (the diff --git a/vendor/github.com/peterhellberg/emojilib/generated.go b/vendor/github.com/peterhellberg/emojilib/generated.go index a5976471..9bc3ea3b 100644 --- a/vendor/github.com/peterhellberg/emojilib/generated.go +++ b/vendor/github.com/peterhellberg/emojilib/generated.go @@ -2,7 +2,7 @@ // // Update it by running: go generate // -// Generated at: 2017-06-16 16:34:02 +0000 +// Generated at: 2018-08-20 09:01:38 +0000 package emojilib @@ -74,6 +74,11 @@ var emojis = Emojis{ Char: "🉑", Category: "symbols", }, + "adult": Emoji{ + Keywords: []string{"gender-neutral", "person"}, + Char: "🧑", + Category: "people", + }, "aerial_tramway": Emoji{ Keywords: []string{"transportation", "vehicle", "ski"}, Char: "🚡", @@ -105,7 +110,7 @@ var emojis = Emojis{ Category: "flags", }, "alembic": Emoji{ - Keywords: []string{"distilling", "science", "experiment"}, + Keywords: []string{"distilling", "science", "experiment", "chemistry"}, Char: "⚗", Category: "objects", }, @@ -355,7 +360,7 @@ var emojis = Emojis{ Category: "symbols", }, "atom_symbol": Emoji{ - Keywords: []string{"science"}, + Keywords: []string{"science", "physics", "chemistry"}, Char: "⚛", Category: "symbols", }, @@ -549,6 +554,11 @@ var emojis = Emojis{ Char: "🐻", Category: "animals_and_nature", }, + "bearded_person": Emoji{ + Keywords: []string{"person", "bewhiskered"}, + Char: "🧔", + Category: "people", + }, "bed": Emoji{ Keywords: []string{"sleep", "rest"}, Char: "🛏", @@ -639,6 +649,11 @@ var emojis = Emojis{ Char: "👙", Category: "people", }, + "billed_hat": Emoji{ + Keywords: []string{"cap", "baseball"}, + Char: "🧢", + Category: "people", + }, "biohazard": Emoji{ Keywords: []string{"danger"}, Char: "☣", @@ -814,16 +829,16 @@ var emojis = Emojis{ Char: "🙇‍♀️", Category: "people", }, + "bowl_with_spoon": Emoji{ + Keywords: []string{"food", "breakfast", "cereal", "oatmeal", "porridge"}, + Char: "🥣", + Category: "food_and_drink", + }, "bowling": Emoji{ Keywords: []string{"sports", "fun", "play"}, Char: "🎳", Category: "activity", }, - "bowtie": Emoji{ - Keywords: []string{"face", "formal", "fashion", "suit", "classy", "magic", "circus"}, - Char: "", - Category: "_custom", - }, "boxing_glove": Emoji{ Keywords: []string{"sports", "fighting"}, Char: "🥊", @@ -834,6 +849,11 @@ var emojis = Emojis{ Char: "👦", Category: "people", }, + "brain": Emoji{ + Keywords: []string{"smart", "intelligent"}, + Char: "🧠", + Category: "people", + }, "brazil": Emoji{ Keywords: []string{"br", "flag", "nation", "country", "banner"}, Char: "🇧🇷", @@ -844,6 +864,11 @@ var emojis = Emojis{ Char: "🍞", Category: "food_and_drink", }, + "breastfeeding": Emoji{ + Keywords: []string{"nursing", "baby"}, + Char: "🤱", + Category: "people", + }, "bride_with_veil": Emoji{ Keywords: []string{"couple", "marriage", "wedding", "woman", "bride"}, Char: "👰", @@ -855,7 +880,7 @@ var emojis = Emojis{ Category: "travel_and_places", }, "briefcase": Emoji{ - Keywords: []string{"business", "documents", "work", "law", "legal"}, + Keywords: []string{"business", "documents", "work", "law", "legal", "job", "career"}, Char: "💼", Category: "people", }, @@ -869,6 +894,11 @@ var emojis = Emojis{ Char: "🇻🇬", Category: "flags", }, + "broccoli": Emoji{ + Keywords: []string{"fruit", "food", "vegetable"}, + Char: "🥦", + Category: "food_and_drink", + }, "broken_heart": Emoji{ Keywords: []string{"sad", "sorry", "break", "heart", "heartbreak"}, Char: "💔", @@ -1034,6 +1064,11 @@ var emojis = Emojis{ Char: "🍬", Category: "food_and_drink", }, + "canned_food": Emoji{ + Keywords: []string{"food", "soup"}, + Char: "🥫", + Category: "food_and_drink", + }, "canoe": Emoji{ Keywords: []string{"boat", "paddle", "water", "ship"}, Char: "🛶", @@ -1169,6 +1204,11 @@ var emojis = Emojis{ Char: "🐔", Category: "animals_and_nature", }, + "child": Emoji{ + Keywords: []string{"gender-neutral", "young"}, + Char: "🧒", + Category: "people", + }, "children_crossing": Emoji{ Keywords: []string{"school", "warning", "danger", "sign", "driving", "yellow-diamond"}, Char: "🚸", @@ -1189,6 +1229,11 @@ var emojis = Emojis{ Char: "🍫", Category: "food_and_drink", }, + "chopsticks": Emoji{ + Keywords: []string{"food"}, + Char: "🥢", + Category: "food_and_drink", + }, "christmas_island": Emoji{ Keywords: []string{"christmas", "island", "flag", "nation", "country", "banner"}, Char: "🇨🇽", @@ -1254,8 +1299,18 @@ var emojis = Emojis{ Char: "🏛", Category: "travel_and_places", }, + "climbing_man": Emoji{ + Keywords: []string{"sports", "hobby", "man", "male", "rock"}, + Char: "🧗‍♂️", + Category: "activity", + }, + "climbing_woman": Emoji{ + Keywords: []string{"sports", "hobby", "woman", "female", "rock"}, + Char: "🧗‍♀️", + Category: "activity", + }, "clinking_glasses": Emoji{ - Keywords: []string{"beverage", "drink", "party", "alcohol", "celebrate", "cheers"}, + Keywords: []string{"beverage", "drink", "party", "alcohol", "celebrate", "cheers", "wine", "champagne", "toast"}, Char: "🥂", Category: "food_and_drink", }, @@ -1439,11 +1494,21 @@ var emojis = Emojis{ Char: "🇨🇳", Category: "flags", }, + "coat": Emoji{ + Keywords: []string{"jacket"}, + Char: "🧥", + Category: "people", + }, "cocktail": Emoji{ Keywords: []string{"drink", "drunk", "alcohol", "beverage", "booze", "mojito"}, Char: "🍸", Category: "food_and_drink", }, + "coconut": Emoji{ + Keywords: []string{"fruit", "nature", "food", "palm"}, + Char: "🥥", + Category: "food_and_drink", + }, "cocos_islands": Emoji{ Keywords: []string{"cocos", "keeling", "islands", "flag", "nation", "country", "banner"}, Char: "🇨🇨", @@ -1455,7 +1520,7 @@ var emojis = Emojis{ Category: "food_and_drink", }, "coffin": Emoji{ - Keywords: []string{"vampire", "dead", "die", "death", "rip", "graveyard", "cemetery"}, + Keywords: []string{"vampire", "dead", "die", "death", "rip", "graveyard", "cemetery", "casket", "funeral", "box"}, Char: "⚰", Category: "objects", }, @@ -1719,6 +1784,11 @@ var emojis = Emojis{ Char: "🥒", Category: "food_and_drink", }, + "cup_with_straw": Emoji{ + Keywords: []string{"drink", "soda"}, + Char: "🥤", + Category: "food_and_drink", + }, "cupid": Emoji{ Keywords: []string{"love", "like", "heart", "affection", "valentines"}, Char: "💘", @@ -1729,6 +1799,11 @@ var emojis = Emojis{ Char: "🇨🇼", Category: "flags", }, + "curling_stone": Emoji{ + Keywords: []string{"sports"}, + Char: "🥌", + Category: "activity", + }, "curly_loop": Emoji{ Keywords: []string{"scribble", "draw", "shape", "squiggle"}, Char: "➰", @@ -1755,7 +1830,7 @@ var emojis = Emojis{ Category: "symbols", }, "cyclone": Emoji{ - Keywords: []string{"weather", "swirl", "blue", "cloud", "vortex", "spiral", "whirlpool", "spin"}, + Keywords: []string{"weather", "swirl", "blue", "cloud", "vortex", "spiral", "whirlpool", "spin", "tornado", "hurricane", "typhoon"}, Char: "🌀", Category: "symbols", }, @@ -1800,7 +1875,7 @@ var emojis = Emojis{ Category: "people", }, "dart": Emoji{ - Keywords: []string{"game", "play", "bar"}, + Keywords: []string{"game", "play", "bar", "target", "bullseye"}, Char: "🎯", Category: "activity", }, @@ -1980,7 +2055,7 @@ var emojis = Emojis{ Category: "animals_and_nature", }, "drum": Emoji{ - Keywords: []string{"music", "instrument", "drumsticks"}, + Keywords: []string{"music", "instrument", "drumsticks", "snare"}, Char: "🥁", Category: "activity", }, @@ -1989,6 +2064,11 @@ var emojis = Emojis{ Char: "🦆", Category: "animals_and_nature", }, + "dumpling": Emoji{ + Keywords: []string{"food", "empanada", "pierogi", "potsticker"}, + Char: "🥟", + Category: "food_and_drink", + }, "dvd": Emoji{ Keywords: []string{"cd", "disk", "disc"}, Char: "📀", @@ -2064,6 +2144,11 @@ var emojis = Emojis{ Char: "✳️", Category: "symbols", }, + "eject_button": Emoji{ + Keywords: []string{"blue-square"}, + Char: "⏏️", + Category: "symbols", + }, "el_salvador": Emoji{ Keywords: []string{"el", "salvador", "flag", "nation", "country", "banner"}, Char: "🇸🇻", @@ -2089,6 +2174,11 @@ var emojis = Emojis{ Char: "🔚", Category: "symbols", }, + "england": Emoji{ + Keywords: []string{"flag", "english"}, + Char: "🏴󠁧󠁢󠁥󠁮󠁧󠁿", + Category: "flags", + }, "envelope_with_arrow": Emoji{ Keywords: []string{"email", "communication"}, Char: "📩", @@ -2149,6 +2239,11 @@ var emojis = Emojis{ Char: "❗", Category: "symbols", }, + "exploding_head": Emoji{ + Keywords: []string{"face", "shocked", "mind", "blown"}, + Char: "🤯", + Category: "people", + }, "expressionless": Emoji{ Keywords: []string{"face", "indifferent", "-_-", "meh", "deadpan"}, Char: "😑", @@ -2344,11 +2439,6 @@ var emojis = Emojis{ Char: "😨", Category: "people", }, - "feelsgood": Emoji{ - Keywords: []string{"doom", "oldschool"}, - Char: "", - Category: "_custom", - }, "female_detective": Emoji{ Keywords: []string{"human", "spy", "detective", "female", "woman"}, Char: "🕵️‍♀️", @@ -2399,11 +2489,6 @@ var emojis = Emojis{ Char: "🇫🇮", Category: "flags", }, - "finnadie": Emoji{ - Keywords: []string{"doom", "oldschool"}, - Char: "", - Category: "_custom", - }, "fire": Emoji{ Keywords: []string{"hot", "cook", "flame"}, Char: "🔥", @@ -2435,7 +2520,7 @@ var emojis = Emojis{ Category: "animals_and_nature", }, "fish_cake": Emoji{ - Keywords: []string{"food", "japan", "sea", "beach"}, + Keywords: []string{"food", "japan", "sea", "beach", "narutomaki", "pink", "swirl", "kamaboko", "surimi", "ramen"}, Char: "🍥", Category: "food_and_drink", }, @@ -2504,6 +2589,11 @@ var emojis = Emojis{ Char: "😳", Category: "people", }, + "flying_saucer": Emoji{ + Keywords: []string{"transportation", "vehicle", "ufo"}, + Char: "🛸", + Category: "travel_and_places", + }, "fog": Emoji{ Keywords: []string{"weather"}, Char: "🌫", @@ -2529,6 +2619,11 @@ var emojis = Emojis{ Char: "🍴", Category: "food_and_drink", }, + "fortune_cookie": Emoji{ + Keywords: []string{"food", "prophecy"}, + Char: "🥠", + Category: "food_and_drink", + }, "fountain": Emoji{ Keywords: []string{"photo", "summer", "water", "fresh"}, Char: "⛲", @@ -2709,6 +2804,11 @@ var emojis = Emojis{ Char: "💝", Category: "symbols", }, + "giraffe": Emoji{ + Keywords: []string{"animal", "nature", "spots", "safari"}, + Char: "🦒", + Category: "animals_and_nature", + }, "girl": Emoji{ Keywords: []string{"female", "woman", "teenager"}, Char: "👧", @@ -2719,6 +2819,11 @@ var emojis = Emojis{ Char: "🌐", Category: "symbols", }, + "gloves": Emoji{ + Keywords: []string{"hands", "winter", "clothes"}, + Char: "🧤", + Category: "people", + }, "goal_net": Emoji{ Keywords: []string{"sports"}, Char: "🥅", @@ -2729,16 +2834,6 @@ var emojis = Emojis{ Char: "🐐", Category: "animals_and_nature", }, - "goberserk": Emoji{ - Keywords: []string{"doom", "rage", "bloody", "hurt"}, - Char: "", - Category: "_custom", - }, - "godmode": Emoji{ - Keywords: []string{"doom", "oldschool"}, - Char: "", - Category: "_custom", - }, "golf": Emoji{ Keywords: []string{"sports", "business", "flag", "hole", "summer"}, Char: "⛳", @@ -2764,6 +2859,11 @@ var emojis = Emojis{ Char: "🍇", Category: "food_and_drink", }, + "grasshopper": Emoji{ + Keywords: []string{"animal", "cricket", "chirp"}, + Char: "🦗", + Category: "animals_and_nature", + }, "greece": Emoji{ Keywords: []string{"gr", "flag", "nation", "country", "banner"}, Char: "🇬🇷", @@ -2919,6 +3019,11 @@ var emojis = Emojis{ Char: "🐹", Category: "animals_and_nature", }, + "hand_over_mouth": Emoji{ + Keywords: []string{"face", "whoops", "shock", "surprise"}, + Char: "🤭", + Category: "people", + }, "handbag": Emoji{ Keywords: []string{"fashion", "accessory", "accessories", "shopping"}, Char: "👜", @@ -3024,6 +3129,11 @@ var emojis = Emojis{ Char: "➕", Category: "symbols", }, + "hedgehog": Emoji{ + Keywords: []string{"animal", "nature", "spiny"}, + Char: "🦔", + Category: "animals_and_nature", + }, "helicopter": Emoji{ Keywords: []string{"transportation", "vehicle", "fly"}, Char: "🚁", @@ -3149,11 +3259,6 @@ var emojis = Emojis{ Char: "🇭🇺", Category: "flags", }, - "hurtrealbad": Emoji{ - Keywords: []string{"mad", "injured", "doom", "oldschool", "custom_"}, - Char: "", - Category: "_custom", - }, "hushed": Emoji{ Keywords: []string{"face", "woo", "shh"}, Char: "😯", @@ -3654,6 +3759,11 @@ var emojis = Emojis{ Char: "💌", Category: "objects", }, + "love_you": Emoji{ + Keywords: []string{"hand", "fingers", "gesture"}, + Char: "🤟", + Category: "people", + }, "low_brightness": Emoji{ Keywords: []string{"sun", "afternoon", "warm", "summer"}, Char: "🔅", @@ -3784,6 +3894,11 @@ var emojis = Emojis{ Char: "🕺", Category: "people", }, + "man_elf": Emoji{ + Keywords: []string{"man", "male"}, + Char: "🧝‍♂️", + Category: "people", + }, "man_facepalming": Emoji{ Keywords: []string{"man", "male", "boy", "disbelief"}, Char: "🤦", @@ -3794,6 +3909,11 @@ var emojis = Emojis{ Char: "👨‍🏭", Category: "people", }, + "man_fairy": Emoji{ + Keywords: []string{"man", "male"}, + Char: "🧚‍♂️", + Category: "people", + }, "man_farmer": Emoji{ Keywords: []string{"rancher", "gardener", "man", "human"}, Char: "👨‍🌾", @@ -3804,11 +3924,26 @@ var emojis = Emojis{ Char: "👨‍🚒", Category: "people", }, + "man_genie": Emoji{ + Keywords: []string{"man", "male"}, + Char: "🧞‍♂️", + Category: "people", + }, "man_health_worker": Emoji{ Keywords: []string{"doctor", "nurse", "therapist", "healthcare", "man", "human"}, Char: "👨‍⚕️", Category: "people", }, + "man_in_lotus_position": Emoji{ + Keywords: []string{"man", "male", "meditation", "yoga", "serenity", "zen", "mindfulness"}, + Char: "🧘‍♂️", + Category: "activity", + }, + "man_in_steamy_room": Emoji{ + Keywords: []string{"male", "man", "spa", "steamroom", "sauna"}, + Char: "🧖‍♂️", + Category: "people", + }, "man_in_tuxedo": Emoji{ Keywords: []string{"couple", "marriage", "wedding", "groom"}, Char: "🤵", @@ -3825,7 +3960,7 @@ var emojis = Emojis{ Category: "activity", }, "man_mechanic": Emoji{ - Keywords: []string{"plumber", "man", "human"}, + Keywords: []string{"plumber", "man", "human", "wrench"}, Char: "👨‍🔧", Category: "people", }, @@ -3875,10 +4010,15 @@ var emojis = Emojis{ Category: "people", }, "man_technologist": Emoji{ - Keywords: []string{"coder", "developer", "engineer", "programmer", "software", "man", "human"}, + Keywords: []string{"coder", "developer", "engineer", "programmer", "software", "man", "human", "laptop", "computer"}, Char: "👨‍💻", Category: "people", }, + "man_vampire": Emoji{ + Keywords: []string{"man", "male", "dracula"}, + Char: "🧛‍♂️", + Category: "people", + }, "man_with_gua_pi_mao": Emoji{ Keywords: []string{"male", "boy", "chinese"}, Char: "👲", @@ -3889,6 +4029,11 @@ var emojis = Emojis{ Char: "👳", Category: "people", }, + "man_zombie": Emoji{ + Keywords: []string{"man", "male", "dracula", "undead", "walking dead"}, + Char: "🧟‍♂️", + Category: "people", + }, "mans_shoe": Emoji{ Keywords: []string{"fashion", "male"}, Char: "👞", @@ -3994,6 +4139,16 @@ var emojis = Emojis{ Char: "🚹", Category: "symbols", }, + "mermaid": Emoji{ + Keywords: []string{"woman", "female", "merwoman", "ariel"}, + Char: "🧜‍♀️", + Category: "people", + }, + "merman": Emoji{ + Keywords: []string{"man", "male", "triton"}, + Char: "🧜‍♂️", + Category: "people", + }, "metal": Emoji{ Keywords: []string{"hand", "fingers", "evil_eye", "sign_of_horns", "rock_on"}, Char: "🤘", @@ -4089,6 +4244,11 @@ var emojis = Emojis{ Char: "🐵", Category: "animals_and_nature", }, + "monocle": Emoji{ + Keywords: []string{"face", "stuffy", "wealthy"}, + Char: "🧐", + Category: "people", + }, "monorail": Emoji{ Keywords: []string{"transportation", "vehicle"}, Char: "🚝", @@ -4269,11 +4429,6 @@ var emojis = Emojis{ Char: "🤢", Category: "people", }, - "neckbeard": Emoji{ - Keywords: []string{"nerdy", "face", "custom_"}, - Char: "", - Category: "_custom", - }, "necktie": Emoji{ Keywords: []string{"shirt", "suitup", "formal", "fashion", "cloth", "business"}, Char: "👔", @@ -4494,11 +4649,6 @@ var emojis = Emojis{ Char: "🌊", Category: "animals_and_nature", }, - "octocat": Emoji{ - Keywords: []string{"animal", "octopus", "github", "custom_"}, - Char: "", - Category: "_custom", - }, "octopus": Emoji{ Keywords: []string{"animal", "creature", "ocean", "sea", "nature", "beach"}, Char: "🐙", @@ -4525,7 +4675,7 @@ var emojis = Emojis{ Category: "symbols", }, "ok_hand": Emoji{ - Keywords: []string{"fingers", "limbs", "perfect", "ok"}, + Keywords: []string{"fingers", "limbs", "perfect", "ok", "okay"}, Char: "👌", Category: "people", }, @@ -4544,6 +4694,11 @@ var emojis = Emojis{ Char: "🗝", Category: "objects", }, + "older_adult": Emoji{ + Keywords: []string{"human", "elder", "senior", "gender-neutral"}, + Char: "🧓", + Category: "people", + }, "older_man": Emoji{ Keywords: []string{"human", "male", "men", "old", "elder", "senior"}, Char: "👴", @@ -4629,6 +4784,11 @@ var emojis = Emojis{ Char: "📙", Category: "objects", }, + "orange_heart": Emoji{ + Keywords: []string{"love", "like", "affection", "valentines"}, + Char: "🧡", + Category: "symbols", + }, "orthodox_cross": Emoji{ Keywords: []string{"suppedaneum", "religion"}, Char: "☦", @@ -4694,6 +4854,11 @@ var emojis = Emojis{ Char: "🌴", Category: "animals_and_nature", }, + "palms_up": Emoji{ + Keywords: []string{"hands", "gesture", "cupped", "prayer"}, + Char: "🤲", + Category: "people", + }, "panama": Emoji{ Keywords: []string{"pa", "flag", "nation", "country", "banner"}, Char: "🇵🇦", @@ -4805,7 +4970,7 @@ var emojis = Emojis{ Category: "animals_and_nature", }, "pensive": Emoji{ - Keywords: []string{"face", "sad", "depressed", "okay", "upset"}, + Keywords: []string{"face", "sad", "depressed", "upset"}, Char: "😔", Category: "people", }, @@ -4844,6 +5009,11 @@ var emojis = Emojis{ Char: "⛏", Category: "objects", }, + "pie": Emoji{ + Keywords: []string{"food", "dessert", "pastry"}, + Char: "🥧", + Category: "food_and_drink", + }, "pig": Emoji{ Keywords: []string{"animal", "oink", "nature"}, Char: "🐷", @@ -5039,6 +5209,11 @@ var emojis = Emojis{ Char: "🤰", Category: "people", }, + "pretzel": Emoji{ + Keywords: []string{"food", "bread", "twisted"}, + Char: "🥨", + Category: "food_and_drink", + }, "previous_track_button": Emoji{ Keywords: []string{"backward"}, Char: "⏮", @@ -5134,26 +5309,6 @@ var emojis = Emojis{ Char: "😡", Category: "people", }, - "rage1": Emoji{ - Keywords: []string{"angry", "mad", "hate", "despise"}, - Char: "", - Category: "_custom", - }, - "rage2": Emoji{ - Keywords: []string{"angry", "mad", "hate", "despise"}, - Char: "", - Category: "_custom", - }, - "rage3": Emoji{ - Keywords: []string{"angry", "mad", "hate", "despise"}, - Char: "", - Category: "_custom", - }, - "rage4": Emoji{ - Keywords: []string{"angry", "mad", "hate", "despise"}, - Char: "", - Category: "_custom", - }, "railway_car": Emoji{ Keywords: []string{"transportation", "vehicle"}, Char: "🚃", @@ -5179,6 +5334,11 @@ var emojis = Emojis{ Char: "🤚", Category: "people", }, + "raised_eyebrow": Emoji{ + Keywords: []string{"face", "distrust", "scepticism", "disapproval", "disbelief", "surprise"}, + Char: "🤨", + Category: "people", + }, "raised_hand": Emoji{ Keywords: []string{"fingers", "stop", "highfive", "palm", "ban"}, Char: "✋", @@ -5464,6 +5624,11 @@ var emojis = Emojis{ Char: "👡", Category: "people", }, + "sandwich": Emoji{ + Keywords: []string{"food", "lunch", "bread"}, + Char: "🥪", + Category: "food_and_drink", + }, "santa": Emoji{ Keywords: []string{"festival", "man", "male", "xmas", "father christmas"}, Char: "🎅", @@ -5484,11 +5649,21 @@ var emojis = Emojis{ Char: "🇸🇦", Category: "flags", }, + "sauropod": Emoji{ + Keywords: []string{"animal", "nature", "dinosaur", "brachiosaurus", "brontosaurus", "diplodocus", "extinct"}, + Char: "🦕", + Category: "animals_and_nature", + }, "saxophone": Emoji{ Keywords: []string{"music", "instrument", "jazz", "blues"}, Char: "🎷", Category: "activity", }, + "scarf": Emoji{ + Keywords: []string{"neck", "winter", "clothes"}, + Char: "🧣", + Category: "people", + }, "school": Emoji{ Keywords: []string{"building", "student", "education", "learn", "teach"}, Char: "🏫", @@ -5514,6 +5689,11 @@ var emojis = Emojis{ Char: "♏", Category: "symbols", }, + "scotland": Emoji{ + Keywords: []string{"flag", "scottish"}, + Char: "🏴󠁧󠁢󠁳󠁣󠁴󠁿", + Category: "flags", + }, "scream": Emoji{ Keywords: []string{"face", "munch", "scared", "omg"}, Char: "😱", @@ -5619,11 +5799,6 @@ var emojis = Emojis{ Char: "🚢", Category: "travel_and_places", }, - "shipit": Emoji{ - Keywords: []string{"squirrel", "detective", "animal", "sherlock", "inspector", "custom_"}, - Char: "", - Category: "_custom", - }, "shopping": Emoji{ Keywords: []string{"mall", "buy", "purchase"}, Char: "🛍", @@ -5644,6 +5819,11 @@ var emojis = Emojis{ Char: "🦐", Category: "animals_and_nature", }, + "shushing": Emoji{ + Keywords: []string{"face", "quiet", "shhh"}, + Char: "🤫", + Category: "people", + }, "sierra_leone": Emoji{ Keywords: []string{"sierra", "leone", "flag", "nation", "country", "banner"}, Char: "🇸🇱", @@ -5685,15 +5865,20 @@ var emojis = Emojis{ Category: "activity", }, "skull": Emoji{ - Keywords: []string{"dead", "skeleton", "creepy"}, + Keywords: []string{"dead", "skeleton", "creepy", "death"}, Char: "💀", Category: "people", }, "skull_and_crossbones": Emoji{ - Keywords: []string{"poison", "danger", "deadly", "scary"}, + Keywords: []string{"poison", "danger", "deadly", "scary", "death", "pirate", "evil"}, Char: "☠", Category: "objects", }, + "sled": Emoji{ + Keywords: []string{"sleigh", "luge", "toboggan"}, + Char: "🛷", + Category: "activity", + }, "sleeping": Emoji{ Keywords: []string{"face", "tired", "sleepy", "night", "zzz"}, Char: "😴", @@ -5844,6 +6029,11 @@ var emojis = Emojis{ Char: "⚽", Category: "activity", }, + "socks": Emoji{ + Keywords: []string{"stockings", "clothes"}, + Char: "🧦", + Category: "people", + }, "solomon_islands": Emoji{ Keywords: []string{"solomon", "islands", "flag", "nation", "country", "banner"}, Char: "🇸🇧", @@ -5859,6 +6049,11 @@ var emojis = Emojis{ Char: "🔜", Category: "symbols", }, + "sorceress": Emoji{ + Keywords: []string{"woman", "female", "mage", "witch"}, + Char: "🧙‍♀️", + Category: "people", + }, "sos": Emoji{ Keywords: []string{"help", "red-square", "words", "emergency", "911"}, Char: "🆘", @@ -6034,6 +6229,11 @@ var emojis = Emojis{ Char: "✡", Category: "symbols", }, + "star_struck": Emoji{ + Keywords: []string{"face", "smile", "starry", "eyes", "grinning"}, + Char: "🤩", + Category: "people", + }, "stars": Emoji{ Keywords: []string{"night", "photo"}, Char: "🌠", @@ -6049,6 +6249,11 @@ var emojis = Emojis{ Char: "🗽", Category: "travel_and_places", }, + "steak": Emoji{ + Keywords: []string{"food", "cow", "meat", "cut", "chop", "lambchop", "porkchop"}, + Char: "🥩", + Category: "food_and_drink", + }, "steam_locomotive": Emoji{ Keywords: []string{"transportation", "vehicle", "train"}, Char: "🚂", @@ -6179,11 +6384,6 @@ var emojis = Emojis{ Char: "🍣", Category: "food_and_drink", }, - "suspect": Emoji{ - Keywords: []string{"mad", "custom_"}, - Char: "", - Category: "_custom", - }, "suspension_railway": Emoji{ Keywords: []string{"vehicle", "transportation"}, Char: "🚟", @@ -6239,6 +6439,11 @@ var emojis = Emojis{ Char: "🔣", Category: "symbols", }, + "symbols_over_mouth": Emoji{ + Keywords: []string{"face", "swearing", "cursing", "cussing", "profanity", "expletive"}, + Char: "🤬", + Category: "people", + }, "synagogue": Emoji{ Keywords: []string{"judaism", "worship", "temple", "jewish"}, Char: "🕍", @@ -6254,6 +6459,11 @@ var emojis = Emojis{ Char: "💉", Category: "objects", }, + "t-rex": Emoji{ + Keywords: []string{"animal", "nature", "dinosaur", "tyrannosaurus", "extinct"}, + Char: "🦖", + Category: "animals_and_nature", + }, "taco": Emoji{ Keywords: []string{"food", "mexican"}, Char: "🌮", @@ -6274,6 +6484,11 @@ var emojis = Emojis{ Char: "🇹🇯", Category: "flags", }, + "takeout_box": Emoji{ + Keywords: []string{"food", "leftovers"}, + Char: "🥡", + Category: "food_and_drink", + }, "tanabata_tree": Emoji{ Keywords: []string{"plant", "nature", "branch", "summer"}, Char: "🎋", @@ -6310,7 +6525,7 @@ var emojis = Emojis{ Category: "objects", }, "telescope": Emoji{ - Keywords: []string{"stars", "space", "zoom"}, + Keywords: []string{"stars", "space", "zoom", "science", "astronomy"}, Char: "🔭", Category: "objects", }, @@ -6514,11 +6729,6 @@ var emojis = Emojis{ Char: "🚎", Category: "travel_and_places", }, - "trollface": Emoji{ - Keywords: []string{"internet", "meme", "custom_"}, - Char: "", - Category: "_custom", - }, "trophy": Emoji{ Keywords: []string{"win", "award", "contest", "place", "ftw", "ceremony"}, Char: "🏆", @@ -6695,7 +6905,7 @@ var emojis = Emojis{ Category: "animals_and_nature", }, "unamused": Emoji{ - Keywords: []string{"indifference", "bored", "straight face", "serious", "sarcasm"}, + Keywords: []string{"indifference", "bored", "straight face", "serious", "sarcasm", "unimpressed", "skeptical", "dubious", "side_eye"}, Char: "😒", Category: "people", }, @@ -6819,6 +7029,11 @@ var emojis = Emojis{ Char: "🏐", Category: "activity", }, + "vomiting": Emoji{ + Keywords: []string{"face", "sick"}, + Char: "🤮", + Category: "people", + }, "vs": Emoji{ Keywords: []string{"words", "orange-square"}, Char: "🆚", @@ -6829,6 +7044,11 @@ var emojis = Emojis{ Char: "🖖", Category: "people", }, + "wales": Emoji{ + Keywords: []string{"flag", "welsh"}, + Char: "🏴󠁧󠁢󠁷󠁬󠁳󠁿", + Category: "flags", + }, "walking_man": Emoji{ Keywords: []string{"human", "feet", "steps"}, Char: "🚶", @@ -7019,6 +7239,11 @@ var emojis = Emojis{ Char: "😉", Category: "people", }, + "wizard": Emoji{ + Keywords: []string{"man", "male", "mage", "sorcerer"}, + Char: "🧙‍♂️", + Category: "people", + }, "wolf": Emoji{ Keywords: []string{"animal", "nature", "wild"}, Char: "🐺", @@ -7049,6 +7274,11 @@ var emojis = Emojis{ Char: "👩‍🍳", Category: "people", }, + "woman_elf": Emoji{ + Keywords: []string{"woman", "female"}, + Char: "🧝‍♀️", + Category: "people", + }, "woman_facepalming": Emoji{ Keywords: []string{"woman", "female", "girl", "disbelief"}, Char: "🤦‍♀️", @@ -7059,6 +7289,11 @@ var emojis = Emojis{ Char: "👩‍🏭", Category: "people", }, + "woman_fairy": Emoji{ + Keywords: []string{"woman", "female"}, + Char: "🧚‍♀️", + Category: "people", + }, "woman_farmer": Emoji{ Keywords: []string{"rancher", "gardener", "woman", "human"}, Char: "👩‍🌾", @@ -7069,11 +7304,26 @@ var emojis = Emojis{ Char: "👩‍🚒", Category: "people", }, + "woman_genie": Emoji{ + Keywords: []string{"woman", "female"}, + Char: "🧞‍♀️", + Category: "people", + }, "woman_health_worker": Emoji{ Keywords: []string{"doctor", "nurse", "therapist", "healthcare", "woman", "human"}, Char: "👩‍⚕️", Category: "people", }, + "woman_in_lotus_position": Emoji{ + Keywords: []string{"woman", "female", "meditation", "yoga", "serenity", "zen", "mindfulness"}, + Char: "🧘‍♀️", + Category: "activity", + }, + "woman_in_steamy_room": Emoji{ + Keywords: []string{"female", "woman", "spa", "steamroom", "sauna"}, + Char: "🧖‍♀️", + Category: "people", + }, "woman_judge": Emoji{ Keywords: []string{"justice", "court", "woman", "human"}, Char: "👩‍⚖️", @@ -7085,7 +7335,7 @@ var emojis = Emojis{ Category: "activity", }, "woman_mechanic": Emoji{ - Keywords: []string{"plumber", "woman", "human"}, + Keywords: []string{"plumber", "woman", "human", "wrench"}, Char: "👩‍🔧", Category: "people", }, @@ -7135,15 +7385,30 @@ var emojis = Emojis{ Category: "people", }, "woman_technologist": Emoji{ - Keywords: []string{"coder", "developer", "engineer", "programmer", "software", "woman", "human"}, + Keywords: []string{"coder", "developer", "engineer", "programmer", "software", "woman", "human", "laptop", "computer"}, Char: "👩‍💻", Category: "people", }, + "woman_vampire": Emoji{ + Keywords: []string{"woman", "female"}, + Char: "🧛‍♀️", + Category: "people", + }, + "woman_with_headscarf": Emoji{ + Keywords: []string{"female", "hijab", "mantilla", "tichel"}, + Char: "🧕", + Category: "people", + }, "woman_with_turban": Emoji{ Keywords: []string{"female", "indian", "hinduism", "arabs", "woman"}, Char: "👳‍♀️", Category: "people", }, + "woman_zombie": Emoji{ + Keywords: []string{"woman", "female", "undead", "walking dead"}, + Char: "🧟‍♀️", + Category: "people", + }, "womans_clothes": Emoji{ Keywords: []string{"fashion", "shopping_bags", "female"}, Char: "👚", @@ -7219,11 +7484,21 @@ var emojis = Emojis{ Char: "🇿🇲", Category: "flags", }, + "zany": Emoji{ + Keywords: []string{"face", "goofy", "crazy"}, + Char: "🤪", + Category: "people", + }, "zap": Emoji{ Keywords: []string{"thunder", "weather", "lightning bolt", "fast"}, Char: "⚡", Category: "animals_and_nature", }, + "zebra": Emoji{ + Keywords: []string{"animal", "nature", "stripes", "safari"}, + Char: "🦓", + Category: "animals_and_nature", + }, "zero": Emoji{ Keywords: []string{"0", "numbers", "blue-square", "null"}, Char: "0️⃣", @@ -7431,6 +7706,7 @@ var keywordLookup = map[string][]string{ "kissing_smiling_eyes", "love_hotel", "love_letter", + "orange_heart", "purple_heart", "revolving_hearts", "sparkling_heart", @@ -7542,10 +7818,6 @@ var keywordLookup = map[string][]string{ "facepunch", "imp", "rage", - "rage1", - "rage2", - "rage3", - "rage4", }, "animal": []string{ "ant", @@ -7583,13 +7855,16 @@ var keywordLookup = map[string][]string{ "fox_face", "fried_shrimp", "frog", + "giraffe", "goat", "gorilla", + "grasshopper", "hamster", "hatched_chick", "hatching_chick", "hear_no_evil", "heart_eyes_cat", + "hedgehog", "honeybee", "horse", "horse_racing", @@ -7603,7 +7878,6 @@ var keywordLookup = map[string][]string{ "monkey_face", "mouse", "mouse2", - "octocat", "octopus", "owl", "ox", @@ -7622,12 +7896,12 @@ var keywordLookup = map[string][]string{ "rat", "rhinoceros", "rooster", + "sauropod", "scorpion", "scream_cat", "see_no_evil", "shark", "sheep", - "shipit", "shrimp", "smile_cat", "smiley_cat", @@ -7638,6 +7912,7 @@ var keywordLookup = map[string][]string{ "spider", "spider_web", "squid", + "t-rex", "tiger", "tiger2", "tropical_fish", @@ -7648,6 +7923,7 @@ var keywordLookup = map[string][]string{ "whale", "whale2", "wolf", + "zebra", }, "annoyed": []string{ "angry", @@ -7697,6 +7973,9 @@ var keywordLookup = map[string][]string{ "straight_ruler", "triangular_ruler", }, + "ariel": []string{ + "mermaid", + }, "arm": []string{ "muscle", }, @@ -7754,6 +8033,9 @@ var keywordLookup = map[string][]string{ "taurus", "virgo", }, + "astronomy": []string{ + "telescope", + }, "at": []string{ "austria", }, @@ -7799,6 +8081,7 @@ var keywordLookup = map[string][]string{ "azerbaijan", }, "baby": []string{ + "breastfeeding", "hatched_chick", "hatching_chick", "pregnant_woman", @@ -8123,6 +8406,9 @@ var keywordLookup = map[string][]string{ "barthélemy": []string{ "st_barthelemy", }, + "baseball": []string{ + "billed_hat", + }, "bath": []string{ "hotsprings", }, @@ -8197,6 +8483,9 @@ var keywordLookup = map[string][]string{ "tumbler_glass", "wine_glass", }, + "bewhiskered": []string{ + "bearded_person", + }, "bg": []string{ "bulgaria", }, @@ -8280,11 +8569,9 @@ var keywordLookup = map[string][]string{ "blood": []string{ "syringe", }, - "bloody": []string{ - "goberserk", - }, "blown": []string{ "boom", + "exploding_head", }, "blue": []string{ "cyclone", @@ -8325,6 +8612,7 @@ var keywordLookup = map[string][]string{ "cool", "customs", "eight", + "eject_button", "fast_forward", "five", "four", @@ -8458,6 +8746,7 @@ var keywordLookup = map[string][]string{ }, "box": []string{ "bento", + "coffin", "package", }, "boy": []string{ @@ -8484,6 +8773,9 @@ var keywordLookup = map[string][]string{ "br": []string{ "brazil", }, + "brachiosaurus": []string{ + "sauropod", + }, "branch": []string{ "tanabata_tree", }, @@ -8496,12 +8788,15 @@ var keywordLookup = map[string][]string{ "bread": []string{ "baguette_bread", "croissant", + "pretzel", + "sandwich", }, "break": []string{ "broken_heart", }, "breakfast": []string{ "bacon", + "bowl_with_spoon", "bread", "egg", "fried_egg", @@ -8538,6 +8833,9 @@ var keywordLookup = map[string][]string{ "bromance": []string{ "two_men_holding_hands", }, + "brontosaurus": []string{ + "sauropod", + }, "brown": []string{ "horse", }, @@ -8591,6 +8889,9 @@ var keywordLookup = map[string][]string{ "city_sunset", "houses", }, + "bullseye": []string{ + "dart", + }, "bunny": []string{ "dancing_men", "dancing_women", @@ -8704,6 +9005,7 @@ var keywordLookup = map[string][]string{ "lollipop", }, "cap": []string{ + "billed_hat", "mortar_board", }, "caption": []string{ @@ -8725,6 +9027,9 @@ var keywordLookup = map[string][]string{ "hearts", "spades", }, + "career": []string{ + "briefcase", + }, "carnival": []string{ "carousel_horse", "circus_tent", @@ -8754,6 +9059,9 @@ var keywordLookup = map[string][]string{ "casino": []string{ "slot_machine", }, + "casket": []string{ + "coffin", + }, "casserole": []string{ "shallow_pan_of_food", }, @@ -8813,6 +9121,9 @@ var keywordLookup = map[string][]string{ "century": []string{ "100", }, + "cereal": []string{ + "bowl_with_spoon", + }, "ceremony": []string{ "trophy", }, @@ -8822,6 +9133,9 @@ var keywordLookup = map[string][]string{ "chadder": []string{ "cheese", }, + "champagne": []string{ + "clinking_glasses", + }, "characters": []string{ "symbols", }, @@ -8852,6 +9166,10 @@ var keywordLookup = map[string][]string{ "man_scientist", "woman_scientist", }, + "chemistry": []string{ + "alembic", + "atom_symbol", + }, "chicken": []string{ "baby_chick", "egg", @@ -8935,9 +9253,15 @@ var keywordLookup = map[string][]string{ "chips": []string{ "fries", }, + "chirp": []string{ + "grasshopper", + }, "chocolate": []string{ "cookie", }, + "chop": []string{ + "steak", + }, "chopsticks": []string{ "ramen", }, @@ -8979,7 +9303,6 @@ var keywordLookup = map[string][]string{ }, "circus": []string{ "balloon", - "bowtie", "confetti_ball", "crystal_ball", "elephant", @@ -8997,7 +9320,6 @@ var keywordLookup = map[string][]string{ "notebook_with_decorative_cover", }, "classy": []string{ - "bowtie", "man", "tophat", }, @@ -9024,6 +9346,9 @@ var keywordLookup = map[string][]string{ }, "clothes": []string{ "dress", + "gloves", + "scarf", + "socks", }, "cloud": []string{ "cyclone", @@ -9110,7 +9435,9 @@ var keywordLookup = map[string][]string{ }, "computer": []string{ "keyboard", + "man_technologist", "robot", + "woman_technologist", }, "computing": []string{ "desktop_computer", @@ -9458,11 +9785,15 @@ var keywordLookup = map[string][]string{ "cow": []string{ "milk_glass", "ox", + "steak", "water_buffalo", }, "cowgirl": []string{ "cowboy_hat_face", }, + "crazy": []string{ + "zany", + }, "create": []string{ "hammer", "hammer_and_pick", @@ -9481,6 +9812,9 @@ var keywordLookup = map[string][]string{ "japanese_ogre", "skull", }, + "cricket": []string{ + "grasshopper", + }, "croak": []string{ "frog", }, @@ -9521,6 +9855,9 @@ var keywordLookup = map[string][]string{ "cupertino": []string{ "motorway", }, + "cupped": []string{ + "palms_up", + }, "curaçao": []string{ "curacao", }, @@ -9531,22 +9868,21 @@ var keywordLookup = map[string][]string{ "pound", "yen", }, + "cursing": []string{ + "symbols_over_mouth", + }, "curtain": []string{ "cinema", }, + "cussing": []string{ + "symbols_over_mouth", + }, "custom": []string{ "passport_control", }, - "custom_": []string{ - "hurtrealbad", - "neckbeard", - "octocat", - "shipit", - "suspect", - "trollface", - }, "cut": []string{ "scissors", + "steak", "u5272", }, "cutlery": []string{ @@ -9631,6 +9967,8 @@ var keywordLookup = map[string][]string{ "death": []string{ "coffin", "funeral_urn", + "skull", + "skull_and_crossbones", }, "december": []string{ "christmas_tree", @@ -9686,10 +10024,6 @@ var keywordLookup = map[string][]string{ }, "despise": []string{ "rage", - "rage1", - "rage2", - "rage3", - "rage4", }, "dessert": []string{ "birthday", @@ -9702,6 +10036,7 @@ var keywordLookup = map[string][]string{ "doughnut", "ice_cream", "icecream", + "pie", "shaved_ice", }, "destination": []string{ @@ -9712,7 +10047,6 @@ var keywordLookup = map[string][]string{ "mag", "mag_right", "male_detective", - "shipit", }, "developer": []string{ "man_technologist", @@ -9758,6 +10092,13 @@ var keywordLookup = map[string][]string{ "dinner": []string{ "plate_with_cutlery", }, + "dinosaur": []string{ + "sauropod", + "t-rex", + }, + "diplodocus": []string{ + "sauropod", + }, "direction": []string{ "arrow_backward", "arrow_double_down", @@ -9794,12 +10135,16 @@ var keywordLookup = map[string][]string{ "disappointed": []string{ "slightly_frowning_face", }, + "disapproval": []string{ + "raised_eyebrow", + }, "disaster": []string{ "ocean", "volcano", }, "disbelief": []string{ "man_facepalming", + "raised_eyebrow", "woman_facepalming", }, "disc": []string{ @@ -9830,6 +10175,9 @@ var keywordLookup = map[string][]string{ "distilling": []string{ "alembic", }, + "distrust": []string{ + "raised_eyebrow", + }, "divide": []string{ "heavy_division_sign", "u5272", @@ -9891,13 +10239,6 @@ var keywordLookup = map[string][]string{ "donut": []string{ "doughnut", }, - "doom": []string{ - "feelsgood", - "finnadie", - "goberserk", - "godmode", - "hurtrealbad", - }, "door": []string{ "key", "old_key", @@ -9920,6 +10261,10 @@ var keywordLookup = map[string][]string{ "downtown": []string{ "night_with_stars", }, + "dracula": []string{ + "man_vampire", + "man_zombie", + }, "drama": []string{ "performing_arts", }, @@ -9946,6 +10291,7 @@ var keywordLookup = map[string][]string{ "champagne", "clinking_glasses", "cocktail", + "cup_with_straw", "milk_glass", "non-potable_water", "sake", @@ -9986,6 +10332,9 @@ var keywordLookup = map[string][]string{ "tumbler_glass", "wine_glass", }, + "dubious": []string{ + "unamused", + }, "dusk": []string{ "clock6", }, @@ -10063,6 +10412,7 @@ var keywordLookup = map[string][]string{ "el_salvador", }, "elder": []string{ + "older_adult", "older_man", "older_woman", }, @@ -10100,6 +10450,9 @@ var keywordLookup = map[string][]string{ "emirates": []string{ "united_arab_emirates", }, + "empanada": []string{ + "dumpling", + }, "empty": []string{ "u7a7a", }, @@ -10126,6 +10479,7 @@ var keywordLookup = map[string][]string{ "uk", }, "english": []string{ + "england", "uk", }, "enter": []string{ @@ -10201,6 +10555,7 @@ var keywordLookup = map[string][]string{ "evil": []string{ "black_heart", "japanese_goblin", + "skull_and_crossbones", "snake", }, "evil_eye": []string{ @@ -10234,6 +10589,9 @@ var keywordLookup = map[string][]string{ "alembic", "microscope", }, + "expletive": []string{ + "symbols_over_mouth", + }, "explode": []string{ "bomb", "boom", @@ -10245,12 +10603,19 @@ var keywordLookup = map[string][]string{ "express": []string{ "articulated_lorry", }, + "extinct": []string{ + "sauropod", + "t-rex", + }, "eye": []string{ "wink", }, "eyeroll": []string{ "roll_eyes", }, + "eyes": []string{ + "star_struck", + }, "eyesight": []string{ "eyeglasses", }, @@ -10262,7 +10627,6 @@ var keywordLookup = map[string][]string{ "anguished", "astonished", "blush", - "bowtie", "clown_face", "cold_sweat", "confounded", @@ -10274,6 +10638,7 @@ var keywordLookup = map[string][]string{ "disappointed_relieved", "drooling_face", "ear", + "exploding_head", "expressionless", "eye", "fearful", @@ -10284,6 +10649,7 @@ var keywordLookup = map[string][]string{ "grimacing", "grin", "grinning", + "hand_over_mouth", "heart_eyes", "hugs", "hushed", @@ -10298,18 +10664,20 @@ var keywordLookup = map[string][]string{ "lying_face", "mask", "money_mouth_face", + "monocle", "nauseated_face", - "neckbeard", "nerd_face", "no_mouth", "open_mouth", "pensive", "persevere", + "raised_eyebrow", "relaxed", "relieved", "rofl", "roll_eyes", "scream", + "shushing", "sleeping", "sleepy", "slightly_frowning_face", @@ -10319,19 +10687,23 @@ var keywordLookup = map[string][]string{ "smirk", "sneezing_face", "sob", + "star_struck", "stuck_out_tongue", "stuck_out_tongue_closed_eyes", "stuck_out_tongue_winking_eye", "sunglasses", "sweat", "sweat_smile", + "symbols_over_mouth", "thinking", "triumph", "upside_down_face", + "vomiting", "weary", "wink", "worried", "yum", + "zany", "zipper_mouth_face", }, "fail": []string{ @@ -10375,7 +10747,6 @@ var keywordLookup = map[string][]string{ "fashion": []string{ "bikini", "boot", - "bowtie", "dress", "eyeglasses", "handbag", @@ -10441,6 +10812,7 @@ var keywordLookup = map[string][]string{ "bikini", "blonde_woman", "bowing_woman", + "climbing_woman", "construction_worker_woman", "dancer", "dancing_women", @@ -10454,6 +10826,7 @@ var keywordLookup = map[string][]string{ "kimono", "lipstick", "massage_woman", + "mermaid", "mountain_biking_woman", "mrs_claus", "no_good_woman", @@ -10465,6 +10838,7 @@ var keywordLookup = map[string][]string{ "raising_hand_woman", "rowing_woman", "running_woman", + "sorceress", "surfing_woman", "swimming_woman", "tipping_hand_woman", @@ -10472,9 +10846,17 @@ var keywordLookup = map[string][]string{ "walking_woman", "weight_lifting_woman", "woman", + "woman_elf", "woman_facepalming", + "woman_fairy", + "woman_genie", + "woman_in_lotus_position", + "woman_in_steamy_room", "woman_shrugging", + "woman_vampire", + "woman_with_headscarf", "woman_with_turban", + "woman_zombie", "womans_clothes", "womans_hat", "womens", @@ -10524,6 +10906,7 @@ var keywordLookup = map[string][]string{ "fingers": []string{ "fist", "fu", + "love_you", "metal", "ok_hand", "open_hands", @@ -10649,6 +11032,7 @@ var keywordLookup = map[string][]string{ "ecuador", "egypt", "el_salvador", + "england", "equatorial_guinea", "eritrea", "es", @@ -10772,6 +11156,7 @@ var keywordLookup = map[string][]string{ "san_marino", "sao_tome_principe", "saudi_arabia", + "scotland", "senegal", "serbia", "seychelles", @@ -10824,6 +11209,7 @@ var keywordLookup = map[string][]string{ "vatican_city", "venezuela", "vietnam", + "wales", "wallis_futuna", "western_sahara", "yemen", @@ -10899,14 +11285,19 @@ var keywordLookup = map[string][]string{ "bento", "birthday", "blowfish", + "bowl_with_spoon", "bread", + "broccoli", "burrito", "cake", + "canned_food", "carrot", "cheese", "cherries", "chestnut", "chocolate_bar", + "chopsticks", + "coconut", "cookie", "corn", "croissant", @@ -10915,11 +11306,13 @@ var keywordLookup = map[string][]string{ "custard", "dango", "doughnut", + "dumpling", "egg", "eggplant", "fish", "fish_cake", "fishing_pole_and_fish", + "fortune_cookie", "fried_egg", "fried_shrimp", "grapes", @@ -10937,24 +11330,29 @@ var keywordLookup = map[string][]string{ "peach", "peanuts", "pear", + "pie", "pineapple", "pizza", "plate_with_cutlery", "popcorn", "potato", "poultry_leg", + "pretzel", "ramen", "rice", "rice_ball", "rice_cracker", + "sandwich", "shallow_pan_of_food", "spaghetti", + "steak", "stew", "strawberry", "stuffed_flatbread", "sushi", "sweet_potato", "taco", + "takeout_box", "tangerine", "tomato", "watermelon", @@ -10973,7 +11371,6 @@ var keywordLookup = map[string][]string{ "u7981", }, "formal": []string{ - "bowtie", "necktie", }, "formula": []string{ @@ -11028,7 +11425,9 @@ var keywordLookup = map[string][]string{ "apple", "avocado", "banana", + "broccoli", "cherries", + "coconut", "cucumber", "grapes", "green_apple", @@ -11064,6 +11463,9 @@ var keywordLookup = map[string][]string{ "man_dancing", "roller_coaster", }, + "funeral": []string{ + "coffin", + }, "funny": []string{ "smile", "smiley", @@ -11144,6 +11546,11 @@ var keywordLookup = map[string][]string{ "restroom", "womens", }, + "gender-neutral": []string{ + "adult", + "child", + "older_adult", + }, "gentleman": []string{ "tophat", }, @@ -11155,6 +11562,8 @@ var keywordLookup = map[string][]string{ }, "gesture": []string{ "call_me_hand", + "love_you", + "palms_up", "raised_hands", "wave", }, @@ -11200,9 +11609,6 @@ var keywordLookup = map[string][]string{ "dancing_women", "woman", }, - "github": []string{ - "octocat", - }, "give up": []string{ "white_flag", }, @@ -11256,6 +11662,9 @@ var keywordLookup = map[string][]string{ "goodbye": []string{ "wave", }, + "goofy": []string{ + "zany", + }, "gp": []string{ "guadeloupe", }, @@ -11322,6 +11731,9 @@ var keywordLookup = map[string][]string{ "grin": []string{ "grinning", }, + "grinning": []string{ + "star_struck", + }, "groceries": []string{ "convenience_store", }, @@ -11400,6 +11812,7 @@ var keywordLookup = map[string][]string{ "fist_left", "fist_right", "fu", + "love_you", "metal", "muscle", "point_down", @@ -11414,7 +11827,9 @@ var keywordLookup = map[string][]string{ "hands": []string{ "call_me_hand", "clap", + "gloves", "open_hands", + "palms_up", "raised_hands", "wave", }, @@ -11455,10 +11870,6 @@ var keywordLookup = map[string][]string{ }, "hate": []string{ "rage", - "rage1", - "rage2", - "rage3", - "rage4", }, "have": []string{ "u6709", @@ -11541,6 +11952,9 @@ var keywordLookup = map[string][]string{ "highway": []string{ "motorway", }, + "hijab": []string{ + "woman_with_headscarf", + }, "hinduism": []string{ "man_with_turban", "om", @@ -11575,6 +11989,8 @@ var keywordLookup = map[string][]string{ }, "hobby": []string{ "8ball", + "climbing_man", + "climbing_woman", "fishing_pole_and_fish", "rowing_man", "rowing_woman", @@ -11740,6 +12156,7 @@ var keywordLookup = map[string][]string{ "mountain_biking_woman", "ok_man", "ok_woman", + "older_adult", "older_man", "older_woman", "put_litter_in_its_place", @@ -11776,9 +12193,11 @@ var keywordLookup = map[string][]string{ "hundred": []string{ "100", }, + "hurricane": []string{ + "cyclone", + }, "hurt": []string{ "face_with_head_bandage", - "goberserk", }, "i18n": []string{ "globe_with_meridians", @@ -11873,7 +12292,6 @@ var keywordLookup = map[string][]string{ }, "injured": []string{ "face_with_head_bandage", - "hurtrealbad", }, "ink": []string{ "printer", @@ -11892,9 +12310,6 @@ var keywordLookup = map[string][]string{ "honeybee", "spider_web", }, - "inspector": []string{ - "shipit", - }, "instructor": []string{ "man_teacher", "woman_teacher", @@ -11907,6 +12322,9 @@ var keywordLookup = map[string][]string{ "saxophone", "violin", }, + "intelligent": []string{ + "brain", + }, "international": []string{ "earth_africa", "earth_americas", @@ -11916,7 +12334,6 @@ var keywordLookup = map[string][]string{ "internet": []string{ "globe_with_meridians", "signal_strength", - "trollface", }, "interstate": []string{ "motorway", @@ -11990,6 +12407,9 @@ var keywordLookup = map[string][]string{ "ivory": []string{ "cote_divoire", }, + "jacket": []string{ + "coat", + }, "jainism": []string{ "om", "wheel_of_dharma", @@ -12066,6 +12486,9 @@ var keywordLookup = map[string][]string{ "jo": []string{ "jordan", }, + "job": []string{ + "briefcase", + }, "join": []string{ "u5408", }, @@ -12099,6 +12522,9 @@ var keywordLookup = map[string][]string{ "man_judge", "woman_judge", }, + "kamaboko": []string{ + "fish_cake", + }, "kanji": []string{ "accept", "congratulations", @@ -12234,6 +12660,9 @@ var keywordLookup = map[string][]string{ "ladybug": []string{ "beetle", }, + "lambchop": []string{ + "steak", + }, "landing": []string{ "flight_departure", }, @@ -12245,6 +12674,8 @@ var keywordLookup = map[string][]string{ }, "laptop": []string{ "computer", + "man_technologist", + "woman_technologist", }, "late": []string{ "clock1", @@ -12322,6 +12753,9 @@ var keywordLookup = map[string][]string{ "arrow_backward", "point_left", }, + "leftovers": []string{ + "takeout_box", + }, "legal": []string{ "briefcase", "copyright", @@ -12421,6 +12855,7 @@ var keywordLookup = map[string][]string{ "kissing_heart", "love_hotel", "love_letter", + "orange_heart", "purple_heart", "revolving_hearts", "smile", @@ -12530,6 +12965,7 @@ var keywordLookup = map[string][]string{ "kissing", "kissing_closed_eyes", "kissing_heart", + "orange_heart", "purple_heart", "revolving_hearts", "rose", @@ -12569,8 +13005,12 @@ var keywordLookup = map[string][]string{ "crossed_fingers", "four_leaf_clover", }, + "luge": []string{ + "sled", + }, "lunch": []string{ "plate_with_cutlery", + "sandwich", }, "lv": []string{ "latvia", @@ -12599,19 +13039,16 @@ var keywordLookup = map[string][]string{ "mad": []string{ "anger", "angry", - "hurtrealbad", "rage", - "rage1", - "rage2", - "rage3", - "rage4", "right_anger_bubble", - "suspect", + }, + "mage": []string{ + "sorceress", + "wizard", }, "magic": []string{ "8ball", "black_joker", - "bowtie", "clubs", "crescent_moon", "crystal_ball", @@ -12636,19 +13073,28 @@ var keywordLookup = map[string][]string{ "blonde_man", "bowing_man", "boy", + "climbing_man", "construction_worker_man", "dancing_men", "frowning_man", "guardsman", "haircut_man", "man_dancing", + "man_elf", "man_facepalming", + "man_fairy", + "man_genie", + "man_in_lotus_position", + "man_in_steamy_room", "man_shrugging", + "man_vampire", "man_with_gua_pi_mao", "man_with_turban", + "man_zombie", "mans_shoe", "massage_man", "mens", + "merman", "no_good_man", "ok_man", "older_man", @@ -12657,6 +13103,7 @@ var keywordLookup = map[string][]string{ "raising_hand_man", "santa", "tipping_hand_man", + "wizard", }, "mall": []string{ "department_store", @@ -12672,17 +13119,23 @@ var keywordLookup = map[string][]string{ "blonde_man", "bowing_man", "boy", + "climbing_man", "frowning_man", "haircut_man", "isle_of_man", "man_artist", "man_astronaut", "man_cook", + "man_elf", "man_facepalming", "man_factory_worker", + "man_fairy", "man_farmer", "man_firefighter", + "man_genie", "man_health_worker", + "man_in_lotus_position", + "man_in_steamy_room", "man_judge", "man_mechanic", "man_office_worker", @@ -12693,7 +13146,10 @@ var keywordLookup = map[string][]string{ "man_student", "man_teacher", "man_technologist", + "man_vampire", + "man_zombie", "massage_man", + "merman", "no_good_man", "ok_man", "policeman", @@ -12703,6 +13159,7 @@ var keywordLookup = map[string][]string{ "running_man", "santa", "tipping_hand_man", + "wizard", }, "manager": []string{ "man_office_worker", @@ -12711,6 +13168,9 @@ var keywordLookup = map[string][]string{ "manicure": []string{ "nail_care", }, + "mantilla": []string{ + "woman_with_headscarf", + }, "map": []string{ "round_pushpin", }, @@ -12776,6 +13236,7 @@ var keywordLookup = map[string][]string{ "dango", "hamburger", "poultry_leg", + "steak", "stew", }, "mecca": []string{ @@ -12786,13 +13247,14 @@ var keywordLookup = map[string][]string{ "pill", "syringe", }, + "meditation": []string{ + "man_in_lotus_position", + "woman_in_lotus_position", + }, "meh": []string{ "expressionless", "neutral_face", }, - "meme": []string{ - "trollface", - }, "memo": []string{ "spiral_notepad", }, @@ -12805,6 +13267,9 @@ var keywordLookup = map[string][]string{ "cat", "cat2", }, + "merwoman": []string{ + "mermaid", + }, "message": []string{ "left_speech_bubble", "speech_balloon", @@ -12842,6 +13307,13 @@ var keywordLookup = map[string][]string{ "minaret": []string{ "mosque", }, + "mind": []string{ + "exploding_head", + }, + "mindfulness": []string{ + "man_in_lotus_position", + "woman_in_lotus_position", + }, "minor": []string{ "underage", }, @@ -13048,6 +13520,9 @@ var keywordLookup = map[string][]string{ "nap": []string{ "sleepy", }, + "narutomaki": []string{ + "fish_cake", + }, "nation": []string{ "afghanistan", "aland_islands", @@ -13317,6 +13792,7 @@ var keywordLookup = map[string][]string{ "cherry_blossom", "chicken", "chipmunk", + "coconut", "cow", "cow2", "crocodile", @@ -13342,11 +13818,13 @@ var keywordLookup = map[string][]string{ "frog", "full_moon", "full_moon_with_face", + "giraffe", "goat", "gorilla", "green_apple", "hamster", "hear_no_evil", + "hedgehog", "honeybee", "horse", "house_with_garden", @@ -13389,6 +13867,7 @@ var keywordLookup = map[string][]string{ "ram", "rhinoceros", "rooster", + "sauropod", "see_no_evil", "seedling", "shamrock", @@ -13404,6 +13883,7 @@ var keywordLookup = map[string][]string{ "sunflower", "sunny", "sweet_potato", + "t-rex", "tanabata_tree", "tangerine", "tiger", @@ -13423,10 +13903,14 @@ var keywordLookup = map[string][]string{ "wilted_flower", "wind_chime", "wolf", + "zebra", }, "ne": []string{ "niger", }, + "neck": []string{ + "scarf", + }, "needle": []string{ "syringe", }, @@ -13435,7 +13919,6 @@ var keywordLookup = map[string][]string{ }, "nerdy": []string{ "eyeglasses", - "neckbeard", "nerd_face", }, "nervous": []string{ @@ -13592,9 +14075,15 @@ var keywordLookup = map[string][]string{ "syringe", "woman_health_worker", }, + "nursing": []string{ + "breastfeeding", + }, "nut": []string{ "peanuts", }, + "oatmeal": []string{ + "bowl_with_spoon", + }, "obtain": []string{ "ideograph_advantage", }, @@ -13612,9 +14101,6 @@ var keywordLookup = map[string][]string{ "whale", "whale2", }, - "octopus": []string{ - "octocat", - }, "office": []string{ "file_folder", "page_facing_up", @@ -13638,7 +14124,7 @@ var keywordLookup = map[string][]string{ "white_check_mark", }, "okay": []string{ - "pensive", + "ok_hand", }, "old": []string{ "older_man", @@ -13646,13 +14132,9 @@ var keywordLookup = map[string][]string{ "radio_button", }, "oldschool": []string{ - "feelsgood", - "finnadie", "floppy_disk", - "godmode", "hourglass", "hourglass_flowing_sand", - "hurtrealbad", "pager", "tv", "vhs", @@ -13767,6 +14249,7 @@ var keywordLookup = map[string][]string{ "palestinian_territories", }, "palm": []string{ + "coconut", "raised_hand", "raised_hand_with_fingers_splayed", "wave", @@ -13842,6 +14325,9 @@ var keywordLookup = map[string][]string{ "lock", "old_key", }, + "pastry": []string{ + "pie", + }, "paul": []string{ "alien", }, @@ -13909,6 +14395,8 @@ var keywordLookup = map[string][]string{ "ok_hand", }, "person": []string{ + "adult", + "bearded_person", "blonde_man", "blonde_woman", "bust_in_silhouette", @@ -13984,6 +14472,9 @@ var keywordLookup = map[string][]string{ "man_scientist", "woman_scientist", }, + "physics": []string{ + "atom_symbol", + }, "piano": []string{ "musical_keyboard", }, @@ -13993,6 +14484,9 @@ var keywordLookup = map[string][]string{ "picnic": []string{ "watermelon", }, + "pierogi": []string{ + "dumpling", + }, "pierre": []string{ "st_pierre_miquelon", }, @@ -14009,6 +14503,7 @@ var keywordLookup = map[string][]string{ "ping_pong", }, "pink": []string{ + "fish_cake", "heartbeat", "heartpulse", "ok_woman", @@ -14022,6 +14517,7 @@ var keywordLookup = map[string][]string{ }, "pirate": []string{ "black_flag", + "skull_and_crossbones", }, "pistol": []string{ "gun", @@ -14161,9 +14657,18 @@ var keywordLookup = map[string][]string{ "pork": []string{ "bacon", }, + "porkchop": []string{ + "steak", + }, + "porridge": []string{ + "bowl_with_spoon", + }, "postal": []string{ "email", }, + "potsticker": []string{ + "dumpling", + }, "potty": []string{ "toilet", }, @@ -14181,6 +14686,7 @@ var keywordLookup = map[string][]string{ "stuck_out_tongue_winking_eye", }, "prayer": []string{ + "palms_up", "place_of_worship", }, "prc": []string{ @@ -14225,6 +14731,9 @@ var keywordLookup = map[string][]string{ "problem": []string{ "warning", }, + "profanity": []string{ + "symbols_over_mouth", + }, "professor": []string{ "man_teacher", "woman_teacher", @@ -14244,6 +14753,9 @@ var keywordLookup = map[string][]string{ "prohibited": []string{ "no_bicycles", }, + "prophecy": []string{ + "fortune_cookie", + }, "propose": []string{ "ring", }, @@ -14330,6 +14842,7 @@ var keywordLookup = map[string][]string{ "mobile_phone_off", "mute", "no_bell", + "shushing", }, "quiz": []string{ "100", @@ -14348,9 +14861,6 @@ var keywordLookup = map[string][]string{ "radio": []string{ "satellite", }, - "rage": []string{ - "goberserk", - }, "rain": []string{ "closed_umbrella", }, @@ -14363,6 +14873,9 @@ var keywordLookup = map[string][]string{ "raised": []string{ "raised_back_of_hand", }, + "ramen": []string{ + "fish_cake", + }, "rancher": []string{ "man_farmer", "woman_farmer", @@ -14531,6 +15044,8 @@ var keywordLookup = map[string][]string{ "tiger2", }, "rock": []string{ + "climbing_man", + "climbing_woman", "moyai", }, "rock_on": []string{ @@ -14614,6 +15129,10 @@ var keywordLookup = map[string][]string{ "sweat", "weary", }, + "safari": []string{ + "giraffe", + "zebra", + }, "sahara": []string{ "western_sahara", }, @@ -14681,6 +15200,10 @@ var keywordLookup = map[string][]string{ "satisfied": []string{ "laughing", }, + "sauna": []string{ + "man_in_steamy_room", + "woman_in_steamy_room", + }, "save": []string{ "bookmark", "bookmark_tabs", @@ -14709,6 +15232,9 @@ var keywordLookup = map[string][]string{ "japanese_ogre", "skull_and_crossbones", }, + "scepticism": []string{ + "raised_eyebrow", + }, "schedule": []string{ "calendar", "clock1", @@ -14749,6 +15275,7 @@ var keywordLookup = map[string][]string{ "alembic", "atom_symbol", "microscope", + "telescope", }, "score": []string{ "100", @@ -14762,6 +15289,9 @@ var keywordLookup = map[string][]string{ "scotch": []string{ "tumbler_glass", }, + "scottish": []string{ + "scotland", + }, "scout": []string{ "fleur_de_lis", }, @@ -14834,9 +15364,14 @@ var keywordLookup = map[string][]string{ "eyes", }, "senior": []string{ + "older_adult", "older_man", "older_woman", }, + "serenity": []string{ + "man_in_lotus_position", + "woman_in_lotus_position", + }, "serious": []string{ "unamused", }, @@ -14881,12 +15416,12 @@ var keywordLookup = map[string][]string{ "shell": []string{ "snail", }, - "sherlock": []string{ - "shipit", - }, "shh": []string{ "hushed", }, + "shhh": []string{ + "shushing", + }, "shield": []string{ "beginner", }, @@ -14921,6 +15456,12 @@ var keywordLookup = map[string][]string{ "shitface": []string{ "poop", }, + "shock": []string{ + "hand_over_mouth", + }, + "shocked": []string{ + "exploding_head", + }, "shoes": []string{ "athletic_shoe", "boot", @@ -14974,6 +15515,10 @@ var keywordLookup = map[string][]string{ "persevere", "sneezing_face", "tired_face", + "vomiting", + }, + "side_eye": []string{ + "unamused", }, "sideways": []string{ "left_right_arrow", @@ -15046,6 +15591,9 @@ var keywordLookup = map[string][]string{ "skeleton": []string{ "skull", }, + "skeptical": []string{ + "unamused", + }, "sketch": []string{ "straight_ruler", "triangular_ruler", @@ -15088,10 +15636,16 @@ var keywordLookup = map[string][]string{ "weary", "zzz", }, + "sleigh": []string{ + "sled", + }, "slow": []string{ "snail", "turtle", }, + "smart": []string{ + "brain", + }, "smell": []string{ "no_smoking", "nose", @@ -15106,6 +15660,7 @@ var keywordLookup = map[string][]string{ "smiley", "smiley_cat", "smirk", + "star_struck", "stuck_out_tongue", "stuck_out_tongue_closed_eyes", "stuck_out_tongue_winking_eye", @@ -15139,6 +15694,9 @@ var keywordLookup = map[string][]string{ "lollipop", "popcorn", }, + "snare": []string{ + "drum", + }, "sneakers": []string{ "athletic_shoe", }, @@ -15155,6 +15713,9 @@ var keywordLookup = map[string][]string{ "so": []string{ "somalia", }, + "soda": []string{ + "cup_with_straw", + }, "software": []string{ "man_technologist", "woman_technologist", @@ -15165,6 +15726,9 @@ var keywordLookup = map[string][]string{ "solong": []string{ "wave", }, + "sorcerer": []string{ + "wizard", + }, "sorry": []string{ "broken_heart", }, @@ -15180,6 +15744,7 @@ var keywordLookup = map[string][]string{ "speaker", }, "soup": []string{ + "canned_food", "stew", }, "south": []string{ @@ -15197,6 +15762,10 @@ var keywordLookup = map[string][]string{ "southwest": []string{ "arrow_lower_left", }, + "spa": []string{ + "man_in_steamy_room", + "woman_in_steamy_room", + }, "space": []string{ "comet", "first_quarter_moon", @@ -15254,6 +15823,9 @@ var keywordLookup = map[string][]string{ "spin": []string{ "cyclone", }, + "spiny": []string{ + "hedgehog", + }, "spiral": []string{ "cyclone", }, @@ -15277,7 +15849,10 @@ var keywordLookup = map[string][]string{ "bow_and_arrow", "bowling", "boxing_glove", + "climbing_man", + "climbing_woman", "cricket", + "curling_stone", "field_hockey", "football", "goal_net", @@ -15317,6 +15892,9 @@ var keywordLookup = map[string][]string{ "woman_playing_water_polo", "women_wrestling", }, + "spots": []string{ + "giraffe", + }, "spring": []string{ "bird", "bouquet", @@ -15349,7 +15927,6 @@ var keywordLookup = map[string][]string{ "squirrel": []string{ "chestnut", "chipmunk", - "shipit", }, "sr": []string{ "suriname", @@ -15383,6 +15960,9 @@ var keywordLookup = map[string][]string{ "stare": []string{ "eye", }, + "starry": []string{ + "star_struck", + }, "stars": []string{ "milky_way", "sparkle", @@ -15425,6 +16005,10 @@ var keywordLookup = map[string][]string{ "chart_with_upwards_trend", "part_alternation_mark", }, + "steamroom": []string{ + "man_in_steamy_room", + "woman_in_steamy_room", + }, "steps": []string{ "walking_man", "walking_woman", @@ -15435,6 +16019,9 @@ var keywordLookup = map[string][]string{ "stiletto": []string{ "high_heel", }, + "stockings": []string{ + "socks", + }, "stone": []string{ "white_large_square", "white_medium_small_square", @@ -15449,6 +16036,9 @@ var keywordLookup = map[string][]string{ "straight face": []string{ "unamused", }, + "stripes": []string{ + "zebra", + }, "strong": []string{ "muscle", }, @@ -15471,6 +16061,9 @@ var keywordLookup = map[string][]string{ "stuffed": []string{ "stuffed_flatbread", }, + "stuffy": []string{ + "monocle", + }, "stunned": []string{ "anguished", }, @@ -15484,7 +16077,6 @@ var keywordLookup = map[string][]string{ "chart_with_upwards_trend", }, "suit": []string{ - "bowtie", "business_suit_levitating", }, "suits": []string{ @@ -15546,12 +16138,17 @@ var keywordLookup = map[string][]string{ "surgery": []string{ "hospital", }, + "surimi": []string{ + "fish_cake", + }, "surprise": []string{ "bangbang", "exclamation", "grey_exclamation", + "hand_over_mouth", "interrobang", "open_mouth", + "raised_eyebrow", }, "surprised": []string{ "astonished", @@ -15562,6 +16159,9 @@ var keywordLookup = map[string][]string{ "sustain": []string{ "battery", }, + "swearing": []string{ + "symbols_over_mouth", + }, "sweat": []string{ "cold_sweat", "disappointed_relieved", @@ -15584,6 +16184,7 @@ var keywordLookup = map[string][]string{ }, "swirl": []string{ "cyclone", + "fish_cake", }, "sword": []string{ "person_fencing", @@ -15635,6 +16236,9 @@ var keywordLookup = map[string][]string{ "film_projector", "loop", }, + "target": []string{ + "dart", + }, "td": []string{ "chad", }, @@ -15760,6 +16364,9 @@ var keywordLookup = map[string][]string{ "cloud_with_lightning", "zap", }, + "tichel": []string{ + "woman_with_headscarf", + }, "tick": []string{ "ballot_box_with_check", "heavy_check_mark", @@ -15830,6 +16437,7 @@ var keywordLookup = map[string][]string{ }, "toast": []string{ "bread", + "clinking_glasses", }, "tobacco": []string{ "smoking", @@ -15837,6 +16445,9 @@ var keywordLookup = map[string][]string{ "tobago": []string{ "trinidad_tobago", }, + "toboggan": []string{ + "sled", + }, "toddler": []string{ "baby", }, @@ -15876,6 +16487,9 @@ var keywordLookup = map[string][]string{ "arrow_up_small", "small_red_triangle", }, + "tornado": []string{ + "cyclone", + }, "tortoise": []string{ "turtle", }, @@ -15920,6 +16534,7 @@ var keywordLookup = map[string][]string{ "bus", "busstop", "fire_engine", + "flying_saucer", "helicopter", "light_rail", "metro", @@ -15977,6 +16592,9 @@ var keywordLookup = map[string][]string{ "tristan": []string{ "st_helena", }, + "triton": []string{ + "merman", + }, "trolley": []string{ "shopping_cart", }, @@ -16026,6 +16644,9 @@ var keywordLookup = map[string][]string{ "waxing_crescent_moon", "waxing_gibbous_moon", }, + "twisted": []string{ + "pretzel", + }, "twister": []string{ "tornado", }, @@ -16038,6 +16659,12 @@ var keywordLookup = map[string][]string{ "type": []string{ "keyboard", }, + "typhoon": []string{ + "cyclone", + }, + "tyrannosaurus": []string{ + "t-rex", + }, "ua": []string{ "ukraine", }, @@ -16045,6 +16672,9 @@ var keywordLookup = map[string][]string{ "oncoming_taxi", "taxi", }, + "ufo": []string{ + "flying_saucer", + }, "ug": []string{ "uganda", }, @@ -16056,6 +16686,10 @@ var keywordLookup = map[string][]string{ "unconscious": []string{ "dizzy_face", }, + "undead": []string{ + "man_zombie", + "woman_zombie", + }, "underground": []string{ "metro", }, @@ -16069,6 +16703,9 @@ var keywordLookup = map[string][]string{ "unicorn_face": []string{ "rainbow", }, + "unimpressed": []string{ + "unamused", + }, "union": []string{ "eu", }, @@ -16152,6 +16789,7 @@ var keywordLookup = map[string][]string{ "kissing_heart", "kissing_smiling_eyes", "love_letter", + "orange_heart", "purple_heart", "revolving_hearts", "ring", @@ -16181,6 +16819,7 @@ var keywordLookup = map[string][]string{ }, "vegetable": []string{ "bamboo", + "broccoli", "cactus", "carrot", "corn", @@ -16205,6 +16844,7 @@ var keywordLookup = map[string][]string{ "bullettrain_side", "bus", "fire_engine", + "flying_saucer", "helicopter", "kick_scooter", "light_rail", @@ -16307,6 +16947,10 @@ var keywordLookup = map[string][]string{ "running_man", "running_woman", }, + "walking dead": []string{ + "man_zombie", + "woman_zombie", + }, "wallis": []string{ "wallis_futuna", }, @@ -16361,6 +17005,9 @@ var keywordLookup = map[string][]string{ "restroom", "toilet", }, + "wealthy": []string{ + "monocle", + }, "weapon": []string{ "crossed_swords", "dagger", @@ -16412,6 +17059,9 @@ var keywordLookup = map[string][]string{ "alien", "confused", }, + "welsh": []string{ + "wales", + }, "western": []string{ "western_sahara", }, @@ -16433,6 +17083,9 @@ var keywordLookup = map[string][]string{ "whoa": []string{ "open_mouth", }, + "whoops": []string{ + "hand_over_mouth", + }, "wifi": []string{ "signal_strength", }, @@ -16449,6 +17102,7 @@ var keywordLookup = map[string][]string{ }, "wine": []string{ "champagne", + "clinking_glasses", "grapes", "sake", }, @@ -16464,7 +17118,9 @@ var keywordLookup = map[string][]string{ "medal_sports", }, "winter": []string{ + "gloves", "mountain_snow", + "scarf", "ski", "skier", "snowboarder", @@ -16482,6 +17138,9 @@ var keywordLookup = map[string][]string{ "wish": []string{ "pray", }, + "witch": []string{ + "sorceress", + }, "without_snow": []string{ "snowman", }, @@ -16492,6 +17151,7 @@ var keywordLookup = map[string][]string{ "blonde_woman", "bowing_woman", "bride_with_veil", + "climbing_woman", "construction_worker_woman", "dancer", "female_detective", @@ -16502,6 +17162,7 @@ var keywordLookup = map[string][]string{ "haircut_woman", "lipstick", "massage_woman", + "mermaid", "mountain_biking_woman", "mrs_claus", "no_good_woman", @@ -16512,6 +17173,7 @@ var keywordLookup = map[string][]string{ "raising_hand_woman", "rowing_woman", "running_woman", + "sorceress", "surfing_woman", "swimming_woman", "tipping_hand_woman", @@ -16520,11 +17182,16 @@ var keywordLookup = map[string][]string{ "woman_artist", "woman_astronaut", "woman_cook", + "woman_elf", "woman_facepalming", "woman_factory_worker", + "woman_fairy", "woman_farmer", "woman_firefighter", + "woman_genie", "woman_health_worker", + "woman_in_lotus_position", + "woman_in_steamy_room", "woman_judge", "woman_mechanic", "woman_office_worker", @@ -16535,7 +17202,9 @@ var keywordLookup = map[string][]string{ "woman_student", "woman_teacher", "woman_technologist", + "woman_vampire", "woman_with_turban", + "woman_zombie", "womens", }, "women": []string{ @@ -16600,6 +17269,10 @@ var keywordLookup = map[string][]string{ "grey_exclamation", "open_mouth", }, + "wrench": []string{ + "man_mechanic", + "woman_mechanic", + }, "wrestlers": []string{ "men_wrestling", "women_wrestling", @@ -16671,6 +17344,13 @@ var keywordLookup = map[string][]string{ "heavy_check_mark", "ok", }, + "yoga": []string{ + "man_in_lotus_position", + "woman_in_lotus_position", + }, + "young": []string{ + "child", + }, "yt": []string{ "mayotte", }, @@ -16680,6 +17360,10 @@ var keywordLookup = map[string][]string{ "zealand": []string{ "new_zealand", }, + "zen": []string{ + "man_in_lotus_position", + "woman_in_lotus_position", + }, "zipper": []string{ "zipper_mouth_face", }, @@ -16733,6 +17417,7 @@ var emojiReplacer = strings.NewReplacer( ":abc:", "🔤", ":abcd:", "🔡", ":accept:", "🉑", + ":adult:", "🧑", ":aerial_tramway:", "🚡", ":afghanistan:", "🇦🇫", ":airplane:", "✈️", @@ -16828,6 +17513,7 @@ var emojiReplacer = strings.NewReplacer( ":battery:", "🔋", ":beach_umbrella:", "🏖", ":bear:", "🐻", + ":bearded_person:", "🧔", ":bed:", "🛏", ":beer:", "🍺", ":beers:", "🍻", @@ -16846,6 +17532,7 @@ var emojiReplacer = strings.NewReplacer( ":biking_man:", "🚴", ":biking_woman:", "🚴‍♀️", ":bikini:", "👙", + ":billed_hat:", "🧢", ":biohazard:", "☣", ":bird:", "🐦", ":birthday:", "🎂", @@ -16881,17 +17568,20 @@ var emojiReplacer = strings.NewReplacer( ":bow_and_arrow:", "🏹", ":bowing_man:", "🙇", ":bowing_woman:", "🙇‍♀️", + ":bowl_with_spoon:", "🥣", ":bowling:", "🎳", - ":bowtie:", "", ":boxing_glove:", "🥊", ":boy:", "👦", + ":brain:", "🧠", ":brazil:", "🇧🇷", ":bread:", "🍞", + ":breastfeeding:", "🤱", ":bride_with_veil:", "👰", ":bridge_at_night:", "🌉", ":briefcase:", "💼", ":british_indian_ocean_territory:", "🇮🇴", ":british_virgin_islands:", "🇻🇬", + ":broccoli:", "🥦", ":broken_heart:", "💔", ":brunei:", "🇧🇳", ":bug:", "🐛", @@ -16925,6 +17615,7 @@ var emojiReplacer = strings.NewReplacer( ":cancer:", "♋", ":candle:", "🕯", ":candy:", "🍬", + ":canned_food:", "🥫", ":canoe:", "🛶", ":cape_verde:", "🇨🇻", ":capital_abcd:", "🔠", @@ -16952,10 +17643,12 @@ var emojiReplacer = strings.NewReplacer( ":cherry_blossom:", "🌸", ":chestnut:", "🌰", ":chicken:", "🐔", + ":child:", "🧒", ":children_crossing:", "🚸", ":chile:", "🇨🇱", ":chipmunk:", "🐿", ":chocolate_bar:", "🍫", + ":chopsticks:", "🥢", ":christmas_island:", "🇨🇽", ":christmas_tree:", "🎄", ":church:", "⛪", @@ -16969,6 +17662,8 @@ var emojiReplacer = strings.NewReplacer( ":clap:", "👏", ":clapper:", "🎬", ":classical_building:", "🏛", + ":climbing_man:", "🧗‍♂️", + ":climbing_woman:", "🧗‍♀️", ":clinking_glasses:", "🥂", ":clipboard:", "📋", ":clock1:", "🕐", @@ -17006,7 +17701,9 @@ var emojiReplacer = strings.NewReplacer( ":clown_face:", "🤡", ":clubs:", "♣️", ":cn:", "🇨🇳", + ":coat:", "🧥", ":cocktail:", "🍸", + ":coconut:", "🥥", ":cocos_islands:", "🇨🇨", ":coffee:", "☕", ":coffin:", "⚰", @@ -17062,8 +17759,10 @@ var emojiReplacer = strings.NewReplacer( ":crystal_ball:", "🔮", ":cuba:", "🇨🇺", ":cucumber:", "🥒", + ":cup_with_straw:", "🥤", ":cupid:", "💘", ":curacao:", "🇨🇼", + ":curling_stone:", "🥌", ":curly_loop:", "➰", ":currency_exchange:", "💱", ":curry:", "🍛", @@ -17116,6 +17815,7 @@ var emojiReplacer = strings.NewReplacer( ":droplet:", "💧", ":drum:", "🥁", ":duck:", "🦆", + ":dumpling:", "🥟", ":dvd:", "📀", ":e-mail:", "📧", ":eagle:", "🦅", @@ -17131,11 +17831,13 @@ var emojiReplacer = strings.NewReplacer( ":eight:", "8️⃣", ":eight_pointed_black_star:", "✴️", ":eight_spoked_asterisk:", "✳️", + ":eject_button:", "⏏️", ":el_salvador:", "🇸🇻", ":electric_plug:", "🔌", ":elephant:", "🐘", ":email:", "✉️", ":end:", "🔚", + ":england:", "🏴󠁧󠁢󠁥󠁮󠁧󠁿", ":envelope_with_arrow:", "📩", ":equatorial_guinea:", "🇬🇶", ":eritrea:", "🇪🇷", @@ -17148,6 +17850,7 @@ var emojiReplacer = strings.NewReplacer( ":european_post_office:", "🏤", ":evergreen_tree:", "🌲", ":exclamation:", "❗", + ":exploding_head:", "🤯", ":expressionless:", "😑", ":eye:", "👁", ":eyeglasses:", "👓", @@ -17187,7 +17890,6 @@ var emojiReplacer = strings.NewReplacer( ":fast_forward:", "⏩", ":fax:", "📠", ":fearful:", "😨", - ":feelsgood:", "", ":female_detective:", "🕵️‍♀️", ":ferris_wheel:", "🎡", ":ferry:", "⛴", @@ -17198,7 +17900,6 @@ var emojiReplacer = strings.NewReplacer( ":film_projector:", "📽", ":film_strip:", "🎞", ":finland:", "🇫🇮", - ":finnadie:", "", ":fire:", "🔥", ":fire_engine:", "🚒", ":fireworks:", "🎆", @@ -17219,11 +17920,13 @@ var emojiReplacer = strings.NewReplacer( ":floppy_disk:", "💾", ":flower_playing_cards:", "🎴", ":flushed:", "😳", + ":flying_saucer:", "🛸", ":fog:", "🌫", ":foggy:", "🌁", ":football:", "🏈", ":footprints:", "👣", ":fork_and_knife:", "🍴", + ":fortune_cookie:", "🥠", ":fountain:", "⛲", ":fountain_pen:", "🖋", ":four:", "4️⃣", @@ -17260,17 +17963,18 @@ var emojiReplacer = strings.NewReplacer( ":gibraltar:", "🇬🇮", ":gift:", "🎁", ":gift_heart:", "💝", + ":giraffe:", "🦒", ":girl:", "👧", ":globe_with_meridians:", "🌐", + ":gloves:", "🧤", ":goal_net:", "🥅", ":goat:", "🐐", - ":goberserk:", "", - ":godmode:", "", ":golf:", "⛳", ":golfing_man:", "🏌", ":golfing_woman:", "🏌️‍♀️", ":gorilla:", "🦍", ":grapes:", "🍇", + ":grasshopper:", "🦗", ":greece:", "🇬🇷", ":green_apple:", "🍏", ":green_book:", "📗", @@ -17302,6 +18006,7 @@ var emojiReplacer = strings.NewReplacer( ":hammer_and_pick:", "⚒", ":hammer_and_wrench:", "🛠", ":hamster:", "🐹", + ":hand_over_mouth:", "🤭", ":handbag:", "👜", ":handshake:", "🤝", ":hash:", "#️⃣", @@ -17323,6 +18028,7 @@ var emojiReplacer = strings.NewReplacer( ":heavy_minus_sign:", "➖", ":heavy_multiplication_x:", "✖️", ":heavy_plus_sign:", "➕", + ":hedgehog:", "🦔", ":helicopter:", "🚁", ":herb:", "🌿", ":hibiscus:", "🌺", @@ -17348,7 +18054,6 @@ var emojiReplacer = strings.NewReplacer( ":houses:", "🏘", ":hugs:", "🤗", ":hungary:", "🇭🇺", - ":hurtrealbad:", "", ":hushed:", "😯", ":ice_cream:", "🍨", ":ice_hockey:", "🏒", @@ -17449,6 +18154,7 @@ var emojiReplacer = strings.NewReplacer( ":loudspeaker:", "📢", ":love_hotel:", "🏩", ":love_letter:", "💌", + ":love_you:", "🤟", ":low_brightness:", "🔅", ":luxembourg:", "🇱🇺", ":lying_face:", "🤥", @@ -17475,11 +18181,16 @@ var emojiReplacer = strings.NewReplacer( ":man_cartwheeling:", "🤸‍♂️", ":man_cook:", "👨‍🍳", ":man_dancing:", "🕺", + ":man_elf:", "🧝‍♂️", ":man_facepalming:", "🤦", ":man_factory_worker:", "👨‍🏭", + ":man_fairy:", "🧚‍♂️", ":man_farmer:", "👨‍🌾", ":man_firefighter:", "👨‍🚒", + ":man_genie:", "🧞‍♂️", ":man_health_worker:", "👨‍⚕️", + ":man_in_lotus_position:", "🧘‍♂️", + ":man_in_steamy_room:", "🧖‍♂️", ":man_in_tuxedo:", "🤵", ":man_judge:", "👨‍⚖️", ":man_juggling:", "🤹‍♂️", @@ -17494,8 +18205,10 @@ var emojiReplacer = strings.NewReplacer( ":man_student:", "👨‍🎓", ":man_teacher:", "👨‍🏫", ":man_technologist:", "👨‍💻", + ":man_vampire:", "🧛‍♂️", ":man_with_gua_pi_mao:", "👲", ":man_with_turban:", "👳", + ":man_zombie:", "🧟‍♂️", ":mans_shoe:", "👞", ":mantelpiece_clock:", "🕰", ":maple_leaf:", "🍁", @@ -17517,6 +18230,8 @@ var emojiReplacer = strings.NewReplacer( ":men_wrestling:", "🤼‍♂️", ":menorah:", "🕎", ":mens:", "🚹", + ":mermaid:", "🧜‍♀️", + ":merman:", "🧜‍♂️", ":metal:", "🤘", ":metro:", "🚇", ":mexico:", "🇲🇽", @@ -17536,6 +18251,7 @@ var emojiReplacer = strings.NewReplacer( ":mongolia:", "🇲🇳", ":monkey:", "🐒", ":monkey_face:", "🐵", + ":monocle:", "🧐", ":monorail:", "🚝", ":montenegro:", "🇲🇪", ":montserrat:", "🇲🇸", @@ -17572,7 +18288,6 @@ var emojiReplacer = strings.NewReplacer( ":national_park:", "🏞", ":nauru:", "🇳🇷", ":nauseated_face:", "🤢", - ":neckbeard:", "", ":necktie:", "👔", ":negative_squared_cross_mark:", "❎", ":nepal:", "🇳🇵", @@ -17617,7 +18332,6 @@ var emojiReplacer = strings.NewReplacer( ":o:", "⭕", ":o2:", "🅾️", ":ocean:", "🌊", - ":octocat:", "", ":octopus:", "🐙", ":oden:", "🍢", ":office:", "🏢", @@ -17627,6 +18341,7 @@ var emojiReplacer = strings.NewReplacer( ":ok_man:", "🙆‍♂️", ":ok_woman:", "🙆", ":old_key:", "🗝", + ":older_adult:", "🧓", ":older_man:", "👴", ":older_woman:", "👵", ":om:", "🕉", @@ -17644,6 +18359,7 @@ var emojiReplacer = strings.NewReplacer( ":open_umbrella:", "☂", ":ophiuchus:", "⛎", ":orange_book:", "📙", + ":orange_heart:", "🧡", ":orthodox_cross:", "☦", ":outbox_tray:", "📤", ":owl:", "🦉", @@ -17657,6 +18373,7 @@ var emojiReplacer = strings.NewReplacer( ":palau:", "🇵🇼", ":palestinian_territories:", "🇵🇸", ":palm_tree:", "🌴", + ":palms_up:", "🤲", ":panama:", "🇵🇦", ":pancakes:", "🥞", ":panda_face:", "🐼", @@ -17687,6 +18404,7 @@ var emojiReplacer = strings.NewReplacer( ":philippines:", "🇵🇭", ":phone:", "☎️", ":pick:", "⛏", + ":pie:", "🥧", ":pig:", "🐷", ":pig2:", "🐖", ":pig_nose:", "🐽", @@ -17726,6 +18444,7 @@ var emojiReplacer = strings.NewReplacer( ":pray:", "🙏", ":prayer_beads:", "📿", ":pregnant_woman:", "🤰", + ":pretzel:", "🥨", ":previous_track_button:", "⏮", ":prince:", "🤴", ":princess:", "👸", @@ -17745,15 +18464,12 @@ var emojiReplacer = strings.NewReplacer( ":radio_button:", "🔘", ":radioactive:", "☢", ":rage:", "😡", - ":rage1:", "", - ":rage2:", "", - ":rage3:", "", - ":rage4:", "", ":railway_car:", "🚃", ":railway_track:", "🛤", ":rainbow:", "🌈", ":rainbow_flag:", "🏳️‍🌈", ":raised_back_of_hand:", "🤚", + ":raised_eyebrow:", "🤨", ":raised_hand:", "✋", ":raised_hand_with_fingers_splayed:", "🖐", ":raised_hands:", "🙌", @@ -17811,16 +18527,20 @@ var emojiReplacer = strings.NewReplacer( ":samoa:", "🇼🇸", ":san_marino:", "🇸🇲", ":sandal:", "👡", + ":sandwich:", "🥪", ":santa:", "🎅", ":sao_tome_principe:", "🇸🇹", ":satellite:", "📡", ":saudi_arabia:", "🇸🇦", + ":sauropod:", "🦕", ":saxophone:", "🎷", + ":scarf:", "🧣", ":school:", "🏫", ":school_satchel:", "🎒", ":scissors:", "✂️", ":scorpion:", "🦂", ":scorpius:", "♏", + ":scotland:", "🏴󠁧󠁢󠁳󠁣󠁴󠁿", ":scream:", "😱", ":scream_cat:", "🙀", ":scroll:", "📜", @@ -17842,11 +18562,11 @@ var emojiReplacer = strings.NewReplacer( ":shield:", "🛡", ":shinto_shrine:", "⛩", ":ship:", "🚢", - ":shipit:", "", ":shopping:", "🛍", ":shopping_cart:", "🛒", ":shower:", "🚿", ":shrimp:", "🦐", + ":shushing:", "🤫", ":sierra_leone:", "🇸🇱", ":signal_strength:", "📶", ":singapore:", "🇸🇬", @@ -17857,6 +18577,7 @@ var emojiReplacer = strings.NewReplacer( ":skier:", "⛷", ":skull:", "💀", ":skull_and_crossbones:", "☠", + ":sled:", "🛷", ":sleeping:", "😴", ":sleeping_bed:", "🛌", ":sleepy:", "😪", @@ -17887,9 +18608,11 @@ var emojiReplacer = strings.NewReplacer( ":snowman_with_snow:", "☃", ":sob:", "😭", ":soccer:", "⚽", + ":socks:", "🧦", ":solomon_islands:", "🇸🇧", ":somalia:", "🇸🇴", ":soon:", "🔜", + ":sorceress:", "🧙‍♀️", ":sos:", "🆘", ":sound:", "🔉", ":south_africa:", "🇿🇦", @@ -17925,9 +18648,11 @@ var emojiReplacer = strings.NewReplacer( ":star2:", "🌟", ":star_and_crescent:", "☪", ":star_of_david:", "✡", + ":star_struck:", "🤩", ":stars:", "🌠", ":station:", "🚉", ":statue_of_liberty:", "🗽", + ":steak:", "🥩", ":steam_locomotive:", "🚂", ":stew:", "🍲", ":stop_button:", "⏹", @@ -17954,7 +18679,6 @@ var emojiReplacer = strings.NewReplacer( ":surfing_woman:", "🏄‍♀️", ":suriname:", "🇸🇷", ":sushi:", "🍣", - ":suspect:", "", ":suspension_railway:", "🚟", ":swaziland:", "🇸🇿", ":sweat:", "😓", @@ -17966,13 +18690,16 @@ var emojiReplacer = strings.NewReplacer( ":swimming_woman:", "🏊‍♀️", ":switzerland:", "🇨🇭", ":symbols:", "🔣", + ":symbols_over_mouth:", "🤬", ":synagogue:", "🕍", ":syria:", "🇸🇾", ":syringe:", "💉", + ":t-rex:", "🦖", ":taco:", "🌮", ":tada:", "🎉", ":taiwan:", "🇹🇼", ":tajikistan:", "🇹🇯", + ":takeout_box:", "🥡", ":tanabata_tree:", "🎋", ":tangerine:", "🍊", ":tanzania:", "🇹🇿", @@ -18021,7 +18748,6 @@ var emojiReplacer = strings.NewReplacer( ":trinidad_tobago:", "🇹🇹", ":triumph:", "😤", ":trolleybus:", "🚎", - ":trollface:", "", ":trophy:", "🏆", ":tropical_drink:", "🍹", ":tropical_fish:", "🐠", @@ -18082,8 +18808,10 @@ var emojiReplacer = strings.NewReplacer( ":virgo:", "♍", ":volcano:", "🌋", ":volleyball:", "🏐", + ":vomiting:", "🤮", ":vs:", "🆚", ":vulcan_salute:", "🖖", + ":wales:", "🏴󠁧󠁢󠁷󠁬󠁳󠁿", ":walking_man:", "🚶", ":walking_woman:", "🚶‍♀️", ":wallis_futuna:", "🇼🇫", @@ -18122,17 +18850,23 @@ var emojiReplacer = strings.NewReplacer( ":wind_face:", "🌬", ":wine_glass:", "🍷", ":wink:", "😉", + ":wizard:", "🧙‍♂️", ":wolf:", "🐺", ":woman:", "👩", ":woman_artist:", "👩‍🎨", ":woman_astronaut:", "👩‍🚀", ":woman_cartwheeling:", "🤸‍♀️", ":woman_cook:", "👩‍🍳", + ":woman_elf:", "🧝‍♀️", ":woman_facepalming:", "🤦‍♀️", ":woman_factory_worker:", "👩‍🏭", + ":woman_fairy:", "🧚‍♀️", ":woman_farmer:", "👩‍🌾", ":woman_firefighter:", "👩‍🚒", + ":woman_genie:", "🧞‍♀️", ":woman_health_worker:", "👩‍⚕️", + ":woman_in_lotus_position:", "🧘‍♀️", + ":woman_in_steamy_room:", "🧖‍♀️", ":woman_judge:", "👩‍⚖️", ":woman_juggling:", "🤹‍♀️", ":woman_mechanic:", "👩‍🔧", @@ -18146,7 +18880,10 @@ var emojiReplacer = strings.NewReplacer( ":woman_student:", "👩‍🎓", ":woman_teacher:", "👩‍🏫", ":woman_technologist:", "👩‍💻", + ":woman_vampire:", "🧛‍♀️", + ":woman_with_headscarf:", "🧕", ":woman_with_turban:", "👳‍♀️", + ":woman_zombie:", "🧟‍♀️", ":womans_clothes:", "👚", ":womans_hat:", "👒", ":women_wrestling:", "🤼‍♀️", @@ -18162,7 +18899,9 @@ var emojiReplacer = strings.NewReplacer( ":yin_yang:", "☯", ":yum:", "😋", ":zambia:", "🇿🇲", + ":zany:", "🤪", ":zap:", "⚡", + ":zebra:", "🦓", ":zero:", "0️⃣", ":zimbabwe:", "🇿🇼", ":zipper_mouth_face:", "🤐", @@ -18183,6 +18922,7 @@ var emojiPaddedReplacer = strings.NewReplacer( ":abc:", "🔤 ", ":abcd:", "🔡 ", ":accept:", "🉑 ", + ":adult:", "🧑 ", ":aerial_tramway:", "🚡 ", ":afghanistan:", "🇦🇫 ", ":airplane:", "✈️ ", @@ -18278,6 +19018,7 @@ var emojiPaddedReplacer = strings.NewReplacer( ":battery:", "🔋 ", ":beach_umbrella:", "🏖 ", ":bear:", "🐻 ", + ":bearded_person:", "🧔 ", ":bed:", "🛏 ", ":beer:", "🍺 ", ":beers:", "🍻 ", @@ -18296,6 +19037,7 @@ var emojiPaddedReplacer = strings.NewReplacer( ":biking_man:", "🚴 ", ":biking_woman:", "🚴‍♀️ ", ":bikini:", "👙 ", + ":billed_hat:", "🧢 ", ":biohazard:", "☣ ", ":bird:", "🐦 ", ":birthday:", "🎂 ", @@ -18331,17 +19073,20 @@ var emojiPaddedReplacer = strings.NewReplacer( ":bow_and_arrow:", "🏹 ", ":bowing_man:", "🙇 ", ":bowing_woman:", "🙇‍♀️ ", + ":bowl_with_spoon:", "🥣 ", ":bowling:", "🎳 ", - ":bowtie:", " ", ":boxing_glove:", "🥊 ", ":boy:", "👦 ", + ":brain:", "🧠 ", ":brazil:", "🇧🇷 ", ":bread:", "🍞 ", + ":breastfeeding:", "🤱 ", ":bride_with_veil:", "👰 ", ":bridge_at_night:", "🌉 ", ":briefcase:", "💼 ", ":british_indian_ocean_territory:", "🇮🇴 ", ":british_virgin_islands:", "🇻🇬 ", + ":broccoli:", "🥦 ", ":broken_heart:", "💔 ", ":brunei:", "🇧🇳 ", ":bug:", "🐛 ", @@ -18375,6 +19120,7 @@ var emojiPaddedReplacer = strings.NewReplacer( ":cancer:", "♋ ", ":candle:", "🕯 ", ":candy:", "🍬 ", + ":canned_food:", "🥫 ", ":canoe:", "🛶 ", ":cape_verde:", "🇨🇻 ", ":capital_abcd:", "🔠 ", @@ -18402,10 +19148,12 @@ var emojiPaddedReplacer = strings.NewReplacer( ":cherry_blossom:", "🌸 ", ":chestnut:", "🌰 ", ":chicken:", "🐔 ", + ":child:", "🧒 ", ":children_crossing:", "🚸 ", ":chile:", "🇨🇱 ", ":chipmunk:", "🐿 ", ":chocolate_bar:", "🍫 ", + ":chopsticks:", "🥢 ", ":christmas_island:", "🇨🇽 ", ":christmas_tree:", "🎄 ", ":church:", "⛪ ", @@ -18419,6 +19167,8 @@ var emojiPaddedReplacer = strings.NewReplacer( ":clap:", "👏 ", ":clapper:", "🎬 ", ":classical_building:", "🏛 ", + ":climbing_man:", "🧗‍♂️ ", + ":climbing_woman:", "🧗‍♀️ ", ":clinking_glasses:", "🥂 ", ":clipboard:", "📋 ", ":clock1:", "🕐 ", @@ -18456,7 +19206,9 @@ var emojiPaddedReplacer = strings.NewReplacer( ":clown_face:", "🤡 ", ":clubs:", "♣️ ", ":cn:", "🇨🇳 ", + ":coat:", "🧥 ", ":cocktail:", "🍸 ", + ":coconut:", "🥥 ", ":cocos_islands:", "🇨🇨 ", ":coffee:", "☕ ", ":coffin:", "⚰ ", @@ -18512,8 +19264,10 @@ var emojiPaddedReplacer = strings.NewReplacer( ":crystal_ball:", "🔮 ", ":cuba:", "🇨🇺 ", ":cucumber:", "🥒 ", + ":cup_with_straw:", "🥤 ", ":cupid:", "💘 ", ":curacao:", "🇨🇼 ", + ":curling_stone:", "🥌 ", ":curly_loop:", "➰ ", ":currency_exchange:", "💱 ", ":curry:", "🍛 ", @@ -18566,6 +19320,7 @@ var emojiPaddedReplacer = strings.NewReplacer( ":droplet:", "💧 ", ":drum:", "🥁 ", ":duck:", "🦆 ", + ":dumpling:", "🥟 ", ":dvd:", "📀 ", ":e-mail:", "📧 ", ":eagle:", "🦅 ", @@ -18581,11 +19336,13 @@ var emojiPaddedReplacer = strings.NewReplacer( ":eight:", "8️⃣ ", ":eight_pointed_black_star:", "✴️ ", ":eight_spoked_asterisk:", "✳️ ", + ":eject_button:", "⏏️ ", ":el_salvador:", "🇸🇻 ", ":electric_plug:", "🔌 ", ":elephant:", "🐘 ", ":email:", "✉️ ", ":end:", "🔚 ", + ":england:", "🏴󠁧󠁢󠁥󠁮󠁧󠁿 ", ":envelope_with_arrow:", "📩 ", ":equatorial_guinea:", "🇬🇶 ", ":eritrea:", "🇪🇷 ", @@ -18598,6 +19355,7 @@ var emojiPaddedReplacer = strings.NewReplacer( ":european_post_office:", "🏤 ", ":evergreen_tree:", "🌲 ", ":exclamation:", "❗ ", + ":exploding_head:", "🤯 ", ":expressionless:", "😑 ", ":eye:", "👁 ", ":eyeglasses:", "👓 ", @@ -18637,7 +19395,6 @@ var emojiPaddedReplacer = strings.NewReplacer( ":fast_forward:", "⏩ ", ":fax:", "📠 ", ":fearful:", "😨 ", - ":feelsgood:", " ", ":female_detective:", "🕵️‍♀️ ", ":ferris_wheel:", "🎡 ", ":ferry:", "⛴ ", @@ -18648,7 +19405,6 @@ var emojiPaddedReplacer = strings.NewReplacer( ":film_projector:", "📽 ", ":film_strip:", "🎞 ", ":finland:", "🇫🇮 ", - ":finnadie:", " ", ":fire:", "🔥 ", ":fire_engine:", "🚒 ", ":fireworks:", "🎆 ", @@ -18669,11 +19425,13 @@ var emojiPaddedReplacer = strings.NewReplacer( ":floppy_disk:", "💾 ", ":flower_playing_cards:", "🎴 ", ":flushed:", "😳 ", + ":flying_saucer:", "🛸 ", ":fog:", "🌫 ", ":foggy:", "🌁 ", ":football:", "🏈 ", ":footprints:", "👣 ", ":fork_and_knife:", "🍴 ", + ":fortune_cookie:", "🥠 ", ":fountain:", "⛲ ", ":fountain_pen:", "🖋 ", ":four:", "4️⃣ ", @@ -18710,17 +19468,18 @@ var emojiPaddedReplacer = strings.NewReplacer( ":gibraltar:", "🇬🇮 ", ":gift:", "🎁 ", ":gift_heart:", "💝 ", + ":giraffe:", "🦒 ", ":girl:", "👧 ", ":globe_with_meridians:", "🌐 ", + ":gloves:", "🧤 ", ":goal_net:", "🥅 ", ":goat:", "🐐 ", - ":goberserk:", " ", - ":godmode:", " ", ":golf:", "⛳ ", ":golfing_man:", "🏌 ", ":golfing_woman:", "🏌️‍♀️ ", ":gorilla:", "🦍 ", ":grapes:", "🍇 ", + ":grasshopper:", "🦗 ", ":greece:", "🇬🇷 ", ":green_apple:", "🍏 ", ":green_book:", "📗 ", @@ -18752,6 +19511,7 @@ var emojiPaddedReplacer = strings.NewReplacer( ":hammer_and_pick:", "⚒ ", ":hammer_and_wrench:", "🛠 ", ":hamster:", "🐹 ", + ":hand_over_mouth:", "🤭 ", ":handbag:", "👜 ", ":handshake:", "🤝 ", ":hash:", "#️⃣ ", @@ -18773,6 +19533,7 @@ var emojiPaddedReplacer = strings.NewReplacer( ":heavy_minus_sign:", "➖ ", ":heavy_multiplication_x:", "✖️ ", ":heavy_plus_sign:", "➕ ", + ":hedgehog:", "🦔 ", ":helicopter:", "🚁 ", ":herb:", "🌿 ", ":hibiscus:", "🌺 ", @@ -18798,7 +19559,6 @@ var emojiPaddedReplacer = strings.NewReplacer( ":houses:", "🏘 ", ":hugs:", "🤗 ", ":hungary:", "🇭🇺 ", - ":hurtrealbad:", " ", ":hushed:", "😯 ", ":ice_cream:", "🍨 ", ":ice_hockey:", "🏒 ", @@ -18899,6 +19659,7 @@ var emojiPaddedReplacer = strings.NewReplacer( ":loudspeaker:", "📢 ", ":love_hotel:", "🏩 ", ":love_letter:", "💌 ", + ":love_you:", "🤟 ", ":low_brightness:", "🔅 ", ":luxembourg:", "🇱🇺 ", ":lying_face:", "🤥 ", @@ -18925,11 +19686,16 @@ var emojiPaddedReplacer = strings.NewReplacer( ":man_cartwheeling:", "🤸‍♂️ ", ":man_cook:", "👨‍🍳 ", ":man_dancing:", "🕺 ", + ":man_elf:", "🧝‍♂️ ", ":man_facepalming:", "🤦 ", ":man_factory_worker:", "👨‍🏭 ", + ":man_fairy:", "🧚‍♂️ ", ":man_farmer:", "👨‍🌾 ", ":man_firefighter:", "👨‍🚒 ", + ":man_genie:", "🧞‍♂️ ", ":man_health_worker:", "👨‍⚕️ ", + ":man_in_lotus_position:", "🧘‍♂️ ", + ":man_in_steamy_room:", "🧖‍♂️ ", ":man_in_tuxedo:", "🤵 ", ":man_judge:", "👨‍⚖️ ", ":man_juggling:", "🤹‍♂️ ", @@ -18944,8 +19710,10 @@ var emojiPaddedReplacer = strings.NewReplacer( ":man_student:", "👨‍🎓 ", ":man_teacher:", "👨‍🏫 ", ":man_technologist:", "👨‍💻 ", + ":man_vampire:", "🧛‍♂️ ", ":man_with_gua_pi_mao:", "👲 ", ":man_with_turban:", "👳 ", + ":man_zombie:", "🧟‍♂️ ", ":mans_shoe:", "👞 ", ":mantelpiece_clock:", "🕰 ", ":maple_leaf:", "🍁 ", @@ -18967,6 +19735,8 @@ var emojiPaddedReplacer = strings.NewReplacer( ":men_wrestling:", "🤼‍♂️ ", ":menorah:", "🕎 ", ":mens:", "🚹 ", + ":mermaid:", "🧜‍♀️ ", + ":merman:", "🧜‍♂️ ", ":metal:", "🤘 ", ":metro:", "🚇 ", ":mexico:", "🇲🇽 ", @@ -18986,6 +19756,7 @@ var emojiPaddedReplacer = strings.NewReplacer( ":mongolia:", "🇲🇳 ", ":monkey:", "🐒 ", ":monkey_face:", "🐵 ", + ":monocle:", "🧐 ", ":monorail:", "🚝 ", ":montenegro:", "🇲🇪 ", ":montserrat:", "🇲🇸 ", @@ -19022,7 +19793,6 @@ var emojiPaddedReplacer = strings.NewReplacer( ":national_park:", "🏞 ", ":nauru:", "🇳🇷 ", ":nauseated_face:", "🤢 ", - ":neckbeard:", " ", ":necktie:", "👔 ", ":negative_squared_cross_mark:", "❎ ", ":nepal:", "🇳🇵 ", @@ -19067,7 +19837,6 @@ var emojiPaddedReplacer = strings.NewReplacer( ":o:", "⭕ ", ":o2:", "🅾️ ", ":ocean:", "🌊 ", - ":octocat:", " ", ":octopus:", "🐙 ", ":oden:", "🍢 ", ":office:", "🏢 ", @@ -19077,6 +19846,7 @@ var emojiPaddedReplacer = strings.NewReplacer( ":ok_man:", "🙆‍♂️ ", ":ok_woman:", "🙆 ", ":old_key:", "🗝 ", + ":older_adult:", "🧓 ", ":older_man:", "👴 ", ":older_woman:", "👵 ", ":om:", "🕉 ", @@ -19094,6 +19864,7 @@ var emojiPaddedReplacer = strings.NewReplacer( ":open_umbrella:", "☂ ", ":ophiuchus:", "⛎ ", ":orange_book:", "📙 ", + ":orange_heart:", "🧡 ", ":orthodox_cross:", "☦ ", ":outbox_tray:", "📤 ", ":owl:", "🦉 ", @@ -19107,6 +19878,7 @@ var emojiPaddedReplacer = strings.NewReplacer( ":palau:", "🇵🇼 ", ":palestinian_territories:", "🇵🇸 ", ":palm_tree:", "🌴 ", + ":palms_up:", "🤲 ", ":panama:", "🇵🇦 ", ":pancakes:", "🥞 ", ":panda_face:", "🐼 ", @@ -19137,6 +19909,7 @@ var emojiPaddedReplacer = strings.NewReplacer( ":philippines:", "🇵🇭 ", ":phone:", "☎️ ", ":pick:", "⛏ ", + ":pie:", "🥧 ", ":pig:", "🐷 ", ":pig2:", "🐖 ", ":pig_nose:", "🐽 ", @@ -19176,6 +19949,7 @@ var emojiPaddedReplacer = strings.NewReplacer( ":pray:", "🙏 ", ":prayer_beads:", "📿 ", ":pregnant_woman:", "🤰 ", + ":pretzel:", "🥨 ", ":previous_track_button:", "⏮ ", ":prince:", "🤴 ", ":princess:", "👸 ", @@ -19195,15 +19969,12 @@ var emojiPaddedReplacer = strings.NewReplacer( ":radio_button:", "🔘 ", ":radioactive:", "☢ ", ":rage:", "😡 ", - ":rage1:", " ", - ":rage2:", " ", - ":rage3:", " ", - ":rage4:", " ", ":railway_car:", "🚃 ", ":railway_track:", "🛤 ", ":rainbow:", "🌈 ", ":rainbow_flag:", "🏳️‍🌈 ", ":raised_back_of_hand:", "🤚 ", + ":raised_eyebrow:", "🤨 ", ":raised_hand:", "✋ ", ":raised_hand_with_fingers_splayed:", "🖐 ", ":raised_hands:", "🙌 ", @@ -19261,16 +20032,20 @@ var emojiPaddedReplacer = strings.NewReplacer( ":samoa:", "🇼🇸 ", ":san_marino:", "🇸🇲 ", ":sandal:", "👡 ", + ":sandwich:", "🥪 ", ":santa:", "🎅 ", ":sao_tome_principe:", "🇸🇹 ", ":satellite:", "📡 ", ":saudi_arabia:", "🇸🇦 ", + ":sauropod:", "🦕 ", ":saxophone:", "🎷 ", + ":scarf:", "🧣 ", ":school:", "🏫 ", ":school_satchel:", "🎒 ", ":scissors:", "✂️ ", ":scorpion:", "🦂 ", ":scorpius:", "♏ ", + ":scotland:", "🏴󠁧󠁢󠁳󠁣󠁴󠁿 ", ":scream:", "😱 ", ":scream_cat:", "🙀 ", ":scroll:", "📜 ", @@ -19292,11 +20067,11 @@ var emojiPaddedReplacer = strings.NewReplacer( ":shield:", "🛡 ", ":shinto_shrine:", "⛩ ", ":ship:", "🚢 ", - ":shipit:", " ", ":shopping:", "🛍 ", ":shopping_cart:", "🛒 ", ":shower:", "🚿 ", ":shrimp:", "🦐 ", + ":shushing:", "🤫 ", ":sierra_leone:", "🇸🇱 ", ":signal_strength:", "📶 ", ":singapore:", "🇸🇬 ", @@ -19307,6 +20082,7 @@ var emojiPaddedReplacer = strings.NewReplacer( ":skier:", "⛷ ", ":skull:", "💀 ", ":skull_and_crossbones:", "☠ ", + ":sled:", "🛷 ", ":sleeping:", "😴 ", ":sleeping_bed:", "🛌 ", ":sleepy:", "😪 ", @@ -19337,9 +20113,11 @@ var emojiPaddedReplacer = strings.NewReplacer( ":snowman_with_snow:", "☃ ", ":sob:", "😭 ", ":soccer:", "⚽ ", + ":socks:", "🧦 ", ":solomon_islands:", "🇸🇧 ", ":somalia:", "🇸🇴 ", ":soon:", "🔜 ", + ":sorceress:", "🧙‍♀️ ", ":sos:", "🆘 ", ":sound:", "🔉 ", ":south_africa:", "🇿🇦 ", @@ -19375,9 +20153,11 @@ var emojiPaddedReplacer = strings.NewReplacer( ":star2:", "🌟 ", ":star_and_crescent:", "☪ ", ":star_of_david:", "✡ ", + ":star_struck:", "🤩 ", ":stars:", "🌠 ", ":station:", "🚉 ", ":statue_of_liberty:", "🗽 ", + ":steak:", "🥩 ", ":steam_locomotive:", "🚂 ", ":stew:", "🍲 ", ":stop_button:", "⏹ ", @@ -19404,7 +20184,6 @@ var emojiPaddedReplacer = strings.NewReplacer( ":surfing_woman:", "🏄‍♀️ ", ":suriname:", "🇸🇷 ", ":sushi:", "🍣 ", - ":suspect:", " ", ":suspension_railway:", "🚟 ", ":swaziland:", "🇸🇿 ", ":sweat:", "😓 ", @@ -19416,13 +20195,16 @@ var emojiPaddedReplacer = strings.NewReplacer( ":swimming_woman:", "🏊‍♀️ ", ":switzerland:", "🇨🇭 ", ":symbols:", "🔣 ", + ":symbols_over_mouth:", "🤬 ", ":synagogue:", "🕍 ", ":syria:", "🇸🇾 ", ":syringe:", "💉 ", + ":t-rex:", "🦖 ", ":taco:", "🌮 ", ":tada:", "🎉 ", ":taiwan:", "🇹🇼 ", ":tajikistan:", "🇹🇯 ", + ":takeout_box:", "🥡 ", ":tanabata_tree:", "🎋 ", ":tangerine:", "🍊 ", ":tanzania:", "🇹🇿 ", @@ -19471,7 +20253,6 @@ var emojiPaddedReplacer = strings.NewReplacer( ":trinidad_tobago:", "🇹🇹 ", ":triumph:", "😤 ", ":trolleybus:", "🚎 ", - ":trollface:", " ", ":trophy:", "🏆 ", ":tropical_drink:", "🍹 ", ":tropical_fish:", "🐠 ", @@ -19532,8 +20313,10 @@ var emojiPaddedReplacer = strings.NewReplacer( ":virgo:", "♍ ", ":volcano:", "🌋 ", ":volleyball:", "🏐 ", + ":vomiting:", "🤮 ", ":vs:", "🆚 ", ":vulcan_salute:", "🖖 ", + ":wales:", "🏴󠁧󠁢󠁷󠁬󠁳󠁿 ", ":walking_man:", "🚶 ", ":walking_woman:", "🚶‍♀️ ", ":wallis_futuna:", "🇼🇫 ", @@ -19572,17 +20355,23 @@ var emojiPaddedReplacer = strings.NewReplacer( ":wind_face:", "🌬 ", ":wine_glass:", "🍷 ", ":wink:", "😉 ", + ":wizard:", "🧙‍♂️ ", ":wolf:", "🐺 ", ":woman:", "👩 ", ":woman_artist:", "👩‍🎨 ", ":woman_astronaut:", "👩‍🚀 ", ":woman_cartwheeling:", "🤸‍♀️ ", ":woman_cook:", "👩‍🍳 ", + ":woman_elf:", "🧝‍♀️ ", ":woman_facepalming:", "🤦‍♀️ ", ":woman_factory_worker:", "👩‍🏭 ", + ":woman_fairy:", "🧚‍♀️ ", ":woman_farmer:", "👩‍🌾 ", ":woman_firefighter:", "👩‍🚒 ", + ":woman_genie:", "🧞‍♀️ ", ":woman_health_worker:", "👩‍⚕️ ", + ":woman_in_lotus_position:", "🧘‍♀️ ", + ":woman_in_steamy_room:", "🧖‍♀️ ", ":woman_judge:", "👩‍⚖️ ", ":woman_juggling:", "🤹‍♀️ ", ":woman_mechanic:", "👩‍🔧 ", @@ -19596,7 +20385,10 @@ var emojiPaddedReplacer = strings.NewReplacer( ":woman_student:", "👩‍🎓 ", ":woman_teacher:", "👩‍🏫 ", ":woman_technologist:", "👩‍💻 ", + ":woman_vampire:", "🧛‍♀️ ", + ":woman_with_headscarf:", "🧕 ", ":woman_with_turban:", "👳‍♀️ ", + ":woman_zombie:", "🧟‍♀️ ", ":womans_clothes:", "👚 ", ":womans_hat:", "👒 ", ":women_wrestling:", "🤼‍♀️ ", @@ -19612,7 +20404,9 @@ var emojiPaddedReplacer = strings.NewReplacer( ":yin_yang:", "☯ ", ":yum:", "😋 ", ":zambia:", "🇿🇲 ", + ":zany:", "🤪 ", ":zap:", "⚡ ", + ":zebra:", "🦓 ", ":zero:", "0️⃣ ", ":zimbabwe:", "🇿🇼 ", ":zipper_mouth_face:", "🤐 ", diff --git a/vendor/github.com/rs/xid/hostid_darwin.go b/vendor/github.com/rs/xid/hostid_darwin.go index abd06840..08351ff7 100644 --- a/vendor/github.com/rs/xid/hostid_darwin.go +++ b/vendor/github.com/rs/xid/hostid_darwin.go @@ -2,8 +2,8 @@ package xid -import "golang.org/x/sys/unix" +import "syscall" func readPlatformMachineID() (string, error) { - return unix.Sysctl("kern.uuid") + return syscall.Sysctl("kern.uuid") } diff --git a/vendor/github.com/rs/xid/hostid_freebsd.go b/vendor/github.com/rs/xid/hostid_freebsd.go index df2bd13c..be25a039 100644 --- a/vendor/github.com/rs/xid/hostid_freebsd.go +++ b/vendor/github.com/rs/xid/hostid_freebsd.go @@ -2,8 +2,8 @@ package xid -import "golang.org/x/sys/unix" +import "syscall" func readPlatformMachineID() (string, error) { - return unix.Sysctl("kern.hostuuid") + return syscall.Sysctl("kern.hostuuid") } diff --git a/vendor/github.com/rs/xid/hostid_windows.go b/vendor/github.com/rs/xid/hostid_windows.go index b8e1c2cb..ec2593ee 100644 --- a/vendor/github.com/rs/xid/hostid_windows.go +++ b/vendor/github.com/rs/xid/hostid_windows.go @@ -4,32 +4,31 @@ package xid import ( "fmt" + "syscall" "unsafe" - - "golang.org/x/sys/windows" ) func readPlatformMachineID() (string, error) { - // source: https://github.com/shirou/gopsutil/blob/master/host/host_windows.go - var h windows.Handle - err := windows.RegOpenKeyEx(windows.HKEY_LOCAL_MACHINE, windows.StringToUTF16Ptr(`SOFTWARE\Microsoft\Cryptography`), 0, windows.KEY_READ|windows.KEY_WOW64_64KEY, &h) + // source: https://github.com/shirou/gopsutil/blob/master/host/host_syscall.go + var h syscall.Handle + err := syscall.RegOpenKeyEx(syscall.HKEY_LOCAL_MACHINE, syscall.StringToUTF16Ptr(`SOFTWARE\Microsoft\Cryptography`), 0, syscall.KEY_READ|syscall.KEY_WOW64_64KEY, &h) if err != nil { return "", err } - defer windows.RegCloseKey(h) + defer syscall.RegCloseKey(h) - const windowsRegBufLen = 74 // len(`{`) + len(`abcdefgh-1234-456789012-123345456671` * 2) + len(`}`) // 2 == bytes/UTF16 + const syscallRegBufLen = 74 // len(`{`) + len(`abcdefgh-1234-456789012-123345456671` * 2) + len(`}`) // 2 == bytes/UTF16 const uuidLen = 36 - var regBuf [windowsRegBufLen]uint16 - bufLen := uint32(windowsRegBufLen) + var regBuf [syscallRegBufLen]uint16 + bufLen := uint32(syscallRegBufLen) var valType uint32 - err = windows.RegQueryValueEx(h, windows.StringToUTF16Ptr(`MachineGuid`), nil, &valType, (*byte)(unsafe.Pointer(®Buf[0])), &bufLen) + err = syscall.RegQueryValueEx(h, syscall.StringToUTF16Ptr(`MachineGuid`), nil, &valType, (*byte)(unsafe.Pointer(®Buf[0])), &bufLen) if err != nil { return "", err } - hostID := windows.UTF16ToString(regBuf[:]) + hostID := syscall.UTF16ToString(regBuf[:]) hostIDLen := len(hostID) if hostIDLen != uuidLen { return "", fmt.Errorf("HostID incorrect: %q\n", hostID) diff --git a/vendor/github.com/rs/xid/id.go b/vendor/github.com/rs/xid/id.go index 51b8d7fa..466faf26 100644 --- a/vendor/github.com/rs/xid/id.go +++ b/vendor/github.com/rs/xid/id.go @@ -42,6 +42,7 @@ package xid import ( + "bytes" "crypto/md5" "crypto/rand" "database/sql/driver" @@ -51,10 +52,9 @@ import ( "hash/crc32" "io/ioutil" "os" + "sort" "sync/atomic" "time" - "bytes" - "sort" ) // Code inspired from mgo/bson ObjectId @@ -143,9 +143,14 @@ func randInt() uint32 { // New generates a globally unique ID func New() ID { + return NewWithTime(time.Now()) +} + +// NewWithTime generates a globally unique ID with the passed in time +func NewWithTime(t time.Time) ID { var id ID // Timestamp, 4 bytes, big endian - binary.BigEndian.PutUint32(id[:], uint32(time.Now().Unix())) + binary.BigEndian.PutUint32(id[:], uint32(t.Unix())) // Machine, first 3 bytes of md5(hostname) id[4] = machineID[0] id[5] = machineID[1] @@ -339,7 +344,6 @@ func (id ID) Compare(other ID) int { return bytes.Compare(id[:], other[:]) } - type sorter []ID func (s sorter) Len() int { diff --git a/vendor/github.com/russross/blackfriday/.travis.yml b/vendor/github.com/russross/blackfriday/.travis.yml index a1687f17..a4eb2577 100644 --- a/vendor/github.com/russross/blackfriday/.travis.yml +++ b/vendor/github.com/russross/blackfriday/.travis.yml @@ -1,30 +1,18 @@ -sudo: false +# Travis CI (http://travis-ci.org/) is a continuous integration service for +# open source projects. This file configures it to run unit tests for +# blackfriday. + language: go + go: - - 1.5.4 - - 1.6.2 - - tip -matrix: - include: - - go: 1.2.2 - script: - - go get -t -v ./... - - go test -v -race ./... - - go: 1.3.3 - script: - - go get -t -v ./... - - go test -v -race ./... - - go: 1.4.3 - script: - - go get -t -v ./... - - go test -v -race ./... - allow_failures: - - go: tip - fast_finish: true + - 1.5 + - 1.6 + - 1.7 + install: - - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). + - go get -d -t -v ./... + - go build -v ./... + script: - - go get -t -v ./... - - diff -u <(echo -n) <(gofmt -d -s .) - - go tool vet . - - go test -v -race ./... + - go test -v ./... + - go test -run=^$ -bench=BenchmarkReference -benchmem diff --git a/vendor/github.com/russross/blackfriday/README.md b/vendor/github.com/russross/blackfriday/README.md index a6c94b79..2e0db355 100644 --- a/vendor/github.com/russross/blackfriday/README.md +++ b/vendor/github.com/russross/blackfriday/README.md @@ -1,6 +1,4 @@ -Blackfriday -[![Build Status][BuildSVG]][BuildURL] -[![Godoc][GodocV2SVG]][GodocV2URL] +Blackfriday [![Build Status](https://travis-ci.org/russross/blackfriday.svg?branch=master)](https://travis-ci.org/russross/blackfriday) =========== Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It @@ -18,27 +16,27 @@ It started as a translation from C of [Sundown][3]. Installation ------------ -Blackfriday is compatible with any modern Go release. With Go and git installed: +Blackfriday is compatible with any modern Go release. With Go 1.7 and git +installed: - go get -u gopkg.in/russross/blackfriday.v2 + go get gopkg.in/russross/blackfriday.v2 -will download, compile, and install the package into your `$GOPATH` directory -hierarchy. +will download, compile, and install the package into your `$GOPATH` +directory hierarchy. Alternatively, you can achieve the same if you +import it into a project: + + import "gopkg.in/russross/blackfriday.v2" + +and `go get` without parameters. Versions -------- Currently maintained and recommended version of Blackfriday is `v2`. It's being -developed on its own branch: https://github.com/russross/blackfriday/tree/v2 and the -documentation is available at -https://godoc.org/gopkg.in/russross/blackfriday.v2. - -It is `go get`-able via via [gopkg.in][6] at `gopkg.in/russross/blackfriday.v2`, -but we highly recommend using package management tool like [dep][7] or -[Glide][8] and make use of semantic versioning. With package management you -should import `github.com/russross/blackfriday` and specify that you're using -version 2.0.0. +developed on its own branch: https://github.com/russross/blackfriday/v2. You +should install and import it via [gopkg.in][6] at +`gopkg.in/russross/blackfriday.v2`. Version 2 offers a number of improvements over v1: @@ -58,43 +56,9 @@ Potential drawbacks: v2. See issue [#348](https://github.com/russross/blackfriday/issues/348) for tracking. -If you are still interested in the legacy `v1`, you can import it from -`github.com/russross/blackfriday`. Documentation for the legacy v1 can be found -here: https://godoc.org/github.com/russross/blackfriday - -### Known issue with `dep` - -There is a known problem with using Blackfriday v1 _transitively_ and `dep`. -Currently `dep` prioritizes semver versions over anything else, and picks the -latest one, plus it does not apply a `[[constraint]]` specifier to transitively -pulled in packages. So if you're using something that uses Blackfriday v1, but -that something does not use `dep` yet, you will get Blackfriday v2 pulled in and -your first dependency will fail to build. - -There are couple of fixes for it, documented here: -https://github.com/golang/dep/blob/master/docs/FAQ.md#how-do-i-constrain-a-transitive-dependencys-version - -Meanwhile, `dep` team is working on a more general solution to the constraints -on transitive dependencies problem: https://github.com/golang/dep/issues/1124. - - Usage ----- -### v1 - -For basic usage, it is as simple as getting your input into a byte -slice and calling: - - output := blackfriday.MarkdownBasic(input) - -This renders it with no extensions enabled. To get a more useful -feature set, use this instead: - - output := blackfriday.MarkdownCommon(input) - -### v2 - For the most sensible markdown processing, it is as simple as getting your input into a byte slice and calling: @@ -121,7 +85,7 @@ Here's an example of simple usage of Blackfriday together with Bluemonday: ```go import ( "github.com/microcosm-cc/bluemonday" - "gopkg.in/russross/blackfriday.v2" + "github.com/russross/blackfriday" ) // ... @@ -129,21 +93,11 @@ unsafe := blackfriday.Run(input) html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) ``` -### Custom options, v1 - -If you want to customize the set of options, first get a renderer -(currently only the HTML output engine), then use it to -call the more general `Markdown` function. For examples, see the -implementations of `MarkdownBasic` and `MarkdownCommon` in -`markdown.go`. - -### Custom options, v2 +### Custom options If you want to customize the set of options, use `blackfriday.WithExtensions`, `blackfriday.WithRenderer` and `blackfriday.WithRefOverride`. -### `blackfriday-tool` - You can also check out `blackfriday-tool` for a more complete example of how to use it. Download and install it using: @@ -163,22 +117,6 @@ installed in `$GOPATH/bin`. This is a statically-linked binary that can be copied to wherever you need it without worrying about dependencies and library versions. -### Sanitized anchor names - -Blackfriday includes an algorithm for creating sanitized anchor names -corresponding to a given input text. This algorithm is used to create -anchors for headings when `EXTENSION_AUTO_HEADER_IDS` is enabled. The -algorithm has a specification, so that other packages can create -compatible anchor names and links to those anchors. - -The specification is located at https://godoc.org/github.com/russross/blackfriday#hdr-Sanitized_Anchor_Names. - -[`SanitizedAnchorName`](https://godoc.org/github.com/russross/blackfriday#SanitizedAnchorName) exposes this functionality, and can be used to -create compatible links to the anchor names generated by blackfriday. -This algorithm is also implemented in a small standalone package at -[`github.com/shurcooL/sanitized_anchor_name`](https://godoc.org/github.com/shurcooL/sanitized_anchor_name). It can be useful for clients -that want a small package and don't need full functionality of blackfriday. - Features -------- @@ -246,7 +184,7 @@ implements the following extensions: and supply a language (to make syntax highlighting simple). Just mark it like this: - ``` go + ```go func getTrue() bool { return true } @@ -255,15 +193,6 @@ implements the following extensions: You can use 3 or more backticks to mark the beginning of the block, and the same number to mark the end of the block. - To preserve classes of fenced code blocks while using the bluemonday - HTML sanitizer, use the following policy: - - ``` go - p := bluemonday.UGCPolicy() - p.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code") - html := p.SanitizeBytes(unsafe) - ``` - * **Definition lists**. A simple definition list is made of a single-line term followed by a colon and the definition for that term. @@ -289,10 +218,8 @@ implements the following extensions: * **Strikethrough**. Use two tildes (`~~`) to mark text that should be crossed out. -* **Hard line breaks**. With this extension enabled (it is off by - default in the `MarkdownBasic` and `MarkdownCommon` convenience - functions), newlines in the input translate into line breaks in - the output. +* **Hard line breaks**. With this extension enabled newlines in the input + translate into line breaks in the output. This extension is off by default. * **Smart quotes**. Smartypants-style punctuation substitution is supported, turning normal double- and single-quote marks into @@ -332,14 +259,14 @@ are a few of note: renders output as LaTeX. -TODO +Todo ---- * More unit testing -* Improve Unicode support. It does not understand all Unicode +* Improve unicode support. It does not understand all unicode rules (about what constitutes a letter, a punctuation symbol, etc.), so it may fail to detect word boundaries correctly in - some instances. It is safe on all UTF-8 input. + some instances. It is safe on all utf-8 input. License @@ -354,10 +281,3 @@ License [4]: https://godoc.org/gopkg.in/russross/blackfriday.v2#Parse "Parse func" [5]: https://github.com/microcosm-cc/bluemonday "Bluemonday" [6]: https://labix.org/gopkg.in "gopkg.in" - [7]: https://github.com/golang/dep/ "dep" - [8]: https://github.com/Masterminds/glide "Glide" - - [BuildSVG]: https://travis-ci.org/russross/blackfriday.svg?branch=master - [BuildURL]: https://travis-ci.org/russross/blackfriday - [GodocV2SVG]: https://godoc.org/gopkg.in/russross/blackfriday.v2?status.svg - [GodocV2URL]: https://godoc.org/gopkg.in/russross/blackfriday.v2 diff --git a/vendor/github.com/russross/blackfriday/block.go b/vendor/github.com/russross/blackfriday/block.go index 7fc731d5..d7da33f2 100644 --- a/vendor/github.com/russross/blackfriday/block.go +++ b/vendor/github.com/russross/blackfriday/block.go @@ -15,17 +15,26 @@ package blackfriday import ( "bytes" - "unicode" + "html" + "regexp" + + "github.com/shurcooL/sanitized_anchor_name" +) + +const ( + charEntity = "&(?:#x[a-f0-9]{1,8}|#[0-9]{1,8}|[a-z][a-z0-9]{1,31});" + escapable = "[!\"#$%&'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]" +) + +var ( + reBackslashOrAmp = regexp.MustCompile("[\\&]") + reEntityOrEscapedChar = regexp.MustCompile("(?i)\\\\" + escapable + "|" + charEntity) ) // Parse block-level data. // Note: this function and many that it calls assume that // the input buffer ends with a newline. -func (p *parser) block(out *bytes.Buffer, data []byte) { - if len(data) == 0 || data[len(data)-1] != '\n' { - panic("block input is missing terminating newline") - } - +func (p *Markdown) block(data []byte) { // this is called recursively: enforce a maximum depth if p.nesting >= p.maxNesting { return @@ -34,14 +43,14 @@ func (p *parser) block(out *bytes.Buffer, data []byte) { // parse out one block-level construct at a time for len(data) > 0 { - // prefixed header: + // prefixed heading: // - // # Header 1 - // ## Header 2 + // # Heading 1 + // ## Heading 2 // ... - // ###### Header 6 - if p.isPrefixHeader(data) { - data = data[p.prefixHeader(out, data):] + // ###### Heading 6 + if p.isPrefixHeading(data) { + data = data[p.prefixHeading(data):] continue } @@ -51,7 +60,7 @@ func (p *parser) block(out *bytes.Buffer, data []byte) { // ... // if data[0] == '<' { - if i := p.html(out, data, true); i > 0 { + if i := p.html(data, true); i > 0 { data = data[i:] continue } @@ -62,9 +71,9 @@ func (p *parser) block(out *bytes.Buffer, data []byte) { // % stuff // % more stuff // % even more stuff - if p.flags&EXTENSION_TITLEBLOCK != 0 { + if p.extensions&Titleblock != 0 { if data[0] == '%' { - if i := p.titleBlock(out, data, true); i > 0 { + if i := p.titleBlock(data, true); i > 0 { data = data[i:] continue } @@ -86,7 +95,7 @@ func (p *parser) block(out *bytes.Buffer, data []byte) { // return b // } if p.codePrefix(data) > 0 { - data = data[p.code(out, data):] + data = data[p.code(data):] continue } @@ -100,8 +109,8 @@ func (p *parser) block(out *bytes.Buffer, data []byte) { // return n * fact(n-1) // } // ``` - if p.flags&EXTENSION_FENCED_CODE != 0 { - if i := p.fencedCodeBlock(out, data, true); i > 0 { + if p.extensions&FencedCode != 0 { + if i := p.fencedCodeBlock(data, true); i > 0 { data = data[i:] continue } @@ -115,9 +124,9 @@ func (p *parser) block(out *bytes.Buffer, data []byte) { // or // ______ if p.isHRule(data) { - p.r.HRule(out) + p.addBlock(HorizontalRule, nil) var i int - for i = 0; data[i] != '\n'; i++ { + for i = 0; i < len(data) && data[i] != '\n'; i++ { } data = data[i:] continue @@ -128,7 +137,7 @@ func (p *parser) block(out *bytes.Buffer, data []byte) { // > A big quote I found somewhere // > on the web if p.quotePrefix(data) > 0 { - data = data[p.quote(out, data):] + data = data[p.quote(data):] continue } @@ -138,8 +147,8 @@ func (p *parser) block(out *bytes.Buffer, data []byte) { // ------|-----|--------- // Bob | 31 | 555-1234 // Alice | 27 | 555-4321 - if p.flags&EXTENSION_TABLES != 0 { - if i := p.table(out, data); i > 0 { + if p.extensions&Tables != 0 { + if i := p.table(data); i > 0 { data = data[i:] continue } @@ -152,7 +161,7 @@ func (p *parser) block(out *bytes.Buffer, data []byte) { // // also works with + or - if p.uliPrefix(data) > 0 { - data = data[p.list(out, data, 0):] + data = data[p.list(data, 0):] continue } @@ -161,7 +170,7 @@ func (p *parser) block(out *bytes.Buffer, data []byte) { // 1. Item 1 // 2. Item 2 if p.oliPrefix(data) > 0 { - data = data[p.list(out, data, LIST_TYPE_ORDERED):] + data = data[p.list(data, ListTypeOrdered):] continue } @@ -173,55 +182,62 @@ func (p *parser) block(out *bytes.Buffer, data []byte) { // // Term 2 // : Definition c - if p.flags&EXTENSION_DEFINITION_LISTS != 0 { + if p.extensions&DefinitionLists != 0 { if p.dliPrefix(data) > 0 { - data = data[p.list(out, data, LIST_TYPE_DEFINITION):] + data = data[p.list(data, ListTypeDefinition):] continue } } // anything else must look like a normal paragraph - // note: this finds underlined headers, too - data = data[p.paragraph(out, data):] + // note: this finds underlined headings, too + data = data[p.paragraph(data):] } p.nesting-- } -func (p *parser) isPrefixHeader(data []byte) bool { +func (p *Markdown) addBlock(typ NodeType, content []byte) *Node { + p.closeUnmatchedBlocks() + container := p.addChild(typ, 0) + container.content = content + return container +} + +func (p *Markdown) isPrefixHeading(data []byte) bool { if data[0] != '#' { return false } - if p.flags&EXTENSION_SPACE_HEADERS != 0 { + if p.extensions&SpaceHeadings != 0 { level := 0 - for level < 6 && data[level] == '#' { + for level < 6 && level < len(data) && data[level] == '#' { level++ } - if data[level] != ' ' { + if level == len(data) || data[level] != ' ' { return false } } return true } -func (p *parser) prefixHeader(out *bytes.Buffer, data []byte) int { +func (p *Markdown) prefixHeading(data []byte) int { level := 0 - for level < 6 && data[level] == '#' { + for level < 6 && level < len(data) && data[level] == '#' { level++ } i := skipChar(data, level, ' ') end := skipUntilChar(data, i, '\n') skip := end id := "" - if p.flags&EXTENSION_HEADER_IDS != 0 { + if p.extensions&HeadingIDs != 0 { j, k := 0, 0 - // find start/end of header id + // find start/end of heading id for j = i; j < end-1 && (data[j] != '{' || data[j+1] != '#'); j++ { } for k = j + 1; k < end && data[k] != '}'; k++ { } - // extract header id iff found + // extract heading id iff found if j < end && k < end { id = string(data[j+2 : k]) end = j @@ -241,45 +257,41 @@ func (p *parser) prefixHeader(out *bytes.Buffer, data []byte) int { end-- } if end > i { - if id == "" && p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { - id = SanitizedAnchorName(string(data[i:end])) + if id == "" && p.extensions&AutoHeadingIDs != 0 { + id = sanitized_anchor_name.Create(string(data[i:end])) } - work := func() bool { - p.inline(out, data[i:end]) - return true - } - p.r.Header(out, work, level, id) + block := p.addBlock(Heading, data[i:end]) + block.HeadingID = id + block.Level = level } return skip } -func (p *parser) isUnderlinedHeader(data []byte) int { - // test of level 1 header +func (p *Markdown) isUnderlinedHeading(data []byte) int { + // test of level 1 heading if data[0] == '=' { i := skipChar(data, 1, '=') i = skipChar(data, i, ' ') - if data[i] == '\n' { + if i < len(data) && data[i] == '\n' { return 1 - } else { - return 0 } + return 0 } - // test of level 2 header + // test of level 2 heading if data[0] == '-' { i := skipChar(data, 1, '-') i = skipChar(data, i, ' ') - if data[i] == '\n' { + if i < len(data) && data[i] == '\n' { return 2 - } else { - return 0 } + return 0 } return 0 } -func (p *parser) titleBlock(out *bytes.Buffer, data []byte, doRender bool) int { +func (p *Markdown) titleBlock(data []byte, doRender bool) int { if data[0] != '%' { return 0 } @@ -293,12 +305,17 @@ func (p *parser) titleBlock(out *bytes.Buffer, data []byte, doRender bool) int { } data = bytes.Join(splitData[0:i], []byte("\n")) - p.r.TitleBlock(out, data) + consumed := len(data) + data = bytes.TrimPrefix(data, []byte("% ")) + data = bytes.Replace(data, []byte("\n% "), []byte("\n"), -1) + block := p.addBlock(Heading, data) + block.Level = 1 + block.IsTitleblock = true - return len(data) + return consumed } -func (p *parser) html(out *bytes.Buffer, data []byte, doRender bool) int { +func (p *Markdown) html(data []byte, doRender bool) int { var i, j int // identify the opening tag @@ -310,17 +327,12 @@ func (p *parser) html(out *bytes.Buffer, data []byte, doRender bool) int { // handle special cases if !tagfound { // check for an HTML comment - if size := p.htmlComment(out, data, doRender); size > 0 { + if size := p.htmlComment(data, doRender); size > 0 { return size } // check for an
    tag - if size := p.htmlHr(out, data, doRender); size > 0 { - return size - } - - // check for HTML CDATA - if size := p.htmlCDATA(out, data, doRender); size > 0 { + if size := p.htmlHr(data, doRender); size > 0 { return size } @@ -395,60 +407,42 @@ func (p *parser) html(out *bytes.Buffer, data []byte, doRender bool) int { for end > 0 && data[end-1] == '\n' { end-- } - p.r.BlockHtml(out, data[:end]) + finalizeHTMLBlock(p.addBlock(HTMLBlock, data[:end])) } return i } -func (p *parser) renderHTMLBlock(out *bytes.Buffer, data []byte, start int, doRender bool) int { - // html block needs to end with a blank line - if i := p.isEmpty(data[start:]); i > 0 { - size := start + i +func finalizeHTMLBlock(block *Node) { + block.Literal = block.content + block.content = nil +} + +// HTML comment, lax form +func (p *Markdown) htmlComment(data []byte, doRender bool) int { + i := p.inlineHTMLComment(data) + // needs to end with a blank line + if j := p.isEmpty(data[i:]); j > 0 { + size := i + j if doRender { // trim trailing newlines end := size for end > 0 && data[end-1] == '\n' { end-- } - p.r.BlockHtml(out, data[:end]) + block := p.addBlock(HTMLBlock, data[:end]) + finalizeHTMLBlock(block) } return size } return 0 } -// HTML comment, lax form -func (p *parser) htmlComment(out *bytes.Buffer, data []byte, doRender bool) int { - i := p.inlineHTMLComment(out, data) - return p.renderHTMLBlock(out, data, i, doRender) -} - -// HTML CDATA section -func (p *parser) htmlCDATA(out *bytes.Buffer, data []byte, doRender bool) int { - const cdataTag = "') { - i++ - } - i++ - // no end-of-comment marker - if i >= len(data) { - return 0 - } - return p.renderHTMLBlock(out, data, i, doRender) -} - // HR, which is the only self-closing block tag considered -func (p *parser) htmlHr(out *bytes.Buffer, data []byte, doRender bool) int { +func (p *Markdown) htmlHr(data []byte, doRender bool) int { + if len(data) < 4 { + return 0 + } if data[0] != '<' || (data[1] != 'h' && data[1] != 'H') || (data[2] != 'r' && data[2] != 'R') { return 0 } @@ -456,22 +450,31 @@ func (p *parser) htmlHr(out *bytes.Buffer, data []byte, doRender bool) int { // not an
    tag after all; at least not a valid one return 0 } - i := 3 - for data[i] != '>' && data[i] != '\n' { + for i < len(data) && data[i] != '>' && data[i] != '\n' { i++ } - - if data[i] == '>' { - return p.renderHTMLBlock(out, data, i+1, doRender) + if i < len(data) && data[i] == '>' { + i++ + if j := p.isEmpty(data[i:]); j > 0 { + size := i + j + if doRender { + // trim newlines + end := size + for end > 0 && data[end-1] == '\n' { + end-- + } + finalizeHTMLBlock(p.addBlock(HTMLBlock, data[:end])) + } + return size + } } - return 0 } -func (p *parser) htmlFindTag(data []byte) (string, bool) { +func (p *Markdown) htmlFindTag(data []byte) (string, bool) { i := 0 - for isalnum(data[i]) { + for i < len(data) && isalnum(data[i]) { i++ } key := string(data[:i]) @@ -481,9 +484,11 @@ func (p *parser) htmlFindTag(data []byte) (string, bool) { return "", false } -func (p *parser) htmlFindEnd(tag string, data []byte) int { +func (p *Markdown) htmlFindEnd(tag string, data []byte) int { // assume data[0] == '<' && data[1] == '/' already tested - + if tag == "hr" { + return 2 + } // check if tag is a match closetag := []byte("") if !bytes.HasPrefix(data, closetag) { @@ -503,7 +508,7 @@ func (p *parser) htmlFindEnd(tag string, data []byte) int { return i } - if p.flags&EXTENSION_LAX_HTML_BLOCKS != 0 { + if p.extensions&LaxHTMLBlocks != 0 { return i } if skip = p.isEmpty(data[i:]); skip == 0 { @@ -514,7 +519,7 @@ func (p *parser) htmlFindEnd(tag string, data []byte) int { return i + skip } -func (*parser) isEmpty(data []byte) int { +func (*Markdown) isEmpty(data []byte) int { // it is okay to call isEmpty on an empty buffer if len(data) == 0 { return 0 @@ -526,10 +531,13 @@ func (*parser) isEmpty(data []byte) int { return 0 } } - return i + 1 + if i < len(data) && data[i] == '\n' { + i++ + } + return i } -func (*parser) isHRule(data []byte) bool { +func (*Markdown) isHRule(data []byte) bool { i := 0 // skip up to three spaces @@ -545,7 +553,7 @@ func (*parser) isHRule(data []byte) bool { // the whole line must be the char or whitespace n := 0 - for data[i] != '\n' { + for i < len(data) && data[i] != '\n' { switch { case data[i] == c: n++ @@ -561,8 +569,7 @@ func (*parser) isHRule(data []byte) bool { // isFenceLine checks if there's a fence line (e.g., ``` or ``` go) at the beginning of data, // and returns the end index if so, or 0 otherwise. It also returns the marker found. // If syntax is not nil, it gets set to the syntax specified in the fence line. -// A final newline is mandatory to recognize the fence line, unless newlineOptional is true. -func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional bool) (end int, marker string) { +func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker string) { i, size := 0, 0 // skip up to three spaces @@ -604,7 +611,7 @@ func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional i = skipChar(data, i, ' ') if i >= len(data) { - if newlineOptional && i == len(data) { + if i == len(data) { return i, marker } return 0, "" @@ -649,33 +656,33 @@ func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional i = skipChar(data, i, ' ') if i >= len(data) || data[i] != '\n' { - if newlineOptional && i == len(data) { + if i == len(data) { return i, marker } return 0, "" } - return i + 1, marker // Take newline into account. } // fencedCodeBlock returns the end index if data contains a fenced code block at the beginning, // or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects. // If doRender is true, a final newline is mandatory to recognize the fenced code block. -func (p *parser) fencedCodeBlock(out *bytes.Buffer, data []byte, doRender bool) int { +func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int { var syntax string - beg, marker := isFenceLine(data, &syntax, "", false) + beg, marker := isFenceLine(data, &syntax, "") if beg == 0 || beg >= len(data) { return 0 } var work bytes.Buffer + work.Write([]byte(syntax)) + work.WriteByte('\n') for { // safe to assume beg < len(data) // check for the end of the code block - newlineOptional := !doRender - fenceEnd, _ := isFenceLine(data[beg:], nil, marker, newlineOptional) + fenceEnd, _ := isFenceLine(data[beg:], nil, marker) if fenceEnd != 0 { beg += fenceEnd break @@ -697,24 +704,55 @@ func (p *parser) fencedCodeBlock(out *bytes.Buffer, data []byte, doRender bool) } if doRender { - p.r.BlockCode(out, work.Bytes(), syntax) + block := p.addBlock(CodeBlock, work.Bytes()) // TODO: get rid of temp buffer + block.IsFenced = true + finalizeCodeBlock(block) } return beg } -func (p *parser) table(out *bytes.Buffer, data []byte) int { - var header bytes.Buffer - i, columns := p.tableHeader(&header, data) +func unescapeChar(str []byte) []byte { + if str[0] == '\\' { + return []byte{str[1]} + } + return []byte(html.UnescapeString(string(str))) +} + +func unescapeString(str []byte) []byte { + if reBackslashOrAmp.Match(str) { + return reEntityOrEscapedChar.ReplaceAllFunc(str, unescapeChar) + } + return str +} + +func finalizeCodeBlock(block *Node) { + if block.IsFenced { + newlinePos := bytes.IndexByte(block.content, '\n') + firstLine := block.content[:newlinePos] + rest := block.content[newlinePos+1:] + block.Info = unescapeString(bytes.Trim(firstLine, "\n")) + block.Literal = rest + } else { + block.Literal = block.content + } + block.content = nil +} + +func (p *Markdown) table(data []byte) int { + table := p.addBlock(Table, nil) + i, columns := p.tableHeader(data) if i == 0 { + p.tip = table.Parent + table.Unlink() return 0 } - var body bytes.Buffer + p.addBlock(TableBody, nil) for i < len(data) { pipes, rowStart := 0, i - for ; data[i] != '\n'; i++ { + for ; i < len(data) && data[i] != '\n'; i++ { if data[i] == '|' { pipes++ } @@ -726,12 +764,12 @@ func (p *parser) table(out *bytes.Buffer, data []byte) int { } // include the newline in data sent to tableRow - i++ - p.tableRow(&body, data[rowStart:i], columns, false) + if i < len(data) && data[i] == '\n' { + i++ + } + p.tableRow(data[rowStart:i], columns, false) } - p.r.Table(out, header.Bytes(), body.Bytes(), columns) - return i } @@ -744,10 +782,10 @@ func isBackslashEscaped(data []byte, i int) bool { return backslashes&1 == 1 } -func (p *parser) tableHeader(out *bytes.Buffer, data []byte) (size int, columns []int) { +func (p *Markdown) tableHeader(data []byte) (size int, columns []CellAlignFlags) { i := 0 colCount := 1 - for i = 0; data[i] != '\n'; i++ { + for i = 0; i < len(data) && data[i] != '\n'; i++ { if data[i] == '|' && !isBackslashEscaped(data, i) { colCount++ } @@ -759,7 +797,11 @@ func (p *parser) tableHeader(out *bytes.Buffer, data []byte) (size int, columns } // include the newline in the data sent to tableRow - header := data[:i+1] + j := i + if j < len(data) && data[j] == '\n' { + j++ + } + header := data[:j] // column count ignores pipes at beginning or end of line if data[0] == '|' { @@ -769,7 +811,7 @@ func (p *parser) tableHeader(out *bytes.Buffer, data []byte) (size int, columns colCount-- } - columns = make([]int, colCount) + columns = make([]CellAlignFlags, colCount) // move on to the header underline i++ @@ -785,27 +827,29 @@ func (p *parser) tableHeader(out *bytes.Buffer, data []byte) (size int, columns // each column header is of form: / *:?-+:? *|/ with # dashes + # colons >= 3 // and trailing | optional on last column col := 0 - for data[i] != '\n' { + for i < len(data) && data[i] != '\n' { dashes := 0 if data[i] == ':' { i++ - columns[col] |= TABLE_ALIGNMENT_LEFT + columns[col] |= TableAlignmentLeft dashes++ } - for data[i] == '-' { + for i < len(data) && data[i] == '-' { i++ dashes++ } - if data[i] == ':' { + if i < len(data) && data[i] == ':' { i++ - columns[col] |= TABLE_ALIGNMENT_RIGHT + columns[col] |= TableAlignmentRight dashes++ } - for data[i] == ' ' { + for i < len(data) && data[i] == ' ' { i++ } - + if i == len(data) { + return + } // end of column test is messy switch { case dashes < 3: @@ -816,12 +860,12 @@ func (p *parser) tableHeader(out *bytes.Buffer, data []byte) (size int, columns // marker found, now skip past trailing whitespace col++ i++ - for data[i] == ' ' { + for i < len(data) && data[i] == ' ' { i++ } // trailing junk found after last column - if col >= colCount && data[i] != '\n' { + if col >= colCount && i < len(data) && data[i] != '\n' { return } @@ -842,27 +886,31 @@ func (p *parser) tableHeader(out *bytes.Buffer, data []byte) (size int, columns return } - p.tableRow(out, header, columns, true) - size = i + 1 + p.addBlock(TableHead, nil) + p.tableRow(header, columns, true) + size = i + if size < len(data) && data[size] == '\n' { + size++ + } return } -func (p *parser) tableRow(out *bytes.Buffer, data []byte, columns []int, header bool) { +func (p *Markdown) tableRow(data []byte, columns []CellAlignFlags, header bool) { + p.addBlock(TableRow, nil) i, col := 0, 0 - var rowWork bytes.Buffer if data[i] == '|' && !isBackslashEscaped(data, i) { i++ } for col = 0; col < len(columns) && i < len(data); col++ { - for data[i] == ' ' { + for i < len(data) && data[i] == ' ' { i++ } cellStart := i - for (data[i] != '|' || isBackslashEscaped(data, i)) && data[i] != '\n' { + for i < len(data) && (data[i] != '|' || isBackslashEscaped(data, i)) && data[i] != '\n' { i++ } @@ -871,42 +919,33 @@ func (p *parser) tableRow(out *bytes.Buffer, data []byte, columns []int, header // skip the end-of-cell marker, possibly taking us past end of buffer i++ - for cellEnd > cellStart && data[cellEnd-1] == ' ' { + for cellEnd > cellStart && cellEnd-1 < len(data) && data[cellEnd-1] == ' ' { cellEnd-- } - var cellWork bytes.Buffer - p.inline(&cellWork, data[cellStart:cellEnd]) - - if header { - p.r.TableHeaderCell(&rowWork, cellWork.Bytes(), columns[col]) - } else { - p.r.TableCell(&rowWork, cellWork.Bytes(), columns[col]) - } + cell := p.addBlock(TableCell, data[cellStart:cellEnd]) + cell.IsHeader = header + cell.Align = columns[col] } // pad it out with empty columns to get the right number for ; col < len(columns); col++ { - if header { - p.r.TableHeaderCell(&rowWork, nil, columns[col]) - } else { - p.r.TableCell(&rowWork, nil, columns[col]) - } + cell := p.addBlock(TableCell, nil) + cell.IsHeader = header + cell.Align = columns[col] } // silently ignore rows with too many cells - - p.r.TableRow(out, rowWork.Bytes()) } // returns blockquote prefix length -func (p *parser) quotePrefix(data []byte) int { +func (p *Markdown) quotePrefix(data []byte) int { i := 0 - for i < 3 && data[i] == ' ' { + for i < 3 && i < len(data) && data[i] == ' ' { i++ } - if data[i] == '>' { - if data[i+1] == ' ' { + if i < len(data) && data[i] == '>' { + if i+1 < len(data) && data[i+1] == ' ' { return i + 2 } return i + 1 @@ -916,7 +955,7 @@ func (p *parser) quotePrefix(data []byte) int { // blockquote ends with at least one blank line // followed by something without a blockquote prefix -func (p *parser) terminateBlockquote(data []byte, beg, end int) bool { +func (p *Markdown) terminateBlockquote(data []byte, beg, end int) bool { if p.isEmpty(data[beg:]) <= 0 { return false } @@ -927,7 +966,8 @@ func (p *parser) terminateBlockquote(data []byte, beg, end int) bool { } // parse a blockquote fragment -func (p *parser) quote(out *bytes.Buffer, data []byte) int { +func (p *Markdown) quote(data []byte) int { + block := p.addBlock(BlockQuote, nil) var raw bytes.Buffer beg, end := 0, 0 for beg < len(data) { @@ -935,9 +975,9 @@ func (p *parser) quote(out *bytes.Buffer, data []byte) int { // Step over whole lines, collecting them. While doing that, check for // fenced code and if one's found, incorporate it altogether, // irregardless of any contents inside it - for data[end] != '\n' { - if p.flags&EXTENSION_FENCED_CODE != 0 { - if i := p.fencedCodeBlock(out, data[end:], false); i > 0 { + for end < len(data) && data[end] != '\n' { + if p.extensions&FencedCode != 0 { + if i := p.fencedCodeBlock(data[end:], false); i > 0 { // -1 to compensate for the extra end++ after the loop: end += i - 1 break @@ -945,44 +985,47 @@ func (p *parser) quote(out *bytes.Buffer, data []byte) int { } end++ } - end++ - + if end < len(data) && data[end] == '\n' { + end++ + } if pre := p.quotePrefix(data[beg:]); pre > 0 { // skip the prefix beg += pre } else if p.terminateBlockquote(data, beg, end) { break } - // this line is part of the blockquote raw.Write(data[beg:end]) beg = end } - - var cooked bytes.Buffer - p.block(&cooked, raw.Bytes()) - p.r.BlockQuote(out, cooked.Bytes()) + p.block(raw.Bytes()) + p.finalize(block) return end } // returns prefix length for block code -func (p *parser) codePrefix(data []byte) int { - if data[0] == ' ' && data[1] == ' ' && data[2] == ' ' && data[3] == ' ' { +func (p *Markdown) codePrefix(data []byte) int { + if len(data) >= 1 && data[0] == '\t' { + return 1 + } + if len(data) >= 4 && data[0] == ' ' && data[1] == ' ' && data[2] == ' ' && data[3] == ' ' { return 4 } return 0 } -func (p *parser) code(out *bytes.Buffer, data []byte) int { +func (p *Markdown) code(data []byte) int { var work bytes.Buffer i := 0 for i < len(data) { beg := i - for data[i] != '\n' { + for i < len(data) && data[i] != '\n' { + i++ + } + if i < len(data) && data[i] == '\n' { i++ } - i++ blankline := p.isEmpty(data[beg:i]) > 0 if pre := p.codePrefix(data[beg:i]); pre > 0 { @@ -993,7 +1036,7 @@ func (p *parser) code(out *bytes.Buffer, data []byte) int { break } - // verbatim copy to the working buffeu + // verbatim copy to the working buffer if blankline { work.WriteByte('\n') } else { @@ -1013,122 +1056,183 @@ func (p *parser) code(out *bytes.Buffer, data []byte) int { work.WriteByte('\n') - p.r.BlockCode(out, work.Bytes(), "") + block := p.addBlock(CodeBlock, work.Bytes()) // TODO: get rid of temp buffer + block.IsFenced = false + finalizeCodeBlock(block) return i } // returns unordered list item prefix -func (p *parser) uliPrefix(data []byte) int { +func (p *Markdown) uliPrefix(data []byte) int { i := 0 - // start with up to 3 spaces - for i < 3 && data[i] == ' ' { + for i < len(data) && i < 3 && data[i] == ' ' { i++ } - - // need a *, +, or - followed by a space + if i >= len(data)-1 { + return 0 + } + // need one of {'*', '+', '-'} followed by a space or a tab if (data[i] != '*' && data[i] != '+' && data[i] != '-') || - data[i+1] != ' ' { + (data[i+1] != ' ' && data[i+1] != '\t') { return 0 } return i + 2 } // returns ordered list item prefix -func (p *parser) oliPrefix(data []byte) int { +func (p *Markdown) oliPrefix(data []byte) int { i := 0 // start with up to 3 spaces - for i < 3 && data[i] == ' ' { + for i < 3 && i < len(data) && data[i] == ' ' { i++ } // count the digits start := i - for data[i] >= '0' && data[i] <= '9' { + for i < len(data) && data[i] >= '0' && data[i] <= '9' { i++ } + if start == i || i >= len(data)-1 { + return 0 + } - // we need >= 1 digits followed by a dot and a space - if start == i || data[i] != '.' || data[i+1] != ' ' { + // we need >= 1 digits followed by a dot and a space or a tab + if data[i] != '.' || !(data[i+1] == ' ' || data[i+1] == '\t') { return 0 } return i + 2 } // returns definition list item prefix -func (p *parser) dliPrefix(data []byte) int { - i := 0 - - // need a : followed by a spaces - if data[i] != ':' || data[i+1] != ' ' { +func (p *Markdown) dliPrefix(data []byte) int { + if len(data) < 2 { return 0 } - for data[i] == ' ' { + i := 0 + // need a ':' followed by a space or a tab + if data[i] != ':' || !(data[i+1] == ' ' || data[i+1] == '\t') { + return 0 + } + for i < len(data) && data[i] == ' ' { i++ } return i + 2 } // parse ordered or unordered list block -func (p *parser) list(out *bytes.Buffer, data []byte, flags int) int { +func (p *Markdown) list(data []byte, flags ListType) int { i := 0 - flags |= LIST_ITEM_BEGINNING_OF_LIST - work := func() bool { - for i < len(data) { - skip := p.listItem(out, data[i:], &flags) - i += skip + flags |= ListItemBeginningOfList + block := p.addBlock(List, nil) + block.ListFlags = flags + block.Tight = true - if skip == 0 || flags&LIST_ITEM_END_OF_LIST != 0 { - break - } - flags &= ^LIST_ITEM_BEGINNING_OF_LIST + for i < len(data) { + skip := p.listItem(data[i:], &flags) + if flags&ListItemContainsBlock != 0 { + block.ListData.Tight = false } - return true + i += skip + if skip == 0 || flags&ListItemEndOfList != 0 { + break + } + flags &= ^ListItemBeginningOfList } - p.r.List(out, work, flags) + above := block.Parent + finalizeList(block) + p.tip = above return i } +// Returns true if block ends with a blank line, descending if needed +// into lists and sublists. +func endsWithBlankLine(block *Node) bool { + // TODO: figure this out. Always false now. + for block != nil { + //if block.lastLineBlank { + //return true + //} + t := block.Type + if t == List || t == Item { + block = block.LastChild + } else { + break + } + } + return false +} + +func finalizeList(block *Node) { + block.open = false + item := block.FirstChild + for item != nil { + // check for non-final list item ending with blank line: + if endsWithBlankLine(item) && item.Next != nil { + block.ListData.Tight = false + break + } + // recurse into children of list item, to see if there are spaces + // between any of them: + subItem := item.FirstChild + for subItem != nil { + if endsWithBlankLine(subItem) && (item.Next != nil || subItem.Next != nil) { + block.ListData.Tight = false + break + } + subItem = subItem.Next + } + item = item.Next + } +} + // Parse a single list item. // Assumes initial prefix is already removed if this is a sublist. -func (p *parser) listItem(out *bytes.Buffer, data []byte, flags *int) int { +func (p *Markdown) listItem(data []byte, flags *ListType) int { // keep track of the indentation of the first line itemIndent := 0 - for itemIndent < 3 && data[itemIndent] == ' ' { - itemIndent++ + if data[0] == '\t' { + itemIndent += 4 + } else { + for itemIndent < 3 && data[itemIndent] == ' ' { + itemIndent++ + } } + var bulletChar byte = '*' i := p.uliPrefix(data) if i == 0 { i = p.oliPrefix(data) + } else { + bulletChar = data[i-2] } if i == 0 { i = p.dliPrefix(data) // reset definition term flag if i > 0 { - *flags &= ^LIST_TYPE_TERM + *flags &= ^ListTypeTerm } } if i == 0 { - // if in defnition list, set term flag and continue - if *flags&LIST_TYPE_DEFINITION != 0 { - *flags |= LIST_TYPE_TERM + // if in definition list, set term flag and continue + if *flags&ListTypeDefinition != 0 { + *flags |= ListTypeTerm } else { return 0 } } // skip leading whitespace on first line - for data[i] == ' ' { + for i < len(data) && data[i] == ' ' { i++ } // find the end of the line line := i - for i > 0 && data[i-1] != '\n' { + for i > 0 && i < len(data) && data[i-1] != '\n' { i++ } @@ -1148,7 +1252,7 @@ gatherlines: i++ // find the end of this line - for data[i-1] != '\n' { + for i < len(data) && data[i-1] != '\n' { i++ } @@ -1156,18 +1260,24 @@ gatherlines: // and move on to the next line if p.isEmpty(data[line:i]) > 0 { containsBlankLine = true - raw.Write(data[line:i]) line = i continue } // calculate the indentation indent := 0 - for indent < 4 && line+indent < i && data[line+indent] == ' ' { - indent++ + indentIndex := 0 + if data[line] == '\t' { + indentIndex++ + indent += 4 + } else { + for indent < 4 && line+indent < i && data[line+indent] == ' ' { + indent++ + indentIndex++ + } } - chunk := data[line+indent : i] + chunk := data[line+indentIndex : i] // evaluate how this line fits in switch { @@ -1177,15 +1287,7 @@ gatherlines: p.dliPrefix(chunk) > 0: if containsBlankLine { - // end the list if the type changed after a blank line - if indent <= itemIndent && - ((*flags&LIST_TYPE_ORDERED != 0 && p.uliPrefix(chunk) > 0) || - (*flags&LIST_TYPE_ORDERED == 0 && p.oliPrefix(chunk) > 0)) { - - *flags |= LIST_ITEM_END_OF_LIST - break gatherlines - } - *flags |= LIST_ITEM_CONTAINS_BLOCK + *flags |= ListItemContainsBlock } // to be a nested list, it must be indented more @@ -1199,93 +1301,89 @@ gatherlines: sublist = raw.Len() } - // is this a nested prefix header? - case p.isPrefixHeader(chunk): - // if the header is not indented, it is not nested in the list + // is this a nested prefix heading? + case p.isPrefixHeading(chunk): + // if the heading is not indented, it is not nested in the list // and thus ends the list if containsBlankLine && indent < 4 { - *flags |= LIST_ITEM_END_OF_LIST + *flags |= ListItemEndOfList break gatherlines } - *flags |= LIST_ITEM_CONTAINS_BLOCK + *flags |= ListItemContainsBlock // anything following an empty line is only part // of this item if it is indented 4 spaces // (regardless of the indentation of the beginning of the item) case containsBlankLine && indent < 4: - if *flags&LIST_TYPE_DEFINITION != 0 && i < len(data)-1 { + if *flags&ListTypeDefinition != 0 && i < len(data)-1 { // is the next item still a part of this list? next := i - for data[next] != '\n' { + for next < len(data) && data[next] != '\n' { next++ } for next < len(data)-1 && data[next] == '\n' { next++ } if i < len(data)-1 && data[i] != ':' && data[next] != ':' { - *flags |= LIST_ITEM_END_OF_LIST + *flags |= ListItemEndOfList } } else { - *flags |= LIST_ITEM_END_OF_LIST + *flags |= ListItemEndOfList } break gatherlines // a blank line means this should be parsed as a block case containsBlankLine: - *flags |= LIST_ITEM_CONTAINS_BLOCK + raw.WriteByte('\n') + *flags |= ListItemContainsBlock } - containsBlankLine = false + // if this line was preceded by one or more blanks, + // re-introduce the blank into the buffer + if containsBlankLine { + containsBlankLine = false + raw.WriteByte('\n') + } // add the line into the working buffer without prefix - raw.Write(data[line+indent : i]) + raw.Write(data[line+indentIndex : i]) line = i } - // If reached end of data, the Renderer.ListItem call we're going to make below - // is definitely the last in the list. - if line >= len(data) { - *flags |= LIST_ITEM_END_OF_LIST - } - rawBytes := raw.Bytes() + block := p.addBlock(Item, nil) + block.ListFlags = *flags + block.Tight = false + block.BulletChar = bulletChar + block.Delimiter = '.' // Only '.' is possible in Markdown, but ')' will also be possible in CommonMark + // render the contents of the list item - var cooked bytes.Buffer - if *flags&LIST_ITEM_CONTAINS_BLOCK != 0 && *flags&LIST_TYPE_TERM == 0 { + if *flags&ListItemContainsBlock != 0 && *flags&ListTypeTerm == 0 { // intermediate render of block item, except for definition term if sublist > 0 { - p.block(&cooked, rawBytes[:sublist]) - p.block(&cooked, rawBytes[sublist:]) + p.block(rawBytes[:sublist]) + p.block(rawBytes[sublist:]) } else { - p.block(&cooked, rawBytes) + p.block(rawBytes) } } else { // intermediate render of inline item if sublist > 0 { - p.inline(&cooked, rawBytes[:sublist]) - p.block(&cooked, rawBytes[sublist:]) + child := p.addChild(Paragraph, 0) + child.content = rawBytes[:sublist] + p.block(rawBytes[sublist:]) } else { - p.inline(&cooked, rawBytes) + child := p.addChild(Paragraph, 0) + child.content = rawBytes } } - - // render the actual list item - cookedBytes := cooked.Bytes() - parsedEnd := len(cookedBytes) - - // strip trailing newlines - for parsedEnd > 0 && cookedBytes[parsedEnd-1] == '\n' { - parsedEnd-- - } - p.r.ListItem(out, cookedBytes[:parsedEnd], *flags) - return line } // render a single paragraph that has already been parsed out -func (p *parser) renderParagraph(out *bytes.Buffer, data []byte) { +func (p *Markdown) renderParagraph(data []byte) { if len(data) == 0 { return } @@ -1296,27 +1394,29 @@ func (p *parser) renderParagraph(out *bytes.Buffer, data []byte) { beg++ } + end := len(data) // trim trailing newline - end := len(data) - 1 + if data[len(data)-1] == '\n' { + end-- + } // trim trailing spaces for end > beg && data[end-1] == ' ' { end-- } - work := func() bool { - p.inline(out, data[beg:end]) - return true - } - p.r.Paragraph(out, work) + p.addBlock(Paragraph, data[beg:end]) } -func (p *parser) paragraph(out *bytes.Buffer, data []byte) int { +func (p *Markdown) paragraph(data []byte) int { // prev: index of 1st char of previous line // line: index of 1st char of current line // i: index of cursor/end of current line var prev, line, i int - + tabSize := TabSizeDefault + if p.extensions&TabSizeEight != 0 { + tabSize = TabSizeDouble + } // keep going until we find something to mark the end of the paragraph for i < len(data) { // mark the beginning of the current line @@ -1324,24 +1424,32 @@ func (p *parser) paragraph(out *bytes.Buffer, data []byte) int { current := data[i:] line = i + // did we find a reference or a footnote? If so, end a paragraph + // preceding it and report that we have consumed up to the end of that + // reference: + if refEnd := isReference(p, current, tabSize); refEnd > 0 { + p.renderParagraph(data[:i]) + return i + refEnd + } + // did we find a blank line marking the end of the paragraph? if n := p.isEmpty(current); n > 0 { // did this blank line followed by a definition list item? - if p.flags&EXTENSION_DEFINITION_LISTS != 0 { + if p.extensions&DefinitionLists != 0 { if i < len(data)-1 && data[i+1] == ':' { - return p.list(out, data[prev:], LIST_TYPE_DEFINITION) + return p.list(data[prev:], ListTypeDefinition) } } - p.renderParagraph(out, data[:i]) + p.renderParagraph(data[:i]) return i + n } - // an underline under some text marks a header, so our paragraph ended on prev line + // an underline under some text marks a heading, so our paragraph ended on prev line if i > 0 { - if level := p.isUnderlinedHeader(current); level > 0 { + if level := p.isUnderlinedHeading(current); level > 0 { // render the paragraph - p.renderParagraph(out, data[:prev]) + p.renderParagraph(data[:prev]) // ignore leading and trailing whitespace eol := i - 1 @@ -1352,24 +1460,17 @@ func (p *parser) paragraph(out *bytes.Buffer, data []byte) int { eol-- } - // render the header - // this ugly double closure avoids forcing variables onto the heap - work := func(o *bytes.Buffer, pp *parser, d []byte) func() bool { - return func() bool { - pp.inline(o, d) - return true - } - }(out, p, data[prev:eol]) - id := "" - if p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { - id = SanitizedAnchorName(string(data[prev:eol])) + if p.extensions&AutoHeadingIDs != 0 { + id = sanitized_anchor_name.Create(string(data[prev:eol])) } - p.r.Header(out, work, level, id) + block := p.addBlock(Heading, data[prev:eol]) + block.Level = level + block.HeadingID = id // find the end of the underline - for data[i] != '\n' { + for i < len(data) && data[i] != '\n' { i++ } return i @@ -1377,74 +1478,72 @@ func (p *parser) paragraph(out *bytes.Buffer, data []byte) int { } // if the next line starts a block of HTML, then the paragraph ends here - if p.flags&EXTENSION_LAX_HTML_BLOCKS != 0 { - if data[i] == '<' && p.html(out, current, false) > 0 { + if p.extensions&LaxHTMLBlocks != 0 { + if data[i] == '<' && p.html(current, false) > 0 { // rewind to before the HTML block - p.renderParagraph(out, data[:i]) + p.renderParagraph(data[:i]) return i } } - // if there's a prefixed header or a horizontal rule after this, paragraph is over - if p.isPrefixHeader(current) || p.isHRule(current) { - p.renderParagraph(out, data[:i]) + // if there's a prefixed heading or a horizontal rule after this, paragraph is over + if p.isPrefixHeading(current) || p.isHRule(current) { + p.renderParagraph(data[:i]) return i } // if there's a fenced code block, paragraph is over - if p.flags&EXTENSION_FENCED_CODE != 0 { - if p.fencedCodeBlock(out, current, false) > 0 { - p.renderParagraph(out, data[:i]) + if p.extensions&FencedCode != 0 { + if p.fencedCodeBlock(current, false) > 0 { + p.renderParagraph(data[:i]) return i } } // if there's a definition list item, prev line is a definition term - if p.flags&EXTENSION_DEFINITION_LISTS != 0 { + if p.extensions&DefinitionLists != 0 { if p.dliPrefix(current) != 0 { - return p.list(out, data[prev:], LIST_TYPE_DEFINITION) + ret := p.list(data[prev:], ListTypeDefinition) + return ret } } // if there's a list after this, paragraph is over - if p.flags&EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK != 0 { + if p.extensions&NoEmptyLineBeforeBlock != 0 { if p.uliPrefix(current) != 0 || p.oliPrefix(current) != 0 || p.quotePrefix(current) != 0 || p.codePrefix(current) != 0 { - p.renderParagraph(out, data[:i]) + p.renderParagraph(data[:i]) return i } } // otherwise, scan to the beginning of the next line - for data[i] != '\n' { - i++ + nl := bytes.IndexByte(data[i:], '\n') + if nl >= 0 { + i += nl + 1 + } else { + i += len(data[i:]) } - i++ } - p.renderParagraph(out, data[:i]) + p.renderParagraph(data[:i]) return i } -// SanitizedAnchorName returns a sanitized anchor name for the given text. -// -// It implements the algorithm specified in the package comment. -func SanitizedAnchorName(text string) string { - var anchorName []rune - futureDash := false - for _, r := range text { - switch { - case unicode.IsLetter(r) || unicode.IsNumber(r): - if futureDash && len(anchorName) > 0 { - anchorName = append(anchorName, '-') - } - futureDash = false - anchorName = append(anchorName, unicode.ToLower(r)) - default: - futureDash = true - } +func skipChar(data []byte, start int, char byte) int { + i := start + for i < len(data) && data[i] == char { + i++ } - return string(anchorName) + return i +} + +func skipUntilChar(text []byte, start int, char byte) int { + i := start + for i < len(text) && text[i] != char { + i++ + } + return i } diff --git a/vendor/github.com/russross/blackfriday/doc.go b/vendor/github.com/russross/blackfriday/doc.go index 9656c42a..5b3fa987 100644 --- a/vendor/github.com/russross/blackfriday/doc.go +++ b/vendor/github.com/russross/blackfriday/doc.go @@ -1,32 +1,18 @@ -// Package blackfriday is a Markdown processor. +// Package blackfriday is a markdown processor. // -// It translates plain text with simple formatting rules into HTML or LaTeX. +// It translates plain text with simple formatting rules into an AST, which can +// then be further processed to HTML (provided by Blackfriday itself) or other +// formats (provided by the community). // -// Sanitized Anchor Names +// The simplest way to invoke Blackfriday is to call the Run function. It will +// take a text input and produce a text output in HTML (or other format). // -// Blackfriday includes an algorithm for creating sanitized anchor names -// corresponding to a given input text. This algorithm is used to create -// anchors for headings when EXTENSION_AUTO_HEADER_IDS is enabled. The -// algorithm is specified below, so that other packages can create -// compatible anchor names and links to those anchors. +// A slightly more sophisticated way to use Blackfriday is to create a Markdown +// processor and to call Parse, which returns a syntax tree for the input +// document. You can leverage Blackfriday's parsing for content extraction from +// markdown documents. You can assign a custom renderer and set various options +// to the Markdown processor. // -// The algorithm iterates over the input text, interpreted as UTF-8, -// one Unicode code point (rune) at a time. All runes that are letters (category L) -// or numbers (category N) are considered valid characters. They are mapped to -// lower case, and included in the output. All other runes are considered -// invalid characters. Invalid characters that preceed the first valid character, -// as well as invalid character that follow the last valid character -// are dropped completely. All other sequences of invalid characters -// between two valid characters are replaced with a single dash character '-'. -// -// SanitizedAnchorName exposes this functionality, and can be used to -// create compatible links to the anchor names generated by blackfriday. -// This algorithm is also implemented in a small standalone package at -// github.com/shurcooL/sanitized_anchor_name. It can be useful for clients -// that want a small package and don't need full functionality of blackfriday. +// If you're interested in calling Blackfriday from command line, see +// https://github.com/russross/blackfriday-tool. package blackfriday - -// NOTE: Keep Sanitized Anchor Name algorithm in sync with package -// github.com/shurcooL/sanitized_anchor_name. -// Otherwise, users of sanitized_anchor_name will get anchor names -// that are incompatible with those generated by blackfriday. diff --git a/vendor/github.com/russross/blackfriday/esc.go b/vendor/github.com/russross/blackfriday/esc.go new file mode 100644 index 00000000..6385f27c --- /dev/null +++ b/vendor/github.com/russross/blackfriday/esc.go @@ -0,0 +1,34 @@ +package blackfriday + +import ( + "html" + "io" +) + +var htmlEscaper = [256][]byte{ + '&': []byte("&"), + '<': []byte("<"), + '>': []byte(">"), + '"': []byte("""), +} + +func escapeHTML(w io.Writer, s []byte) { + var start, end int + for end < len(s) { + escSeq := htmlEscaper[s[end]] + if escSeq != nil { + w.Write(s[start:end]) + w.Write(escSeq) + start = end + 1 + } + end++ + } + if start < len(s) && end <= len(s) { + w.Write(s[start:end]) + } +} + +func escLink(w io.Writer, text []byte) { + unesc := html.UnescapeString(string(text)) + escapeHTML(w, []byte(unesc)) +} diff --git a/vendor/github.com/russross/blackfriday/html.go b/vendor/github.com/russross/blackfriday/html.go index c917c7d3..25fb185e 100644 --- a/vendor/github.com/russross/blackfriday/html.go +++ b/vendor/github.com/russross/blackfriday/html.go @@ -18,46 +18,62 @@ package blackfriday import ( "bytes" "fmt" + "io" "regexp" - "strconv" "strings" ) -// Html renderer configuration options. +// HTMLFlags control optional behavior of HTML renderer. +type HTMLFlags int + +// HTML renderer configuration options. const ( - HTML_SKIP_HTML = 1 << iota // skip preformatted HTML blocks - HTML_SKIP_STYLE // skip embedded