mirror of
https://github.com/cwinfo/matterbridge.git
synced 2025-06-27 13:29:25 +00:00
Compare commits
434 Commits
Author | SHA1 | Date | |
---|---|---|---|
a5259f56c5 | |||
3f75ed9c18 | |||
49ece51167 | |||
e77c3eb20a | |||
59b2a5f8d0 | |||
28710d0bc7 | |||
ad4d461606 | |||
67905089ba | |||
f2483af561 | |||
c28b87641e | |||
f8e6a69d6e | |||
54216cec4b | |||
12989bbd99 | |||
38d09dba2e | |||
fafd0c68e9 | |||
41195c8e48 | |||
a97804548e | |||
ba653c0841 | |||
5b191f78a0 | |||
83ef61287e | |||
3527e09bc5 | |||
ddc5b3268f | |||
22307b1934 | |||
bd97357f8d | |||
10dab1366e | |||
52fc94c1fe | |||
c1c7961dd6 | |||
d3eef051b1 | |||
57654df81e | |||
0f791d7a9a | |||
58779e0d65 | |||
4ac361b5fd | |||
1e2f27c061 | |||
0302e4da82 | |||
dc8743e0c0 | |||
cc5ce3d5ae | |||
caaf6f3012 | |||
c5de8fd1cc | |||
c9f23869e3 | |||
61208c0e35 | |||
dcffc74255 | |||
23e23be1a6 | |||
710427248a | |||
a868042de2 | |||
15296cd8b4 | |||
717023245f | |||
320be5bffa | |||
778abea2d9 | |||
835a1ac3a6 | |||
20a7ef33f1 | |||
e72612c7ff | |||
04e0f001b0 | |||
5db24aa901 | |||
aec5e3d77b | |||
335ddf8db5 | |||
4abaf2b236 | |||
183d212431 | |||
e99532fb89 | |||
4aa646f6b0 | |||
9dcd51fb80 | |||
6dee988b76 | |||
5af40db396 | |||
b3553bee7a | |||
ac19c94b9f | |||
845f7dc331 | |||
2adeae37e1 | |||
16eb12b2a0 | |||
8411f2aa32 | |||
e8acc49cbd | |||
4bed073c65 | |||
272735fb26 | |||
b75cf2c189 | |||
1aaa992250 | |||
6256c066f1 | |||
870b89a8f0 | |||
65ac96913c | |||
480945cb09 | |||
bfc7130ed8 | |||
a0938d9386 | |||
2338c69d40 | |||
c714501a0e | |||
a58a3e5000 | |||
ba35212b67 | |||
f3e0358de7 | |||
8064744d3a | |||
d261949db2 | |||
877f0fe2e8 | |||
003d85772c | |||
e7e10131de | |||
830361e48b | |||
1b1a9ce250 | |||
25ac4c708f | |||
c268e90f49 | |||
c17512b7ab | |||
1b837b3dc7 | |||
2ece724f75 | |||
276ac840aa | |||
1f91461853 | |||
1f9874102a | |||
822605c157 | |||
e49266ae43 | |||
62e9de1a3b | |||
2ddc4f7ae9 | |||
2dd402675d | |||
25b1af1e11 | |||
75fb2b8156 | |||
2a403f8b85 | |||
c3d45a9f06 | |||
c07b85b625 | |||
511f653e6e | |||
5636eaca6d | |||
4b839b9958 | |||
3f79da84d5 | |||
d540638223 | |||
4ec9b6dd4e | |||
3bc219167a | |||
8a55c97b4e | |||
9e34162a09 | |||
860a371eeb | |||
41a46526a1 | |||
46b798ac1b | |||
359d0f2910 | |||
ad3cb0386b | |||
3a183cb218 | |||
2eecaccd1c | |||
5f30a98bc1 | |||
b8a2fcbaff | |||
01496cd080 | |||
6a968ab82a | |||
c0c4890887 | |||
171a53592d | |||
7811c330db | |||
9bcd131e66 | |||
c791423dd5 | |||
80bdf38388 | |||
9d9cb32f4e | |||
87229bab13 | |||
f065e9e4d5 | |||
3812693111 | |||
dd3c572256 | |||
c5dfe40326 | |||
ef278301e3 | |||
2888fd64b0 | |||
27c0f37e49 | |||
0774f6a5e7 | |||
4036d4459b | |||
ee643de5b6 | |||
8c7549a09e | |||
7a16146304 | |||
3d3809a21b | |||
29465397dd | |||
d300bb1735 | |||
2e703472f1 | |||
8fede90b9e | |||
d128f157c4 | |||
4fcedabfd0 | |||
246c8e4f74 | |||
4d2207aba7 | |||
17b8b86d68 | |||
fdb57230a3 | |||
7469732bbc | |||
d1dd6c3440 | |||
02612c0061 | |||
a4db63a773 | |||
035c2b906a | |||
6ea8be5749 | |||
36024d5439 | |||
8d52c98373 | |||
b4a4eb0057 | |||
b469c8ddbd | |||
eee0036c7f | |||
89c66b9430 | |||
bd38319d83 | |||
33dffd5ea8 | |||
57176dadd4 | |||
dd449a8705 | |||
587ad9f41d | |||
a16ad8bf3b | |||
1e0490bd36 | |||
8afc641f0c | |||
2e4d58cb92 | |||
02d7e2db65 | |||
f935c573e9 | |||
4a25e66c00 | |||
95f4e3448e | |||
eacb1c1771 | |||
07fd825349 | |||
be15cc8a36 | |||
2f68519b3c | |||
efe641f202 | |||
9bd663046a | |||
11b07f01ba | |||
6c2f370e6b | |||
936bccccd2 | |||
c30ffeb81e | |||
e05a323afd | |||
80895deae2 | |||
eddc691fc9 | |||
deb2d7194d | |||
fd8cfb11fb | |||
9407aa4600 | |||
263b8da37d | |||
b95988b4e2 | |||
35025e164a | |||
32bbab8518 | |||
84c0b745af | |||
8b286fb009 | |||
386fa58b67 | |||
c5cfbc2297 | |||
cd0a2beb11 | |||
73f01ad8d8 | |||
930b639cc9 | |||
58483ea70c | |||
072cac0347 | |||
956d7cf3f3 | |||
7558a2162e | |||
62b165c0b4 | |||
fe258e1b67 | |||
dc37232100 | |||
163f55f9c2 | |||
2d16fd085e | |||
e1a5f5bca5 | |||
6e772ee189 | |||
2b0f178ba3 | |||
79e6c9fa6c | |||
1426ddec5f | |||
e9105003b0 | |||
587bb06558 | |||
53e9664cde | |||
482fbac68f | |||
dcccd43427 | |||
397b8ff892 | |||
38a4cf315a | |||
5f8b24e32c | |||
678a7ceb4e | |||
077d494c7b | |||
09b243d8c2 | |||
991183e514 | |||
9bf10e4b58 | |||
884599d27d | |||
f8a6e65bfd | |||
6df6c5d615 | |||
93114b7682 | |||
9987ac3f13 | |||
01a32b2154 | |||
b3c3142bb2 | |||
77f1a959c3 | |||
e3dda0e812 | |||
38103d36b4 | |||
7685fe1724 | |||
01afe03a3f | |||
7fbbf89c58 | |||
84d259d8b3 | |||
8b47670a74 | |||
7f5dc1d461 | |||
43e765f4f9 | |||
adec73f542 | |||
fee159541f | |||
d81e6bf6ce | |||
70c93d970c | |||
4960273832 | |||
6c018ee6fe | |||
4ef32103ca | |||
e4ec27c5e2 | |||
20c04f7977 | |||
571f50d734 | |||
780ea6f7c0 | |||
4279906f6e | |||
2e54b97fc2 | |||
e1641b2c2e | |||
e0e1e4be80 | |||
d5845ce900 | |||
85f2cde4c3 | |||
cef64e01b3 | |||
94ea775232 | |||
2e4b7fac11 | |||
2867ec459a | |||
cd18d89894 | |||
449ed31e25 | |||
1f36904588 | |||
f7495dd0c3 | |||
a11f77835d | |||
af1ad82c8e | |||
4976338677 | |||
99d130d1ed | |||
4fb0544b0e | |||
0b4ac61435 | |||
1d5cd1d7c4 | |||
08ebee6b4f | |||
14830d9f1c | |||
a3dd0f1345 | |||
37873acfcd | |||
2dbe0eb557 | |||
50a0df4279 | |||
c3a8b7a997 | |||
95fac548bb | |||
581847f415 | |||
1b15897135 | |||
8e606e3cef | |||
be513622ac | |||
6f309f2108 | |||
92d9db5a2d | |||
96620a3c2c | |||
5249568b8e | |||
4a336a6bba | |||
60223d7f63 | |||
5131253191 | |||
035dc042a1 | |||
dfc513530b | |||
721e0a2dcd | |||
8452eb12da | |||
475bed5e19 | |||
40a967523c | |||
d3a34af073 | |||
e7107cf782 | |||
b7c918a195 | |||
61e4c9b28c | |||
e93847a95e | |||
545377742c | |||
47d38192b2 | |||
ac80c47036 | |||
1e84afbd90 | |||
d31e641bac | |||
4380c48b4b | |||
db0e4ba8c5 | |||
2d6ed51d94 | |||
9ca4fe7a5e | |||
e52b040b9c | |||
1accee1653 | |||
fff6f08cb6 | |||
0e527a4252 | |||
f10251a1a3 | |||
0d4bad16a3 | |||
8c6be434ac | |||
3ca4309e8a | |||
e8a2e1af63 | |||
1d240140c9 | |||
272eef544f | |||
fd756c5332 | |||
dce600ad51 | |||
d02a737e0c | |||
98ff59c716 | |||
0e96e9f9be | |||
e8c7898583 | |||
11f4a6897a | |||
002c5fd0d1 | |||
18504ec08d | |||
4737442185 | |||
596096d6da | |||
6af82401fc | |||
a0b84beb9b | |||
0816e96831 | |||
7baf386ede | |||
6e410b096e | |||
f9e5994348 | |||
ee77272cfd | |||
16ed2aca6a | |||
0f530e7902 | |||
4ed66ce20e | |||
b30e85836e | |||
e449a97bd0 | |||
39043f3fa4 | |||
12389d602e | |||
44144587a0 | |||
d0a30e354b | |||
c261dc89d5 | |||
c2c135bca2 | |||
eb20cb237d | |||
106404d32f | |||
e06efbad9f | |||
3311c7f923 | |||
3a6c655dfb | |||
e11d786775 | |||
889b6debc4 | |||
9cb3413d9c | |||
131826e1d1 | |||
96e21dd051 | |||
32e5f396e7 | |||
6c6000dbbd | |||
24defcb970 | |||
a1a11a88b3 | |||
a997ae29ad | |||
ff94796700 | |||
1f72ca4c4e | |||
46faad8b57 | |||
30f30364d5 | |||
073d90da88 | |||
c769e23a9a | |||
9db48f4794 | |||
911c597377 | |||
28244ffd9a | |||
3e38c7945c | |||
79ffb76f6e | |||
5fe4b749cf | |||
6991d85da9 | |||
c1c187a1ab | |||
055d12e3ef | |||
b49429d722 | |||
815c7f8d64 | |||
c879f79456 | |||
3bc25f4707 | |||
300cfe044a | |||
fb586f4a96 | |||
ced371bece | |||
a87cac1982 | |||
8fb5c7afa6 | |||
aceb830378 | |||
0f2976c5ce | |||
78b17977c5 | |||
6ec77e06ea | |||
e48db67649 | |||
e03f331f55 | |||
ff5aeeb1e1 | |||
33844fa60c | |||
85faa43145 | |||
59e6abcc11 | |||
38e3bbe5c9 | |||
51265d5464 | |||
de4c780410 | |||
6b18257185 | |||
4b1ebaf7d5 | |||
93db74e7e1 | |||
0e6fe4070a | |||
69b534ee99 | |||
71a504945b | |||
99ac7dc114 | |||
4984473c1b | |||
3fcce2d8a0 | |||
a53e699112 | |||
f29822db02 | |||
a63433e41b | |||
e0379ca5af | |||
4759ee6132 | |||
5ec94fdb43 |
22
.github/ISSUE_TEMPLATE.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
If you have a configuration problem, please first try to create a basic configuration following the instructions on [the wiki](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) before filing an issue.
|
||||||
|
|
||||||
|
Please answer the following questions.
|
||||||
|
|
||||||
|
### Which version of matterbridge are you using?
|
||||||
|
run ```matterbridge -version```
|
||||||
|
|
||||||
|
### If you're having problems with mattermost please specify mattermost version.
|
||||||
|
|
||||||
|
|
||||||
|
### Please describe the expected behavior.
|
||||||
|
|
||||||
|
|
||||||
|
### Please describe the actual behavior.
|
||||||
|
#### Use logs from running ```matterbridge -debug``` if possible.
|
||||||
|
|
||||||
|
|
||||||
|
### Any steps to reproduce the behavior?
|
||||||
|
|
||||||
|
|
||||||
|
### Please add your configuration file
|
||||||
|
#### (be sure to exclude or anonymize private data (tokens/passwords))
|
49
.travis.yml
Normal file
49
.travis.yml
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
language: go
|
||||||
|
go:
|
||||||
|
#- 1.7.x
|
||||||
|
- 1.8.x
|
||||||
|
# - tip
|
||||||
|
|
||||||
|
# we have everything vendored
|
||||||
|
install: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
- GOOS=linux GOARCH=amd64
|
||||||
|
# - GOOS=windows GOARCH=amd64
|
||||||
|
#- GOOS=linux GOARCH=arm
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
# It's ok if our code fails on unstable development versions of Go.
|
||||||
|
allow_failures:
|
||||||
|
- go: tip
|
||||||
|
# Don't wait for tip tests to finish. Mark the test run green if the
|
||||||
|
# tests pass on the stable versions of Go.
|
||||||
|
fast_finish: true
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
- /bin/bash ci/bintray.sh
|
||||||
|
#- golint -set_exit_status $PKGS # one last linter
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
provider: bintray
|
||||||
|
file: ci/deploy.json
|
||||||
|
user: 42wim
|
||||||
|
key:
|
||||||
|
secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI="
|
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
FROM alpine:edge
|
||||||
|
ENTRYPOINT ["/bin/matterbridge"]
|
||||||
|
|
||||||
|
COPY . /go/src/github.com/42wim/matterbridge
|
||||||
|
RUN apk update && apk add go git gcc musl-dev ca-certificates \
|
||||||
|
&& cd /go/src/github.com/42wim/matterbridge \
|
||||||
|
&& export GOPATH=/go \
|
||||||
|
&& go get \
|
||||||
|
&& go build -x -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge \
|
||||||
|
&& rm -rf /go \
|
||||||
|
&& apk del --purge git go gcc musl-dev
|
223
README.md
223
README.md
@ -1,17 +1,63 @@
|
|||||||
# matterbridge
|
# matterbridge
|
||||||
|
Click on one of the badges below to join the chat
|
||||||
|
|
||||||
Simple bridge between mattermost and IRC. Uses the in/outgoing webhooks.
|
[](https://gitter.im/42wim/matterbridge) [](https://webchat.freenode.net/?channels=matterbridgechat) [](https://discord.gg/AkKPtrQ) [](https://riot.im/app/#/room/#matterbridge:matrix.org) [](https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA) [](https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e) 
|
||||||
Relays public channel messages between mattermost and IRC.
|
|
||||||
|
|
||||||
Requires mattermost 1.2.0+
|
[](https://github.com/42wim/matterbridge/releases/latest) [](https://bintray.com/42wim/nightly/Matterbridge/_latestVersion)
|
||||||
|
|
||||||
There is also [matterbridge-plus] (https://github.com/42wim/matterbridge-plus) which uses the mattermost API and needs a dedicated user (bot). But requires no incoming/outgoing webhook setup.
|

|
||||||
|
|
||||||
## binaries
|
Simple bridge between Mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat, Hipchat(via xmpp), Matrix and Steam.
|
||||||
Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/tag/v0.3)
|
Has a REST API.
|
||||||
|
|
||||||
## building
|
# Table of Contents
|
||||||
Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH] (https://golang.org/doc/code.html#GOPATH)
|
* [Features](#features)
|
||||||
|
* [Requirements](#requirements)
|
||||||
|
* [Screenshots](https://github.com/42wim/matterbridge/wiki/)
|
||||||
|
* [Installing](#installing)
|
||||||
|
* [Binaries](#binaries)
|
||||||
|
* [Building](#building)
|
||||||
|
* [Configuration](#configuration)
|
||||||
|
* [Howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config)
|
||||||
|
* [Examples](#examples)
|
||||||
|
* [Running](#running)
|
||||||
|
* [Docker](#docker)
|
||||||
|
* [Changelog](#changelog)
|
||||||
|
* [FAQ](#faq)
|
||||||
|
* [Thanks](#thanks)
|
||||||
|
|
||||||
|
# Features
|
||||||
|
* Relays public channel messages between multiple mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat, Hipchat (via xmpp), Matrix and Steam.
|
||||||
|
Pick and mix.
|
||||||
|
* Matterbridge can also work with private groups on your mattermost/slack.
|
||||||
|
* Allow for bridging the same bridges, which means you can eg bridge between multiple mattermosts.
|
||||||
|
* The bridge is now a gateway which has support multiple in and out bridges. (and supports multiple gateways).
|
||||||
|
* REST API to read/post messages to bridges (WIP).
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
Accounts to one of the supported bridges
|
||||||
|
* [Mattermost](https://github.com/mattermost/platform/) 3.5.x - 3.10.x, 4.0.x
|
||||||
|
* [IRC](http://www.mirc.com/servers.html)
|
||||||
|
* [XMPP](https://jabber.org)
|
||||||
|
* [Gitter](https://gitter.im)
|
||||||
|
* [Slack](https://slack.com)
|
||||||
|
* [Discord](https://discordapp.com)
|
||||||
|
* [Telegram](https://telegram.org)
|
||||||
|
* [Hipchat](https://www.hipchat.com)
|
||||||
|
* [Rocket.chat](https://rocket.chat)
|
||||||
|
* [Matrix](https://matrix.org)
|
||||||
|
* [Steam](https://store.steampowered.com/)
|
||||||
|
|
||||||
|
# Screenshots
|
||||||
|
See https://github.com/42wim/matterbridge/wiki
|
||||||
|
|
||||||
|
# Installing
|
||||||
|
## Binaries
|
||||||
|
* Latest stable release [v1.0.1](https://github.com/42wim/matterbridge/releases/latest)
|
||||||
|
* Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/)
|
||||||
|
|
||||||
|
## Building
|
||||||
|
Go 1.7+ 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)
|
||||||
|
|
||||||
```
|
```
|
||||||
cd $GOPATH
|
cd $GOPATH
|
||||||
@ -25,78 +71,113 @@ $ ls bin/
|
|||||||
matterbridge
|
matterbridge
|
||||||
```
|
```
|
||||||
|
|
||||||
## running
|
# Configuration
|
||||||
1) Copy the matterbridge.conf.sample to matterbridge.conf in the same directory as the matterbridge binary.
|
## Basic configuration
|
||||||
2) Edit matterbridge.conf with the settings for your environment. See below for more config information.
|
See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
|
||||||
3) Now you can run matterbridge.
|
|
||||||
|
|
||||||
|
## 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)
|
||||||
```
|
```
|
||||||
Usage of matterbridge:
|
[irc]
|
||||||
-conf="matterbridge.conf": config file
|
[irc.freenode]
|
||||||
```
|
Server="irc.freenode.net:6667"
|
||||||
|
Nick="yourbotname"
|
||||||
Matterbridge will:
|
|
||||||
* start a webserver listening on the port specified in the configuration.
|
|
||||||
* connect to specified irc server and channel.
|
|
||||||
* send messages from mattermost to irc and vice versa, messages in mattermost will appear with irc-nick
|
|
||||||
|
|
||||||
## config
|
|
||||||
### matterbridge
|
|
||||||
matterbridge looks for matterbridge.conf in current directory. (use -conf to specify another file)
|
|
||||||
|
|
||||||
Look at matterbridge.conf.sample for an example
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
[IRC]
|
|
||||||
server="irc.freenode.net"
|
|
||||||
port=6667
|
|
||||||
UseTLS=false
|
|
||||||
SkipTLSVerify=true
|
|
||||||
nick="matterbot"
|
|
||||||
channel="#matterbridge"
|
|
||||||
UseSlackCircumfix=false
|
|
||||||
|
|
||||||
[mattermost]
|
[mattermost]
|
||||||
#url is your incoming webhook url (account settings - integrations - incoming webhooks)
|
[mattermost.work]
|
||||||
url="http://mattermost.yourdomain.com/hooks/incomingwebhookkey"
|
Server="yourmattermostserver.tld"
|
||||||
#port the bridge webserver will listen on
|
Team="yourteam"
|
||||||
port=9999
|
Login="yourlogin"
|
||||||
#address the webserver will bind to
|
Password="yourpass"
|
||||||
BindAddress="0.0.0.0"
|
PrefixMessagesWithNick=true
|
||||||
showjoinpart=true #show irc users joining and parting
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
#the token you get from the outgoing webhook in mattermost. If empty no token check will be done.
|
|
||||||
#if you use multiple IRC channel (see below, this must be empty!)
|
|
||||||
token=yourtokenfrommattermost
|
|
||||||
#disable certificate checking (selfsigned certificates)
|
|
||||||
#SkipTLSVerify=true
|
|
||||||
|
|
||||||
#multiple channel config
|
[[gateway]]
|
||||||
#token you can find in your outgoing webhook
|
name="mygateway"
|
||||||
[Token "outgoingwebhooktoken1"]
|
enable=true
|
||||||
IRCChannel="#off-topic"
|
[[gateway.inout]]
|
||||||
MMChannel="off-topic"
|
account="irc.freenode"
|
||||||
|
channel="#testing"
|
||||||
|
|
||||||
[Token "outgoingwebhooktoken2"]
|
[[gateway.inout]]
|
||||||
IRCChannel="#testing"
|
account="mattermost.work"
|
||||||
MMChannel="testing"
|
channel="off-topic"
|
||||||
|
|
||||||
[general]
|
|
||||||
#request your API key on https://github.com/giphy/GiphyAPI. This is a public beta key
|
|
||||||
GiphyApiKey="dc6zaTOxFJmzC"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### mattermost
|
### Bridge slack (#general) - discord (general)
|
||||||
You'll have to configure the incoming en outgoing webhooks.
|
```
|
||||||
|
[slack]
|
||||||
|
[slack.test]
|
||||||
|
Token="yourslacktoken"
|
||||||
|
PrefixMessagesWithNick=true
|
||||||
|
|
||||||
* incoming webhooks
|
[discord]
|
||||||
Go to "account settings" - integrations - "incoming webhooks".
|
[discord.test]
|
||||||
Choose a channel at "Add a new incoming webhook", this will create a webhook URL right below.
|
Token="yourdiscordtoken"
|
||||||
This URL should be set in the matterbridge.conf in the [mattermost] section (see above)
|
Server="yourdiscordservername"
|
||||||
|
|
||||||
* outgoing webhooks
|
[general]
|
||||||
Go to "account settings" - integrations - "outgoing webhooks".
|
RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
|
||||||
Choose a channel (the same as the one from incoming webhooks) and fill in the address and port of the server matterbridge will run on.
|
|
||||||
|
|
||||||
e.g. http://192.168.1.1:9999 (9999 is the port specified in [mattermost] section of matterbridge.conf)
|
[[gateway]]
|
||||||
|
name = "mygateway"
|
||||||
|
enable=true
|
||||||
|
|
||||||
|
[[gateway.inout]]
|
||||||
|
account = "discord.test"
|
||||||
|
channel="general"
|
||||||
|
|
||||||
|
[[gateway.inout]]
|
||||||
|
account ="slack.test"
|
||||||
|
channel = "general"
|
||||||
|
```
|
||||||
|
|
||||||
|
# Running
|
||||||
|
|
||||||
|
See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage of ./matterbridge:
|
||||||
|
-conf string
|
||||||
|
config file (default "matterbridge.toml")
|
||||||
|
-debug
|
||||||
|
enable debug
|
||||||
|
-gops
|
||||||
|
enable gops agent
|
||||||
|
-version
|
||||||
|
show version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
Create your matterbridge.toml file locally eg in ```/tmp/matterbridge.toml```
|
||||||
|
```
|
||||||
|
docker run -ti -v /tmp/matterbridge.toml:/matterbridge.toml 42wim/matterbridge
|
||||||
|
```
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.md)
|
||||||
|
|
||||||
|
# FAQ
|
||||||
|
|
||||||
|
See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
|
||||||
|
|
||||||
|
Want to tip ?
|
||||||
|
* eth: 0xb3f9b5387c66ad6be892bcb7bbc67862f3abc16f
|
||||||
|
* btc: 1N7cKHj5SfqBHBzDJ6kad4BzeqUBBS2zhs
|
||||||
|
|
||||||
|
# Thanks
|
||||||
|
Matterbridge wouldn't exist without these libraries:
|
||||||
|
* discord - https://github.com/bwmarrin/discordgo
|
||||||
|
* echo - https://github.com/labstack/echo
|
||||||
|
* gitter - https://github.com/sromku/go-gitter
|
||||||
|
* gops - https://github.com/google/gops
|
||||||
|
* irc - https://github.com/thoj/go-ircevent
|
||||||
|
* mattermost - https://github.com/mattermost/platform
|
||||||
|
* 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
|
||||||
|
101
bridge/api/api.go
Normal file
101
bridge/api/api.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/labstack/echo"
|
||||||
|
"github.com/labstack/echo/middleware"
|
||||||
|
"github.com/zfjagann/golang-ring"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Api struct {
|
||||||
|
Config *config.Protocol
|
||||||
|
Remote chan config.Message
|
||||||
|
Account string
|
||||||
|
Messages ring.Ring
|
||||||
|
sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiMessage struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
UserID string `json:"userid"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
Gateway string `json:"gateway"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var flog *log.Entry
|
||||||
|
var protocol = "api"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flog = log.WithFields(log.Fields{"module": protocol})
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.Protocol, account string, c chan config.Message) *Api {
|
||||||
|
b := &Api{}
|
||||||
|
e := echo.New()
|
||||||
|
b.Messages = ring.Ring{}
|
||||||
|
b.Messages.SetCapacity(cfg.Buffer)
|
||||||
|
b.Config = &cfg
|
||||||
|
b.Account = account
|
||||||
|
b.Remote = c
|
||||||
|
if b.Config.Token != "" {
|
||||||
|
e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
|
||||||
|
return key == b.Config.Token, nil
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
e.GET("/api/messages", b.handleMessages)
|
||||||
|
e.POST("/api/message", b.handlePostMessage)
|
||||||
|
go func() {
|
||||||
|
flog.Fatal(e.Start(cfg.BindAddress))
|
||||||
|
}()
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Api) Connect() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (b *Api) Disconnect() error {
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
func (b *Api) JoinChannel(channel config.ChannelInfo) error {
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Api) Send(msg config.Message) error {
|
||||||
|
b.Lock()
|
||||||
|
defer b.Unlock()
|
||||||
|
b.Messages.Enqueue(&msg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Api) handlePostMessage(c echo.Context) error {
|
||||||
|
message := &ApiMessage{}
|
||||||
|
if err := c.Bind(message); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
flog.Debugf("Sending message from %s on %s to gateway", message.Username, "api")
|
||||||
|
b.Remote <- config.Message{
|
||||||
|
Text: message.Text,
|
||||||
|
Username: message.Username,
|
||||||
|
UserID: message.UserID,
|
||||||
|
Channel: "api",
|
||||||
|
Avatar: message.Avatar,
|
||||||
|
Account: b.Account,
|
||||||
|
Gateway: message.Gateway,
|
||||||
|
Protocol: "api",
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Api) handleMessages(c echo.Context) error {
|
||||||
|
b.Lock()
|
||||||
|
defer b.Unlock()
|
||||||
|
c.JSONPretty(http.StatusOK, b.Messages.Values(), " ")
|
||||||
|
b.Messages = ring.Ring{}
|
||||||
|
return nil
|
||||||
|
}
|
106
bridge/bridge.go
Normal file
106
bridge/bridge.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package bridge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/42wim/matterbridge/bridge/api"
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
"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/steam"
|
||||||
|
"github.com/42wim/matterbridge/bridge/telegram"
|
||||||
|
"github.com/42wim/matterbridge/bridge/xmpp"
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bridger interface {
|
||||||
|
Send(msg config.Message) error
|
||||||
|
Connect() error
|
||||||
|
JoinChannel(channel config.ChannelInfo) error
|
||||||
|
Disconnect() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bridge struct {
|
||||||
|
Config config.Protocol
|
||||||
|
Bridger
|
||||||
|
Name string
|
||||||
|
Account string
|
||||||
|
Protocol string
|
||||||
|
Channels map[string]config.ChannelInfo
|
||||||
|
Joined map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg *config.Config, bridge *config.Bridge, c chan config.Message) *Bridge {
|
||||||
|
b := new(Bridge)
|
||||||
|
b.Channels = make(map[string]config.ChannelInfo)
|
||||||
|
accInfo := strings.Split(bridge.Account, ".")
|
||||||
|
protocol := accInfo[0]
|
||||||
|
name := accInfo[1]
|
||||||
|
b.Name = name
|
||||||
|
b.Protocol = protocol
|
||||||
|
b.Account = bridge.Account
|
||||||
|
b.Joined = make(map[string]bool)
|
||||||
|
|
||||||
|
// override config from environment
|
||||||
|
config.OverrideCfgFromEnv(cfg, protocol, name)
|
||||||
|
switch protocol {
|
||||||
|
case "mattermost":
|
||||||
|
b.Config = cfg.Mattermost[name]
|
||||||
|
b.Bridger = bmattermost.New(cfg.Mattermost[name], bridge.Account, c)
|
||||||
|
case "irc":
|
||||||
|
b.Config = cfg.IRC[name]
|
||||||
|
b.Bridger = birc.New(cfg.IRC[name], bridge.Account, c)
|
||||||
|
case "gitter":
|
||||||
|
b.Config = cfg.Gitter[name]
|
||||||
|
b.Bridger = bgitter.New(cfg.Gitter[name], bridge.Account, c)
|
||||||
|
case "slack":
|
||||||
|
b.Config = cfg.Slack[name]
|
||||||
|
b.Bridger = bslack.New(cfg.Slack[name], bridge.Account, c)
|
||||||
|
case "xmpp":
|
||||||
|
b.Config = cfg.Xmpp[name]
|
||||||
|
b.Bridger = bxmpp.New(cfg.Xmpp[name], bridge.Account, c)
|
||||||
|
case "discord":
|
||||||
|
b.Config = cfg.Discord[name]
|
||||||
|
b.Bridger = bdiscord.New(cfg.Discord[name], bridge.Account, c)
|
||||||
|
case "telegram":
|
||||||
|
b.Config = cfg.Telegram[name]
|
||||||
|
b.Bridger = btelegram.New(cfg.Telegram[name], bridge.Account, c)
|
||||||
|
case "rocketchat":
|
||||||
|
b.Config = cfg.Rocketchat[name]
|
||||||
|
b.Bridger = brocketchat.New(cfg.Rocketchat[name], bridge.Account, c)
|
||||||
|
case "matrix":
|
||||||
|
b.Config = cfg.Matrix[name]
|
||||||
|
b.Bridger = bmatrix.New(cfg.Matrix[name], bridge.Account, c)
|
||||||
|
case "steam":
|
||||||
|
b.Config = cfg.Steam[name]
|
||||||
|
b.Bridger = bsteam.New(cfg.Steam[name], bridge.Account, c)
|
||||||
|
case "api":
|
||||||
|
b.Config = cfg.Api[name]
|
||||||
|
b.Bridger = api.New(cfg.Api[name], bridge.Account, c)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bridge) JoinChannels() error {
|
||||||
|
err := b.joinChannels(b.Channels, b.Joined)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map[string]bool) error {
|
||||||
|
for ID, channel := range channels {
|
||||||
|
if !exists[ID] {
|
||||||
|
log.Infof("%s: joining %s (%s)", b.Account, channel.Name, ID)
|
||||||
|
err := b.JoinChannel(channel)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
exists[ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
221
bridge/config/config.go
Normal file
221
bridge/config/config.go
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
EVENT_JOIN_LEAVE = "join_leave"
|
||||||
|
EVENT_FAILURE = "failure"
|
||||||
|
EVENT_REJOIN_CHANNELS = "rejoin_channels"
|
||||||
|
EVENT_USER_ACTION = "user_action"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
UserID string `json:"userid"` // userid on the bridge
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
Account string `json:"account"`
|
||||||
|
Event string `json:"event"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Gateway string `json:"gateway"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChannelInfo struct {
|
||||||
|
Name string
|
||||||
|
Account string
|
||||||
|
Direction string
|
||||||
|
ID string
|
||||||
|
SameChannel map[string]bool
|
||||||
|
Options ChannelOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
type Protocol struct {
|
||||||
|
AuthCode string // steam
|
||||||
|
BindAddress string // mattermost, slack // DEPRECATED
|
||||||
|
Buffer int // api
|
||||||
|
EditSuffix string // mattermost, slack, discord, telegram, gitter
|
||||||
|
EditDisable bool // mattermost, slack, discord, telegram, gitter
|
||||||
|
IconURL string // mattermost, slack
|
||||||
|
IgnoreNicks string // all protocols
|
||||||
|
IgnoreMessages string // all protocols
|
||||||
|
Jid string // xmpp
|
||||||
|
Login string // mattermost, matrix
|
||||||
|
Muc string // xmpp
|
||||||
|
Name string // all protocols
|
||||||
|
Nick string // all protocols
|
||||||
|
NickFormatter string // mattermost, slack
|
||||||
|
NickServNick string // IRC
|
||||||
|
NickServPassword string // IRC
|
||||||
|
NicksPerRow int // mattermost, slack
|
||||||
|
NoHomeServerSuffix bool // matrix
|
||||||
|
NoTLS bool // mattermost
|
||||||
|
Password string // IRC,mattermost,XMPP,matrix
|
||||||
|
PrefixMessagesWithNick bool // mattemost, slack
|
||||||
|
Protocol string //all protocols
|
||||||
|
MessageQueue int // IRC, size of message queue for flood control
|
||||||
|
MessageDelay int // IRC, time in millisecond to wait between messages
|
||||||
|
MessageLength int // IRC, max length of a message allowed
|
||||||
|
MessageFormat string // telegram
|
||||||
|
RemoteNickFormat string // all protocols
|
||||||
|
Server string // IRC,mattermost,XMPP,discord
|
||||||
|
ShowJoinPart bool // all protocols
|
||||||
|
ShowEmbeds bool // discord
|
||||||
|
SkipTLSVerify bool // IRC, mattermost
|
||||||
|
Team string // mattermost
|
||||||
|
Token string // gitter, slack, discord, api
|
||||||
|
URL string // mattermost, slack // DEPRECATED
|
||||||
|
UseAPI bool // mattermost, slack
|
||||||
|
UseSASL bool // IRC
|
||||||
|
UseTLS bool // IRC
|
||||||
|
UseFirstName bool // telegram
|
||||||
|
UseUserName bool // discord
|
||||||
|
UseInsecureURL bool // telegram
|
||||||
|
WebhookBindAddress string // mattermost, slack
|
||||||
|
WebhookURL string // mattermost, slack
|
||||||
|
WebhookUse string // mattermost, slack, discord
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChannelOptions struct {
|
||||||
|
Key string // irc
|
||||||
|
WebhookURL string // discord
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bridge struct {
|
||||||
|
Account string
|
||||||
|
Channel string
|
||||||
|
Options ChannelOptions
|
||||||
|
SameChannel bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Gateway struct {
|
||||||
|
Name string
|
||||||
|
Enable bool
|
||||||
|
In []Bridge
|
||||||
|
Out []Bridge
|
||||||
|
InOut []Bridge
|
||||||
|
}
|
||||||
|
|
||||||
|
type SameChannelGateway struct {
|
||||||
|
Name string
|
||||||
|
Enable bool
|
||||||
|
Channels []string
|
||||||
|
Accounts []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Api map[string]Protocol
|
||||||
|
IRC map[string]Protocol
|
||||||
|
Mattermost map[string]Protocol
|
||||||
|
Matrix map[string]Protocol
|
||||||
|
Slack map[string]Protocol
|
||||||
|
Steam map[string]Protocol
|
||||||
|
Gitter map[string]Protocol
|
||||||
|
Xmpp map[string]Protocol
|
||||||
|
Discord map[string]Protocol
|
||||||
|
Telegram map[string]Protocol
|
||||||
|
Rocketchat map[string]Protocol
|
||||||
|
General Protocol
|
||||||
|
Gateway []Gateway
|
||||||
|
SameChannelGateway []SameChannelGateway
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfig(cfgfile string) *Config {
|
||||||
|
var cfg Config
|
||||||
|
if _, err := toml.DecodeFile(cfgfile, &cfg); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fail := false
|
||||||
|
for k, v := range cfg.Mattermost {
|
||||||
|
res := Deprecated(v, "mattermost."+k)
|
||||||
|
if res {
|
||||||
|
fail = res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k, v := range cfg.Slack {
|
||||||
|
res := Deprecated(v, "slack."+k)
|
||||||
|
if res {
|
||||||
|
fail = res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k, v := range cfg.Rocketchat {
|
||||||
|
res := Deprecated(v, "rocketchat."+k)
|
||||||
|
if res {
|
||||||
|
fail = res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fail {
|
||||||
|
log.Fatalf("Fix your config. Please see changelog for more information")
|
||||||
|
}
|
||||||
|
return &cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func OverrideCfgFromEnv(cfg *Config, protocol string, account string) {
|
||||||
|
var protoCfg Protocol
|
||||||
|
val := reflect.ValueOf(cfg).Elem()
|
||||||
|
// loop over the Config struct
|
||||||
|
for i := 0; i < val.NumField(); i++ {
|
||||||
|
typeField := val.Type().Field(i)
|
||||||
|
// look for the protocol map (both lowercase)
|
||||||
|
if strings.ToLower(typeField.Name) == protocol {
|
||||||
|
// get the Protocol struct from the map
|
||||||
|
data := val.Field(i).MapIndex(reflect.ValueOf(account))
|
||||||
|
protoCfg = data.Interface().(Protocol)
|
||||||
|
protoStruct := reflect.ValueOf(&protoCfg).Elem()
|
||||||
|
// loop over the found protocol struct
|
||||||
|
for i := 0; i < protoStruct.NumField(); i++ {
|
||||||
|
typeField := protoStruct.Type().Field(i)
|
||||||
|
// build our environment key (eg MATTERBRIDGE_MATTERMOST_WORK_LOGIN)
|
||||||
|
key := "matterbridge_" + protocol + "_" + account + "_" + typeField.Name
|
||||||
|
key = strings.ToUpper(key)
|
||||||
|
// search the environment
|
||||||
|
res := os.Getenv(key)
|
||||||
|
// if it exists and the current field is a string
|
||||||
|
// then update the current field
|
||||||
|
if res != "" {
|
||||||
|
fieldVal := protoStruct.Field(i)
|
||||||
|
if fieldVal.Kind() == reflect.String {
|
||||||
|
log.Printf("config: overriding %s from env with %s\n", key, res)
|
||||||
|
fieldVal.Set(reflect.ValueOf(res))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// update the map with the modified Protocol (cfg.Protocol[account] = Protocol)
|
||||||
|
val.Field(i).SetMapIndex(reflect.ValueOf(account), reflect.ValueOf(protoCfg))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetIconURL(msg *Message, cfg *Protocol) string {
|
||||||
|
iconURL := cfg.IconURL
|
||||||
|
info := strings.Split(msg.Account, ".")
|
||||||
|
protocol := info[0]
|
||||||
|
name := info[1]
|
||||||
|
iconURL = strings.Replace(iconURL, "{NICK}", msg.Username, -1)
|
||||||
|
iconURL = strings.Replace(iconURL, "{BRIDGE}", name, -1)
|
||||||
|
iconURL = strings.Replace(iconURL, "{PROTOCOL}", protocol, -1)
|
||||||
|
return iconURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func Deprecated(cfg Protocol, account string) bool {
|
||||||
|
if cfg.BindAddress != "" {
|
||||||
|
log.Printf("ERROR: %s BindAddress is deprecated, you need to change it to WebhookBindAddress.", account)
|
||||||
|
} else if cfg.URL != "" {
|
||||||
|
log.Printf("ERROR: %s URL is deprecated, you need to change it to WebhookURL.", account)
|
||||||
|
} else if cfg.UseAPI {
|
||||||
|
log.Printf("ERROR: %s UseAPI is deprecated, it's enabled by default, please remove it from your config file.", account)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
//log.Fatalf("ERROR: Fix your config: %s", account)
|
||||||
|
}
|
360
bridge/discord/discord.go
Normal file
360
bridge/discord/discord.go
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
package bdiscord
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/bwmarrin/discordgo"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bdiscord struct {
|
||||||
|
c *discordgo.Session
|
||||||
|
Config *config.Protocol
|
||||||
|
Remote chan config.Message
|
||||||
|
Account string
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
var flog *log.Entry
|
||||||
|
var protocol = "discord"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flog = log.WithFields(log.Fields{"module": protocol})
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.Protocol, account string, c chan config.Message) *bdiscord {
|
||||||
|
b := &bdiscord{}
|
||||||
|
b.Config = &cfg
|
||||||
|
b.Remote = c
|
||||||
|
b.Account = account
|
||||||
|
b.userMemberMap = make(map[string]*discordgo.Member)
|
||||||
|
b.channelInfoMap = make(map[string]*config.ChannelInfo)
|
||||||
|
if b.Config.WebhookURL != "" {
|
||||||
|
flog.Debug("Configuring Discord Incoming Webhook")
|
||||||
|
b.webhookID, b.webhookToken = b.splitURL(b.Config.WebhookURL)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bdiscord) Connect() error {
|
||||||
|
var err error
|
||||||
|
flog.Info("Connecting")
|
||||||
|
if b.Config.WebhookURL == "" {
|
||||||
|
flog.Info("Connecting using token")
|
||||||
|
} else {
|
||||||
|
flog.Info("Connecting using webhookurl (for posting) and token")
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(b.Config.Token, "Bot ") {
|
||||||
|
b.Config.Token = "Bot " + b.Config.Token
|
||||||
|
}
|
||||||
|
b.c, err = discordgo.New(b.Config.Token)
|
||||||
|
if err != nil {
|
||||||
|
flog.Debugf("%#v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
flog.Info("Connection succeeded")
|
||||||
|
b.c.AddHandler(b.messageCreate)
|
||||||
|
b.c.AddHandler(b.memberUpdate)
|
||||||
|
b.c.AddHandler(b.messageUpdate)
|
||||||
|
err = b.c.Open()
|
||||||
|
if err != nil {
|
||||||
|
flog.Debugf("%#v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
guilds, err := b.c.UserGuilds(100, "", "")
|
||||||
|
if err != nil {
|
||||||
|
flog.Debugf("%#v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
userinfo, err := b.c.User("@me")
|
||||||
|
if err != nil {
|
||||||
|
flog.Debugf("%#v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.Nick = userinfo.Username
|
||||||
|
for _, guild := range guilds {
|
||||||
|
if guild.Name == b.Config.Server {
|
||||||
|
b.Channels, err = b.c.GuildChannels(guild.ID)
|
||||||
|
b.guildID = guild.ID
|
||||||
|
if err != nil {
|
||||||
|
flog.Debugf("%#v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bdiscord) Disconnect() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bdiscord) JoinChannel(channel config.ChannelInfo) error {
|
||||||
|
b.channelInfoMap[channel.ID] = &channel
|
||||||
|
idcheck := strings.Split(channel.Name, "ID:")
|
||||||
|
if len(idcheck) > 1 {
|
||||||
|
b.UseChannelID = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bdiscord) Send(msg config.Message) error {
|
||||||
|
flog.Debugf("Receiving %#v", msg)
|
||||||
|
channelID := b.getChannelID(msg.Channel)
|
||||||
|
if channelID == "" {
|
||||||
|
flog.Errorf("Could not find channelID for %v", msg.Channel)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if msg.Event == config.EVENT_USER_ACTION {
|
||||||
|
msg.Text = "_" + msg.Text + "_"
|
||||||
|
}
|
||||||
|
|
||||||
|
wID := b.webhookID
|
||||||
|
wToken := b.webhookToken
|
||||||
|
if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
|
||||||
|
if ci.Options.WebhookURL != "" {
|
||||||
|
wID, wToken = b.splitURL(ci.Options.WebhookURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if wID == "" {
|
||||||
|
flog.Debugf("Broadcasting using token (API)")
|
||||||
|
b.c.ChannelMessageSend(channelID, msg.Username+msg.Text)
|
||||||
|
} else {
|
||||||
|
flog.Debugf("Broadcasting using Webhook")
|
||||||
|
b.c.WebhookExecute(
|
||||||
|
wID,
|
||||||
|
wToken,
|
||||||
|
true,
|
||||||
|
&discordgo.WebhookParams{
|
||||||
|
Content: msg.Text,
|
||||||
|
Username: msg.Username,
|
||||||
|
AvatarURL: msg.Avatar,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) {
|
||||||
|
if b.Config.EditDisable {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// only when message is actually edited
|
||||||
|
if m.Message.EditedTimestamp != "" {
|
||||||
|
flog.Debugf("Sending edit message")
|
||||||
|
m.Content = m.Content + b.Config.EditSuffix
|
||||||
|
b.messageCreate(s, (*discordgo.MessageCreate)(m))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.Attachments) > 0 {
|
||||||
|
for _, attach := range m.Attachments {
|
||||||
|
m.Content = m.Content + "\n" + attach.URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var text string
|
||||||
|
if m.Content != "" {
|
||||||
|
flog.Debugf("Receiving message %#v", m.Message)
|
||||||
|
if len(m.MentionRoles) > 0 {
|
||||||
|
m.Message.Content = b.replaceRoleMentions(m.Message.Content)
|
||||||
|
}
|
||||||
|
m.Message.Content = b.stripCustomoji(m.Message.Content)
|
||||||
|
m.Message.Content = b.replaceChannelMentions(m.Message.Content)
|
||||||
|
text = m.ContentWithMentionsReplaced()
|
||||||
|
}
|
||||||
|
|
||||||
|
rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg",
|
||||||
|
UserID: m.Author.ID}
|
||||||
|
|
||||||
|
rmsg.Channel = b.getChannelName(m.ChannelID)
|
||||||
|
if b.UseChannelID {
|
||||||
|
rmsg.Channel = "ID:" + m.ChannelID
|
||||||
|
}
|
||||||
|
|
||||||
|
if !b.Config.UseUserName {
|
||||||
|
rmsg.Username = b.getNick(m.Author)
|
||||||
|
} else {
|
||||||
|
rmsg.Username = m.Author.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Config.ShowEmbeds && m.Message.Embeds != nil {
|
||||||
|
for _, embed := range m.Message.Embeds {
|
||||||
|
text = text + "embed: " + embed.Title + " - " + embed.Description + " - " + embed.URL + "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no empty messages
|
||||||
|
if text == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
text, ok := b.replaceAction(text)
|
||||||
|
if ok {
|
||||||
|
rmsg.Event = config.EVENT_USER_ACTION
|
||||||
|
}
|
||||||
|
|
||||||
|
rmsg.Text = text
|
||||||
|
flog.Debugf("Sending message from %s on %s to gateway", m.Author.Username, b.Account)
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) {
|
||||||
|
b.Lock()
|
||||||
|
if _, ok := b.userMemberMap[m.Member.User.ID]; ok {
|
||||||
|
flog.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) replaceRoleMentions(text string) string {
|
||||||
|
roles, err := b.c.GuildRoles(b.guildID)
|
||||||
|
if err != nil {
|
||||||
|
flog.Debugf("%#v", string(err.(*discordgo.RESTError).ResponseBody))
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
for _, role := range roles {
|
||||||
|
text = strings.Replace(text, "<@&"+role.ID+">", "@"+role.Name, -1)
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "/")
|
||||||
|
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.Config.WebhookURL != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, channel := range b.channelInfoMap {
|
||||||
|
if channel.Options.WebhookURL != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isWebhookID returns true if the specified id is used in a defined webhook
|
||||||
|
func (b *bdiscord) isWebhookID(id string) bool {
|
||||||
|
if b.Config.WebhookURL != "" {
|
||||||
|
wID, _ := b.splitURL(b.Config.WebhookURL)
|
||||||
|
if wID == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, channel := range b.channelInfoMap {
|
||||||
|
if channel.Options.WebhookURL != "" {
|
||||||
|
wID, _ := b.splitURL(channel.Options.WebhookURL)
|
||||||
|
if wID == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
130
bridge/gitter/gitter.go
Normal file
130
bridge/gitter/gitter.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package bgitter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/sromku/go-gitter"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bgitter struct {
|
||||||
|
c *gitter.Gitter
|
||||||
|
Config *config.Protocol
|
||||||
|
Remote chan config.Message
|
||||||
|
Account string
|
||||||
|
Users []gitter.User
|
||||||
|
Rooms []gitter.Room
|
||||||
|
}
|
||||||
|
|
||||||
|
var flog *log.Entry
|
||||||
|
var protocol = "gitter"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flog = log.WithFields(log.Fields{"module": protocol})
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.Protocol, account string, c chan config.Message) *Bgitter {
|
||||||
|
b := &Bgitter{}
|
||||||
|
b.Config = &cfg
|
||||||
|
b.Remote = c
|
||||||
|
b.Account = account
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bgitter) Connect() error {
|
||||||
|
var err error
|
||||||
|
flog.Info("Connecting")
|
||||||
|
b.c = gitter.New(b.Config.Token)
|
||||||
|
_, err = b.c.GetUser()
|
||||||
|
if err != nil {
|
||||||
|
flog.Debugf("%#v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
flog.Info("Connection succeeded")
|
||||||
|
b.Rooms, _ = b.c.GetRooms()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bgitter) Disconnect() error {
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bgitter) JoinChannel(channel config.ChannelInfo) error {
|
||||||
|
roomID, err := b.c.GetRoomId(channel.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Could not find roomID for %v. Please create the room on gitter.im", channel.Name)
|
||||||
|
}
|
||||||
|
room, err := b.c.GetRoom(roomID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.Rooms = append(b.Rooms, *room)
|
||||||
|
user, err := b.c.GetUser()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = b.c.JoinRoom(roomID, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
users, _ := b.c.GetUsersInRoom(roomID)
|
||||||
|
b.Users = append(b.Users, users...)
|
||||||
|
stream := b.c.Stream(roomID)
|
||||||
|
go b.c.Listen(stream)
|
||||||
|
|
||||||
|
go func(stream *gitter.Stream, room string) {
|
||||||
|
for event := range stream.Event {
|
||||||
|
switch ev := event.Data.(type) {
|
||||||
|
case *gitter.MessageReceived:
|
||||||
|
// check for ZWSP to see if it's not an echo
|
||||||
|
if !strings.HasSuffix(ev.Message.Text, "") {
|
||||||
|
flog.Debugf("Sending message from %s on %s to gateway", ev.Message.From.Username, b.Account)
|
||||||
|
rmsg := config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room,
|
||||||
|
Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username), UserID: ev.Message.From.ID}
|
||||||
|
if strings.HasPrefix(ev.Message.Text, "@"+ev.Message.From.Username) {
|
||||||
|
rmsg.Event = config.EVENT_USER_ACTION
|
||||||
|
rmsg.Text = strings.Replace(rmsg.Text, "@"+ev.Message.From.Username+" ", "", -1)
|
||||||
|
}
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
case *gitter.GitterConnectionClosed:
|
||||||
|
flog.Errorf("connection with gitter closed for room %s", room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(stream, room.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bgitter) Send(msg config.Message) error {
|
||||||
|
flog.Debugf("Receiving %#v", msg)
|
||||||
|
roomID := b.getRoomID(msg.Channel)
|
||||||
|
if roomID == "" {
|
||||||
|
flog.Errorf("Could not find roomID for %v", msg.Channel)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// add ZWSP because gitter echoes our own messages
|
||||||
|
return b.c.SendMessage(roomID, msg.Username+msg.Text+" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bgitter) getRoomID(channel string) string {
|
||||||
|
for _, v := range b.Rooms {
|
||||||
|
if v.URI == channel {
|
||||||
|
return v.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bgitter) getAvatar(user string) string {
|
||||||
|
var avatar string
|
||||||
|
if b.Users != nil {
|
||||||
|
for _, u := range b.Users {
|
||||||
|
if user == u.Username {
|
||||||
|
return u.AvatarURLSmall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return avatar
|
||||||
|
}
|
61
bridge/irc/helper.go
Normal file
61
bridge/irc/helper.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
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
|
||||||
|
}
|
334
bridge/irc/irc.go
Normal file
334
bridge/irc/irc.go
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
package birc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"github.com/42wim/go-ircevent"
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/paulrosania/go-charset/charset"
|
||||||
|
_ "github.com/paulrosania/go-charset/data"
|
||||||
|
"github.com/saintfish/chardet"
|
||||||
|
ircm "github.com/sorcix/irc"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Birc struct {
|
||||||
|
i *irc.Connection
|
||||||
|
Nick string
|
||||||
|
names map[string][]string
|
||||||
|
Config *config.Protocol
|
||||||
|
Remote chan config.Message
|
||||||
|
connected chan struct{}
|
||||||
|
Local chan config.Message // local queue for flood control
|
||||||
|
Account string
|
||||||
|
FirstConnection bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var flog *log.Entry
|
||||||
|
var protocol = "irc"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flog = log.WithFields(log.Fields{"module": protocol})
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.Protocol, account string, c chan config.Message) *Birc {
|
||||||
|
b := &Birc{}
|
||||||
|
b.Config = &cfg
|
||||||
|
b.Nick = b.Config.Nick
|
||||||
|
b.Remote = c
|
||||||
|
b.names = make(map[string][]string)
|
||||||
|
b.Account = account
|
||||||
|
b.connected = make(chan struct{})
|
||||||
|
if b.Config.MessageDelay == 0 {
|
||||||
|
b.Config.MessageDelay = 1300
|
||||||
|
}
|
||||||
|
if b.Config.MessageQueue == 0 {
|
||||||
|
b.Config.MessageQueue = 30
|
||||||
|
}
|
||||||
|
if b.Config.MessageLength == 0 {
|
||||||
|
b.Config.MessageLength = 400
|
||||||
|
}
|
||||||
|
b.FirstConnection = true
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Birc) Command(msg *config.Message) string {
|
||||||
|
switch msg.Text {
|
||||||
|
case "!users":
|
||||||
|
b.i.AddCallback(ircm.RPL_NAMREPLY, b.storeNames)
|
||||||
|
b.i.AddCallback(ircm.RPL_ENDOFNAMES, b.endNames)
|
||||||
|
b.i.SendRaw("NAMES " + msg.Channel)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Birc) Connect() error {
|
||||||
|
b.Local = make(chan config.Message, b.Config.MessageQueue+10)
|
||||||
|
flog.Infof("Connecting %s", b.Config.Server)
|
||||||
|
i := irc.IRC(b.Config.Nick, b.Config.Nick)
|
||||||
|
if log.GetLevel() == log.DebugLevel {
|
||||||
|
i.Debug = true
|
||||||
|
}
|
||||||
|
i.UseTLS = b.Config.UseTLS
|
||||||
|
i.UseSASL = b.Config.UseSASL
|
||||||
|
i.SASLLogin = b.Config.NickServNick
|
||||||
|
i.SASLPassword = b.Config.NickServPassword
|
||||||
|
i.TLSConfig = &tls.Config{InsecureSkipVerify: b.Config.SkipTLSVerify}
|
||||||
|
i.KeepAlive = time.Minute
|
||||||
|
i.PingFreq = time.Minute
|
||||||
|
if b.Config.Password != "" {
|
||||||
|
i.Password = b.Config.Password
|
||||||
|
}
|
||||||
|
i.AddCallback(ircm.RPL_WELCOME, b.handleNewConnection)
|
||||||
|
err := i.Connect(b.Config.Server)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.i = i
|
||||||
|
select {
|
||||||
|
case <-b.connected:
|
||||||
|
flog.Info("Connection succeeded")
|
||||||
|
case <-time.After(time.Second * 30):
|
||||||
|
return fmt.Errorf("connection timed out")
|
||||||
|
}
|
||||||
|
i.Debug = false
|
||||||
|
// clear on reconnects
|
||||||
|
i.ClearCallback(ircm.RPL_WELCOME)
|
||||||
|
i.AddCallback(ircm.RPL_WELCOME, func(event *irc.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.Nick
|
||||||
|
})
|
||||||
|
go i.Loop()
|
||||||
|
go b.doSend()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Birc) Disconnect() error {
|
||||||
|
//b.i.Disconnect()
|
||||||
|
close(b.Local)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
|
||||||
|
if channel.Options.Key != "" {
|
||||||
|
flog.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name)
|
||||||
|
b.i.Join(channel.Name + " " + channel.Options.Key)
|
||||||
|
} else {
|
||||||
|
b.i.Join(channel.Name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Birc) Send(msg config.Message) error {
|
||||||
|
flog.Debugf("Receiving %#v", msg)
|
||||||
|
if strings.HasPrefix(msg.Text, "!") {
|
||||||
|
b.Command(&msg)
|
||||||
|
}
|
||||||
|
for _, text := range strings.Split(msg.Text, "\n") {
|
||||||
|
if len(text) > b.Config.MessageLength {
|
||||||
|
text = text[:b.Config.MessageLength] + " <message clipped>"
|
||||||
|
}
|
||||||
|
if len(b.Local) < b.Config.MessageQueue {
|
||||||
|
if len(b.Local) == b.Config.MessageQueue-1 {
|
||||||
|
text = text + " <message clipped>"
|
||||||
|
}
|
||||||
|
b.Local <- config.Message{Text: text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event}
|
||||||
|
} else {
|
||||||
|
flog.Debugf("flooding, dropping message (queue at %d)", len(b.Local))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Birc) doSend() {
|
||||||
|
rate := time.Millisecond * time.Duration(b.Config.MessageDelay)
|
||||||
|
throttle := time.NewTicker(rate)
|
||||||
|
for msg := range b.Local {
|
||||||
|
<-throttle.C
|
||||||
|
if msg.Event == config.EVENT_USER_ACTION {
|
||||||
|
b.i.Action(msg.Channel, msg.Username+msg.Text)
|
||||||
|
} else {
|
||||||
|
b.i.Privmsg(msg.Channel, msg.Username+msg.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Birc) endNames(event *irc.Event) {
|
||||||
|
channel := event.Arguments[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),
|
||||||
|
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),
|
||||||
|
Channel: channel, Account: b.Account}
|
||||||
|
b.names[channel] = nil
|
||||||
|
b.i.ClearCallback(ircm.RPL_NAMREPLY)
|
||||||
|
b.i.ClearCallback(ircm.RPL_ENDOFNAMES)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Birc) handleNewConnection(event *irc.Event) {
|
||||||
|
flog.Debug("Registering callbacks")
|
||||||
|
i := b.i
|
||||||
|
b.Nick = event.Arguments[0]
|
||||||
|
i.AddCallback("PRIVMSG", b.handlePrivMsg)
|
||||||
|
i.AddCallback("CTCP_ACTION", b.handlePrivMsg)
|
||||||
|
i.AddCallback(ircm.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
|
||||||
|
i.AddCallback(ircm.NOTICE, b.handleNotice)
|
||||||
|
//i.AddCallback(ircm.RPL_MYINFO, func(e *irc.Event) { flog.Infof("%s: %s", e.Code, strings.Join(e.Arguments[1:], " ")) })
|
||||||
|
i.AddCallback("PING", func(e *irc.Event) {
|
||||||
|
i.SendRaw("PONG :" + e.Message())
|
||||||
|
flog.Debugf("PING/PONG")
|
||||||
|
})
|
||||||
|
i.AddCallback("JOIN", b.handleJoinPart)
|
||||||
|
i.AddCallback("PART", b.handleJoinPart)
|
||||||
|
i.AddCallback("QUIT", b.handleJoinPart)
|
||||||
|
i.AddCallback("KICK", b.handleJoinPart)
|
||||||
|
i.AddCallback("*", b.handleOther)
|
||||||
|
// we are now fully connected
|
||||||
|
b.connected <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Birc) handleJoinPart(event *irc.Event) {
|
||||||
|
channel := event.Arguments[0]
|
||||||
|
if event.Code == "KICK" {
|
||||||
|
flog.Infof("Got kicked from %s by %s", channel, event.Nick)
|
||||||
|
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if event.Code == "QUIT" {
|
||||||
|
if event.Nick == b.Nick && strings.Contains(event.Raw, "Ping timeout") {
|
||||||
|
flog.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.Nick != b.Nick {
|
||||||
|
flog.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
|
||||||
|
b.Remote <- config.Message{Username: "system", Text: event.Nick + " " + strings.ToLower(event.Code) + "s", Channel: channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
flog.Debugf("handle %#v", event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Birc) handleNotice(event *irc.Event) {
|
||||||
|
if strings.Contains(event.Message(), "This nickname is registered") && event.Nick == b.Config.NickServNick {
|
||||||
|
b.i.Privmsg(b.Config.NickServNick, "IDENTIFY "+b.Config.NickServPassword)
|
||||||
|
} else {
|
||||||
|
b.handlePrivMsg(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Birc) handleOther(event *irc.Event) {
|
||||||
|
switch event.Code {
|
||||||
|
case "372", "375", "376", "250", "251", "252", "253", "254", "255", "265", "266", "002", "003", "004", "005":
|
||||||
|
return
|
||||||
|
}
|
||||||
|
flog.Debugf("%#v", event.Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Birc) handlePrivMsg(event *irc.Event) {
|
||||||
|
b.Nick = b.i.GetNick()
|
||||||
|
// freenode doesn't send 001 as first reply
|
||||||
|
if event.Code == "NOTICE" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// don't forward queries to the bot
|
||||||
|
if event.Arguments[0] == b.Nick {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// don't forward message from ourself
|
||||||
|
if event.Nick == b.Nick {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rmsg := config.Message{Username: event.Nick, Channel: event.Arguments[0], Account: b.Account, UserID: event.User + "@" + event.Host}
|
||||||
|
flog.Debugf("handlePrivMsg() %s %s %#v", event.Nick, event.Message(), event)
|
||||||
|
msg := ""
|
||||||
|
if event.Code == "CTCP_ACTION" {
|
||||||
|
// msg = event.Nick + " "
|
||||||
|
rmsg.Event = config.EVENT_USER_ACTION
|
||||||
|
}
|
||||||
|
msg += event.Message()
|
||||||
|
// strip IRC colors
|
||||||
|
re := regexp.MustCompile(`[[:cntrl:]](\d+,|)\d+`)
|
||||||
|
msg = re.ReplaceAllString(msg, "")
|
||||||
|
|
||||||
|
// detect what were sending so that we convert it to utf-8
|
||||||
|
detector := chardet.NewTextDetector()
|
||||||
|
result, err := detector.DetectBest([]byte(msg))
|
||||||
|
if err != nil {
|
||||||
|
flog.Infof("detection failed for msg: %#v", msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
flog.Debugf("detected %s confidence %#v", result.Charset, result.Confidence)
|
||||||
|
var r io.Reader
|
||||||
|
r, err = charset.NewReader(result.Charset, strings.NewReader(msg))
|
||||||
|
// if we're not sure, just pick ISO-8859-1
|
||||||
|
if result.Confidence < 80 {
|
||||||
|
r, err = charset.NewReader("ISO-8859-1", strings.NewReader(msg))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
flog.Errorf("charset to utf-8 conversion failed: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
output, _ := ioutil.ReadAll(r)
|
||||||
|
msg = string(output)
|
||||||
|
|
||||||
|
flog.Debugf("Sending message from %s on %s to gateway", event.Arguments[0], b.Account)
|
||||||
|
rmsg.Text = msg
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Birc) handleTopicWhoTime(event *irc.Event) {
|
||||||
|
parts := strings.Split(event.Arguments[2], "!")
|
||||||
|
t, err := strconv.ParseInt(event.Arguments[3], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
flog.Errorf("Invalid time stamp: %s", event.Arguments[3])
|
||||||
|
}
|
||||||
|
user := parts[0]
|
||||||
|
if len(parts) > 1 {
|
||||||
|
user += " [" + parts[1] + "]"
|
||||||
|
}
|
||||||
|
flog.Debugf("%s: Topic set by %s [%s]", event.Code, user, time.Unix(t, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Birc) nicksPerRow() int {
|
||||||
|
return 4
|
||||||
|
/*
|
||||||
|
if b.Config.Mattermost.NicksPerRow < 1 {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
return b.Config.Mattermost.NicksPerRow
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Birc) storeNames(event *irc.Event) {
|
||||||
|
channel := event.Arguments[2]
|
||||||
|
b.names[channel] = append(
|
||||||
|
b.names[channel],
|
||||||
|
strings.Split(strings.TrimSpace(event.Message()), " ")...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Birc) formatnicks(nicks []string, continued bool) string {
|
||||||
|
return plainformatter(nicks, b.nicksPerRow())
|
||||||
|
/*
|
||||||
|
switch b.Config.Mattermost.NickFormatter {
|
||||||
|
case "table":
|
||||||
|
return tableformatter(nicks, b.nicksPerRow(), continued)
|
||||||
|
default:
|
||||||
|
return plainformatter(nicks, b.nicksPerRow())
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
133
bridge/matrix/matrix.go
Normal file
133
bridge/matrix/matrix.go
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package bmatrix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
matrix "github.com/matrix-org/gomatrix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bmatrix struct {
|
||||||
|
mc *matrix.Client
|
||||||
|
Config *config.Protocol
|
||||||
|
Remote chan config.Message
|
||||||
|
Account string
|
||||||
|
UserID string
|
||||||
|
RoomMap map[string]string
|
||||||
|
sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var flog *log.Entry
|
||||||
|
var protocol = "matrix"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flog = log.WithFields(log.Fields{"module": protocol})
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.Protocol, account string, c chan config.Message) *Bmatrix {
|
||||||
|
b := &Bmatrix{}
|
||||||
|
b.RoomMap = make(map[string]string)
|
||||||
|
b.Config = &cfg
|
||||||
|
b.Account = account
|
||||||
|
b.Remote = c
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bmatrix) Connect() error {
|
||||||
|
var err error
|
||||||
|
flog.Infof("Connecting %s", b.Config.Server)
|
||||||
|
b.mc, err = matrix.NewClient(b.Config.Server, "", "")
|
||||||
|
if err != nil {
|
||||||
|
flog.Debugf("%#v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp, err := b.mc.Login(&matrix.ReqLogin{
|
||||||
|
Type: "m.login.password",
|
||||||
|
User: b.Config.Login,
|
||||||
|
Password: b.Config.Password,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
flog.Debugf("%#v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.mc.SetCredentials(resp.UserID, resp.AccessToken)
|
||||||
|
b.UserID = resp.UserID
|
||||||
|
flog.Info("Connection succeeded")
|
||||||
|
go b.handlematrix()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bmatrix) Disconnect() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {
|
||||||
|
resp, err := b.mc.JoinRoom(channel.Name, "", nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.Lock()
|
||||||
|
b.RoomMap[resp.RoomID] = channel.Name
|
||||||
|
b.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bmatrix) Send(msg config.Message) error {
|
||||||
|
flog.Debugf("Receiving %#v", msg)
|
||||||
|
channel := b.getRoomID(msg.Channel)
|
||||||
|
flog.Debugf("Sending to channel %s", channel)
|
||||||
|
if msg.Event == config.EVENT_USER_ACTION {
|
||||||
|
b.mc.SendMessageEvent(channel, "m.room.message",
|
||||||
|
matrix.TextMessage{"m.emote", msg.Username + msg.Text})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b.mc.SendText(channel, msg.Username+msg.Text)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bmatrix) getRoomID(channel string) string {
|
||||||
|
b.RLock()
|
||||||
|
defer b.RUnlock()
|
||||||
|
for ID, name := range b.RoomMap {
|
||||||
|
if name == channel {
|
||||||
|
return ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
func (b *Bmatrix) handlematrix() error {
|
||||||
|
syncer := b.mc.Syncer.(*matrix.DefaultSyncer)
|
||||||
|
syncer.OnEventType("m.room.message", func(ev *matrix.Event) {
|
||||||
|
if (ev.Content["msgtype"].(string) == "m.text" || ev.Content["msgtype"].(string) == "m.emote") && ev.Sender != b.UserID {
|
||||||
|
b.RLock()
|
||||||
|
channel, ok := b.RoomMap[ev.RoomID]
|
||||||
|
b.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
flog.Debugf("Unknown room %s", ev.RoomID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username := ev.Sender[1:]
|
||||||
|
if b.Config.NoHomeServerSuffix {
|
||||||
|
re := regexp.MustCompile("(.*?):.*")
|
||||||
|
username = re.ReplaceAllString(username, `$1`)
|
||||||
|
}
|
||||||
|
rmsg := config.Message{Username: username, Text: ev.Content["body"].(string), Channel: channel, Account: b.Account, UserID: ev.Sender}
|
||||||
|
if ev.Content["msgtype"].(string) == "m.emote" {
|
||||||
|
rmsg.Event = config.EVENT_USER_ACTION
|
||||||
|
}
|
||||||
|
flog.Debugf("Sending message from %s on %s to gateway", ev.Sender, b.Account)
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
flog.Debugf("Received: %#v", ev)
|
||||||
|
})
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
if err := b.mc.Sync(); err != nil {
|
||||||
|
flog.Println("Sync() returned ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
245
bridge/mattermost/mattermost.go
Normal file
245
bridge/mattermost/mattermost.go
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
package bmattermost
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
"github.com/42wim/matterbridge/matterclient"
|
||||||
|
"github.com/42wim/matterbridge/matterhook"
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MMhook struct {
|
||||||
|
mh *matterhook.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type MMapi struct {
|
||||||
|
mc *matterclient.MMClient
|
||||||
|
mmMap map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type MMMessage struct {
|
||||||
|
Text string
|
||||||
|
Channel string
|
||||||
|
Username string
|
||||||
|
UserID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bmattermost struct {
|
||||||
|
MMhook
|
||||||
|
MMapi
|
||||||
|
Config *config.Protocol
|
||||||
|
Remote chan config.Message
|
||||||
|
TeamId string
|
||||||
|
Account string
|
||||||
|
}
|
||||||
|
|
||||||
|
var flog *log.Entry
|
||||||
|
var protocol = "mattermost"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flog = log.WithFields(log.Fields{"module": protocol})
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.Protocol, account string, c chan config.Message) *Bmattermost {
|
||||||
|
b := &Bmattermost{}
|
||||||
|
b.Config = &cfg
|
||||||
|
b.Remote = c
|
||||||
|
b.Account = account
|
||||||
|
b.mmMap = make(map[string]string)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bmattermost) Command(cmd string) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bmattermost) Connect() error {
|
||||||
|
if b.Config.WebhookBindAddress != "" {
|
||||||
|
if b.Config.WebhookURL != "" {
|
||||||
|
flog.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
|
||||||
|
b.mh = matterhook.New(b.Config.WebhookURL,
|
||||||
|
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
|
||||||
|
BindAddress: b.Config.WebhookBindAddress})
|
||||||
|
} else if b.Config.Login != "" {
|
||||||
|
flog.Info("Connecting using login/password (sending)")
|
||||||
|
err := b.apiLogin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
flog.Info("Connecting using webhookbindaddress (receiving)")
|
||||||
|
b.mh = matterhook.New(b.Config.WebhookURL,
|
||||||
|
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
|
||||||
|
BindAddress: b.Config.WebhookBindAddress})
|
||||||
|
}
|
||||||
|
go b.handleMatter()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if b.Config.WebhookURL != "" {
|
||||||
|
flog.Info("Connecting using webhookurl (sending)")
|
||||||
|
b.mh = matterhook.New(b.Config.WebhookURL,
|
||||||
|
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
|
||||||
|
DisableServer: true})
|
||||||
|
if b.Config.Login != "" {
|
||||||
|
flog.Info("Connecting using login/password (receiving)")
|
||||||
|
err := b.apiLogin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go b.handleMatter()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
} else if b.Config.Login != "" {
|
||||||
|
flog.Info("Connecting using login/password (sending and receiving)")
|
||||||
|
err := b.apiLogin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go b.handleMatter()
|
||||||
|
}
|
||||||
|
if b.Config.WebhookBindAddress == "" && b.Config.WebhookURL == "" && b.Config.Login == "" {
|
||||||
|
return errors.New("No connection method found. See that you have WebhookBindAddress, WebhookURL or Login/Password/Server/Team configured.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bmattermost) Disconnect() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error {
|
||||||
|
// we can only join channels using the API
|
||||||
|
if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" {
|
||||||
|
return b.mc.JoinChannel(b.mc.GetChannelId(channel.Name, ""))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bmattermost) Send(msg config.Message) error {
|
||||||
|
flog.Debugf("Receiving %#v", msg)
|
||||||
|
if msg.Event == config.EVENT_USER_ACTION {
|
||||||
|
msg.Text = "*" + msg.Text + "*"
|
||||||
|
}
|
||||||
|
nick := msg.Username
|
||||||
|
message := msg.Text
|
||||||
|
channel := msg.Channel
|
||||||
|
|
||||||
|
if b.Config.PrefixMessagesWithNick {
|
||||||
|
message = nick + message
|
||||||
|
}
|
||||||
|
if b.Config.WebhookURL != "" {
|
||||||
|
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
|
||||||
|
matterMessage.IconURL = msg.Avatar
|
||||||
|
matterMessage.Channel = channel
|
||||||
|
matterMessage.UserName = nick
|
||||||
|
matterMessage.Type = ""
|
||||||
|
matterMessage.Text = message
|
||||||
|
err := b.mh.Send(matterMessage)
|
||||||
|
if err != nil {
|
||||||
|
flog.Info(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b.mc.PostMessage(b.mc.GetChannelId(channel, ""), message)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bmattermost) handleMatter() {
|
||||||
|
mchan := make(chan *MMMessage)
|
||||||
|
if b.Config.WebhookBindAddress != "" {
|
||||||
|
flog.Debugf("Choosing webhooks based receiving")
|
||||||
|
go b.handleMatterHook(mchan)
|
||||||
|
} else {
|
||||||
|
flog.Debugf("Choosing login/password based receiving")
|
||||||
|
go b.handleMatterClient(mchan)
|
||||||
|
}
|
||||||
|
for message := range mchan {
|
||||||
|
rmsg := config.Message{Username: message.Username, Channel: message.Channel, Account: b.Account, UserID: message.UserID}
|
||||||
|
text, ok := b.replaceAction(message.Text)
|
||||||
|
if ok {
|
||||||
|
rmsg.Event = config.EVENT_USER_ACTION
|
||||||
|
}
|
||||||
|
rmsg.Text = text
|
||||||
|
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account)
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bmattermost) handleMatterClient(mchan chan *MMMessage) {
|
||||||
|
for message := range b.mc.MessageChan {
|
||||||
|
flog.Debugf("%#v", message.Raw.Data)
|
||||||
|
if message.Type == "system_join_leave" ||
|
||||||
|
message.Type == "system_join_channel" ||
|
||||||
|
message.Type == "system_leave_channel" {
|
||||||
|
flog.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}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (message.Raw.Event == "post_edited") && b.Config.EditDisable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// do not post our own messages back to irc
|
||||||
|
// only listen to message from our team
|
||||||
|
if (message.Raw.Event == "posted" || message.Raw.Event == "post_edited") &&
|
||||||
|
b.mc.User.Username != message.Username && message.Raw.Data["team_id"].(string) == b.TeamId {
|
||||||
|
// if the message has reactions don't repost it (for now, until we can correlate reaction with message)
|
||||||
|
if message.Post.HasReactions {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
flog.Debugf("Receiving from matterclient %#v", message)
|
||||||
|
m := &MMMessage{}
|
||||||
|
m.UserID = message.UserID
|
||||||
|
m.Username = message.Username
|
||||||
|
m.Channel = message.Channel
|
||||||
|
m.Text = message.Text
|
||||||
|
if message.Raw.Event == "post_edited" && !b.Config.EditDisable {
|
||||||
|
m.Text = message.Text + b.Config.EditSuffix
|
||||||
|
}
|
||||||
|
if len(message.Post.FileIds) > 0 {
|
||||||
|
for _, link := range b.mc.GetFileLinks(message.Post.FileIds) {
|
||||||
|
m.Text = m.Text + "\n" + link
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mchan <- m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bmattermost) handleMatterHook(mchan chan *MMMessage) {
|
||||||
|
for {
|
||||||
|
message := b.mh.Receive()
|
||||||
|
flog.Debugf("Receiving from matterhook %#v", message)
|
||||||
|
m := &MMMessage{}
|
||||||
|
m.UserID = message.UserID
|
||||||
|
m.Username = message.UserName
|
||||||
|
m.Text = message.Text
|
||||||
|
m.Channel = message.ChannelName
|
||||||
|
mchan <- m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bmattermost) apiLogin() error {
|
||||||
|
b.mc = matterclient.New(b.Config.Login, b.Config.Password,
|
||||||
|
b.Config.Team, b.Config.Server)
|
||||||
|
b.mc.SkipTLSVerify = b.Config.SkipTLSVerify
|
||||||
|
b.mc.NoTLS = b.Config.NoTLS
|
||||||
|
flog.Infof("Connecting %s (team: %s) on %s", b.Config.Login, b.Config.Team, b.Config.Server)
|
||||||
|
err := b.mc.Login()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
flog.Info("Connection succeeded")
|
||||||
|
b.TeamId = b.mc.GetTeamId()
|
||||||
|
go b.mc.WsReceiver()
|
||||||
|
go b.mc.StatusLoop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bmattermost) replaceAction(text string) (string, bool) {
|
||||||
|
if strings.HasPrefix(text, "*") && strings.HasSuffix(text, "*") {
|
||||||
|
return strings.Replace(text, "*", "", -1), true
|
||||||
|
}
|
||||||
|
return text, false
|
||||||
|
}
|
86
bridge/rocketchat/rocketchat.go
Normal file
86
bridge/rocketchat/rocketchat.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package brocketchat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
"github.com/42wim/matterbridge/hook/rockethook"
|
||||||
|
"github.com/42wim/matterbridge/matterhook"
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MMhook struct {
|
||||||
|
mh *matterhook.Client
|
||||||
|
rh *rockethook.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type Brocketchat struct {
|
||||||
|
MMhook
|
||||||
|
Config *config.Protocol
|
||||||
|
Remote chan config.Message
|
||||||
|
Account string
|
||||||
|
}
|
||||||
|
|
||||||
|
var flog *log.Entry
|
||||||
|
var protocol = "rocketchat"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flog = log.WithFields(log.Fields{"module": protocol})
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.Protocol, account string, c chan config.Message) *Brocketchat {
|
||||||
|
b := &Brocketchat{}
|
||||||
|
b.Config = &cfg
|
||||||
|
b.Remote = c
|
||||||
|
b.Account = account
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Brocketchat) Command(cmd string) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Brocketchat) Connect() error {
|
||||||
|
flog.Info("Connecting webhooks")
|
||||||
|
b.mh = matterhook.New(b.Config.WebhookURL,
|
||||||
|
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
|
||||||
|
DisableServer: true})
|
||||||
|
b.rh = rockethook.New(b.Config.WebhookURL, rockethook.Config{BindAddress: b.Config.WebhookBindAddress})
|
||||||
|
go b.handleRocketHook()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Brocketchat) Disconnect() error {
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Brocketchat) JoinChannel(channel config.ChannelInfo) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Brocketchat) Send(msg config.Message) error {
|
||||||
|
flog.Debugf("Receiving %#v", msg)
|
||||||
|
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
|
||||||
|
matterMessage.Channel = msg.Channel
|
||||||
|
matterMessage.UserName = msg.Username
|
||||||
|
matterMessage.Type = ""
|
||||||
|
matterMessage.Text = msg.Text
|
||||||
|
err := b.mh.Send(matterMessage)
|
||||||
|
if err != nil {
|
||||||
|
flog.Info(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Brocketchat) handleRocketHook() {
|
||||||
|
for {
|
||||||
|
message := b.rh.Receive()
|
||||||
|
flog.Debugf("Receiving from rockethook %#v", message)
|
||||||
|
// do not loop
|
||||||
|
if message.UserName == b.Config.Nick {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
flog.Debugf("Sending message from %s on %s to gateway", message.UserName, b.Account)
|
||||||
|
b.Remote <- config.Message{Text: message.Text, Username: message.UserName, Channel: message.ChannelName, Account: b.Account, UserID: message.UserID}
|
||||||
|
}
|
||||||
|
}
|
368
bridge/slack/slack.go
Normal file
368
bridge/slack/slack.go
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
package bslack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
"github.com/42wim/matterbridge/matterhook"
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/nlopes/slack"
|
||||||
|
"html"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MMMessage struct {
|
||||||
|
Text string
|
||||||
|
Channel string
|
||||||
|
Username string
|
||||||
|
UserID string
|
||||||
|
Raw *slack.MessageEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bslack struct {
|
||||||
|
mh *matterhook.Client
|
||||||
|
sc *slack.Client
|
||||||
|
Config *config.Protocol
|
||||||
|
rtm *slack.RTM
|
||||||
|
Plus bool
|
||||||
|
Remote chan config.Message
|
||||||
|
Users []slack.User
|
||||||
|
Account string
|
||||||
|
si *slack.Info
|
||||||
|
channels []slack.Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
var flog *log.Entry
|
||||||
|
var protocol = "slack"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flog = log.WithFields(log.Fields{"module": protocol})
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.Protocol, account string, c chan config.Message) *Bslack {
|
||||||
|
b := &Bslack{}
|
||||||
|
b.Config = &cfg
|
||||||
|
b.Remote = c
|
||||||
|
b.Account = account
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bslack) Command(cmd string) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bslack) Connect() error {
|
||||||
|
if b.Config.WebhookBindAddress != "" {
|
||||||
|
if b.Config.WebhookURL != "" {
|
||||||
|
flog.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
|
||||||
|
b.mh = matterhook.New(b.Config.WebhookURL,
|
||||||
|
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
|
||||||
|
BindAddress: b.Config.WebhookBindAddress})
|
||||||
|
} else if b.Config.Token != "" {
|
||||||
|
flog.Info("Connecting using token (sending)")
|
||||||
|
b.sc = slack.New(b.Config.Token)
|
||||||
|
b.rtm = b.sc.NewRTM()
|
||||||
|
go b.rtm.ManageConnection()
|
||||||
|
flog.Info("Connecting using webhookbindaddress (receiving)")
|
||||||
|
b.mh = matterhook.New(b.Config.WebhookURL,
|
||||||
|
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
|
||||||
|
BindAddress: b.Config.WebhookBindAddress})
|
||||||
|
} else {
|
||||||
|
flog.Info("Connecting using webhookbindaddress (receiving)")
|
||||||
|
b.mh = matterhook.New(b.Config.WebhookURL,
|
||||||
|
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
|
||||||
|
BindAddress: b.Config.WebhookBindAddress})
|
||||||
|
}
|
||||||
|
go b.handleSlack()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if b.Config.WebhookURL != "" {
|
||||||
|
flog.Info("Connecting using webhookurl (sending)")
|
||||||
|
b.mh = matterhook.New(b.Config.WebhookURL,
|
||||||
|
matterhook.Config{InsecureSkipVerify: b.Config.SkipTLSVerify,
|
||||||
|
DisableServer: true})
|
||||||
|
if b.Config.Token != "" {
|
||||||
|
flog.Info("Connecting using token (receiving)")
|
||||||
|
b.sc = slack.New(b.Config.Token)
|
||||||
|
b.rtm = b.sc.NewRTM()
|
||||||
|
go b.rtm.ManageConnection()
|
||||||
|
go b.handleSlack()
|
||||||
|
}
|
||||||
|
} else if b.Config.Token != "" {
|
||||||
|
flog.Info("Connecting using token (sending and receiving)")
|
||||||
|
b.sc = slack.New(b.Config.Token)
|
||||||
|
b.rtm = b.sc.NewRTM()
|
||||||
|
go b.rtm.ManageConnection()
|
||||||
|
go b.handleSlack()
|
||||||
|
}
|
||||||
|
if b.Config.WebhookBindAddress == "" && b.Config.WebhookURL == "" && b.Config.Token == "" {
|
||||||
|
return errors.New("No connection method found. See that you have WebhookBindAddress, WebhookURL or Token configured.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bslack) Disconnect() error {
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
|
||||||
|
// we can only join channels using the API
|
||||||
|
if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" {
|
||||||
|
if strings.HasPrefix(b.Config.Token, "xoxb") {
|
||||||
|
// TODO check if bot has already joined channel
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := b.sc.JoinChannel(channel.Name)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() != "name_taken" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bslack) Send(msg config.Message) error {
|
||||||
|
flog.Debugf("Receiving %#v", msg)
|
||||||
|
if msg.Event == config.EVENT_USER_ACTION {
|
||||||
|
msg.Text = "_" + msg.Text + "_"
|
||||||
|
}
|
||||||
|
nick := msg.Username
|
||||||
|
message := msg.Text
|
||||||
|
channel := msg.Channel
|
||||||
|
if b.Config.PrefixMessagesWithNick {
|
||||||
|
message = nick + " " + message
|
||||||
|
}
|
||||||
|
if b.Config.WebhookURL != "" {
|
||||||
|
matterMessage := matterhook.OMessage{IconURL: b.Config.IconURL}
|
||||||
|
matterMessage.Channel = channel
|
||||||
|
matterMessage.UserName = nick
|
||||||
|
matterMessage.Type = ""
|
||||||
|
matterMessage.Text = message
|
||||||
|
err := b.mh.Send(matterMessage)
|
||||||
|
if err != nil {
|
||||||
|
flog.Info(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
schannel, err := b.getChannelByName(channel)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
np := slack.NewPostMessageParameters()
|
||||||
|
if b.Config.PrefixMessagesWithNick {
|
||||||
|
np.AsUser = true
|
||||||
|
}
|
||||||
|
np.Username = nick
|
||||||
|
np.IconURL = config.GetIconURL(&msg, b.Config)
|
||||||
|
if msg.Avatar != "" {
|
||||||
|
np.IconURL = msg.Avatar
|
||||||
|
}
|
||||||
|
np.Attachments = append(np.Attachments, slack.Attachment{CallbackID: "matterbridge"})
|
||||||
|
b.sc.PostMessage(schannel.ID, message, np)
|
||||||
|
|
||||||
|
/*
|
||||||
|
newmsg := b.rtm.NewOutgoingMessage(message, schannel.ID)
|
||||||
|
b.rtm.SendMessage(newmsg)
|
||||||
|
*/
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bslack) getAvatar(user string) string {
|
||||||
|
var avatar string
|
||||||
|
if b.Users != nil {
|
||||||
|
for _, u := range b.Users {
|
||||||
|
if user == u.Name {
|
||||||
|
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() {
|
||||||
|
mchan := make(chan *MMMessage)
|
||||||
|
if b.Config.WebhookBindAddress != "" {
|
||||||
|
flog.Debugf("Choosing webhooks based receiving")
|
||||||
|
go b.handleMatterHook(mchan)
|
||||||
|
} else {
|
||||||
|
flog.Debugf("Choosing token based receiving")
|
||||||
|
go b.handleSlackClient(mchan)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
flog.Debug("Start listening for Slack messages")
|
||||||
|
for message := range mchan {
|
||||||
|
// do not send messages from ourself
|
||||||
|
if b.Config.WebhookURL == "" && b.Config.WebhookBindAddress == "" && message.Username == b.si.User.Name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if message.Text == "" || message.Username == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
texts := strings.Split(message.Text, "\n")
|
||||||
|
for _, text := range texts {
|
||||||
|
text = b.replaceURL(text)
|
||||||
|
text = html.UnescapeString(text)
|
||||||
|
flog.Debugf("Sending message from %s on %s to gateway", message.Username, b.Account)
|
||||||
|
msg := config.Message{Text: text, Username: message.Username, Channel: message.Channel, Account: b.Account, Avatar: b.getAvatar(message.Username), UserID: message.UserID}
|
||||||
|
if message.Raw.SubType == "me_message" {
|
||||||
|
msg.Event = config.EVENT_USER_ACTION
|
||||||
|
}
|
||||||
|
b.Remote <- msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
|
||||||
|
count := 0
|
||||||
|
for msg := range b.rtm.IncomingEvents {
|
||||||
|
switch ev := msg.Data.(type) {
|
||||||
|
case *slack.MessageEvent:
|
||||||
|
// ignore first message
|
||||||
|
if count > 0 {
|
||||||
|
flog.Debugf("Receiving from slackclient %#v", ev)
|
||||||
|
if len(ev.Attachments) > 0 {
|
||||||
|
// skip messages we made ourselves
|
||||||
|
if ev.Attachments[0].CallbackID == "matterbridge" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !b.Config.EditDisable && ev.SubMessage != nil && ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp {
|
||||||
|
flog.Debugf("SubMessage %#v", ev.SubMessage)
|
||||||
|
ev.User = ev.SubMessage.User
|
||||||
|
ev.Text = ev.SubMessage.Text + b.Config.EditSuffix
|
||||||
|
}
|
||||||
|
// use our own func because rtm.GetChannelInfo doesn't work for private channels
|
||||||
|
channel, err := b.getChannelByID(ev.Channel)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m := &MMMessage{}
|
||||||
|
if ev.BotID == "" {
|
||||||
|
user, err := b.rtm.GetUserInfo(ev.User)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.UserID = user.ID
|
||||||
|
m.Username = user.Name
|
||||||
|
}
|
||||||
|
m.Channel = channel.Name
|
||||||
|
m.Text = ev.Text
|
||||||
|
if m.Text == "" {
|
||||||
|
for _, attach := range ev.Attachments {
|
||||||
|
if attach.Text != "" {
|
||||||
|
m.Text = attach.Text
|
||||||
|
} else {
|
||||||
|
m.Text = attach.Fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.Raw = ev
|
||||||
|
m.Text = b.replaceMention(m.Text)
|
||||||
|
// when using webhookURL we can't check if it's our webhook or not for now
|
||||||
|
if ev.BotID != "" && b.Config.WebhookURL == "" {
|
||||||
|
bot, err := b.rtm.GetBotInfo(ev.BotID)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if bot.Name != "" {
|
||||||
|
m.Username = bot.Name
|
||||||
|
m.UserID = bot.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mchan <- m
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
case *slack.OutgoingErrorEvent:
|
||||||
|
flog.Debugf("%#v", ev.Error())
|
||||||
|
case *slack.ChannelJoinedEvent:
|
||||||
|
b.Users, _ = b.sc.GetUsers()
|
||||||
|
case *slack.ConnectedEvent:
|
||||||
|
b.channels = ev.Info.Channels
|
||||||
|
b.si = ev.Info
|
||||||
|
b.Users, _ = b.sc.GetUsers()
|
||||||
|
// add private channels
|
||||||
|
groups, _ := b.sc.GetGroups(true)
|
||||||
|
for _, g := range groups {
|
||||||
|
channel := new(slack.Channel)
|
||||||
|
channel.ID = g.ID
|
||||||
|
channel.Name = g.Name
|
||||||
|
b.channels = append(b.channels, *channel)
|
||||||
|
}
|
||||||
|
case *slack.InvalidAuthEvent:
|
||||||
|
flog.Fatalf("Invalid Token %#v", ev)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bslack) handleMatterHook(mchan chan *MMMessage) {
|
||||||
|
for {
|
||||||
|
message := b.mh.Receive()
|
||||||
|
flog.Debugf("receiving from matterhook (slack) %#v", message)
|
||||||
|
m := &MMMessage{}
|
||||||
|
m.Username = message.UserName
|
||||||
|
m.Text = message.Text
|
||||||
|
m.Text = b.replaceMention(m.Text)
|
||||||
|
m.Channel = message.ChannelName
|
||||||
|
if m.Username == "slackbot" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mchan <- m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bslack) userName(id string) string {
|
||||||
|
for _, u := range b.Users {
|
||||||
|
if u.ID == id {
|
||||||
|
return u.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bslack) replaceURL(text string) string {
|
||||||
|
results := regexp.MustCompile(`<(.*?)\|.*?>`).FindAllStringSubmatch(text, -1)
|
||||||
|
for _, r := range results {
|
||||||
|
text = strings.Replace(text, r[0], r[1], -1)
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
160
bridge/steam/steam.go
Normal file
160
bridge/steam/steam.go
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
package bsteam
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
"github.com/Philipp15b/go-steam"
|
||||||
|
"github.com/Philipp15b/go-steam/protocol/steamlang"
|
||||||
|
"github.com/Philipp15b/go-steam/steamid"
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
//"io/ioutil"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bsteam struct {
|
||||||
|
c *steam.Client
|
||||||
|
connected chan struct{}
|
||||||
|
Config *config.Protocol
|
||||||
|
Remote chan config.Message
|
||||||
|
Account string
|
||||||
|
userMap map[steamid.SteamId]string
|
||||||
|
sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var flog *log.Entry
|
||||||
|
var protocol = "steam"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flog = log.WithFields(log.Fields{"module": protocol})
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.Protocol, account string, c chan config.Message) *Bsteam {
|
||||||
|
b := &Bsteam{}
|
||||||
|
b.Config = &cfg
|
||||||
|
b.Remote = c
|
||||||
|
b.Account = account
|
||||||
|
b.userMap = make(map[steamid.SteamId]string)
|
||||||
|
b.connected = make(chan struct{})
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bsteam) Connect() error {
|
||||||
|
flog.Info("Connecting")
|
||||||
|
b.c = steam.NewClient()
|
||||||
|
go b.handleEvents()
|
||||||
|
go b.c.Connect()
|
||||||
|
select {
|
||||||
|
case <-b.connected:
|
||||||
|
flog.Info("Connection succeeded")
|
||||||
|
case <-time.After(time.Second * 30):
|
||||||
|
return fmt.Errorf("connection timed out")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bsteam) Disconnect() error {
|
||||||
|
b.c.Disconnect()
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bsteam) JoinChannel(channel config.ChannelInfo) error {
|
||||||
|
id, err := steamid.NewId(channel.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.c.Social.JoinChat(id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bsteam) Send(msg config.Message) error {
|
||||||
|
id, err := steamid.NewId(msg.Channel)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bsteam) getNick(id steamid.SteamId) string {
|
||||||
|
b.RLock()
|
||||||
|
defer b.RUnlock()
|
||||||
|
if name, ok := b.userMap[id]; ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bsteam) handleEvents() {
|
||||||
|
myLoginInfo := new(steam.LogOnDetails)
|
||||||
|
myLoginInfo.Username = b.Config.Login
|
||||||
|
myLoginInfo.Password = b.Config.Password
|
||||||
|
myLoginInfo.AuthCode = b.Config.AuthCode
|
||||||
|
// Attempt to read existing auth hash to avoid steam guard.
|
||||||
|
// Maybe works
|
||||||
|
//myLoginInfo.SentryFileHash, _ = ioutil.ReadFile("sentry")
|
||||||
|
for event := range b.c.Events() {
|
||||||
|
//flog.Info(event)
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *steam.ChatMsgEvent:
|
||||||
|
flog.Debugf("Receiving ChatMsgEvent: %#v", e)
|
||||||
|
flog.Debugf("Sending message from %s on %s to gateway", b.getNick(e.ChatterId), b.Account)
|
||||||
|
// 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:
|
||||||
|
flog.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:
|
||||||
|
/*
|
||||||
|
flog.Info("authupdate", e)
|
||||||
|
flog.Info("hash", e.Hash)
|
||||||
|
ioutil.WriteFile("sentry", e.Hash, 0666)
|
||||||
|
*/
|
||||||
|
case *steam.LogOnFailedEvent:
|
||||||
|
flog.Info("Logon failed", e)
|
||||||
|
switch e.Result {
|
||||||
|
case steamlang.EResult_AccountLogonDeniedNeedTwoFactorCode:
|
||||||
|
{
|
||||||
|
flog.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:
|
||||||
|
{
|
||||||
|
flog.Info("Steam guard isn't letting me in! Enter auth code:")
|
||||||
|
var code string
|
||||||
|
fmt.Scanf("%s", &code)
|
||||||
|
myLoginInfo.AuthCode = code
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Errorf("LogOnFailedEvent: %#v ", e.Result)
|
||||||
|
// TODO: Handle EResult_InvalidLoginAuthCode
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case *steam.LoggedOnEvent:
|
||||||
|
flog.Debugf("LoggedOnEvent: %#v", e)
|
||||||
|
b.connected <- struct{}{}
|
||||||
|
flog.Debugf("setting online")
|
||||||
|
b.c.Social.SetPersonaState(steamlang.EPersonaState_Online)
|
||||||
|
case *steam.DisconnectedEvent:
|
||||||
|
flog.Info("Disconnected")
|
||||||
|
flog.Info("Attempting to reconnect...")
|
||||||
|
b.c.Connect()
|
||||||
|
case steam.FatalErrorEvent:
|
||||||
|
flog.Error(e)
|
||||||
|
case error:
|
||||||
|
flog.Error(e)
|
||||||
|
default:
|
||||||
|
flog.Debugf("unknown event %#v", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
bridge/telegram/html.go
Normal file
64
bridge/telegram/html.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package btelegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/russross/blackfriday"
|
||||||
|
"html"
|
||||||
|
)
|
||||||
|
|
||||||
|
type customHtml struct {
|
||||||
|
blackfriday.Renderer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *customHtml) Paragraph(out *bytes.Buffer, text func() bool) {
|
||||||
|
marker := out.Len()
|
||||||
|
|
||||||
|
if !text() {
|
||||||
|
out.Truncate(marker)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *customHtml) BlockCode(out *bytes.Buffer, text []byte, lang string) {
|
||||||
|
out.WriteString("<pre>")
|
||||||
|
|
||||||
|
out.WriteString(html.EscapeString(string(text)))
|
||||||
|
out.WriteString("</pre>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *customHtml) Header(out *bytes.Buffer, text func() bool, level int, id string) {
|
||||||
|
options.Paragraph(out, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *customHtml) HRule(out *bytes.Buffer) {
|
||||||
|
out.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *customHtml) BlockQuote(out *bytes.Buffer, text []byte) {
|
||||||
|
out.WriteString("> ")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *customHtml) List(out *bytes.Buffer, text func() bool, flags int) {
|
||||||
|
options.Paragraph(out, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *customHtml) ListItem(out *bytes.Buffer, text []byte, flags int) {
|
||||||
|
out.WriteString("- ")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
145
bridge/telegram/telegram.go
Normal file
145
bridge/telegram/telegram.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
package btelegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/go-telegram-bot-api/telegram-bot-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Btelegram struct {
|
||||||
|
c *tgbotapi.BotAPI
|
||||||
|
Config *config.Protocol
|
||||||
|
Remote chan config.Message
|
||||||
|
Account string
|
||||||
|
}
|
||||||
|
|
||||||
|
var flog *log.Entry
|
||||||
|
var protocol = "telegram"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flog = log.WithFields(log.Fields{"module": protocol})
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.Protocol, account string, c chan config.Message) *Btelegram {
|
||||||
|
b := &Btelegram{}
|
||||||
|
b.Config = &cfg
|
||||||
|
b.Remote = c
|
||||||
|
b.Account = account
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Btelegram) Connect() error {
|
||||||
|
var err error
|
||||||
|
flog.Info("Connecting")
|
||||||
|
b.c, err = tgbotapi.NewBotAPI(b.Config.Token)
|
||||||
|
if err != nil {
|
||||||
|
flog.Debugf("%#v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updates, err := b.c.GetUpdatesChan(tgbotapi.NewUpdate(0))
|
||||||
|
if err != nil {
|
||||||
|
flog.Debugf("%#v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
flog.Info("Connection succeeded")
|
||||||
|
go b.handleRecv(updates)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Btelegram) Disconnect() error {
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Btelegram) JoinChannel(channel config.ChannelInfo) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Btelegram) Send(msg config.Message) error {
|
||||||
|
flog.Debugf("Receiving %#v", msg)
|
||||||
|
chatid, err := strconv.ParseInt(msg.Channel, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Config.MessageFormat == "HTML" {
|
||||||
|
msg.Text = makeHTML(msg.Text)
|
||||||
|
}
|
||||||
|
m := tgbotapi.NewMessage(chatid, msg.Username+msg.Text)
|
||||||
|
if b.Config.MessageFormat == "HTML" {
|
||||||
|
m.ParseMode = tgbotapi.ModeHTML
|
||||||
|
}
|
||||||
|
_, err = b.c.Send(m)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
||||||
|
for update := range updates {
|
||||||
|
flog.Debugf("Receiving from telegram: %#v", update.Message)
|
||||||
|
var message *tgbotapi.Message
|
||||||
|
username := ""
|
||||||
|
channel := ""
|
||||||
|
text := ""
|
||||||
|
// handle channels
|
||||||
|
if update.ChannelPost != nil {
|
||||||
|
message = update.ChannelPost
|
||||||
|
}
|
||||||
|
if update.EditedChannelPost != nil && !b.Config.EditDisable {
|
||||||
|
message = update.EditedChannelPost
|
||||||
|
message.Text = message.Text + b.Config.EditSuffix
|
||||||
|
}
|
||||||
|
// handle groups
|
||||||
|
if update.Message != nil {
|
||||||
|
message = update.Message
|
||||||
|
}
|
||||||
|
if update.EditedMessage != nil && !b.Config.EditDisable {
|
||||||
|
message = update.EditedMessage
|
||||||
|
message.Text = message.Text + b.Config.EditSuffix
|
||||||
|
}
|
||||||
|
if message.From != nil {
|
||||||
|
if b.Config.UseFirstName {
|
||||||
|
username = message.From.FirstName
|
||||||
|
}
|
||||||
|
if username == "" {
|
||||||
|
username = message.From.UserName
|
||||||
|
if username == "" {
|
||||||
|
username = message.From.FirstName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text = message.Text
|
||||||
|
channel = strconv.FormatInt(message.Chat.ID, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
if username == "" {
|
||||||
|
username = "unknown"
|
||||||
|
}
|
||||||
|
if message.Sticker != nil && b.Config.UseInsecureURL {
|
||||||
|
text = text + " " + b.getFileDirectURL(message.Sticker.FileID)
|
||||||
|
}
|
||||||
|
if message.Video != nil && b.Config.UseInsecureURL {
|
||||||
|
text = text + " " + b.getFileDirectURL(message.Video.FileID)
|
||||||
|
}
|
||||||
|
if message.Photo != nil && b.Config.UseInsecureURL {
|
||||||
|
photos := *message.Photo
|
||||||
|
// last photo is the biggest
|
||||||
|
text = text + " " + b.getFileDirectURL(photos[len(photos)-1].FileID)
|
||||||
|
}
|
||||||
|
if message.Document != nil && b.Config.UseInsecureURL {
|
||||||
|
text = text + " " + message.Document.FileName + " : " + b.getFileDirectURL(message.Document.FileID)
|
||||||
|
}
|
||||||
|
if text != "" {
|
||||||
|
flog.Debugf("Sending message from %s on %s to gateway", username, b.Account)
|
||||||
|
b.Remote <- config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: strconv.Itoa(message.From.ID)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Btelegram) getFileDirectURL(id string) string {
|
||||||
|
res, err := b.c.GetFileDirectURL(id)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
178
bridge/xmpp/xmpp.go
Normal file
178
bridge/xmpp/xmpp.go
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
package bxmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/jpillora/backoff"
|
||||||
|
"github.com/mattn/go-xmpp"
|
||||||
|
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bxmpp struct {
|
||||||
|
xc *xmpp.Client
|
||||||
|
xmppMap map[string]string
|
||||||
|
Config *config.Protocol
|
||||||
|
Remote chan config.Message
|
||||||
|
Account string
|
||||||
|
}
|
||||||
|
|
||||||
|
var flog *log.Entry
|
||||||
|
var protocol = "xmpp"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flog = log.WithFields(log.Fields{"module": protocol})
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.Protocol, account string, c chan config.Message) *Bxmpp {
|
||||||
|
b := &Bxmpp{}
|
||||||
|
b.xmppMap = make(map[string]string)
|
||||||
|
b.Config = &cfg
|
||||||
|
b.Account = account
|
||||||
|
b.Remote = c
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bxmpp) Connect() error {
|
||||||
|
var err error
|
||||||
|
flog.Infof("Connecting %s", b.Config.Server)
|
||||||
|
b.xc, err = b.createXMPP()
|
||||||
|
if err != nil {
|
||||||
|
flog.Debugf("%#v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
flog.Info("Connection succeeded")
|
||||||
|
go func() {
|
||||||
|
initial := true
|
||||||
|
bf := &backoff.Backoff{
|
||||||
|
Min: time.Second,
|
||||||
|
Max: 5 * time.Minute,
|
||||||
|
Jitter: true,
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
if initial {
|
||||||
|
b.handleXmpp()
|
||||||
|
initial = false
|
||||||
|
}
|
||||||
|
d := bf.Duration()
|
||||||
|
flog.Infof("Disconnected. Reconnecting in %s", d)
|
||||||
|
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.handleXmpp()
|
||||||
|
bf.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bxmpp) Disconnect() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bxmpp) JoinChannel(channel config.ChannelInfo) error {
|
||||||
|
b.xc.JoinMUCNoHistory(channel.Name+"@"+b.Config.Muc, b.Config.Nick)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bxmpp) Send(msg config.Message) error {
|
||||||
|
flog.Debugf("Receiving %#v", msg)
|
||||||
|
b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.Config.Muc, Text: msg.Username + msg.Text})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bxmpp) createXMPP() (*xmpp.Client, error) {
|
||||||
|
tc := new(tls.Config)
|
||||||
|
tc.InsecureSkipVerify = b.Config.SkipTLSVerify
|
||||||
|
tc.ServerName = strings.Split(b.Config.Server, ":")[0]
|
||||||
|
options := xmpp.Options{
|
||||||
|
Host: b.Config.Server,
|
||||||
|
User: b.Config.Jid,
|
||||||
|
Password: b.Config.Password,
|
||||||
|
NoTLS: true,
|
||||||
|
StartTLS: true,
|
||||||
|
TLSConfig: tc,
|
||||||
|
|
||||||
|
//StartTLS: false,
|
||||||
|
Debug: true,
|
||||||
|
Session: true,
|
||||||
|
Status: "",
|
||||||
|
StatusMessage: "",
|
||||||
|
Resource: "",
|
||||||
|
InsecureAllowUnencryptedAuth: false,
|
||||||
|
//InsecureAllowUnencryptedAuth: true,
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
b.xc, err = options.NewClient()
|
||||||
|
return b.xc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bxmpp) xmppKeepAlive() chan bool {
|
||||||
|
done := make(chan bool)
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(90 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
flog.Debugf("PING")
|
||||||
|
err := b.xc.PingC2S("", "")
|
||||||
|
if err != nil {
|
||||||
|
flog.Debugf("PING failed %#v", err)
|
||||||
|
}
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return done
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bxmpp) handleXmpp() error {
|
||||||
|
var ok bool
|
||||||
|
done := b.xmppKeepAlive()
|
||||||
|
defer close(done)
|
||||||
|
nodelay := time.Time{}
|
||||||
|
for {
|
||||||
|
m, err := b.xc.Recv()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch v := m.(type) {
|
||||||
|
case xmpp.Chat:
|
||||||
|
var channel, nick string
|
||||||
|
if v.Type == "groupchat" {
|
||||||
|
s := strings.Split(v.Remote, "@")
|
||||||
|
if len(s) >= 2 {
|
||||||
|
channel = s[0]
|
||||||
|
}
|
||||||
|
s = strings.Split(s[1], "/")
|
||||||
|
if len(s) == 2 {
|
||||||
|
nick = s[1]
|
||||||
|
}
|
||||||
|
if nick != b.Config.Nick && v.Stamp == nodelay && v.Text != "" {
|
||||||
|
rmsg := config.Message{Username: nick, Text: v.Text, Channel: channel, Account: b.Account, UserID: v.Remote}
|
||||||
|
rmsg.Text, ok = b.replaceAction(rmsg.Text)
|
||||||
|
if ok {
|
||||||
|
rmsg.Event = config.EVENT_USER_ACTION
|
||||||
|
}
|
||||||
|
flog.Debugf("Sending message from %s on %s to gateway", nick, b.Account)
|
||||||
|
b.Remote <- rmsg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case xmpp.Presence:
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bxmpp) replaceAction(text string) (string, bool) {
|
||||||
|
if strings.HasPrefix(text, "/me ") {
|
||||||
|
return strings.Replace(text, "/me ", "", -1), true
|
||||||
|
}
|
||||||
|
return text, false
|
||||||
|
}
|
433
changelog.md
Normal file
433
changelog.md
Normal file
@ -0,0 +1,433 @@
|
|||||||
|
# v1.0.1
|
||||||
|
## New features
|
||||||
|
* mattermost: add support for mattermost 4.1.x
|
||||||
|
* discord: allow a webhookURL per channel #239
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
|
||||||
|
# v1.0.0
|
||||||
|
## New features
|
||||||
|
* general: Add action support for slack,mattermost,irc,gitter,matrix,xmpp,discord. #199
|
||||||
|
* discord: Shows the username instead of the server nickname #234
|
||||||
|
|
||||||
|
# v1.0.0-rc1
|
||||||
|
## New features
|
||||||
|
* general: Add action support for slack,mattermost,irc,gitter,matrix,xmpp,discord. #199
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* general: Handle same account in multiple gateways better
|
||||||
|
* mattermost: ignore edited messages with reactions
|
||||||
|
* mattermost: Fix double posting of edited messages by using lru cache
|
||||||
|
* irc: update vendor
|
||||||
|
|
||||||
|
# v0.16.3
|
||||||
|
## Bugfix
|
||||||
|
* general: Fix in/out logic. Closes #224
|
||||||
|
* general: Fix message modification
|
||||||
|
* slack: Disable message from other bots when using webhooks (slack)
|
||||||
|
* mattermost: Return better error messages on mattermost connect
|
||||||
|
|
||||||
|
# v0.16.2
|
||||||
|
## New features
|
||||||
|
* general: binary builds against latest commit are now available on https://bintray.com/42wim/nightly/Matterbridge/_latestVersion
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* slack: fix loop introduced by relaying message of other bots #219
|
||||||
|
* slack: Suppress parent message when child message is received #218
|
||||||
|
* mattermost: fix regression when using webhookurl and webhookbindaddress #221
|
||||||
|
|
||||||
|
# v0.16.1
|
||||||
|
## New features
|
||||||
|
* slack: also relay messages of other bots #213
|
||||||
|
* mattermost: show also links if public links have not been enabled.
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* mattermost, slack: fix connecting logic #216
|
||||||
|
|
||||||
|
# v0.16.0
|
||||||
|
## Breaking Changes
|
||||||
|
* URL,UseAPI,BindAddress is deprecated. Your config has to be updated.
|
||||||
|
* URL => WebhookURL
|
||||||
|
* BindAddress => WebhookBindAddress
|
||||||
|
* UseAPI => removed
|
||||||
|
This change allows you to specify a WebhookURL and a token (slack,discord), so that
|
||||||
|
messages will be sent with the webhook, but received via the token (API)
|
||||||
|
If you have not specified WebhookURL and WebhookBindAddress the API (login or token)
|
||||||
|
will be used automatically. (no need for UseAPI)
|
||||||
|
|
||||||
|
## New features
|
||||||
|
* mattermost: add support for mattermost 4.0
|
||||||
|
* steam: New protocol support added (http://store.steampowered.com/)
|
||||||
|
* discord: Support for embedded messages (sent by other bots)
|
||||||
|
Shows title, description and URL of embedded messages (sent by other bots)
|
||||||
|
To enable add ```ShowEmbeds=true``` to your discord config
|
||||||
|
* discord: ```WebhookURL``` posting support added (thanks @saury07) #204
|
||||||
|
Discord API does not allow to change the name of the user posting, but webhooks does.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
* general: all :emoji: will be converted to unicode, providing consistent emojis across all bridges
|
||||||
|
* telegram: Add ```UseInsecureURL``` option for telegram (default false)
|
||||||
|
WARNING! If enabled this will relay GIF/stickers/documents and other attachments as URLs
|
||||||
|
Those URLs will contain your bot-token. This may not be what you want.
|
||||||
|
For now there is no secure way to relay GIF/stickers/documents without seeing your token.
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* irc: detect charset and try to convert it to utf-8 before sending it to other bridges. #209 #210
|
||||||
|
* slack: Remove label from URLs (slack). #205
|
||||||
|
* slack: Relay <>& correctly to other bridges #215
|
||||||
|
* steam: Fix channel id bug in steam (channels are off by 0x18000000000000)
|
||||||
|
* general: various improvements
|
||||||
|
* general: samechannelgateway now relays messages correct again #207
|
||||||
|
|
||||||
|
|
||||||
|
# v0.16.0-rc2
|
||||||
|
## Breaking Changes
|
||||||
|
* URL,UseAPI,BindAddress is deprecated. Your config has to be updated.
|
||||||
|
* URL => WebhookURL
|
||||||
|
* BindAddress => WebhookBindAddress
|
||||||
|
* UseAPI => removed
|
||||||
|
This change allows you to specify a WebhookURL and a token (slack,discord), so that
|
||||||
|
messages will be sent with the webhook, but received via the token (API)
|
||||||
|
If you have not specified WebhookURL and WebhookBindAddress the API (login or token)
|
||||||
|
will be used automatically. (no need for UseAPI)
|
||||||
|
|
||||||
|
## Bugfix since rc1
|
||||||
|
* steam: Fix channel id bug in steam (channels are off by 0x18000000000000)
|
||||||
|
* telegram: Add UseInsecureURL option for telegram (default false)
|
||||||
|
WARNING! If enabled this will relay GIF/stickers/documents and other attachments as URLs
|
||||||
|
Those URLs will contain your bot-token. This may not be what you want.
|
||||||
|
For now there is no secure way to relay GIF/stickers/documents without seeing your token.
|
||||||
|
* irc: detect charset and try to convert it to utf-8 before sending it to other bridges. #209 #210
|
||||||
|
* general: various improvements
|
||||||
|
|
||||||
|
|
||||||
|
# v0.16.0-rc1
|
||||||
|
## Breaking Changes
|
||||||
|
* URL,UseAPI,BindAddress is deprecated. Your config has to be updated.
|
||||||
|
* URL => WebhookURL
|
||||||
|
* BindAddress => WebhookBindAddress
|
||||||
|
* UseAPI => removed
|
||||||
|
This change allows you to specify a WebhookURL and a token (slack,discord), so that
|
||||||
|
messages will be sent with the webhook, but received via the token (API)
|
||||||
|
If you have not specified WebhookURL and WebhookBindAddress the API (login or token)
|
||||||
|
will be used automatically. (no need for UseAPI)
|
||||||
|
|
||||||
|
## New features
|
||||||
|
* steam: New protocol support added (http://store.steampowered.com/)
|
||||||
|
* discord: WebhookURL posting support added (thanks @saury07) #204
|
||||||
|
Discord API does not allow to change the name of the user posting, but webhooks does.
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* general: samechannelgateway now relays messages correct again #207
|
||||||
|
* slack: Remove label from URLs (slack). #205
|
||||||
|
|
||||||
|
# v0.15.0
|
||||||
|
## New features
|
||||||
|
* general: add option IgnoreMessages for all protocols (see mattebridge.toml.sample)
|
||||||
|
Messages matching these regexp will be ignored and not sent to other bridges
|
||||||
|
e.g. IgnoreMessages="^~~ badword"
|
||||||
|
* telegram: add support for sticker/video/photo/document #184
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
* api: add userid to each message #200
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* discord: fix crash in memberupdate #198
|
||||||
|
* mattermost: Fix incorrect behaviour of EditDisable (mattermost). Fixes #197
|
||||||
|
* irc: Do not relay join/part of ourselves (irc). Closes #190
|
||||||
|
* irc: make reconnections more robust. #153
|
||||||
|
* gitter: update library, fixes possible crash
|
||||||
|
|
||||||
|
# v0.14.0
|
||||||
|
## New features
|
||||||
|
* api: add token authentication
|
||||||
|
* mattermost: add support for mattermost 3.10.0
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
* api: gateway name is added in JSON messages
|
||||||
|
* api: lowercase JSON keys
|
||||||
|
* api: channel name isn't needed in config #195
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* discord: Add hashtag to channelname (when translating from id) (discord)
|
||||||
|
* mattermost: Fix a panic. #186
|
||||||
|
* mattermost: use teamid cache if possible. Fixes a panic
|
||||||
|
* api: post valid json. #185
|
||||||
|
* api: allow reuse of api in different gateways. #189
|
||||||
|
* general: Fix utf-8 issues for {NOPINGNICK}. #193
|
||||||
|
|
||||||
|
# v0.13.0
|
||||||
|
## New features
|
||||||
|
* irc: Limit message length. ```MessageLength=400```
|
||||||
|
Maximum length of message sent to irc server. If it exceeds <message clipped> will be add to the message.
|
||||||
|
* irc: Add NOPINGNICK option.
|
||||||
|
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
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* slack: Fix sending to different channels on same account (slack). Closes #177
|
||||||
|
* telegram: Fix incorrect usernames being sent. Closes #181
|
||||||
|
|
||||||
|
|
||||||
|
# v0.12.1
|
||||||
|
## New features
|
||||||
|
* telegram: Add UseFirstName option (telegram). Closes #144
|
||||||
|
* matrix: Add NoHomeServerSuffix. Option to disable homeserver on username (matrix). Closes #160.
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* xmpp: Add Compatibility for Cisco Jabber (xmpp) (#166)
|
||||||
|
* irc: Fix JoinChannel argument to use IRC channel key (#172)
|
||||||
|
* discord: Fix possible crash on nil (discord)
|
||||||
|
* discord: Replace long ids in channel metions (discord). Fixes #174
|
||||||
|
|
||||||
|
# v0.12.0
|
||||||
|
## Changes
|
||||||
|
* general: edited messages are now being sent by default on discord/mattermost/telegram/slack. See "New Features"
|
||||||
|
|
||||||
|
## New features
|
||||||
|
* general: add support for edited messages.
|
||||||
|
Add new keyword EditDisable (false/true), default false. Which means by default edited messages will be sent to other bridges.
|
||||||
|
Add new keyword EditSuffix , default "". You can change this eg to "(edited)", this will be appended to every edit message.
|
||||||
|
* mattermost: support mattermost v3.9.x
|
||||||
|
* general: Add support for HTTP{S}_PROXY env variables (#162)
|
||||||
|
* discord: Strip custom emoji metadata (discord). Closes #148
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* slack: Ignore error on private channel join (slack) Fixes #150
|
||||||
|
* mattermost: fix crash on reconnects when server is down. Closes #163
|
||||||
|
* irc: Relay messages starting with ! (irc). Closes #164
|
||||||
|
|
||||||
|
# v0.11.0
|
||||||
|
## New features
|
||||||
|
* general: reusing the same account on multiple gateways now also reuses the connection.
|
||||||
|
This is particuarly useful for irc. See #87
|
||||||
|
* general: the Name is now REQUIRED and needs to be UNIQUE for each gateway configuration
|
||||||
|
* telegram: Support edited messages (telegram). See #141
|
||||||
|
* mattermost: Add support for showing/hiding join/leave messages from mattermost. Closes #147
|
||||||
|
* mattermost: Reconnect on session removal/timeout (mattermost)
|
||||||
|
* mattermost: Support mattermost v3.8.x
|
||||||
|
* irc: Rejoin channel when kicked (irc).
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* mattermost: Remove space after nick (mattermost). Closes #142
|
||||||
|
* mattermost: Modify iconurl correctly (mattermost).
|
||||||
|
* irc: Fix join/leave regression (irc)
|
||||||
|
|
||||||
|
# v0.10.3
|
||||||
|
## Bugfix
|
||||||
|
* slack: Allow bot tokens for now without warning (slack). Closes #140 (fixes user_is_bot message on channel join)
|
||||||
|
|
||||||
|
# v0.10.2
|
||||||
|
## New features
|
||||||
|
* general: gops agent added. Allows for more debugging. See #134
|
||||||
|
* general: toml inline table support added for config file
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* all: vendored libs updated
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
* general: add more informative messages on startup
|
||||||
|
|
||||||
|
# v0.10.1
|
||||||
|
## Bugfix
|
||||||
|
* gitter: Fix sending messages on new channel join.
|
||||||
|
|
||||||
|
# v0.10.0
|
||||||
|
## New features
|
||||||
|
* matrix: New protocol support added (https://matrix.org)
|
||||||
|
* mattermost: works with mattermost release v3.7.0
|
||||||
|
* discord: Replace role ids in mentions to role names (discord). Closes #133
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* mattermost: Add ReadTimeout to close lingering connections (mattermost). See #125
|
||||||
|
* gitter: Join rooms not already joined by the bot (gitter). See #135
|
||||||
|
* general: Fail when bridge is unable to join a channel (general)
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
* telegram: Do not use HTML parsemode by default. Set ```MessageFormat="HTML"``` to use it. Closes #126
|
||||||
|
|
||||||
|
# v0.9.3
|
||||||
|
## New features
|
||||||
|
* API: rest interface to read / post messages (see API section in matterbridge.toml.sample)
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* slack: fix receiving messages from private channels #118
|
||||||
|
* slack: fix echo when using webhooks #119
|
||||||
|
* mattermost: reconnecting should work better now
|
||||||
|
* irc: keeps reconnecting (every 60 seconds) now after ping timeout/disconnects.
|
||||||
|
|
||||||
|
# v0.9.2
|
||||||
|
## New features
|
||||||
|
* slack: support private channels #118
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* general: make ignorenicks work again #115
|
||||||
|
* telegram: fix receiving from channels and groups #112
|
||||||
|
* telegram: use html for username
|
||||||
|
* telegram: use ```unknown``` as username when username is not visible.
|
||||||
|
* irc: update vendor (fixes some crashes) #117
|
||||||
|
* xmpp: fix tls by setting ServerName #114
|
||||||
|
|
||||||
|
# v0.9.1
|
||||||
|
## New features
|
||||||
|
* Rocket.Chat: New protocol support added (https://rocket.chat)
|
||||||
|
* irc: add channel key support #27 (see matterbrige.toml.sample for example)
|
||||||
|
* xmpp: add SkipTLSVerify #106
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* general: Exit when a bridge fails to start
|
||||||
|
* mattermost: Check errors only on first connect. Keep retrying after first connection succeeds. #95
|
||||||
|
* telegram: fix missing username #102
|
||||||
|
* slack: do not use API functions in webhook (slack) #110
|
||||||
|
|
||||||
|
# v0.9.0
|
||||||
|
## New features
|
||||||
|
* Telegram: New protocol support added (https://telegram.org)
|
||||||
|
* Hipchat: Add sample config to connect to hipchat via xmpp
|
||||||
|
* discord: add "Bot " tag to discord tokens automatically
|
||||||
|
* slack: Add support for dynamic Iconurl #43
|
||||||
|
* general: Add ```gateway.inout``` config option for bidirectional bridges #85
|
||||||
|
* general: Add ```[general]``` section so that ```RemoteNickFormat``` can be set globally
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* general: when using samechannelgateway NickFormat get doubled by the NICK #77
|
||||||
|
* general: fix ShowJoinPart for messages from irc bridge #72
|
||||||
|
* gitter: fix high cpu usage #89
|
||||||
|
* irc: fix !users command #78
|
||||||
|
* xmpp: fix keepalive
|
||||||
|
* xmpp: do not relay delayed/empty messages
|
||||||
|
* slack: Replace id-mentions to usernames #86
|
||||||
|
* mattermost: fix public links not working (API changes)
|
||||||
|
|
||||||
|
# v0.8.1
|
||||||
|
## Bugfix
|
||||||
|
* general: when using samechannelgateway NickFormat get doubled by the NICK #77
|
||||||
|
* irc: fix !users command #78
|
||||||
|
|
||||||
|
# v0.8.0
|
||||||
|
Release because of breaking mattermost API changes
|
||||||
|
## New features
|
||||||
|
* Supports mattermost v3.5.0
|
||||||
|
|
||||||
|
# v0.7.1
|
||||||
|
## Bugfix
|
||||||
|
* general: when using samechannelgateway NickFormat get doubled by the NICK #77
|
||||||
|
* irc: fix !users command #78
|
||||||
|
|
||||||
|
# v0.7.0
|
||||||
|
## Breaking config changes from 0.6 to 0.7
|
||||||
|
Matterbridge now uses TOML configuration (https://github.com/toml-lang/toml)
|
||||||
|
See matterbridge.toml.sample for an example
|
||||||
|
|
||||||
|
## New features
|
||||||
|
### General
|
||||||
|
* Allow for bridging the same type of bridge, which means you can eg bridge between multiple mattermosts.
|
||||||
|
* The bridge is now actually a gateway which has support multiple in and out bridges. (and supports multiple gateways).
|
||||||
|
* Discord support added. See matterbridge.toml.sample for more information.
|
||||||
|
* Samechannelgateway support added, easier configuration for 1:1 mapping of protocols with same channel names. #35
|
||||||
|
* Support for override from environment variables. #50
|
||||||
|
* Better debugging output.
|
||||||
|
* discord: New protocol support added. (http://www.discordapp.com)
|
||||||
|
* mattermost: Support attachments.
|
||||||
|
* irc: Strip colors. #33
|
||||||
|
* irc: Anti-flooding support. #40
|
||||||
|
* irc: Forward channel notices.
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* irc: Split newlines. #37
|
||||||
|
* irc: Only respond to nick related notices from nickserv.
|
||||||
|
* irc: Ignore queries send to the bot.
|
||||||
|
* irc: Ignore messages from ourself.
|
||||||
|
* irc: Only output the "users on irc information" when asked with "!users".
|
||||||
|
* irc: Actually wait until connection is complete before saying it is.
|
||||||
|
* mattermost: Fix mattermost channel joins.
|
||||||
|
* mattermost: Drop messages not from our team.
|
||||||
|
* slack: Do not panic on non-existing channels.
|
||||||
|
* general: Exit when a bridge fails to start.
|
||||||
|
|
||||||
|
# v0.6.1
|
||||||
|
## New features
|
||||||
|
* Slack support added. See matterbridge.conf.sample for more information
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* Fix 100% CPU bug on incorrect closed connections
|
||||||
|
|
||||||
|
# v0.6.0-beta2
|
||||||
|
## New features
|
||||||
|
* Gitter support added. See matterbridge.conf.sample for more information
|
||||||
|
|
||||||
|
# v0.6.0-beta1
|
||||||
|
## Breaking changes from 0.5 to 0.6
|
||||||
|
### commandline
|
||||||
|
* -plus switch deprecated. Use ```Plus=true``` or ```Plus``` in ```[general]``` section
|
||||||
|
|
||||||
|
### IRC section
|
||||||
|
* ```Enabled``` added (default false)
|
||||||
|
Add ```Enabled=true``` or ```Enabled``` to the ```[IRC]``` section if you want to enable the IRC bridge
|
||||||
|
|
||||||
|
### Mattermost section
|
||||||
|
* ```Enabled``` added (default false)
|
||||||
|
Add ```Enabled=true``` or ```Enabled``` to the ```[mattermost]``` section if you want to enable the mattermost bridge
|
||||||
|
|
||||||
|
### General section
|
||||||
|
* Use ```Plus=true``` or ```Plus``` in ```[general]``` section to enable the API version of matterbridge
|
||||||
|
|
||||||
|
## New features
|
||||||
|
* Matterbridge now bridges between any specified protocol (not only mattermost anymore)
|
||||||
|
* XMPP support added. See matterbridge.conf.sample for more information
|
||||||
|
* RemoteNickFormat {BRIDGE} variable added
|
||||||
|
You can now add the originating bridge to ```RemoteNickFormat```
|
||||||
|
eg ```RemoteNickFormat="[{BRIDGE}] <{NICK}> "```
|
||||||
|
|
||||||
|
|
||||||
|
# v0.5.0
|
||||||
|
## Breaking changes from 0.4 to 0.5 for matterbridge (webhooks version)
|
||||||
|
### IRC section
|
||||||
|
#### Server
|
||||||
|
Port removed, added to server
|
||||||
|
```
|
||||||
|
server="irc.freenode.net"
|
||||||
|
port=6667
|
||||||
|
```
|
||||||
|
changed to
|
||||||
|
```
|
||||||
|
server="irc.freenode.net:6667"
|
||||||
|
```
|
||||||
|
#### Channel
|
||||||
|
Removed see Channels section below
|
||||||
|
|
||||||
|
#### UseSlackCircumfix=true
|
||||||
|
Removed, can be done by using ```RemoteNickFormat="<{NICK}> "```
|
||||||
|
|
||||||
|
### Mattermost section
|
||||||
|
#### BindAddress
|
||||||
|
Port removed, added to BindAddress
|
||||||
|
|
||||||
|
```
|
||||||
|
BindAddress="0.0.0.0"
|
||||||
|
port=9999
|
||||||
|
```
|
||||||
|
|
||||||
|
changed to
|
||||||
|
|
||||||
|
```
|
||||||
|
BindAddress="0.0.0.0:9999"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Token
|
||||||
|
Removed
|
||||||
|
|
||||||
|
### Channels section
|
||||||
|
```
|
||||||
|
[Token "outgoingwebhooktoken1"]
|
||||||
|
IRCChannel="#off-topic"
|
||||||
|
MMChannel="off-topic"
|
||||||
|
```
|
||||||
|
|
||||||
|
changed to
|
||||||
|
|
||||||
|
```
|
||||||
|
[Channel "channelnameofchoice"]
|
||||||
|
IRC="#off-topic"
|
||||||
|
Mattermost="off-topic"
|
||||||
|
```
|
26
ci/bintray.sh
Executable file
26
ci/bintray.sh
Executable file
@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
go version |grep go1.8 || 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-win64.exe
|
||||||
|
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-linux64
|
||||||
|
GOOS=linux GOARCH=arm go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-linux-arm
|
||||||
|
cd ci
|
||||||
|
cat > deploy.json <<EOF
|
||||||
|
{
|
||||||
|
"package": {
|
||||||
|
"name": "Matterbridge",
|
||||||
|
"repo": "nightly",
|
||||||
|
"subject": "42wim"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"name": "$VERSION"
|
||||||
|
},
|
||||||
|
"files":
|
||||||
|
[
|
||||||
|
{"includePattern": "ci/binaries/(.*)", "uploadPattern":"\$1"}
|
||||||
|
],
|
||||||
|
"publish": true
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
50
config.go
50
config.go
@ -1,50 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gopkg.in/gcfg.v1"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
IRC struct {
|
|
||||||
UseTLS bool
|
|
||||||
SkipTLSVerify bool
|
|
||||||
Server string
|
|
||||||
Port int
|
|
||||||
Nick string
|
|
||||||
Password string
|
|
||||||
Channel string
|
|
||||||
UseSlackCircumfix bool
|
|
||||||
}
|
|
||||||
Mattermost struct {
|
|
||||||
URL string
|
|
||||||
Port int
|
|
||||||
ShowJoinPart bool
|
|
||||||
Token string
|
|
||||||
IconURL string
|
|
||||||
SkipTLSVerify bool
|
|
||||||
BindAddress string
|
|
||||||
Channel string
|
|
||||||
}
|
|
||||||
Token map[string]*struct {
|
|
||||||
IRCChannel string
|
|
||||||
MMChannel string
|
|
||||||
}
|
|
||||||
General struct {
|
|
||||||
GiphyAPIKey string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConfig(cfgfile string) *Config {
|
|
||||||
var cfg Config
|
|
||||||
content, err := ioutil.ReadFile(cfgfile)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
err = gcfg.ReadStringInto(&cfg, string(content))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Failed to parse "+cfgfile+":", err)
|
|
||||||
}
|
|
||||||
return &cfg
|
|
||||||
}
|
|
257
gateway/gateway.go
Normal file
257
gateway/gateway.go
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/42wim/matterbridge/bridge"
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
// "github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/peterhellberg/emojilib"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Gateway struct {
|
||||||
|
*config.Config
|
||||||
|
Router *Router
|
||||||
|
MyConfig *config.Gateway
|
||||||
|
Bridges map[string]*bridge.Bridge
|
||||||
|
Channels map[string]*config.ChannelInfo
|
||||||
|
ChannelOptions map[string]config.ChannelOptions
|
||||||
|
Message chan config.Message
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.Gateway, r *Router) *Gateway {
|
||||||
|
gw := &Gateway{Channels: make(map[string]*config.ChannelInfo), Message: r.Message,
|
||||||
|
Router: r, Bridges: make(map[string]*bridge.Bridge), Config: r.Config}
|
||||||
|
gw.AddConfig(&cfg)
|
||||||
|
return gw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) AddBridge(cfg *config.Bridge) error {
|
||||||
|
br := gw.Router.getBridge(cfg.Account)
|
||||||
|
if br == nil {
|
||||||
|
br = bridge.New(gw.Config, cfg, gw.Message)
|
||||||
|
}
|
||||||
|
gw.mapChannelsToBridge(br)
|
||||||
|
gw.Bridges[cfg.Account] = br
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) AddConfig(cfg *config.Gateway) error {
|
||||||
|
gw.Name = cfg.Name
|
||||||
|
gw.MyConfig = cfg
|
||||||
|
gw.mapChannels()
|
||||||
|
for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) {
|
||||||
|
err := gw.AddBridge(&br)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) mapChannelsToBridge(br *bridge.Bridge) {
|
||||||
|
for ID, channel := range gw.Channels {
|
||||||
|
if br.Account == channel.Account {
|
||||||
|
br.Channels[ID] = *channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) reconnectBridge(br *bridge.Bridge) {
|
||||||
|
br.Disconnect()
|
||||||
|
time.Sleep(time.Second * 5)
|
||||||
|
RECONNECT:
|
||||||
|
log.Infof("Reconnecting %s", br.Account)
|
||||||
|
err := br.Connect()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Reconnection failed: %s. Trying again in 60 seconds", err)
|
||||||
|
time.Sleep(time.Second * 60)
|
||||||
|
goto RECONNECT
|
||||||
|
}
|
||||||
|
br.Joined = make(map[string]bool)
|
||||||
|
br.JoinChannels()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) mapChannelConfig(cfg []config.Bridge, direction string) {
|
||||||
|
for _, br := range cfg {
|
||||||
|
if isApi(br.Account) {
|
||||||
|
br.Channel = "api"
|
||||||
|
}
|
||||||
|
ID := br.Channel + br.Account
|
||||||
|
if _, ok := gw.Channels[ID]; !ok {
|
||||||
|
channel := &config.ChannelInfo{Name: br.Channel, Direction: direction, ID: ID, Options: br.Options, Account: br.Account,
|
||||||
|
SameChannel: make(map[string]bool)}
|
||||||
|
channel.SameChannel[gw.Name] = br.SameChannel
|
||||||
|
gw.Channels[channel.ID] = channel
|
||||||
|
} else {
|
||||||
|
// if we already have a key and it's not our current direction it means we have a bidirectional inout
|
||||||
|
if gw.Channels[ID].Direction != direction {
|
||||||
|
gw.Channels[ID].Direction = "inout"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gw.Channels[ID].SameChannel[gw.Name] = br.SameChannel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) mapChannels() error {
|
||||||
|
gw.mapChannelConfig(gw.MyConfig.In, "in")
|
||||||
|
gw.mapChannelConfig(gw.MyConfig.Out, "out")
|
||||||
|
gw.mapChannelConfig(gw.MyConfig.InOut, "inout")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []config.ChannelInfo {
|
||||||
|
var channels []config.ChannelInfo
|
||||||
|
// if source channel is in only, do nothing
|
||||||
|
for _, channel := range gw.Channels {
|
||||||
|
// lookup the channel from the message
|
||||||
|
if channel.ID == getChannelID(*msg) {
|
||||||
|
// we only have destinations if the original message is from an "in" (sending) channel
|
||||||
|
if !strings.Contains(channel.Direction, "in") {
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, channel := range gw.Channels {
|
||||||
|
if _, ok := gw.Channels[getChannelID(*msg)]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// do samechannelgateway logic
|
||||||
|
if channel.SameChannel[msg.Gateway] {
|
||||||
|
if msg.Channel == channel.Name && msg.Account != dest.Account {
|
||||||
|
channels = append(channels, *channel)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(channel.Direction, "out") && channel.Account == dest.Account && gw.validGatewayDest(msg, channel) {
|
||||||
|
channels = append(channels, *channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) {
|
||||||
|
// only relay join/part when configged
|
||||||
|
if msg.Event == config.EVENT_JOIN_LEAVE && !gw.Bridges[dest.Account].Config.ShowJoinPart {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// broadcast to every out channel (irc QUIT)
|
||||||
|
if msg.Channel == "" && msg.Event != config.EVENT_JOIN_LEAVE {
|
||||||
|
log.Debug("empty channel")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
originchannel := msg.Channel
|
||||||
|
origmsg := msg
|
||||||
|
channels := gw.getDestChannel(&msg, *dest)
|
||||||
|
for _, channel := range channels {
|
||||||
|
// do not send to ourself
|
||||||
|
if channel.ID == getChannelID(origmsg) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.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)
|
||||||
|
// for api we need originchannel as channel
|
||||||
|
if dest.Protocol == "api" {
|
||||||
|
msg.Channel = originchannel
|
||||||
|
}
|
||||||
|
err := dest.Send(msg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
|
||||||
|
// if we don't have the bridge, ignore it
|
||||||
|
if _, ok := gw.Bridges[msg.Account]; !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if msg.Text == "" {
|
||||||
|
log.Debugf("ignoring empty message %#v from %s", msg, msg.Account)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, entry := range strings.Fields(gw.Bridges[msg.Account].Config.IgnoreNicks) {
|
||||||
|
if msg.Username == entry {
|
||||||
|
log.Debugf("ignoring %s from %s", msg.Username, msg.Account)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO do not compile regexps everytime
|
||||||
|
for _, entry := range strings.Fields(gw.Bridges[msg.Account].Config.IgnoreMessages) {
|
||||||
|
if entry != "" {
|
||||||
|
re, err := regexp.Compile(entry)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("incorrect regexp %s for %s", entry, msg.Account)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if re.MatchString(msg.Text) {
|
||||||
|
log.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
|
||||||
|
nick := gw.Config.General.RemoteNickFormat
|
||||||
|
if nick == "" {
|
||||||
|
nick = dest.Config.RemoteNickFormat
|
||||||
|
}
|
||||||
|
if len(msg.Username) > 0 {
|
||||||
|
// fix utf-8 issue #193
|
||||||
|
i := 0
|
||||||
|
for index := range msg.Username {
|
||||||
|
if i == 1 {
|
||||||
|
i = index
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
nick = strings.Replace(nick, "{NOPINGNICK}", msg.Username[:i]+""+msg.Username[i:], -1)
|
||||||
|
}
|
||||||
|
nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
|
||||||
|
nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1)
|
||||||
|
nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1)
|
||||||
|
return nick
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) modifyAvatar(msg config.Message, dest *bridge.Bridge) string {
|
||||||
|
iconurl := gw.Config.General.IconURL
|
||||||
|
if iconurl == "" {
|
||||||
|
iconurl = dest.Config.IconURL
|
||||||
|
}
|
||||||
|
iconurl = strings.Replace(iconurl, "{NICK}", msg.Username, -1)
|
||||||
|
if msg.Avatar == "" {
|
||||||
|
msg.Avatar = iconurl
|
||||||
|
}
|
||||||
|
return msg.Avatar
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) modifyMessage(msg *config.Message) {
|
||||||
|
// replace :emoji: to unicode
|
||||||
|
msg.Text = emojilib.Replace(msg.Text)
|
||||||
|
msg.Gateway = gw.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func getChannelID(msg config.Message) string {
|
||||||
|
return msg.Channel + msg.Account
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gw *Gateway) validGatewayDest(msg *config.Message, channel *config.ChannelInfo) bool {
|
||||||
|
return msg.Gateway == gw.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func isApi(account string) bool {
|
||||||
|
return strings.HasPrefix(account, "api.")
|
||||||
|
}
|
288
gateway/gateway_test.go
Normal file
288
gateway/gateway_test.go
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testconfig = `
|
||||||
|
[irc.freenode]
|
||||||
|
[mattermost.test]
|
||||||
|
[gitter.42wim]
|
||||||
|
[discord.test]
|
||||||
|
[slack.test]
|
||||||
|
|
||||||
|
[[gateway]]
|
||||||
|
name = "bridge1"
|
||||||
|
enable=true
|
||||||
|
|
||||||
|
[[gateway.inout]]
|
||||||
|
account = "irc.freenode"
|
||||||
|
channel = "#wimtesting"
|
||||||
|
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="gitter.42wim"
|
||||||
|
channel="42wim/testroom"
|
||||||
|
#channel="matterbridge/Lobby"
|
||||||
|
|
||||||
|
[[gateway.inout]]
|
||||||
|
account = "discord.test"
|
||||||
|
channel = "general"
|
||||||
|
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="slack.test"
|
||||||
|
channel="testing"
|
||||||
|
`
|
||||||
|
|
||||||
|
var testconfig2 = `
|
||||||
|
[irc.freenode]
|
||||||
|
[mattermost.test]
|
||||||
|
[gitter.42wim]
|
||||||
|
[discord.test]
|
||||||
|
[slack.test]
|
||||||
|
|
||||||
|
[[gateway]]
|
||||||
|
name = "bridge1"
|
||||||
|
enable=true
|
||||||
|
|
||||||
|
[[gateway.in]]
|
||||||
|
account = "irc.freenode"
|
||||||
|
channel = "#wimtesting"
|
||||||
|
|
||||||
|
[[gateway.in]]
|
||||||
|
account="gitter.42wim"
|
||||||
|
channel="42wim/testroom"
|
||||||
|
|
||||||
|
[[gateway.inout]]
|
||||||
|
account = "discord.test"
|
||||||
|
channel = "general"
|
||||||
|
|
||||||
|
[[gateway.out]]
|
||||||
|
account="slack.test"
|
||||||
|
channel="testing"
|
||||||
|
[[gateway]]
|
||||||
|
name = "bridge2"
|
||||||
|
enable=true
|
||||||
|
|
||||||
|
[[gateway.in]]
|
||||||
|
account = "irc.freenode"
|
||||||
|
channel = "#wimtesting2"
|
||||||
|
|
||||||
|
[[gateway.out]]
|
||||||
|
account="gitter.42wim"
|
||||||
|
channel="42wim/testroom"
|
||||||
|
|
||||||
|
[[gateway.out]]
|
||||||
|
account = "discord.test"
|
||||||
|
channel = "general2"
|
||||||
|
`
|
||||||
|
var testconfig3 = `
|
||||||
|
[irc.zzz]
|
||||||
|
[telegram.zzz]
|
||||||
|
[slack.zzz]
|
||||||
|
[[gateway]]
|
||||||
|
name="bridge"
|
||||||
|
enable=true
|
||||||
|
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="irc.zzz"
|
||||||
|
channel="#main"
|
||||||
|
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="telegram.zzz"
|
||||||
|
channel="-1111111111111"
|
||||||
|
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="slack.zzz"
|
||||||
|
channel="irc"
|
||||||
|
|
||||||
|
[[gateway]]
|
||||||
|
name="announcements"
|
||||||
|
enable=true
|
||||||
|
|
||||||
|
[[gateway.in]]
|
||||||
|
account="telegram.zzz"
|
||||||
|
channel="-2222222222222"
|
||||||
|
|
||||||
|
[[gateway.out]]
|
||||||
|
account="irc.zzz"
|
||||||
|
channel="#main"
|
||||||
|
|
||||||
|
[[gateway.out]]
|
||||||
|
account="irc.zzz"
|
||||||
|
channel="#main-help"
|
||||||
|
|
||||||
|
[[gateway.out]]
|
||||||
|
account="telegram.zzz"
|
||||||
|
channel="--333333333333"
|
||||||
|
|
||||||
|
[[gateway.out]]
|
||||||
|
account="slack.zzz"
|
||||||
|
channel="general"
|
||||||
|
|
||||||
|
[[gateway]]
|
||||||
|
name="bridge2"
|
||||||
|
enable=true
|
||||||
|
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="irc.zzz"
|
||||||
|
channel="#main-help"
|
||||||
|
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="telegram.zzz"
|
||||||
|
channel="--444444444444"
|
||||||
|
|
||||||
|
|
||||||
|
[[gateway]]
|
||||||
|
name="bridge3"
|
||||||
|
enable=true
|
||||||
|
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="irc.zzz"
|
||||||
|
channel="#main-telegram"
|
||||||
|
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="telegram.zzz"
|
||||||
|
channel="--333333333333"
|
||||||
|
`
|
||||||
|
|
||||||
|
func maketestRouter(input string) *Router {
|
||||||
|
var cfg *config.Config
|
||||||
|
if _, err := toml.Decode(input, &cfg); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
r, err := NewRouter(cfg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
func TestNewRouter(t *testing.T) {
|
||||||
|
var cfg *config.Config
|
||||||
|
if _, err := toml.Decode(testconfig, &cfg); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
r, err := NewRouter(cfg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, 1, len(r.Gateways))
|
||||||
|
assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges))
|
||||||
|
assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels))
|
||||||
|
|
||||||
|
r = maketestRouter(testconfig2)
|
||||||
|
assert.Equal(t, 2, len(r.Gateways))
|
||||||
|
assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges))
|
||||||
|
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"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDestChannel(t *testing.T) {
|
||||||
|
r := maketestRouter(testconfig2)
|
||||||
|
msg := &config.Message{Text: "test", Channel: "general", Account: "discord.test", Gateway: "bridge1", Protocol: "discord", Username: "test"}
|
||||||
|
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))
|
||||||
|
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))
|
||||||
|
case "gitter.42wim":
|
||||||
|
assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br))
|
||||||
|
case "irc.freenode":
|
||||||
|
assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDestChannelAdvanced(t *testing.T) {
|
||||||
|
r := maketestRouter(testconfig3)
|
||||||
|
var msgs []*config.Message
|
||||||
|
i := 0
|
||||||
|
for _, gw := range r.Gateways {
|
||||||
|
for _, channel := range gw.Channels {
|
||||||
|
msgs = append(msgs, &config.Message{Text: "text" + strconv.Itoa(i), Channel: channel.Name, Account: channel.Account, Gateway: gw.Name, Username: "user" + strconv.Itoa(i)})
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hits := make(map[string]int)
|
||||||
|
for _, gw := range r.Gateways {
|
||||||
|
for _, br := range gw.Bridges {
|
||||||
|
for _, msg := range msgs {
|
||||||
|
channels := gw.getDestChannel(msg, *br)
|
||||||
|
if gw.Name != msg.Gateway {
|
||||||
|
assert.Equal(t, []config.ChannelInfo(nil), channels)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
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") {
|
||||||
|
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 "bridge2":
|
||||||
|
if (msg.Channel == "#main-help" || msg.Channel == "--444444444444") && (msg.Account == "irc.zzz" || msg.Account == "telegram.zzz") {
|
||||||
|
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 "bridge3":
|
||||||
|
if (msg.Channel == "#main-telegram" || msg.Channel == "--333333333333") && (msg.Account == "irc.zzz" || msg.Account == "telegram.zzz") {
|
||||||
|
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 "announcements":
|
||||||
|
if msg.Channel != "-2222222222222" && msg.Account != "telegram" {
|
||||||
|
assert.Equal(t, []config.ChannelInfo(nil), channels)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Equal(t, map[string]int{"bridge3": 4, "bridge": 9, "announcements": 3, "bridge2": 4}, hits)
|
||||||
|
}
|
106
gateway/router.go
Normal file
106
gateway/router.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/42wim/matterbridge/bridge"
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
"github.com/42wim/matterbridge/gateway/samechannel"
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
// "github.com/davecgh/go-spew/spew"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Router struct {
|
||||||
|
Gateways map[string]*Gateway
|
||||||
|
Message chan config.Message
|
||||||
|
*config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRouter(cfg *config.Config) (*Router, error) {
|
||||||
|
r := &Router{}
|
||||||
|
r.Config = cfg
|
||||||
|
r.Message = make(chan config.Message)
|
||||||
|
r.Gateways = make(map[string]*Gateway)
|
||||||
|
sgw := samechannelgateway.New(cfg)
|
||||||
|
gwconfigs := sgw.GetConfig()
|
||||||
|
|
||||||
|
for _, entry := range append(gwconfigs, cfg.Gateway...) {
|
||||||
|
if !entry.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if entry.Name == "" {
|
||||||
|
return nil, fmt.Errorf("%s", "Gateway without name found")
|
||||||
|
}
|
||||||
|
if _, ok := r.Gateways[entry.Name]; ok {
|
||||||
|
return nil, fmt.Errorf("Gateway with name %s already exists", entry.Name)
|
||||||
|
}
|
||||||
|
r.Gateways[entry.Name] = New(entry, r)
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) Start() error {
|
||||||
|
m := make(map[string]*bridge.Bridge)
|
||||||
|
for _, gw := range r.Gateways {
|
||||||
|
for _, br := range gw.Bridges {
|
||||||
|
m[br.Account] = br
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, br := range m {
|
||||||
|
log.Infof("Starting bridge: %s ", br.Account)
|
||||||
|
err := br.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Bridge %s failed to start: %v", br.Account, err)
|
||||||
|
}
|
||||||
|
err = br.JoinChannels()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Bridge %s failed to join channel: %v", br.Account, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
go r.handleReceive()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) getBridge(account string) *bridge.Bridge {
|
||||||
|
for _, gw := range r.Gateways {
|
||||||
|
if br, ok := gw.Bridges[account]; ok {
|
||||||
|
return br
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, gw := range r.Gateways {
|
||||||
|
if !gw.ignoreMessage(&msg) {
|
||||||
|
msg.Timestamp = time.Now()
|
||||||
|
gw.modifyMessage(&msg)
|
||||||
|
for _, br := range gw.Bridges {
|
||||||
|
gw.handleMessage(msg, br)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
gateway/samechannel/samechannel.go
Normal file
28
gateway/samechannel/samechannel.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package samechannelgateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SameChannelGateway struct {
|
||||||
|
*config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
gwconfig := config.Gateway{Name: gw.Name, Enable: gw.Enable}
|
||||||
|
for _, account := range gw.Accounts {
|
||||||
|
for _, channel := range gw.Channels {
|
||||||
|
gwconfig.InOut = append(gwconfig.InOut, config.Bridge{Account: account, Channel: channel, SameChannel: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gwconfigs = append(gwconfigs, gwconfig)
|
||||||
|
}
|
||||||
|
return gwconfigs
|
||||||
|
}
|
31
gateway/samechannel/samechannel_test.go
Normal file
31
gateway/samechannel/samechannel_test.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package samechannelgateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testconfig = `
|
||||||
|
[mattermost.test]
|
||||||
|
[slack.test]
|
||||||
|
|
||||||
|
[[samechannelgateway]]
|
||||||
|
enable = true
|
||||||
|
name = "blah"
|
||||||
|
accounts = [ "mattermost.test","slack.test" ]
|
||||||
|
channels = [ "testing","testing2","testing10"]
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestGetConfig(t *testing.T) {
|
||||||
|
var cfg *config.Config
|
||||||
|
if _, err := toml.Decode(testconfig, &cfg); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
107
hook/rockethook/rockethook.go
Normal file
107
hook/rockethook/rockethook.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package rockethook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message for rocketchat outgoing webhook.
|
||||||
|
type Message struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
ChannelID string `json:"channel_id"`
|
||||||
|
ChannelName string `json:"channel_name"`
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
UserName string `json:"user_name"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client for Rocketchat.
|
||||||
|
type Client struct {
|
||||||
|
In chan Message
|
||||||
|
httpclient *http.Client
|
||||||
|
Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config for client.
|
||||||
|
type Config struct {
|
||||||
|
BindAddress string // Address to listen on
|
||||||
|
Token string // Only allow this token from Rocketchat. (Allow everything when empty)
|
||||||
|
InsecureSkipVerify bool // disable certificate checking
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Rocketchat client.
|
||||||
|
func New(url string, config Config) *Client {
|
||||||
|
c := &Client{In: make(chan Message), Config: config}
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify},
|
||||||
|
}
|
||||||
|
c.httpclient = &http.Client{Transport: tr}
|
||||||
|
_, _, err := net.SplitHostPort(c.BindAddress)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("incorrect bindaddress %s", c.BindAddress)
|
||||||
|
}
|
||||||
|
go c.StartServer()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartServer starts a webserver listening for incoming mattermost POSTS.
|
||||||
|
func (c *Client) StartServer() {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("/", c)
|
||||||
|
log.Printf("Listening on http://%v...\n", c.BindAddress)
|
||||||
|
if err := http.ListenAndServe(c.BindAddress, mux); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP implementation.
|
||||||
|
func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
log.Println("invalid " + r.Method + " connection from " + r.RemoteAddr)
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg := Message{}
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
log.Println(string(body))
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
err = json.Unmarshal(body, &msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if msg.Token == "" {
|
||||||
|
log.Println("no token from " + r.RemoteAddr)
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg.ChannelName = "#" + msg.ChannelName
|
||||||
|
if c.Token != "" {
|
||||||
|
if msg.Token != c.Token {
|
||||||
|
log.Println("invalid token " + msg.Token + " from " + r.RemoteAddr)
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.In <- msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive returns an incoming message from mattermost outgoing webhooks URL.
|
||||||
|
func (c *Client) Receive() Message {
|
||||||
|
var msg Message
|
||||||
|
for msg = range c.In {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
@ -1,32 +0,0 @@
|
|||||||
[IRC]
|
|
||||||
server="irc.freenode.net"
|
|
||||||
port=6667
|
|
||||||
UseTLS=false
|
|
||||||
SkipTLSVerify=true
|
|
||||||
nick="matterbot"
|
|
||||||
channel="#matterbridge"
|
|
||||||
UseSlackCircumfix=false
|
|
||||||
|
|
||||||
[mattermost]
|
|
||||||
url="http://yourdomain/hooks/yourhookkey"
|
|
||||||
port=9999
|
|
||||||
showjoinpart=true
|
|
||||||
#remove token when using multiple channels!
|
|
||||||
token=yourtokenfrommattermost
|
|
||||||
IconURL="http://youricon.png"
|
|
||||||
#SkipTLSVerify=true
|
|
||||||
#BindAddress="0.0.0.0"
|
|
||||||
|
|
||||||
[general]
|
|
||||||
GiphyAPIKey=dc6zaTOxFJmzC
|
|
||||||
|
|
||||||
#multiple channel config
|
|
||||||
#token you can find in your outgoing webhook
|
|
||||||
[Token "outgoingwebhooktoken1"]
|
|
||||||
IRCChannel="#off-topic"
|
|
||||||
MMChannel="off-topic"
|
|
||||||
|
|
||||||
[Token "outgoingwebhooktoken2"]
|
|
||||||
IRCChannel="#testing"
|
|
||||||
MMChannel="testing"
|
|
||||||
|
|
190
matterbridge.go
190
matterbridge.go
@ -1,163 +1,55 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"flag"
|
"flag"
|
||||||
"github.com/42wim/matterbridge/matterhook"
|
"fmt"
|
||||||
"github.com/peterhellberg/giphy"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
"github.com/thoj/go-ircevent"
|
"github.com/42wim/matterbridge/gateway"
|
||||||
"log"
|
log "github.com/Sirupsen/logrus"
|
||||||
"strconv"
|
"github.com/google/gops/agent"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Bridge struct {
|
var (
|
||||||
i *irc.Connection
|
version = "1.0.1"
|
||||||
m *matterhook.Client
|
githash string
|
||||||
cmap map[string]string
|
)
|
||||||
*Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBridge(name string, config *Config) *Bridge {
|
func init() {
|
||||||
b := &Bridge{}
|
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
|
||||||
b.Config = config
|
|
||||||
b.cmap = make(map[string]string)
|
|
||||||
if len(b.Config.Token) > 0 {
|
|
||||||
for _, val := range b.Config.Token {
|
|
||||||
b.cmap[val.IRCChannel] = val.MMChannel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.m = matterhook.New(b.Config.Mattermost.URL,
|
|
||||||
matterhook.Config{Port: b.Config.Mattermost.Port, Token: b.Config.Mattermost.Token,
|
|
||||||
InsecureSkipVerify: b.Config.Mattermost.SkipTLSVerify,
|
|
||||||
BindAddress: b.Config.Mattermost.BindAddress})
|
|
||||||
b.i = b.createIRC(name)
|
|
||||||
go b.handleMatter()
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) createIRC(name string) *irc.Connection {
|
|
||||||
i := irc.IRC(b.Config.IRC.Nick, b.Config.IRC.Nick)
|
|
||||||
i.UseTLS = b.Config.IRC.UseTLS
|
|
||||||
i.TLSConfig = &tls.Config{InsecureSkipVerify: b.Config.IRC.SkipTLSVerify}
|
|
||||||
if b.Config.IRC.Password != "" {
|
|
||||||
i.Password = b.Config.IRC.Password
|
|
||||||
}
|
|
||||||
i.Connect(b.Config.IRC.Server + ":" + strconv.Itoa(b.Config.IRC.Port))
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
log.Println("Joining", b.Config.IRC.Channel, "as", b.Config.IRC.Nick)
|
|
||||||
i.Join(b.Config.IRC.Channel)
|
|
||||||
for _, val := range b.Config.Token {
|
|
||||||
log.Println("Joining", val.IRCChannel, "as", b.Config.IRC.Nick)
|
|
||||||
i.Join(val.IRCChannel)
|
|
||||||
}
|
|
||||||
i.AddCallback("PRIVMSG", b.handlePrivMsg)
|
|
||||||
i.AddCallback("CTCP_ACTION", b.handlePrivMsg)
|
|
||||||
if b.Config.Mattermost.ShowJoinPart {
|
|
||||||
i.AddCallback("JOIN", b.handleJoinPart)
|
|
||||||
i.AddCallback("PART", b.handleJoinPart)
|
|
||||||
}
|
|
||||||
//i.AddCallback("353", b.handleOther)
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) handlePrivMsg(event *irc.Event) {
|
|
||||||
msg := ""
|
|
||||||
if event.Code == "CTCP_ACTION" {
|
|
||||||
msg = event.Nick + " "
|
|
||||||
}
|
|
||||||
msg += event.Message()
|
|
||||||
b.Send("irc-"+event.Nick, msg, b.getMMChannel(event.Arguments[0]))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) handleJoinPart(event *irc.Event) {
|
|
||||||
b.Send(b.Config.IRC.Nick, "irc-"+event.Nick+" "+strings.ToLower(event.Code)+"s "+event.Message(), b.getMMChannel(event.Arguments[0]))
|
|
||||||
//b.SendType(b.Config.IRC.Nick, "irc-"+event.Nick+" "+strings.ToLower(event.Code)+"s "+event.Message(), b.getMMChannel(event.Arguments[0]), "join_leave")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) handleOther(event *irc.Event) {
|
|
||||||
switch event.Code {
|
|
||||||
case "353":
|
|
||||||
log.Println("handleOther", b.getMMChannel(event.Arguments[0]))
|
|
||||||
b.Send(b.Config.IRC.Nick, event.Message()+" currently on IRC", b.getMMChannel(event.Arguments[0]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) Send(nick string, message string, channel string) error {
|
|
||||||
return b.SendType(nick, message, channel, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) SendType(nick string, message string, channel string, mtype string) error {
|
|
||||||
matterMessage := matterhook.OMessage{IconURL: b.Config.Mattermost.IconURL}
|
|
||||||
matterMessage.Channel = channel
|
|
||||||
matterMessage.UserName = nick
|
|
||||||
matterMessage.Text = message
|
|
||||||
matterMessage.Type = mtype
|
|
||||||
err := b.m.Send(matterMessage)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) handleMatter() {
|
|
||||||
var username string
|
|
||||||
for {
|
|
||||||
message := b.m.Receive()
|
|
||||||
username = message.UserName + ": "
|
|
||||||
if b.Config.IRC.UseSlackCircumfix {
|
|
||||||
username = "<" + message.UserName + "> "
|
|
||||||
}
|
|
||||||
cmd := strings.Fields(message.Text)[0]
|
|
||||||
switch cmd {
|
|
||||||
case "!users":
|
|
||||||
log.Println("received !users from", message.UserName)
|
|
||||||
b.i.SendRaw("NAMES " + b.getIRCChannel(message.Token))
|
|
||||||
case "!gif":
|
|
||||||
message.Text = b.giphyRandom(strings.Fields(strings.Replace(message.Text, "!gif ", "", 1)))
|
|
||||||
b.Send(b.Config.IRC.Nick, message.Text, b.getIRCChannel(message.Token))
|
|
||||||
}
|
|
||||||
texts := strings.Split(message.Text, "\n")
|
|
||||||
for _, text := range texts {
|
|
||||||
b.i.Privmsg(b.getIRCChannel(message.Token), username+text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) giphyRandom(query []string) string {
|
|
||||||
g := giphy.DefaultClient
|
|
||||||
if b.Config.General.GiphyAPIKey != "" {
|
|
||||||
g.APIKey = b.Config.General.GiphyAPIKey
|
|
||||||
}
|
|
||||||
res, err := g.Random(query)
|
|
||||||
if err != nil {
|
|
||||||
return "error"
|
|
||||||
}
|
|
||||||
return res.Data.FixedHeightDownsampledURL
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) getMMChannel(ircChannel string) string {
|
|
||||||
mmchannel, ok := b.cmap[ircChannel]
|
|
||||||
if !ok {
|
|
||||||
mmchannel = b.Config.Mattermost.Channel
|
|
||||||
}
|
|
||||||
return mmchannel
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) getIRCChannel(token string) string {
|
|
||||||
ircchannel := b.Config.IRC.Channel
|
|
||||||
_, ok := b.Config.Token[token]
|
|
||||||
if ok {
|
|
||||||
ircchannel = b.Config.Token[token].IRCChannel
|
|
||||||
}
|
|
||||||
return ircchannel
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flagConfig := flag.String("conf", "matterbridge.conf", "config file")
|
flagConfig := flag.String("conf", "matterbridge.toml", "config file")
|
||||||
|
flagDebug := flag.Bool("debug", false, "enable debug")
|
||||||
|
flagVersion := flag.Bool("version", false, "show version")
|
||||||
|
flagGops := flag.Bool("gops", false, "enable gops agent")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
NewBridge("matterbot", NewConfig(*flagConfig))
|
if *flagGops {
|
||||||
|
agent.Listen(&agent.Options{})
|
||||||
|
defer agent.Close()
|
||||||
|
}
|
||||||
|
if *flagVersion {
|
||||||
|
fmt.Printf("version: %s %s\n", version, githash)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if *flagDebug {
|
||||||
|
log.Info("Enabling debug")
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
}
|
||||||
|
log.Printf("Running version %s %s", version, githash)
|
||||||
|
if strings.Contains(version, "-dev") {
|
||||||
|
log.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.")
|
||||||
|
}
|
||||||
|
cfg := config.NewConfig(*flagConfig)
|
||||||
|
r, err := gateway.NewRouter(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Starting gateway failed: %s", err)
|
||||||
|
}
|
||||||
|
err = r.Start()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Starting gateway failed: %s", err)
|
||||||
|
}
|
||||||
|
log.Printf("Gateway(s) started succesfully. Now relaying messages")
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
|
863
matterbridge.toml.sample
Normal file
863
matterbridge.toml.sample
Normal file
@ -0,0 +1,863 @@
|
|||||||
|
#This is configuration for matterbridge.
|
||||||
|
#WARNING: as this file contains credentials, be sure to set correct file permissions
|
||||||
|
###################################################################
|
||||||
|
#IRC section
|
||||||
|
###################################################################
|
||||||
|
#REQUIRED to start IRC section
|
||||||
|
[irc]
|
||||||
|
|
||||||
|
#You can configure multiple servers "[irc.name]" or "[irc.name2]"
|
||||||
|
#In this example we use [irc.freenode]
|
||||||
|
#REQUIRED
|
||||||
|
[irc.freenode]
|
||||||
|
#irc server to connect to.
|
||||||
|
#REQUIRED
|
||||||
|
Server="irc.freenode.net:6667"
|
||||||
|
|
||||||
|
#Password for irc server (if necessary)
|
||||||
|
#OPTIONAL (default "")
|
||||||
|
Password=""
|
||||||
|
|
||||||
|
#Enable to use TLS connection to your irc server.
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
UseTLS=false
|
||||||
|
|
||||||
|
#Enable SASL (PLAIN) authentication. (freenode requires this from eg AWS hosts)
|
||||||
|
#It uses NickServNick and NickServPassword as login and password
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
UseSASL=false
|
||||||
|
|
||||||
|
#Enable to not verify the certificate on your irc server. i
|
||||||
|
#e.g. when using selfsigned certificates
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
SkipTLSVerify=true
|
||||||
|
|
||||||
|
#Your nick on irc.
|
||||||
|
#REQUIRED
|
||||||
|
Nick="matterbot"
|
||||||
|
|
||||||
|
#If you registered your bot with a service like Nickserv on freenode.
|
||||||
|
#Also being used when UseSASL=true
|
||||||
|
#OPTIONAL
|
||||||
|
NickServNick="nickserv"
|
||||||
|
NickServPassword="secret"
|
||||||
|
|
||||||
|
#Flood control
|
||||||
|
#Delay in milliseconds between each message send to the IRC server
|
||||||
|
#OPTIONAL (default 1300)
|
||||||
|
MessageDelay=1300
|
||||||
|
|
||||||
|
#Maximum amount of messages to hold in queue. If queue is full
|
||||||
|
#messages will be dropped.
|
||||||
|
#<message clipped> will be add to the message that fills the queue.
|
||||||
|
#OPTIONAL (default 30)
|
||||||
|
MessageQueue=30
|
||||||
|
|
||||||
|
#Maximum length of message sent to irc server. If it exceeds
|
||||||
|
#<message clipped> will be add to the message.
|
||||||
|
#OPTIONAL (default 400)
|
||||||
|
MessageLength=400
|
||||||
|
|
||||||
|
#Nicks you want to ignore.
|
||||||
|
#Messages from those users will not be sent to other bridges.
|
||||||
|
#OPTIONAL
|
||||||
|
IgnoreNicks="ircspammer1 ircspammer2"
|
||||||
|
|
||||||
|
#Messages you want to ignore.
|
||||||
|
#Messages matching these regexp will be ignored and not sent to other bridges
|
||||||
|
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
|
||||||
|
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||||
|
IgnoreMessages="^~~ badword"
|
||||||
|
|
||||||
|
#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 "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||||
|
#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
|
||||||
|
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
#XMPP section
|
||||||
|
###################################################################
|
||||||
|
[xmpp]
|
||||||
|
|
||||||
|
#You can configure multiple servers "[xmpp.name]" or "[xmpp.name2]"
|
||||||
|
#In this example we use [xmpp.jabber]
|
||||||
|
#REQUIRED
|
||||||
|
[xmpp.jabber]
|
||||||
|
#xmpp server to connect to.
|
||||||
|
#REQUIRED
|
||||||
|
Server="jabber.example.com:5222"
|
||||||
|
|
||||||
|
#Jid
|
||||||
|
#REQUIRED
|
||||||
|
Jid="user@example.com"
|
||||||
|
|
||||||
|
#Password
|
||||||
|
#REQUIRED
|
||||||
|
Password="yourpass"
|
||||||
|
|
||||||
|
#MUC
|
||||||
|
#REQUIRED
|
||||||
|
Muc="conference.jabber.example.com"
|
||||||
|
|
||||||
|
#Your nick in the rooms
|
||||||
|
#REQUIRED
|
||||||
|
Nick="xmppbot"
|
||||||
|
|
||||||
|
#Enable to not verify the certificate on your xmpp server.
|
||||||
|
#e.g. when using selfsigned certificates
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
SkipTLSVerify=true
|
||||||
|
|
||||||
|
#Nicks you want to ignore.
|
||||||
|
#Messages from those users will not be sent to other bridges.
|
||||||
|
#OPTIONAL
|
||||||
|
IgnoreNicks="ircspammer1 ircspammer2"
|
||||||
|
|
||||||
|
#Messages you want to ignore.
|
||||||
|
#Messages matching these regexp will be ignored and not sent to other bridges
|
||||||
|
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
|
||||||
|
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||||
|
IgnoreMessages="^~~ badword"
|
||||||
|
|
||||||
|
#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 "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
|
#Enable to show users joins/parts from other bridges
|
||||||
|
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
#hipchat section
|
||||||
|
###################################################################
|
||||||
|
#Go to https://www.hipchat.com/account/xmpp this will show you the necessary data
|
||||||
|
#to fill in the section below
|
||||||
|
[xmpp.hipchat]
|
||||||
|
#xmpp server to connect to.
|
||||||
|
#REQUIRED
|
||||||
|
Server="chat.hipchat.com:5222"
|
||||||
|
|
||||||
|
#Jabber ID
|
||||||
|
#REQUIRED
|
||||||
|
Jid="12345_12345@chat.hipchat.com"
|
||||||
|
|
||||||
|
#Password (your hipchat password)
|
||||||
|
#REQUIRED
|
||||||
|
Password="yourpass"
|
||||||
|
|
||||||
|
#Conference (MUC) domain
|
||||||
|
#REQUIRED
|
||||||
|
Muc="conf.hipchat.com"
|
||||||
|
|
||||||
|
#Room nickname
|
||||||
|
#REQUIRED
|
||||||
|
Nick="yourlogin"
|
||||||
|
|
||||||
|
#Nicks you want to ignore.
|
||||||
|
#Messages from those users will not be sent to other bridges.
|
||||||
|
#OPTIONAL
|
||||||
|
IgnoreNicks="spammer1 spammer2"
|
||||||
|
|
||||||
|
#Messages you want to ignore.
|
||||||
|
#Messages matching these regexp will be ignored and not sent to other bridges
|
||||||
|
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
|
||||||
|
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||||
|
IgnoreMessages="^~~ badword"
|
||||||
|
|
||||||
|
#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 "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
|
||||||
|
|
||||||
|
#Enable to show users joins/parts from other bridges
|
||||||
|
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
#mattermost section
|
||||||
|
###################################################################
|
||||||
|
[mattermost]
|
||||||
|
#You can configure multiple servers "[mattermost.name]" or "[mattermost.name2]"
|
||||||
|
#In this example we use [mattermost.work]
|
||||||
|
#REQUIRED
|
||||||
|
|
||||||
|
[mattermost.work]
|
||||||
|
#The mattermost hostname. (do not prefix it with http or https)
|
||||||
|
#REQUIRED (when not using webhooks)
|
||||||
|
Server="yourmattermostserver.domain"
|
||||||
|
|
||||||
|
#Your team on mattermost.
|
||||||
|
#REQUIRED (when not using webhooks)
|
||||||
|
Team="yourteam"
|
||||||
|
|
||||||
|
#login/pass of your bot.
|
||||||
|
#Use a dedicated user for this and not your own!
|
||||||
|
#REQUIRED (when not using webhooks)
|
||||||
|
Login="yourlogin"
|
||||||
|
Password="yourpass"
|
||||||
|
|
||||||
|
#Enable this to make a http connection (instead of https) to your mattermost.
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
NoTLS=false
|
||||||
|
|
||||||
|
#### Settings for webhook matterbridge.
|
||||||
|
#NOT RECOMMENDED TO USE INCOMING/OUTGOING WEBHOOK. USE DEDICATED BOT USER WHEN POSSIBLE!
|
||||||
|
#You don't need to configure this, if you have configured the settings
|
||||||
|
#above.
|
||||||
|
|
||||||
|
#Url is your incoming webhook url as specified in mattermost.
|
||||||
|
#See account settings - integrations - incoming webhooks on mattermost.
|
||||||
|
#If specified, messages will be sent to mattermost using this URL
|
||||||
|
#OPTIONAL
|
||||||
|
WebhookURL="https://yourdomain/hooks/yourhookkey"
|
||||||
|
|
||||||
|
#Address to listen on for outgoing webhook requests from mattermost.
|
||||||
|
#See account settings - integrations - outgoing webhooks on mattermost.
|
||||||
|
#If specified, messages will be received from mattermost on this ip:port
|
||||||
|
#(this will only work if WebhookURL above is also configured)
|
||||||
|
#OPTIONAL
|
||||||
|
WebhookBindAddress="0.0.0.0:9999"
|
||||||
|
|
||||||
|
#Icon that will be showed in mattermost.
|
||||||
|
#This only works when WebhookURL is configured
|
||||||
|
#OPTIONAL
|
||||||
|
IconURL="http://youricon.png"
|
||||||
|
|
||||||
|
#### End settings for webhook matterbridge.
|
||||||
|
|
||||||
|
#Enable to not verify the certificate on your mattermost server.
|
||||||
|
#e.g. when using selfsigned certificates
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
SkipTLSVerify=true
|
||||||
|
|
||||||
|
#how to format the list of IRC nicks when displayed in mattermost.
|
||||||
|
#Possible options are "table" and "plain"
|
||||||
|
#OPTIONAL (default plain)
|
||||||
|
NickFormatter="plain"
|
||||||
|
#How many nicks to list per row for formatters that support this.
|
||||||
|
#OPTIONAL (default 4)
|
||||||
|
NicksPerRow=4
|
||||||
|
|
||||||
|
#Whether to prefix messages from other bridges to mattermost with the sender's nick.
|
||||||
|
#Useful if username overrides for incoming webhooks isn't enabled on the
|
||||||
|
#mattermost server. If you set PrefixMessagesWithNick to true, each message
|
||||||
|
#from bridge to Mattermost will by default be prefixed by "bridge-" + nick. You can,
|
||||||
|
#however, modify how the messages appear, by setting (and modifying) RemoteNickFormat
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
PrefixMessagesWithNick=false
|
||||||
|
|
||||||
|
#Disable sending of edits to other bridges
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
EditDisable=false
|
||||||
|
|
||||||
|
#Message to be appended to every edited message
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
EditSuffix=" (edited)"
|
||||||
|
|
||||||
|
#Nicks you want to ignore.
|
||||||
|
#Messages from those users will not be sent to other bridges.
|
||||||
|
#OPTIONAL
|
||||||
|
IgnoreNicks="ircspammer1 ircspammer2"
|
||||||
|
|
||||||
|
#Messages you want to ignore.
|
||||||
|
#Messages matching these regexp will be ignored and not sent to other bridges
|
||||||
|
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
|
||||||
|
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||||
|
IgnoreMessages="^~~ badword"
|
||||||
|
|
||||||
|
#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 "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
|
#Enable to show users joins/parts from other bridges
|
||||||
|
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
#Gitter section
|
||||||
|
#Best to make a dedicated gitter account for the bot.
|
||||||
|
###################################################################
|
||||||
|
|
||||||
|
[gitter]
|
||||||
|
|
||||||
|
#You can configure multiple servers "[gitter.name]" or "[gitter.name2]"
|
||||||
|
#In this example we use [gitter.myproject]
|
||||||
|
#REQUIRED
|
||||||
|
[gitter.myproject]
|
||||||
|
#Token to connect with Gitter API
|
||||||
|
#You can get your token by going to https://developer.gitter.im/docs/welcome and SIGN IN
|
||||||
|
#REQUIRED
|
||||||
|
Token="Yourtokenhere"
|
||||||
|
|
||||||
|
#Nicks you want to ignore.
|
||||||
|
#Messages from those users will not be sent to other bridges.
|
||||||
|
#OPTIONAL
|
||||||
|
IgnoreNicks="ircspammer1 ircspammer2"
|
||||||
|
|
||||||
|
#Messages you want to ignore.
|
||||||
|
#Messages matching these regexp will be ignored and not sent to other bridges
|
||||||
|
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
|
||||||
|
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||||
|
IgnoreMessages="^~~ badword"
|
||||||
|
|
||||||
|
#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 "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
|
#Enable to show users joins/parts from other bridges
|
||||||
|
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
#slack section
|
||||||
|
###################################################################
|
||||||
|
[slack]
|
||||||
|
|
||||||
|
#You can configure multiple servers "[slack.name]" or "[slack.name2]"
|
||||||
|
#In this example we use [slack.hobby]
|
||||||
|
#REQUIRED
|
||||||
|
[slack.hobby]
|
||||||
|
#Token to connect with the Slack API
|
||||||
|
#You'll have to use a test/api-token using a dedicated user and not a bot token.
|
||||||
|
#See https://github.com/42wim/matterbridge/issues/75 for more info.
|
||||||
|
#Use https://api.slack.com/custom-integrations/legacy-tokens
|
||||||
|
#REQUIRED (when not using webhooks)
|
||||||
|
Token="yourslacktoken"
|
||||||
|
|
||||||
|
#### Settings for webhook matterbridge.
|
||||||
|
#NOT RECOMMENDED TO USE INCOMING/OUTGOING WEBHOOK. USE SLACK API
|
||||||
|
#AND DEDICATED BOT USER WHEN POSSIBLE!
|
||||||
|
#Url is your incoming webhook url as specified in slack
|
||||||
|
#See account settings - integrations - incoming webhooks on slack
|
||||||
|
#OPTIONAL
|
||||||
|
WebhookURL="https://hooks.slack.com/services/yourhook"
|
||||||
|
|
||||||
|
#NOT RECOMMENDED TO USE INCOMING/OUTGOING WEBHOOK. USE SLACK API
|
||||||
|
#AND DEDICATED BOT USER WHEN POSSIBLE!
|
||||||
|
#Address to listen on for outgoing webhook requests from slack
|
||||||
|
#See account settings - integrations - outgoing webhooks on slack
|
||||||
|
#This setting will not be used when useAPI is eanbled
|
||||||
|
#webhooks
|
||||||
|
#OPTIONAL
|
||||||
|
WebhookBindAddress="0.0.0.0:9999"
|
||||||
|
|
||||||
|
#Icon that will be showed in slack
|
||||||
|
#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 "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||||
|
#OPTIONAL
|
||||||
|
IconURL="https://robohash.org/{NICK}.png?size=48x48"
|
||||||
|
|
||||||
|
#how to format the list of IRC nicks when displayed in slack
|
||||||
|
#Possible options are "table" and "plain"
|
||||||
|
#OPTIONAL (default plain)
|
||||||
|
NickFormatter="plain"
|
||||||
|
#How many nicks to list per row for formatters that support this.
|
||||||
|
#OPTIONAL (default 4)
|
||||||
|
NicksPerRow=4
|
||||||
|
|
||||||
|
#Disable sending of edits to other bridges
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
EditDisable=true
|
||||||
|
|
||||||
|
#Message to be appended to every edited message
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
EditSuffix=" (edited)"
|
||||||
|
|
||||||
|
#Whether to prefix messages from other bridges to mattermost with RemoteNickFormat
|
||||||
|
#Useful if username overrides for incoming webhooks isn't enabled on the
|
||||||
|
#slack server. If you set PrefixMessagesWithNick to true, each message
|
||||||
|
#from bridge to Slack will by default be prefixed by "bridge-" + nick. You can,
|
||||||
|
#however, modify how the messages appear, by setting (and modifying) RemoteNickFormat
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
PrefixMessagesWithNick=false
|
||||||
|
|
||||||
|
#Nicks you want to ignore.
|
||||||
|
#Messages from those users will not be sent to other bridges.
|
||||||
|
#OPTIONAL
|
||||||
|
IgnoreNicks="ircspammer1 ircspammer2"
|
||||||
|
|
||||||
|
#Messages you want to ignore.
|
||||||
|
#Messages matching these regexp will be ignored and not sent to other bridges
|
||||||
|
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
|
||||||
|
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||||
|
IgnoreMessages="^~~ badword"
|
||||||
|
|
||||||
|
#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 "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
|
#Enable to show users joins/parts from other bridges
|
||||||
|
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
#discord section
|
||||||
|
###################################################################
|
||||||
|
[discord]
|
||||||
|
|
||||||
|
#You can configure multiple servers "[discord.name]" or "[discord.name2]"
|
||||||
|
#In this example we use [discord.game]
|
||||||
|
#REQUIRED
|
||||||
|
[discord.game]
|
||||||
|
#Token to connect with Discord API
|
||||||
|
#You can get your token by following the instructions on
|
||||||
|
#https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token
|
||||||
|
#If you want roles/groups mentions to be shown with names instead of ID, you'll need to give your bot the "Manage Roles" permission.
|
||||||
|
#REQUIRED
|
||||||
|
Token="Yourtokenhere"
|
||||||
|
|
||||||
|
#REQUIRED
|
||||||
|
Server="yourservername"
|
||||||
|
|
||||||
|
#Shows title, description and URL of embedded messages (sent by other bots)
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
ShowEmbeds=false
|
||||||
|
|
||||||
|
#Shows the username (minus the discriminator) instead of the server nickname
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
UseUserName=false
|
||||||
|
|
||||||
|
#Specify WebhookURL. If given, will relay messages using the Webhook, which gives a better look to messages.
|
||||||
|
#This only works if you have one discord channel, if you have multiple discord channels you'll have to specify it in the gateway config
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
WebhookURL="Yourwebhooktokenhere"
|
||||||
|
|
||||||
|
#Disable sending of edits to other bridges
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
EditDisable=false
|
||||||
|
|
||||||
|
#Message to be appended to every edited message
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
EditSuffix=" (edited)"
|
||||||
|
|
||||||
|
#Nicks you want to ignore.
|
||||||
|
#Messages from those users will not be sent to other bridges.
|
||||||
|
#OPTIONAL
|
||||||
|
IgnoreNicks="ircspammer1 ircspammer2"
|
||||||
|
|
||||||
|
#Messages you want to ignore.
|
||||||
|
#Messages matching these regexp will be ignored and not sent to other bridges
|
||||||
|
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
|
||||||
|
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||||
|
IgnoreMessages="^~~ badword"
|
||||||
|
|
||||||
|
#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 "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
|
#Enable to show users joins/parts from other bridges
|
||||||
|
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
#telegram section
|
||||||
|
###################################################################
|
||||||
|
[telegram]
|
||||||
|
|
||||||
|
#You can configure multiple servers "[telegram.name]" or "[telegram.name2]"
|
||||||
|
#In this example we use [telegram.secure]
|
||||||
|
#REQUIRED
|
||||||
|
[telegram.secure]
|
||||||
|
#Token to connect with telegram API
|
||||||
|
#See https://core.telegram.org/bots#6-botfather and https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau
|
||||||
|
#REQUIRED
|
||||||
|
Token="Yourtokenhere"
|
||||||
|
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
#Only supported format is "HTML", messages will be sent in html parsemode.
|
||||||
|
#See https://core.telegram.org/bots/api#html-style
|
||||||
|
MessageFormat=""
|
||||||
|
|
||||||
|
#If enabled use the "First Name" as username. If this is empty use the Username
|
||||||
|
#If disabled use the "Username" as username. If this is empty use the First Name
|
||||||
|
#If all names are empty, username will be "unknown"
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
UseFirstName=false
|
||||||
|
|
||||||
|
#WARNING! If enabled this will relay GIF/stickers/documents and other attachments as URLs
|
||||||
|
#Those URLs will contain your bot-token. This may not be what you want.
|
||||||
|
#For now there is no secure way to relay GIF/stickers/documents without seeing your token.
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
UseInsecureURL=false
|
||||||
|
|
||||||
|
#Disable sending of edits to other bridges
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
EditDisable=false
|
||||||
|
|
||||||
|
#Message to be appended to every edited message
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
EditSuffix=" (edited)"
|
||||||
|
|
||||||
|
#Nicks you want to ignore.
|
||||||
|
#Messages from those users will not be sent to other bridges.
|
||||||
|
#OPTIONAL
|
||||||
|
IgnoreNicks="spammer1 spammer2"
|
||||||
|
|
||||||
|
#Messages you want to ignore.
|
||||||
|
#Messages matching these regexp will be ignored and not sent to other bridges
|
||||||
|
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
|
||||||
|
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||||
|
IgnoreMessages="^~~ badword"
|
||||||
|
|
||||||
|
#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 "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
|
#Enable to show users joins/parts from other bridges
|
||||||
|
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
#rocketchat section
|
||||||
|
###################################################################
|
||||||
|
[rocketchat]
|
||||||
|
#You can configure multiple servers "[rocketchat.name]" or "[rocketchat.name2]"
|
||||||
|
#In this example we use [rocketchat.work]
|
||||||
|
#REQUIRED
|
||||||
|
|
||||||
|
[rocketchat.rockme]
|
||||||
|
#Url is your incoming webhook url as specified in rocketchat
|
||||||
|
#Read #https://rocket.chat/docs/administrator-guides/integrations/#how-to-create-a-new-incoming-webhook
|
||||||
|
#See administration - integrations - new integration - incoming webhook
|
||||||
|
#REQUIRED
|
||||||
|
WebhookURL="https://yourdomain/hooks/yourhookkey"
|
||||||
|
|
||||||
|
#Address to listen on for outgoing webhook requests from rocketchat.
|
||||||
|
#See administration - integrations - new integration - outgoing webhook
|
||||||
|
#REQUIRED
|
||||||
|
WebhookBindAddress="0.0.0.0:9999"
|
||||||
|
|
||||||
|
#Your nick/username as specified in your incoming webhook "Post as" setting
|
||||||
|
#REQUIRED
|
||||||
|
Nick="matterbot"
|
||||||
|
|
||||||
|
#Enable this to make a http connection (instead of https) to your rocketchat
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
NoTLS=false
|
||||||
|
|
||||||
|
#Enable to not verify the certificate on your rocketchat server.
|
||||||
|
#e.g. when using selfsigned certificates
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
SkipTLSVerify=true
|
||||||
|
|
||||||
|
#Whether to prefix messages from other bridges to rocketchat with the sender's nick.
|
||||||
|
#Useful if username overrides for incoming webhooks isn't enabled on the
|
||||||
|
#rocketchat server. If you set PrefixMessagesWithNick to true, each message
|
||||||
|
#from bridge to rocketchat will by default be prefixed by the RemoteNickFormat setting. i
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
PrefixMessagesWithNick=false
|
||||||
|
|
||||||
|
#Nicks you want to ignore.
|
||||||
|
#Messages from those users will not be sent to other bridges.
|
||||||
|
#OPTIONAL
|
||||||
|
IgnoreNicks="ircspammer1 ircspammer2"
|
||||||
|
|
||||||
|
#Messages you want to ignore.
|
||||||
|
#Messages matching these regexp will be ignored and not sent to other bridges
|
||||||
|
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
|
||||||
|
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||||
|
IgnoreMessages="^~~ badword"
|
||||||
|
|
||||||
|
#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 "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
|
#Enable to show users joins/parts from other bridges
|
||||||
|
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
#matrix section
|
||||||
|
###################################################################
|
||||||
|
[matrix]
|
||||||
|
#You can configure multiple servers "[matrix.name]" or "[matrix.name2]"
|
||||||
|
#In this example we use [matrix.neo]
|
||||||
|
#REQUIRED
|
||||||
|
|
||||||
|
[matrix.neo]
|
||||||
|
#Server is your homeserver (eg https://matrix.org)
|
||||||
|
#REQUIRED
|
||||||
|
Server="https://matrix.org"
|
||||||
|
|
||||||
|
#login/pass of your bot.
|
||||||
|
#Use a dedicated user for this and not your own!
|
||||||
|
#Messages sent from this user will not be relayed to avoid loops.
|
||||||
|
#REQUIRED
|
||||||
|
Login="yourlogin"
|
||||||
|
Password="yourpass"
|
||||||
|
|
||||||
|
#Whether to send the homeserver suffix. eg ":matrix.org" in @username:matrix.org
|
||||||
|
#to other bridges, or only send "username".(true only sends username)
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
NoHomeServerSuffix=false
|
||||||
|
|
||||||
|
#Whether to prefix messages from other bridges to matrix with the sender's nick.
|
||||||
|
#Useful if username overrides for incoming webhooks isn't enabled on the
|
||||||
|
#matrix server. If you set PrefixMessagesWithNick to true, each message
|
||||||
|
#from bridge to matrix will by default be prefixed by the RemoteNickFormat setting. i
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
PrefixMessagesWithNick=false
|
||||||
|
|
||||||
|
#Nicks you want to ignore.
|
||||||
|
#Messages from those users will not be sent to other bridges.
|
||||||
|
#OPTIONAL
|
||||||
|
IgnoreNicks="spammer1 spammer2"
|
||||||
|
|
||||||
|
#Messages you want to ignore.
|
||||||
|
#Messages matching these regexp will be ignored and not sent to other bridges
|
||||||
|
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
|
||||||
|
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||||
|
IgnoreMessages="^~~ badword"
|
||||||
|
|
||||||
|
#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 "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
|
#Enable to show users joins/parts from other bridges
|
||||||
|
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
#steam section
|
||||||
|
###################################################################
|
||||||
|
[steam]
|
||||||
|
#You can configure multiple servers "[steam.name]" or "[steam.name2]"
|
||||||
|
#In this example we use [steam.gamechat]
|
||||||
|
#REQUIRED
|
||||||
|
|
||||||
|
[steam.gamechat]
|
||||||
|
#login/pass of your bot.
|
||||||
|
#Use a dedicated user for this and not your own account!
|
||||||
|
#REQUIRED
|
||||||
|
Login="yourlogin"
|
||||||
|
Password="yourpass"
|
||||||
|
|
||||||
|
#steamguard mail authcode (not the 2FA code)
|
||||||
|
#OPTIONAL
|
||||||
|
Authcode="ABCE12"
|
||||||
|
|
||||||
|
#Whether to prefix messages from other bridges to matrix with the sender's nick.
|
||||||
|
#Useful if username overrides for incoming webhooks isn't enabled on the
|
||||||
|
#matrix server. If you set PrefixMessagesWithNick to true, each message
|
||||||
|
#from bridge to matrix will by default be prefixed by the RemoteNickFormat setting. i
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
PrefixMessagesWithNick=false
|
||||||
|
|
||||||
|
#Nicks you want to ignore.
|
||||||
|
#Messages from those users will not be sent to other bridges.
|
||||||
|
#OPTIONAL
|
||||||
|
IgnoreNicks="spammer1 spammer2"
|
||||||
|
|
||||||
|
#Messages you want to ignore.
|
||||||
|
#Messages matching these regexp will be ignored and not sent to other bridges
|
||||||
|
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
|
||||||
|
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
|
||||||
|
IgnoreMessages="^~~ badword"
|
||||||
|
|
||||||
|
#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 "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
|
#Enable to show users joins/parts from other bridges
|
||||||
|
#Only works hiding/show messages from irc and mattermost bridge for now
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
#API
|
||||||
|
###################################################################
|
||||||
|
[api]
|
||||||
|
#You can configure multiple API hooks
|
||||||
|
#In this example we use [api.local]
|
||||||
|
#REQUIRED
|
||||||
|
|
||||||
|
[api.local]
|
||||||
|
#Address to listen on for API
|
||||||
|
#REQUIRED
|
||||||
|
BindAddress="127.0.0.1:4242"
|
||||||
|
|
||||||
|
#Amount of messages to keep in memory
|
||||||
|
Buffer=1000
|
||||||
|
|
||||||
|
#Bearer token used for authentication
|
||||||
|
#curl -H "Authorization: Bearer token" http://localhost:4242/api/messages
|
||||||
|
#OPTIONAL (no authorization if token is empty)
|
||||||
|
Token="mytoken"
|
||||||
|
|
||||||
|
#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 "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
RemoteNickFormat="{NICK}"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
#General configuration
|
||||||
|
###################################################################
|
||||||
|
#Settings here override specific settings for each protocol
|
||||||
|
[general]
|
||||||
|
#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 "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
|
||||||
|
#OPTIONAL (default empty)
|
||||||
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
#Gateway configuration
|
||||||
|
###################################################################
|
||||||
|
|
||||||
|
#You can specify multiple gateways using [[gateway]]
|
||||||
|
#Each gateway has a [[gateway.in]] and a [[gateway.out]]
|
||||||
|
#[[gateway.in]] specifies the account and channels we will receive messages from.
|
||||||
|
#[[gateway.out]] specifies the account and channels we will send the messages
|
||||||
|
#from [[gateway.in]] to.
|
||||||
|
#
|
||||||
|
#Most of the time [[gateway.in]] and [[gateway.out]] are the same if you
|
||||||
|
#want bidirectional bridging. You can then use [[gateway.inout]]
|
||||||
|
#
|
||||||
|
|
||||||
|
[[gateway]]
|
||||||
|
#REQUIRED and UNIQUE
|
||||||
|
name="gateway1"
|
||||||
|
#Enable enables this gateway
|
||||||
|
##OPTIONAL (default false)
|
||||||
|
enable=true
|
||||||
|
|
||||||
|
#[[gateway.in]] specifies the account and channels we will receive messages from.
|
||||||
|
#The following example bridges between mattermost and irc
|
||||||
|
[[gateway.in]]
|
||||||
|
|
||||||
|
#account specified above
|
||||||
|
#REQUIRED
|
||||||
|
account="irc.freenode"
|
||||||
|
#channel to connect on that account
|
||||||
|
#How to specify them for the different bridges:
|
||||||
|
#
|
||||||
|
#irc - #channel (# is required) (this needs to be lowercase!)
|
||||||
|
#mattermost - channel (the channel name as seen in the URL, not the displayname)
|
||||||
|
#gitter - username/room
|
||||||
|
#xmpp - channel
|
||||||
|
#slack - channel (the channel name as seen in the URL, not the displayname)
|
||||||
|
#discord - channel (without the #)
|
||||||
|
# - ID:123456789 (where 123456789 is the channel ID)
|
||||||
|
# (https://github.com/42wim/matterbridge/issues/57)
|
||||||
|
#telegram - chatid (a large negative number, eg -123456789)
|
||||||
|
# see (https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau)
|
||||||
|
#hipchat - id_channel (see https://www.hipchat.com/account/xmpp for the correct channel)
|
||||||
|
#rocketchat - #channel (# is required (also needed for private channels!)
|
||||||
|
#matrix - #channel:server (eg #yourchannel:matrix.org)
|
||||||
|
# - encrypted rooms are not supported in matrix
|
||||||
|
#steam - chatid (a large number).
|
||||||
|
# The number in the URL when you click "enter chat room" in the browser
|
||||||
|
#
|
||||||
|
#REQUIRED
|
||||||
|
channel="#testing"
|
||||||
|
|
||||||
|
#OPTIONAL - only used for IRC protocol at the moment
|
||||||
|
[gateway.in.options]
|
||||||
|
#OPTIONAL - your irc channel key
|
||||||
|
key="yourkey"
|
||||||
|
|
||||||
|
|
||||||
|
#[[gateway.out]] specifies the account and channels we will sent messages to.
|
||||||
|
[[gateway.out]]
|
||||||
|
account="irc.freenode"
|
||||||
|
channel="#testing"
|
||||||
|
|
||||||
|
#OPTIONAL - only used for IRC protocol at the moment
|
||||||
|
[gateway.out.options]
|
||||||
|
#OPTIONAL - your irc channel key
|
||||||
|
key="yourkey"
|
||||||
|
|
||||||
|
#[[gateway.inout]] can be used when then channel will be used to receive from
|
||||||
|
#and send messages to
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="mattermost.work"
|
||||||
|
channel="off-topic"
|
||||||
|
|
||||||
|
#OPTIONAL - only used for IRC protocol at the moment
|
||||||
|
[gateway.inout.options]
|
||||||
|
#OPTIONAL - your irc channel key
|
||||||
|
key="yourkey"
|
||||||
|
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="discord.game"
|
||||||
|
channel="mygreatgame"
|
||||||
|
|
||||||
|
#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"
|
||||||
|
|
||||||
|
#API example
|
||||||
|
#[[gateway.inout]]
|
||||||
|
#account="api.local"
|
||||||
|
#channel="api"
|
||||||
|
#To send data to the api:
|
||||||
|
#curl -XPOST -H 'Content-Type: application/json' -d '{"text":"test","username":"randomuser","gateway":"gateway1"}' http://localhost:4242/api/message
|
||||||
|
#To read from the api:
|
||||||
|
#curl http://localhost:4242/api/messages
|
||||||
|
|
||||||
|
#If you want to do a 1:1 mapping between protocols where the channelnames are the same
|
||||||
|
#e.g. slack and mattermost you can use the samechannelgateway configuration
|
||||||
|
#the example configuration below send messages from channel testing on mattermost to
|
||||||
|
#channel testing on slack and vice versa. (and for the channel testing2 and testing3)
|
||||||
|
|
||||||
|
[[samechannelgateway]]
|
||||||
|
name="samechannel1"
|
||||||
|
enable = false
|
||||||
|
accounts = [ "mattermost.work","slack.hobby" ]
|
||||||
|
channels = [ "testing","testing2","testing3"]
|
35
matterbridge.toml.simple
Normal file
35
matterbridge.toml.simple
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#WARNING: as this file contains credentials, be sure to set correct file permissions
|
||||||
|
[irc]
|
||||||
|
[irc.freenode]
|
||||||
|
Server="irc.freenode.net:6667"
|
||||||
|
Nick="matterbot"
|
||||||
|
|
||||||
|
[mattermost]
|
||||||
|
[mattermost.work]
|
||||||
|
useAPI=true
|
||||||
|
#do not prefix it wit http:// or https://
|
||||||
|
Server="yourmattermostserver.domain"
|
||||||
|
Team="yourteam"
|
||||||
|
Login="yourlogin"
|
||||||
|
Password="yourpass"
|
||||||
|
PrefixMessagesWithNick=true
|
||||||
|
|
||||||
|
[[gateway]]
|
||||||
|
name="gateway1"
|
||||||
|
enable=true
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="irc.freenode"
|
||||||
|
channel="#testing"
|
||||||
|
|
||||||
|
[[gateway.inout]]
|
||||||
|
account="mattermost.work"
|
||||||
|
channel="off-topic"
|
||||||
|
|
||||||
|
#simpler config possible since v0.10.2
|
||||||
|
#[[gateway]]
|
||||||
|
#name="gateway2"
|
||||||
|
#enable=true
|
||||||
|
#inout = [
|
||||||
|
# { account="irc.freenode", channel="#testing", options={key="channelkey"}},
|
||||||
|
# { account="mattermost.work", channel="off-topic" },
|
||||||
|
#]
|
881
matterclient/matterclient.go
Normal file
881
matterclient/matterclient.go
Normal file
@ -0,0 +1,881 @@
|
|||||||
|
package matterclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/hashicorp/golang-lru"
|
||||||
|
"github.com/jpillora/backoff"
|
||||||
|
"github.com/mattermost/platform/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Credentials struct {
|
||||||
|
Login string
|
||||||
|
Team string
|
||||||
|
Pass string
|
||||||
|
Server string
|
||||||
|
NoTLS bool
|
||||||
|
SkipTLSVerify bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Raw *model.WebSocketEvent
|
||||||
|
Post *model.Post
|
||||||
|
Team string
|
||||||
|
Channel string
|
||||||
|
Username string
|
||||||
|
Text string
|
||||||
|
Type string
|
||||||
|
UserID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Team struct {
|
||||||
|
Team *model.Team
|
||||||
|
Id string
|
||||||
|
Channels *model.ChannelList
|
||||||
|
MoreChannels *model.ChannelList
|
||||||
|
Users map[string]*model.User
|
||||||
|
}
|
||||||
|
|
||||||
|
type MMClient struct {
|
||||||
|
sync.RWMutex
|
||||||
|
*Credentials
|
||||||
|
Team *Team
|
||||||
|
OtherTeams []*Team
|
||||||
|
Client *model.Client
|
||||||
|
User *model.User
|
||||||
|
Users map[string]*model.User
|
||||||
|
MessageChan chan *Message
|
||||||
|
log *log.Entry
|
||||||
|
WsClient *websocket.Conn
|
||||||
|
WsQuit bool
|
||||||
|
WsAway bool
|
||||||
|
WsConnected bool
|
||||||
|
WsSequence int64
|
||||||
|
WsPingChan chan *model.WebSocketResponse
|
||||||
|
ServerVersion string
|
||||||
|
OnWsConnect func()
|
||||||
|
lruCache *lru.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(login, pass, team, server string) *MMClient {
|
||||||
|
cred := &Credentials{Login: login, Pass: pass, Team: team, Server: server}
|
||||||
|
mmclient := &MMClient{Credentials: cred, MessageChan: make(chan *Message, 100), Users: make(map[string]*model.User)}
|
||||||
|
mmclient.log = log.WithFields(log.Fields{"module": "matterclient"})
|
||||||
|
log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
|
||||||
|
mmclient.lruCache, _ = lru.New(500)
|
||||||
|
return mmclient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) SetLogLevel(level string) {
|
||||||
|
l, err := log.ParseLevel(level)
|
||||||
|
if err != nil {
|
||||||
|
log.SetLevel(log.InfoLevel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.SetLevel(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) Login() error {
|
||||||
|
// check if this is a first connect or a reconnection
|
||||||
|
firstConnection := true
|
||||||
|
if m.WsConnected {
|
||||||
|
firstConnection = false
|
||||||
|
}
|
||||||
|
m.WsConnected = false
|
||||||
|
if m.WsQuit {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b := &backoff.Backoff{
|
||||||
|
Min: time.Second,
|
||||||
|
Max: 5 * time.Minute,
|
||||||
|
Jitter: true,
|
||||||
|
}
|
||||||
|
uriScheme := "https://"
|
||||||
|
if m.NoTLS {
|
||||||
|
uriScheme = "http://"
|
||||||
|
}
|
||||||
|
// login to mattermost
|
||||||
|
m.Client = model.NewClient(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
|
||||||
|
_, err := m.Client.GetClientProperties()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%#v", err.Error())
|
||||||
|
}
|
||||||
|
if firstConnection && !supportedVersion(m.Client.ServerVersion) {
|
||||||
|
return fmt.Errorf("unsupported mattermost version: %s", m.Client.ServerVersion)
|
||||||
|
}
|
||||||
|
m.ServerVersion = m.Client.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.Reset()
|
||||||
|
|
||||||
|
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 %s", 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.Client.HttpClient.Jar = m.createCookieJar(token[1])
|
||||||
|
m.Client.MockSession(token[1])
|
||||||
|
myinfo, appErr = m.Client.GetMe("")
|
||||||
|
if appErr != nil {
|
||||||
|
return errors.New(appErr.DetailedError)
|
||||||
|
}
|
||||||
|
if myinfo.Data.(*model.User) == nil {
|
||||||
|
m.log.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass)
|
||||||
|
return errors.New("invalid " + model.SESSION_COOKIE_TOKEN)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, appErr = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
|
||||||
|
err := m.initUser()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Team == nil {
|
||||||
|
return errors.New("team not found")
|
||||||
|
}
|
||||||
|
// set our team id as default route
|
||||||
|
m.Client.SetTeamId(m.Team.Id)
|
||||||
|
|
||||||
|
m.wsConnect()
|
||||||
|
|
||||||
|
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_V3 + "/users/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
|
||||||
|
m.WsClient.Close()
|
||||||
|
m.WsClient.UnderlyingConn().Close()
|
||||||
|
if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) {
|
||||||
|
m.log.Debug("Not invalidating session in logout, credential is a token")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := m.Client.Logout()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) WsReceiver() {
|
||||||
|
for {
|
||||||
|
var rawMsg json.RawMessage
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if m.WsQuit {
|
||||||
|
m.log.Debug("exiting WsReceiver")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.WsConnected {
|
||||||
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, rawMsg, err = m.WsClient.ReadMessage(); err != nil {
|
||||||
|
m.log.Error("error:", err)
|
||||||
|
// reconnect
|
||||||
|
m.wsConnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
var event model.WebSocketEvent
|
||||||
|
if err := json.Unmarshal(rawMsg, &event); err == nil && event.IsValid() {
|
||||||
|
m.log.Debugf("WsReceiver event: %#v", event)
|
||||||
|
msg := &Message{Raw: &event, Team: m.Credentials.Team}
|
||||||
|
m.parseMessage(msg)
|
||||||
|
// check if we didn't empty the message
|
||||||
|
if msg.Text != "" {
|
||||||
|
m.MessageChan <- msg
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var response model.WebSocketResponse
|
||||||
|
if err := json.Unmarshal(rawMsg, &response); err == nil && response.IsValid() {
|
||||||
|
m.log.Debugf("WsReceiver response: %#v", response)
|
||||||
|
m.parseResponse(response)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) parseMessage(rmsg *Message) {
|
||||||
|
switch rmsg.Raw.Event {
|
||||||
|
case model.WEBSOCKET_EVENT_POSTED, model.WEBSOCKET_EVENT_POST_EDITED:
|
||||||
|
m.parseActionPost(rmsg)
|
||||||
|
/*
|
||||||
|
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, err := m.Client.GetProfiles(0, 50000, "")
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(err.DetailedError)
|
||||||
|
}
|
||||||
|
m.Lock()
|
||||||
|
m.Users = mmusers.Data.(map[string]*model.User)
|
||||||
|
m.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) UpdateChannels() error {
|
||||||
|
mmchannels, err := m.Client.GetChannels("")
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(err.DetailedError)
|
||||||
|
}
|
||||||
|
var mmchannels2 *model.Result
|
||||||
|
if m.mmVersion() >= 3.08 {
|
||||||
|
mmchannels2, err = m.Client.GetMoreChannelsPage(0, 5000)
|
||||||
|
} else {
|
||||||
|
mmchannels2, err = m.Client.GetMoreChannels("")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(err.DetailedError)
|
||||||
|
}
|
||||||
|
m.Lock()
|
||||||
|
m.Team.Channels = mmchannels.Data.(*model.ChannelList)
|
||||||
|
m.Team.MoreChannels = mmchannels2.Data.(*model.ChannelList)
|
||||||
|
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) {
|
||||||
|
post := &model.Post{ChannelId: channelId, Message: text}
|
||||||
|
m.Client.CreatePost(post)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
_, err := m.Client.JoinChannel(channelId)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to join")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList {
|
||||||
|
res, err := m.Client.GetPostsSince(channelId, time)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return res.Data.(*model.PostList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) SearchPosts(query string) *model.PostList {
|
||||||
|
res, err := m.Client.SearchPosts(query, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return res.Data.(*model.PostList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList {
|
||||||
|
res, err := m.Client.GetPosts(channelId, 0, limit, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return res.Data.(*model.PostList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) GetPublicLink(filename string) string {
|
||||||
|
res, err := m.Client.GetPublicLink(filename)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) GetPublicLinks(filenames []string) []string {
|
||||||
|
var output []string
|
||||||
|
for _, f := range filenames {
|
||||||
|
res, err := m.Client.GetPublicLink(f)
|
||||||
|
if err != 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, err := m.Client.GetPublicLink(f)
|
||||||
|
if err != nil {
|
||||||
|
// public links is probably disabled, create the link ourselves
|
||||||
|
output = append(output, uriScheme+m.Credentials.Server+model.API_URL_SUFFIX_V3+"/files/"+f+"/get")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
output = append(output, res)
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) UpdateChannelHeader(channelId string, header string) {
|
||||||
|
data := make(map[string]string)
|
||||||
|
data["channel_id"] = channelId
|
||||||
|
data["channel_header"] = header
|
||||||
|
m.log.Debugf("updating channelheader %#v, %#v", channelId, header)
|
||||||
|
_, err := m.Client.UpdateChannelHeader(data)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) UpdateLastViewed(channelId string) {
|
||||||
|
m.log.Debugf("posting lastview %#v", channelId)
|
||||||
|
if m.mmVersion() >= 3.08 {
|
||||||
|
view := model.ChannelView{ChannelId: channelId}
|
||||||
|
res, _ := m.Client.ViewChannel(view)
|
||||||
|
if !res {
|
||||||
|
m.log.Errorf("ChannelView update for %s failed", channelId)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err := m.Client.UpdateLastViewedAt(channelId, true)
|
||||||
|
if err != nil {
|
||||||
|
m.log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) UsernamesInChannel(channelId string) []string {
|
||||||
|
res, err := m.Client.GetProfilesInChannel(channelId, 0, 50000, "")
|
||||||
|
if err != nil {
|
||||||
|
m.log.Errorf("UsernamesInChannel(%s) failed: %s", channelId, err)
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
members := res.Data.(map[string]*model.User)
|
||||||
|
result := []string{}
|
||||||
|
for _, member := range members {
|
||||||
|
result = append(result, member.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)
|
||||||
|
_, err := m.Client.CreateDirectChannel(toUserId)
|
||||||
|
if err != nil {
|
||||||
|
m.log.Debugf("SendDirectMessage to %#v failed: %s", toUserId, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
channelName := model.GetDMNameFromIds(toUserId, m.User.Id)
|
||||||
|
|
||||||
|
// update our channels
|
||||||
|
mmchannels, err := m.Client.GetChannels("")
|
||||||
|
if err != nil {
|
||||||
|
m.log.Debug("SendDirectMessage: Couldn't update channels")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.Lock()
|
||||||
|
m.Team.Channels = mmchannels.Data.(*model.ChannelList)
|
||||||
|
m.Unlock()
|
||||||
|
|
||||||
|
// 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, err := m.Client.GetChannel(channelId, "")
|
||||||
|
if err != nil {
|
||||||
|
return model.GetMillis()
|
||||||
|
}
|
||||||
|
data := res.Data.(*model.ChannelData)
|
||||||
|
return data.Member.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, err := m.Client.GetProfilesByIds([]string{userId})
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
u := res.Data.(map[string]*model.User)[userId]
|
||||||
|
m.Users[userId] = u
|
||||||
|
}
|
||||||
|
return m.Users[userId]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) GetUserName(userId string) string {
|
||||||
|
user := m.GetUser(userId)
|
||||||
|
if user != nil {
|
||||||
|
return user.Username
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) GetStatus(userId string) string {
|
||||||
|
res, err := m.Client.GetStatusesByIds([]string{userId})
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
status := res.Data.(map[string]string)
|
||||||
|
if status[userId] == model.STATUS_AWAY {
|
||||||
|
return "away"
|
||||||
|
}
|
||||||
|
if status[userId] == model.STATUS_ONLINE {
|
||||||
|
return "online"
|
||||||
|
}
|
||||||
|
return "offline"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) GetStatuses() map[string]string {
|
||||||
|
var ok bool
|
||||||
|
statuses := make(map[string]string)
|
||||||
|
res, err := m.Client.GetStatuses()
|
||||||
|
if err != nil {
|
||||||
|
return statuses
|
||||||
|
}
|
||||||
|
if statuses, ok = res.Data.(map[string]string); ok {
|
||||||
|
for userId, status := range statuses {
|
||||||
|
statuses[userId] = "offline"
|
||||||
|
if status == model.STATUS_AWAY {
|
||||||
|
statuses[userId] = "away"
|
||||||
|
}
|
||||||
|
if status == model.STATUS_ONLINE {
|
||||||
|
statuses[userId] = "online"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return statuses
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) GetTeamId() string {
|
||||||
|
return m.Team.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MMClient) StatusLoop() {
|
||||||
|
retries := 0
|
||||||
|
backoff := time.Second * 60
|
||||||
|
if m.OnWsConnect != nil {
|
||||||
|
m.OnWsConnect()
|
||||||
|
}
|
||||||
|
m.log.Debug("StatusLoop:", m.OnWsConnect)
|
||||||
|
for {
|
||||||
|
if m.WsQuit {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if m.WsConnected {
|
||||||
|
m.log.Debug("WS PING")
|
||||||
|
m.sendWSRequest("ping", nil)
|
||||||
|
select {
|
||||||
|
case <-m.WsPingChan:
|
||||||
|
m.log.Debug("WS PONG received")
|
||||||
|
backoff = time.Second * 60
|
||||||
|
case <-time.After(time.Second * 5):
|
||||||
|
if retries > 3 {
|
||||||
|
m.Logout()
|
||||||
|
m.WsQuit = false
|
||||||
|
m.Login()
|
||||||
|
if m.OnWsConnect != nil {
|
||||||
|
m.OnWsConnect()
|
||||||
|
}
|
||||||
|
go m.WsReceiver()
|
||||||
|
} else {
|
||||||
|
retries++
|
||||||
|
backoff = time.Second * 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(backoff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize user and teams
|
||||||
|
func (m *MMClient) initUser() error {
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
initLoad, err := m.Client.GetInitialLoad()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
initData := initLoad.Data.(*model.InitialLoad)
|
||||||
|
m.User = initData.User
|
||||||
|
// 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")
|
||||||
|
for _, v := range initData.Teams {
|
||||||
|
m.Client.SetTeamId(v.Id)
|
||||||
|
mmusers, err := m.Client.GetProfiles(0, 50000, "")
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(err.DetailedError)
|
||||||
|
}
|
||||||
|
t := &Team{Team: v, Users: mmusers.Data.(map[string]*model.User), Id: v.Id}
|
||||||
|
mmchannels, err := m.Client.GetChannels("")
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(err.DetailedError)
|
||||||
|
}
|
||||||
|
t.Channels = mmchannels.Data.(*model.ChannelList)
|
||||||
|
if m.mmVersion() >= 3.08 {
|
||||||
|
mmchannels, err = m.Client.GetMoreChannelsPage(0, 5000)
|
||||||
|
} else {
|
||||||
|
mmchannels, err = m.Client.GetMoreChannels("")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(err.DetailedError)
|
||||||
|
}
|
||||||
|
t.MoreChannels = mmchannels.Data.(*model.ChannelList)
|
||||||
|
m.OtherTeams = append(m.OtherTeams, t)
|
||||||
|
if v.Name == m.Credentials.Team {
|
||||||
|
m.Team = t
|
||||||
|
m.log.Debugf("initUser(): found our team %s (id: %s)", v.Name, v.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 (m *MMClient) mmVersion() float64 {
|
||||||
|
v, _ := strconv.ParseFloat(string(m.ServerVersion[0:2])+"0"+string(m.ServerVersion[2]), 64)
|
||||||
|
if string(m.ServerVersion[4]) == "." {
|
||||||
|
v, _ = strconv.ParseFloat(m.ServerVersion[0:4], 64)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func supportedVersion(version string) bool {
|
||||||
|
if strings.HasPrefix(version, "3.5.0") ||
|
||||||
|
strings.HasPrefix(version, "3.6.0") ||
|
||||||
|
strings.HasPrefix(version, "3.7.0") ||
|
||||||
|
strings.HasPrefix(version, "3.8.0") ||
|
||||||
|
strings.HasPrefix(version, "3.9.0") ||
|
||||||
|
strings.HasPrefix(version, "3.10.0") ||
|
||||||
|
strings.HasPrefix(version, "4.0") ||
|
||||||
|
strings.HasPrefix(version, "4.1") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func digestString(s string) string {
|
||||||
|
return fmt.Sprintf("%x", md5.Sum([]byte(s)))
|
||||||
|
}
|
@ -10,8 +10,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OMessage for mattermost incoming webhook. (send to mattermost)
|
// OMessage for mattermost incoming webhook. (send to mattermost)
|
||||||
@ -27,17 +28,22 @@ type OMessage struct {
|
|||||||
|
|
||||||
// IMessage for mattermost outgoing webhook. (received from mattermost)
|
// IMessage for mattermost outgoing webhook. (received from mattermost)
|
||||||
type IMessage struct {
|
type IMessage struct {
|
||||||
|
BotID string `schema:"bot_id"`
|
||||||
|
BotName string `schema:"bot_name"`
|
||||||
Token string `schema:"token"`
|
Token string `schema:"token"`
|
||||||
TeamID string `schema:"team_id"`
|
TeamID string `schema:"team_id"`
|
||||||
TeamDomain string `schema:"team_domain"`
|
TeamDomain string `schema:"team_domain"`
|
||||||
ChannelID string `schema:"channel_id"`
|
ChannelID string `schema:"channel_id"`
|
||||||
ServiceID string `schema:"service_id"`
|
|
||||||
ChannelName string `schema:"channel_name"`
|
ChannelName string `schema:"channel_name"`
|
||||||
Timestamp string `schema:"timestamp"`
|
Timestamp string `schema:"timestamp"`
|
||||||
UserID string `schema:"user_id"`
|
UserID string `schema:"user_id"`
|
||||||
UserName string `schema:"user_name"`
|
UserName string `schema:"user_name"`
|
||||||
|
PostId string `schema:"post_id"`
|
||||||
|
RawText string `schema:"raw_text"`
|
||||||
|
ServiceId string `schema:"service_id"`
|
||||||
Text string `schema:"text"`
|
Text string `schema:"text"`
|
||||||
TriggerWord string `schema:"trigger_word"`
|
TriggerWord string `schema:"trigger_word"`
|
||||||
|
FileIDs string `schema:"file_ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client for Mattermost.
|
// Client for Mattermost.
|
||||||
@ -51,7 +57,6 @@ type Client struct {
|
|||||||
|
|
||||||
// Config for client.
|
// Config for client.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Port int // Port to listen on.
|
|
||||||
BindAddress string // Address to listen on
|
BindAddress string // Address to listen on
|
||||||
Token string // Only allow this token from Mattermost. (Allow everything when empty)
|
Token string // Only allow this token from Mattermost. (Allow everything when empty)
|
||||||
InsecureSkipVerify bool // disable certificate checking
|
InsecureSkipVerify bool // disable certificate checking
|
||||||
@ -61,15 +66,15 @@ type Config struct {
|
|||||||
// New Mattermost client.
|
// New Mattermost client.
|
||||||
func New(url string, config Config) *Client {
|
func New(url string, config Config) *Client {
|
||||||
c := &Client{Url: url, In: make(chan IMessage), Out: make(chan OMessage), Config: config}
|
c := &Client{Url: url, In: make(chan IMessage), Out: make(chan OMessage), Config: config}
|
||||||
if c.Port == 0 {
|
|
||||||
c.Port = 9999
|
|
||||||
}
|
|
||||||
c.BindAddress += ":"
|
|
||||||
tr := &http.Transport{
|
tr := &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify},
|
||||||
}
|
}
|
||||||
c.httpclient = &http.Client{Transport: tr}
|
c.httpclient = &http.Client{Transport: tr}
|
||||||
if !c.DisableServer {
|
if !c.DisableServer {
|
||||||
|
_, _, err := net.SplitHostPort(c.BindAddress)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("incorrect bindaddress %s", c.BindAddress)
|
||||||
|
}
|
||||||
go c.StartServer()
|
go c.StartServer()
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
@ -79,8 +84,14 @@ func New(url string, config Config) *Client {
|
|||||||
func (c *Client) StartServer() {
|
func (c *Client) StartServer() {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.Handle("/", c)
|
mux.Handle("/", c)
|
||||||
log.Printf("Listening on http://%v:%v...\n", c.BindAddress, c.Port)
|
srv := &http.Server{
|
||||||
if err := http.ListenAndServe((c.BindAddress + strconv.Itoa(c.Port)), mux); err != nil {
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
Handler: mux,
|
||||||
|
Addr: c.BindAddress,
|
||||||
|
}
|
||||||
|
log.Printf("Listening on http://%v...\n", c.BindAddress)
|
||||||
|
if err := srv.ListenAndServe(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,12 +135,11 @@ func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Receive returns an incoming message from mattermost outgoing webhooks URL.
|
// Receive returns an incoming message from mattermost outgoing webhooks URL.
|
||||||
func (c *Client) Receive() IMessage {
|
func (c *Client) Receive() IMessage {
|
||||||
for {
|
var msg IMessage
|
||||||
select {
|
for msg := range c.In {
|
||||||
case msg := <-c.In:
|
return msg
|
||||||
return msg
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send sends a msg to mattermost incoming webhooks URL.
|
// Send sends a msg to mattermost incoming webhooks URL.
|
||||||
|
27
vendor/github.com/42wim/go-ircevent/LICENSE
generated
vendored
Normal file
27
vendor/github.com/42wim/go-ircevent/LICENSE
generated
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (c) 2009 Thomas Jager. 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.
|
572
vendor/github.com/42wim/go-ircevent/irc.go
generated
vendored
Normal file
572
vendor/github.com/42wim/go-ircevent/irc.go
generated
vendored
Normal file
@ -0,0 +1,572 @@
|
|||||||
|
// Copyright 2009 Thomas Jager <mail@jager.no> All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
/*
|
||||||
|
This package provides an event based IRC client library. It allows to
|
||||||
|
register callbacks for the events you need to handle. Its features
|
||||||
|
include handling standard CTCP, reconnecting on errors and detecting
|
||||||
|
stones servers.
|
||||||
|
Details of the IRC protocol can be found in the following RFCs:
|
||||||
|
https://tools.ietf.org/html/rfc1459
|
||||||
|
https://tools.ietf.org/html/rfc2810
|
||||||
|
https://tools.ietf.org/html/rfc2811
|
||||||
|
https://tools.ietf.org/html/rfc2812
|
||||||
|
https://tools.ietf.org/html/rfc2813
|
||||||
|
The details of the client-to-client protocol (CTCP) can be found here: http://www.irchelp.org/irchelp/rfc/ctcpspec.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
VERSION = "go-ircevent v2.1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrDisconnected = errors.New("Disconnect Called")
|
||||||
|
|
||||||
|
// Read data from a connection. To be used as a goroutine.
|
||||||
|
func (irc *Connection) readLoop() {
|
||||||
|
defer irc.Done()
|
||||||
|
br := bufio.NewReaderSize(irc.socket, 512)
|
||||||
|
|
||||||
|
errChan := irc.ErrorChan()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-irc.end:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
// Set a read deadline based on the combined timeout and ping frequency
|
||||||
|
// We should ALWAYS have received a response from the server within the timeout
|
||||||
|
// after our own pings
|
||||||
|
if irc.socket != nil {
|
||||||
|
irc.socket.SetReadDeadline(time.Now().Add(irc.Timeout + irc.PingFreq))
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := br.ReadString('\n')
|
||||||
|
|
||||||
|
// We got past our blocking read, so bin timeout
|
||||||
|
if irc.socket != nil {
|
||||||
|
var zero time.Time
|
||||||
|
irc.socket.SetReadDeadline(zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if irc.Debug {
|
||||||
|
irc.Log.Printf("<-- %s\n", strings.TrimSpace(msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
irc.Lock()
|
||||||
|
irc.lastMessage = time.Now()
|
||||||
|
irc.Unlock()
|
||||||
|
event, err := parseToEvent(msg)
|
||||||
|
event.Connection = irc
|
||||||
|
if err == nil {
|
||||||
|
/* XXX: len(args) == 0: args should be empty */
|
||||||
|
irc.RunCallbacks(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unescape tag values as defined in the IRCv3.2 message tags spec
|
||||||
|
// http://ircv3.net/specs/core/message-tags-3.2.html
|
||||||
|
func unescapeTagValue(value string) string {
|
||||||
|
value = strings.Replace(value, "\\:", ";", -1)
|
||||||
|
value = strings.Replace(value, "\\s", " ", -1)
|
||||||
|
value = strings.Replace(value, "\\\\", "\\", -1)
|
||||||
|
value = strings.Replace(value, "\\r", "\r", -1)
|
||||||
|
value = strings.Replace(value, "\\n", "\n", -1)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
//Parse raw irc messages
|
||||||
|
func parseToEvent(msg string) (*Event, error) {
|
||||||
|
msg = strings.TrimSuffix(msg, "\n") //Remove \r\n
|
||||||
|
msg = strings.TrimSuffix(msg, "\r")
|
||||||
|
event := &Event{Raw: msg}
|
||||||
|
if len(msg) < 5 {
|
||||||
|
return nil, errors.New("Malformed msg from server")
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg[0] == '@' {
|
||||||
|
// IRCv3 Message Tags
|
||||||
|
if i := strings.Index(msg, " "); i > -1 {
|
||||||
|
event.Tags = make(map[string]string)
|
||||||
|
tags := strings.Split(msg[1:i], ";")
|
||||||
|
for _, data := range tags {
|
||||||
|
parts := strings.SplitN(data, "=", 2)
|
||||||
|
if len(parts) == 1 {
|
||||||
|
event.Tags[parts[0]] = ""
|
||||||
|
} else {
|
||||||
|
event.Tags[parts[0]] = unescapeTagValue(parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg = msg[i+1 : len(msg)]
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("Malformed msg from server")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg[0] == ':' {
|
||||||
|
if i := strings.Index(msg, " "); i > -1 {
|
||||||
|
event.Source = msg[1:i]
|
||||||
|
msg = msg[i+1 : len(msg)]
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("Malformed msg from server")
|
||||||
|
}
|
||||||
|
|
||||||
|
if i, j := strings.Index(event.Source, "!"), strings.Index(event.Source, "@"); i > -1 && j > -1 && i < j {
|
||||||
|
event.Nick = event.Source[0:i]
|
||||||
|
event.User = event.Source[i+1 : j]
|
||||||
|
event.Host = event.Source[j+1 : len(event.Source)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
split := strings.SplitN(msg, " :", 2)
|
||||||
|
args := strings.Split(split[0], " ")
|
||||||
|
event.Code = strings.ToUpper(args[0])
|
||||||
|
event.Arguments = args[1:]
|
||||||
|
if len(split) > 1 {
|
||||||
|
event.Arguments = append(event.Arguments, split[1])
|
||||||
|
}
|
||||||
|
return event, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop to write to a connection. To be used as a goroutine.
|
||||||
|
func (irc *Connection) writeLoop() {
|
||||||
|
defer irc.Done()
|
||||||
|
errChan := irc.ErrorChan()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-irc.end:
|
||||||
|
return
|
||||||
|
case b, ok := <-irc.pwrite:
|
||||||
|
if !ok || b == "" || irc.socket == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if irc.Debug {
|
||||||
|
irc.Log.Printf("--> %s\n", strings.TrimSpace(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a write deadline based on the time out
|
||||||
|
irc.socket.SetWriteDeadline(time.Now().Add(irc.Timeout))
|
||||||
|
|
||||||
|
_, err := irc.socket.Write([]byte(b))
|
||||||
|
|
||||||
|
// Past blocking write, bin timeout
|
||||||
|
var zero time.Time
|
||||||
|
irc.socket.SetWriteDeadline(zero)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pings the server if we have not received any messages for 5 minutes
|
||||||
|
// to keep the connection alive. To be used as a goroutine.
|
||||||
|
func (irc *Connection) pingLoop() {
|
||||||
|
defer irc.Done()
|
||||||
|
ticker := time.NewTicker(1 * time.Minute) // Tick every minute for monitoring
|
||||||
|
ticker2 := time.NewTicker(irc.PingFreq) // Tick at the ping frequency.
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
//Ping if we haven't received anything from the server within the keep alive period
|
||||||
|
if time.Since(irc.lastMessage) >= irc.KeepAlive {
|
||||||
|
irc.SendRawf("PING %d", time.Now().UnixNano())
|
||||||
|
}
|
||||||
|
case <-ticker2.C:
|
||||||
|
//Ping at the ping frequency
|
||||||
|
irc.SendRawf("PING %d", time.Now().UnixNano())
|
||||||
|
//Try to recapture nickname if it's not as configured.
|
||||||
|
irc.Lock()
|
||||||
|
if irc.nick != irc.nickcurrent {
|
||||||
|
irc.nickcurrent = irc.nick
|
||||||
|
irc.SendRawf("NICK %s", irc.nick)
|
||||||
|
}
|
||||||
|
irc.Unlock()
|
||||||
|
case <-irc.end:
|
||||||
|
ticker.Stop()
|
||||||
|
ticker2.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (irc *Connection) isQuitting() bool {
|
||||||
|
irc.Lock()
|
||||||
|
defer irc.Unlock()
|
||||||
|
return irc.quit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main loop to control the connection.
|
||||||
|
func (irc *Connection) Loop() {
|
||||||
|
errChan := irc.ErrorChan()
|
||||||
|
for !irc.isQuitting() {
|
||||||
|
err := <-errChan
|
||||||
|
close(irc.end)
|
||||||
|
irc.Wait()
|
||||||
|
for !irc.isQuitting() {
|
||||||
|
irc.Log.Printf("Error, disconnected: %s\n", err)
|
||||||
|
if err = irc.Reconnect(); err != nil {
|
||||||
|
irc.Log.Printf("Error while reconnecting: %s\n", err)
|
||||||
|
time.Sleep(60 * time.Second)
|
||||||
|
} else {
|
||||||
|
errChan = irc.ErrorChan()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit the current connection and disconnect from the server
|
||||||
|
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1.6
|
||||||
|
func (irc *Connection) Quit() {
|
||||||
|
quit := "QUIT"
|
||||||
|
|
||||||
|
if irc.QuitMessage != "" {
|
||||||
|
quit = fmt.Sprintf("QUIT :%s", irc.QuitMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
irc.SendRaw(quit)
|
||||||
|
irc.Lock()
|
||||||
|
irc.stopped = true
|
||||||
|
irc.quit = true
|
||||||
|
irc.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the connection to join a given channel.
|
||||||
|
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.1
|
||||||
|
func (irc *Connection) Join(channel string) {
|
||||||
|
irc.pwrite <- fmt.Sprintf("JOIN %s\r\n", channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leave a given channel.
|
||||||
|
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.2
|
||||||
|
func (irc *Connection) Part(channel string) {
|
||||||
|
irc.pwrite <- fmt.Sprintf("PART %s\r\n", channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a notification to a nickname. This is similar to Privmsg but must not receive replies.
|
||||||
|
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.2
|
||||||
|
func (irc *Connection) Notice(target, message string) {
|
||||||
|
irc.pwrite <- fmt.Sprintf("NOTICE %s :%s\r\n", target, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a formated notification to a nickname.
|
||||||
|
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.2
|
||||||
|
func (irc *Connection) Noticef(target, format string, a ...interface{}) {
|
||||||
|
irc.Notice(target, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send (action) message to a target (channel or nickname).
|
||||||
|
// No clear RFC on this one...
|
||||||
|
func (irc *Connection) Action(target, message string) {
|
||||||
|
irc.pwrite <- fmt.Sprintf("PRIVMSG %s :\001ACTION %s\001\r\n", target, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send formatted (action) message to a target (channel or nickname).
|
||||||
|
func (irc *Connection) Actionf(target, format string, a ...interface{}) {
|
||||||
|
irc.Action(target, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send (private) message to a target (channel or nickname).
|
||||||
|
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.4.1
|
||||||
|
func (irc *Connection) Privmsg(target, message string) {
|
||||||
|
irc.pwrite <- fmt.Sprintf("PRIVMSG %s :%s\r\n", target, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send formated string to specified target (channel or nickname).
|
||||||
|
func (irc *Connection) Privmsgf(target, format string, a ...interface{}) {
|
||||||
|
irc.Privmsg(target, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kick <user> from <channel> with <msg>. For no message, pass empty string ("")
|
||||||
|
func (irc *Connection) Kick(user, channel, msg string) {
|
||||||
|
var cmd bytes.Buffer
|
||||||
|
cmd.WriteString(fmt.Sprintf("KICK %s %s", channel, user))
|
||||||
|
if msg != "" {
|
||||||
|
cmd.WriteString(fmt.Sprintf(" :%s", msg))
|
||||||
|
}
|
||||||
|
cmd.WriteString("\r\n")
|
||||||
|
irc.pwrite <- cmd.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kick all <users> from <channel> with <msg>. For no message, pass
|
||||||
|
// empty string ("")
|
||||||
|
func (irc *Connection) MultiKick(users []string, channel string, msg string) {
|
||||||
|
var cmd bytes.Buffer
|
||||||
|
cmd.WriteString(fmt.Sprintf("KICK %s %s", channel, strings.Join(users, ",")))
|
||||||
|
if msg != "" {
|
||||||
|
cmd.WriteString(fmt.Sprintf(" :%s", msg))
|
||||||
|
}
|
||||||
|
cmd.WriteString("\r\n")
|
||||||
|
irc.pwrite <- cmd.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send raw string.
|
||||||
|
func (irc *Connection) SendRaw(message string) {
|
||||||
|
irc.pwrite <- message + "\r\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send raw formated string.
|
||||||
|
func (irc *Connection) SendRawf(format string, a ...interface{}) {
|
||||||
|
irc.SendRaw(fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set (new) nickname.
|
||||||
|
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1.2
|
||||||
|
func (irc *Connection) Nick(n string) {
|
||||||
|
irc.nick = n
|
||||||
|
irc.SendRawf("NICK %s", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine nick currently used with the connection.
|
||||||
|
func (irc *Connection) GetNick() string {
|
||||||
|
return irc.nickcurrent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query information about a particular nickname.
|
||||||
|
// RFC 1459: https://tools.ietf.org/html/rfc1459#section-4.5.2
|
||||||
|
func (irc *Connection) Whois(nick string) {
|
||||||
|
irc.SendRawf("WHOIS %s", nick)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query information about a given nickname in the server.
|
||||||
|
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.5.1
|
||||||
|
func (irc *Connection) Who(nick string) {
|
||||||
|
irc.SendRawf("WHO %s", nick)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set different modes for a target (channel or nickname).
|
||||||
|
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.2.3
|
||||||
|
func (irc *Connection) Mode(target string, modestring ...string) {
|
||||||
|
if len(modestring) > 0 {
|
||||||
|
mode := strings.Join(modestring, " ")
|
||||||
|
irc.SendRawf("MODE %s %s", target, mode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
irc.SendRawf("MODE %s", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (irc *Connection) ErrorChan() chan error {
|
||||||
|
return irc.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the connection is connected to an IRC server.
|
||||||
|
func (irc *Connection) Connected() bool {
|
||||||
|
return !irc.stopped
|
||||||
|
}
|
||||||
|
|
||||||
|
// A disconnect sends all buffered messages (if possible),
|
||||||
|
// stops all goroutines and then closes the socket.
|
||||||
|
func (irc *Connection) Disconnect() {
|
||||||
|
if irc.socket != nil {
|
||||||
|
irc.socket.Close()
|
||||||
|
}
|
||||||
|
irc.ErrorChan() <- ErrDisconnected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconnect to a server using the current connection.
|
||||||
|
func (irc *Connection) Reconnect() error {
|
||||||
|
irc.end = make(chan struct{})
|
||||||
|
return irc.Connect(irc.Server)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to a given server using the current connection configuration.
|
||||||
|
// This function also takes care of identification if a password is provided.
|
||||||
|
// RFC 1459 details: https://tools.ietf.org/html/rfc1459#section-4.1
|
||||||
|
func (irc *Connection) Connect(server string) error {
|
||||||
|
irc.Server = server
|
||||||
|
// mark Server as stopped since there can be an error during connect
|
||||||
|
irc.stopped = true
|
||||||
|
|
||||||
|
// make sure everything is ready for connection
|
||||||
|
if len(irc.Server) == 0 {
|
||||||
|
return errors.New("empty 'server'")
|
||||||
|
}
|
||||||
|
if strings.Count(irc.Server, ":") != 1 {
|
||||||
|
return errors.New("wrong number of ':' in address")
|
||||||
|
}
|
||||||
|
if strings.Index(irc.Server, ":") == 0 {
|
||||||
|
return errors.New("hostname is missing")
|
||||||
|
}
|
||||||
|
if strings.Index(irc.Server, ":") == len(irc.Server)-1 {
|
||||||
|
return errors.New("port missing")
|
||||||
|
}
|
||||||
|
// check for valid range
|
||||||
|
ports := strings.Split(irc.Server, ":")[1]
|
||||||
|
port, err := strconv.Atoi(ports)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("extracting port failed")
|
||||||
|
}
|
||||||
|
if !((port >= 0) && (port <= 65535)) {
|
||||||
|
return errors.New("port number outside valid range")
|
||||||
|
}
|
||||||
|
if irc.Log == nil {
|
||||||
|
return errors.New("'Log' points to nil")
|
||||||
|
}
|
||||||
|
if len(irc.nick) == 0 {
|
||||||
|
return errors.New("empty 'nick'")
|
||||||
|
}
|
||||||
|
if len(irc.user) == 0 {
|
||||||
|
return errors.New("empty 'user'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if irc.UseTLS {
|
||||||
|
dialer := &net.Dialer{Timeout: irc.Timeout}
|
||||||
|
irc.socket, err = tls.DialWithDialer(dialer, "tcp", irc.Server, irc.TLSConfig)
|
||||||
|
} else {
|
||||||
|
irc.socket, err = net.DialTimeout("tcp", irc.Server, irc.Timeout)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
irc.stopped = false
|
||||||
|
irc.Log.Printf("Connected to %s (%s)\n", irc.Server, irc.socket.RemoteAddr())
|
||||||
|
|
||||||
|
irc.pwrite = make(chan string, 10)
|
||||||
|
irc.Error = make(chan error, 2)
|
||||||
|
irc.Add(3)
|
||||||
|
go irc.readLoop()
|
||||||
|
go irc.writeLoop()
|
||||||
|
go irc.pingLoop()
|
||||||
|
if len(irc.Password) > 0 {
|
||||||
|
irc.pwrite <- fmt.Sprintf("PASS %s\r\n", irc.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = irc.negotiateCaps()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
irc.pwrite <- fmt.Sprintf("NICK %s\r\n", irc.nick)
|
||||||
|
irc.pwrite <- fmt.Sprintf("USER %s 0.0.0.0 0.0.0.0 :%s\r\n", irc.user, irc.user)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negotiate IRCv3 capabilities
|
||||||
|
func (irc *Connection) negotiateCaps() error {
|
||||||
|
saslResChan := make(chan *SASLResult)
|
||||||
|
if irc.UseSASL {
|
||||||
|
irc.RequestCaps = append(irc.RequestCaps, "sasl")
|
||||||
|
irc.setupSASLCallbacks(saslResChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(irc.RequestCaps) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cap_chan := make(chan bool, len(irc.RequestCaps))
|
||||||
|
irc.AddCallback("CAP", func(e *Event) {
|
||||||
|
if len(e.Arguments) != 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
command := e.Arguments[1]
|
||||||
|
|
||||||
|
if command == "LS" {
|
||||||
|
missing_caps := len(irc.RequestCaps)
|
||||||
|
for _, cap_name := range strings.Split(e.Arguments[2], " ") {
|
||||||
|
for _, req_cap := range irc.RequestCaps {
|
||||||
|
if cap_name == req_cap {
|
||||||
|
irc.pwrite <- fmt.Sprintf("CAP REQ :%s\r\n", cap_name)
|
||||||
|
missing_caps--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < missing_caps; i++ {
|
||||||
|
cap_chan <- true
|
||||||
|
}
|
||||||
|
} else if command == "ACK" || command == "NAK" {
|
||||||
|
for _, cap_name := range strings.Split(strings.TrimSpace(e.Arguments[2]), " ") {
|
||||||
|
if cap_name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if command == "ACK" {
|
||||||
|
irc.AcknowledgedCaps = append(irc.AcknowledgedCaps, cap_name)
|
||||||
|
}
|
||||||
|
cap_chan <- true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
irc.pwrite <- "CAP LS\r\n"
|
||||||
|
|
||||||
|
if irc.UseSASL {
|
||||||
|
select {
|
||||||
|
case res := <-saslResChan:
|
||||||
|
if res.Failed {
|
||||||
|
close(saslResChan)
|
||||||
|
return res.Err
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second * 15):
|
||||||
|
close(saslResChan)
|
||||||
|
return errors.New("SASL setup timed out. This shouldn't happen.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all capabilities to be ACKed or NAKed before ending negotiation
|
||||||
|
for i := 0; i < len(irc.RequestCaps); i++ {
|
||||||
|
<-cap_chan
|
||||||
|
}
|
||||||
|
irc.pwrite <- fmt.Sprintf("CAP END\r\n")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a connection with the (publicly visible) nickname and username.
|
||||||
|
// The nickname is later used to address the user. Returns nil if nick
|
||||||
|
// or user are empty.
|
||||||
|
func IRC(nick, user string) *Connection {
|
||||||
|
// catch invalid values
|
||||||
|
if len(nick) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(user) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
irc := &Connection{
|
||||||
|
nick: nick,
|
||||||
|
nickcurrent: nick,
|
||||||
|
user: user,
|
||||||
|
Log: log.New(os.Stdout, "", log.LstdFlags),
|
||||||
|
end: make(chan struct{}),
|
||||||
|
Version: VERSION,
|
||||||
|
KeepAlive: 4 * time.Minute,
|
||||||
|
Timeout: 1 * time.Minute,
|
||||||
|
PingFreq: 15 * time.Minute,
|
||||||
|
SASLMech: "PLAIN",
|
||||||
|
QuitMessage: "",
|
||||||
|
}
|
||||||
|
irc.setupCallbacks()
|
||||||
|
return irc
|
||||||
|
}
|
222
vendor/github.com/42wim/go-ircevent/irc_callback.go
generated
vendored
Normal file
222
vendor/github.com/42wim/go-ircevent/irc_callback.go
generated
vendored
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Register a callback to a connection and event code. A callback is a function
|
||||||
|
// which takes only an Event pointer as parameter. Valid event codes are all
|
||||||
|
// IRC/CTCP commands and error/response codes. This function returns the ID of
|
||||||
|
// the registered callback for later management.
|
||||||
|
func (irc *Connection) AddCallback(eventcode string, callback func(*Event)) int {
|
||||||
|
eventcode = strings.ToUpper(eventcode)
|
||||||
|
id := 0
|
||||||
|
if _, ok := irc.events[eventcode]; !ok {
|
||||||
|
irc.events[eventcode] = make(map[int]func(*Event))
|
||||||
|
id = 0
|
||||||
|
} else {
|
||||||
|
id = len(irc.events[eventcode])
|
||||||
|
}
|
||||||
|
irc.events[eventcode][id] = callback
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove callback i (ID) from the given event code. This functions returns
|
||||||
|
// true upon success, false if any error occurs.
|
||||||
|
func (irc *Connection) RemoveCallback(eventcode string, i int) bool {
|
||||||
|
eventcode = strings.ToUpper(eventcode)
|
||||||
|
|
||||||
|
if event, ok := irc.events[eventcode]; ok {
|
||||||
|
if _, ok := event[i]; ok {
|
||||||
|
delete(irc.events[eventcode], i)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
irc.Log.Printf("Event found, but no callback found at id %d\n", i)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
irc.Log.Println("Event not found")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all callbacks from a given event code. It returns true
|
||||||
|
// if given event code is found and cleared.
|
||||||
|
func (irc *Connection) ClearCallback(eventcode string) bool {
|
||||||
|
eventcode = strings.ToUpper(eventcode)
|
||||||
|
|
||||||
|
if _, ok := irc.events[eventcode]; ok {
|
||||||
|
irc.events[eventcode] = make(map[int]func(*Event))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
irc.Log.Println("Event not found")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace callback i (ID) associated with a given event code with a new callback function.
|
||||||
|
func (irc *Connection) ReplaceCallback(eventcode string, i int, callback func(*Event)) {
|
||||||
|
eventcode = strings.ToUpper(eventcode)
|
||||||
|
|
||||||
|
if event, ok := irc.events[eventcode]; ok {
|
||||||
|
if _, ok := event[i]; ok {
|
||||||
|
event[i] = callback
|
||||||
|
return
|
||||||
|
}
|
||||||
|
irc.Log.Printf("Event found, but no callback found at id %d\n", i)
|
||||||
|
}
|
||||||
|
irc.Log.Printf("Event not found. Use AddCallBack\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute all callbacks associated with a given event.
|
||||||
|
func (irc *Connection) RunCallbacks(event *Event) {
|
||||||
|
msg := event.Message()
|
||||||
|
if event.Code == "PRIVMSG" && len(msg) > 2 && msg[0] == '\x01' {
|
||||||
|
event.Code = "CTCP" //Unknown CTCP
|
||||||
|
|
||||||
|
if i := strings.LastIndex(msg, "\x01"); i > 0 {
|
||||||
|
msg = msg[1:i]
|
||||||
|
} else {
|
||||||
|
irc.Log.Printf("Invalid CTCP Message: %s\n", strconv.Quote(msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg == "VERSION" {
|
||||||
|
event.Code = "CTCP_VERSION"
|
||||||
|
|
||||||
|
} else if msg == "TIME" {
|
||||||
|
event.Code = "CTCP_TIME"
|
||||||
|
|
||||||
|
} else if strings.HasPrefix(msg, "PING") {
|
||||||
|
event.Code = "CTCP_PING"
|
||||||
|
|
||||||
|
} else if msg == "USERINFO" {
|
||||||
|
event.Code = "CTCP_USERINFO"
|
||||||
|
|
||||||
|
} else if msg == "CLIENTINFO" {
|
||||||
|
event.Code = "CTCP_CLIENTINFO"
|
||||||
|
|
||||||
|
} else if strings.HasPrefix(msg, "ACTION") {
|
||||||
|
event.Code = "CTCP_ACTION"
|
||||||
|
if len(msg) > 6 {
|
||||||
|
msg = msg[7:]
|
||||||
|
} else {
|
||||||
|
msg = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.Arguments[len(event.Arguments)-1] = msg
|
||||||
|
}
|
||||||
|
|
||||||
|
if callbacks, ok := irc.events[event.Code]; ok {
|
||||||
|
if irc.VerboseCallbackHandler {
|
||||||
|
irc.Log.Printf("%v (%v) >> %#v\n", event.Code, len(callbacks), event)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, callback := range callbacks {
|
||||||
|
callback(event)
|
||||||
|
}
|
||||||
|
} else if irc.VerboseCallbackHandler {
|
||||||
|
irc.Log.Printf("%v (0) >> %#v\n", event.Code, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
if callbacks, ok := irc.events["*"]; ok {
|
||||||
|
if irc.VerboseCallbackHandler {
|
||||||
|
irc.Log.Printf("%v (0) >> %#v\n", event.Code, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, callback := range callbacks {
|
||||||
|
callback(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up some initial callbacks to handle the IRC/CTCP protocol.
|
||||||
|
func (irc *Connection) setupCallbacks() {
|
||||||
|
irc.events = make(map[string]map[int]func(*Event))
|
||||||
|
|
||||||
|
//Handle error events.
|
||||||
|
irc.AddCallback("ERROR", func(e *Event) { irc.Disconnect() })
|
||||||
|
|
||||||
|
//Handle ping events
|
||||||
|
irc.AddCallback("PING", func(e *Event) { irc.SendRaw("PONG :" + e.Message()) })
|
||||||
|
|
||||||
|
//Version handler
|
||||||
|
irc.AddCallback("CTCP_VERSION", func(e *Event) {
|
||||||
|
irc.SendRawf("NOTICE %s :\x01VERSION %s\x01", e.Nick, irc.Version)
|
||||||
|
})
|
||||||
|
|
||||||
|
irc.AddCallback("CTCP_USERINFO", func(e *Event) {
|
||||||
|
irc.SendRawf("NOTICE %s :\x01USERINFO %s\x01", e.Nick, irc.user)
|
||||||
|
})
|
||||||
|
|
||||||
|
irc.AddCallback("CTCP_CLIENTINFO", func(e *Event) {
|
||||||
|
irc.SendRawf("NOTICE %s :\x01CLIENTINFO PING VERSION TIME USERINFO CLIENTINFO\x01", e.Nick)
|
||||||
|
})
|
||||||
|
|
||||||
|
irc.AddCallback("CTCP_TIME", func(e *Event) {
|
||||||
|
ltime := time.Now()
|
||||||
|
irc.SendRawf("NOTICE %s :\x01TIME %s\x01", e.Nick, ltime.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
irc.AddCallback("CTCP_PING", func(e *Event) { irc.SendRawf("NOTICE %s :\x01%s\x01", e.Nick, e.Message()) })
|
||||||
|
|
||||||
|
// 437: ERR_UNAVAILRESOURCE "<nick/channel> :Nick/channel is temporarily unavailable"
|
||||||
|
// Add a _ to current nick. If irc.nickcurrent is empty this cannot
|
||||||
|
// work. It has to be set somewhere first in case the nick is already
|
||||||
|
// taken or unavailable from the beginning.
|
||||||
|
irc.AddCallback("437", func(e *Event) {
|
||||||
|
// If irc.nickcurrent hasn't been set yet, set to irc.nick
|
||||||
|
if irc.nickcurrent == "" {
|
||||||
|
irc.nickcurrent = irc.nick
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(irc.nickcurrent) > 8 {
|
||||||
|
irc.nickcurrent = "_" + irc.nickcurrent
|
||||||
|
} else {
|
||||||
|
irc.nickcurrent = irc.nickcurrent + "_"
|
||||||
|
}
|
||||||
|
irc.SendRawf("NICK %s", irc.nickcurrent)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 433: ERR_NICKNAMEINUSE "<nick> :Nickname is already in use"
|
||||||
|
// Add a _ to current nick.
|
||||||
|
irc.AddCallback("433", func(e *Event) {
|
||||||
|
// If irc.nickcurrent hasn't been set yet, set to irc.nick
|
||||||
|
if irc.nickcurrent == "" {
|
||||||
|
irc.nickcurrent = irc.nick
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(irc.nickcurrent) > 8 {
|
||||||
|
irc.nickcurrent = "_" + irc.nickcurrent
|
||||||
|
} else {
|
||||||
|
irc.nickcurrent = irc.nickcurrent + "_"
|
||||||
|
}
|
||||||
|
irc.SendRawf("NICK %s", irc.nickcurrent)
|
||||||
|
})
|
||||||
|
|
||||||
|
irc.AddCallback("PONG", func(e *Event) {
|
||||||
|
ns, _ := strconv.ParseInt(e.Message(), 10, 64)
|
||||||
|
delta := time.Duration(time.Now().UnixNano() - ns)
|
||||||
|
if irc.Debug {
|
||||||
|
irc.Log.Printf("Lag: %.3f s\n", delta.Seconds())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// NICK Define a nickname.
|
||||||
|
// Set irc.nickcurrent to the new nick actually used in this connection.
|
||||||
|
irc.AddCallback("NICK", func(e *Event) {
|
||||||
|
if e.Nick == irc.nick {
|
||||||
|
irc.nickcurrent = e.Message()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 1: RPL_WELCOME "Welcome to the Internet Relay Network <nick>!<user>@<host>"
|
||||||
|
// Set irc.nickcurrent to the actually used nick in this connection.
|
||||||
|
irc.AddCallback("001", func(e *Event) {
|
||||||
|
irc.Lock()
|
||||||
|
irc.nickcurrent = e.Arguments[0]
|
||||||
|
irc.Unlock()
|
||||||
|
})
|
||||||
|
}
|
53
vendor/github.com/42wim/go-ircevent/irc_sasl.go
generated
vendored
Normal file
53
vendor/github.com/42wim/go-ircevent/irc_sasl.go
generated
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SASLResult struct {
|
||||||
|
Failed bool
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (irc *Connection) setupSASLCallbacks(result chan<- *SASLResult) {
|
||||||
|
irc.AddCallback("CAP", func(e *Event) {
|
||||||
|
if len(e.Arguments) == 3 {
|
||||||
|
if e.Arguments[1] == "LS" {
|
||||||
|
if !strings.Contains(e.Arguments[2], "sasl") {
|
||||||
|
result <- &SASLResult{true, errors.New("no SASL capability " + e.Arguments[2])}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.Arguments[1] == "ACK" {
|
||||||
|
if irc.SASLMech != "PLAIN" {
|
||||||
|
result <- &SASLResult{true, errors.New("only PLAIN is supported")}
|
||||||
|
}
|
||||||
|
irc.SendRaw("AUTHENTICATE " + irc.SASLMech)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
irc.AddCallback("AUTHENTICATE", func(e *Event) {
|
||||||
|
str := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s\x00%s\x00%s", irc.SASLLogin, irc.SASLLogin, irc.SASLPassword)))
|
||||||
|
irc.SendRaw("AUTHENTICATE " + str)
|
||||||
|
})
|
||||||
|
irc.AddCallback("901", func(e *Event) {
|
||||||
|
irc.SendRaw("CAP END")
|
||||||
|
irc.SendRaw("QUIT")
|
||||||
|
result <- &SASLResult{true, errors.New(e.Arguments[1])}
|
||||||
|
})
|
||||||
|
irc.AddCallback("902", func(e *Event) {
|
||||||
|
irc.SendRaw("CAP END")
|
||||||
|
irc.SendRaw("QUIT")
|
||||||
|
result <- &SASLResult{true, errors.New(e.Arguments[1])}
|
||||||
|
})
|
||||||
|
irc.AddCallback("903", func(e *Event) {
|
||||||
|
result <- &SASLResult{false, nil}
|
||||||
|
})
|
||||||
|
irc.AddCallback("904", func(e *Event) {
|
||||||
|
irc.SendRaw("CAP END")
|
||||||
|
irc.SendRaw("QUIT")
|
||||||
|
result <- &SASLResult{true, errors.New(e.Arguments[1])}
|
||||||
|
})
|
||||||
|
}
|
76
vendor/github.com/42wim/go-ircevent/irc_struct.go
generated
vendored
Normal file
76
vendor/github.com/42wim/go-ircevent/irc_struct.go
generated
vendored
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// Copyright 2009 Thomas Jager <mail@jager.no> All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Connection struct {
|
||||||
|
sync.Mutex
|
||||||
|
sync.WaitGroup
|
||||||
|
Debug bool
|
||||||
|
Error chan error
|
||||||
|
Password string
|
||||||
|
UseTLS bool
|
||||||
|
UseSASL bool
|
||||||
|
RequestCaps []string
|
||||||
|
AcknowledgedCaps []string
|
||||||
|
SASLLogin string
|
||||||
|
SASLPassword string
|
||||||
|
SASLMech string
|
||||||
|
TLSConfig *tls.Config
|
||||||
|
Version string
|
||||||
|
Timeout time.Duration
|
||||||
|
PingFreq time.Duration
|
||||||
|
KeepAlive time.Duration
|
||||||
|
Server string
|
||||||
|
|
||||||
|
socket net.Conn
|
||||||
|
pwrite chan string
|
||||||
|
end chan struct{}
|
||||||
|
|
||||||
|
nick string //The nickname we want.
|
||||||
|
nickcurrent string //The nickname we currently have.
|
||||||
|
user string
|
||||||
|
registered bool
|
||||||
|
events map[string]map[int]func(*Event)
|
||||||
|
|
||||||
|
QuitMessage string
|
||||||
|
lastMessage time.Time
|
||||||
|
|
||||||
|
VerboseCallbackHandler bool
|
||||||
|
Log *log.Logger
|
||||||
|
|
||||||
|
stopped bool
|
||||||
|
quit bool //User called Quit, do not reconnect.
|
||||||
|
}
|
||||||
|
|
||||||
|
// A struct to represent an event.
|
||||||
|
type Event struct {
|
||||||
|
Code string
|
||||||
|
Raw string
|
||||||
|
Nick string //<nick>
|
||||||
|
Host string //<nick>!<usr>@<host>
|
||||||
|
Source string //<host>
|
||||||
|
User string //<usr>
|
||||||
|
Arguments []string
|
||||||
|
Tags map[string]string
|
||||||
|
Connection *Connection
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the last message from Event arguments.
|
||||||
|
// This function leaves the arguments untouched and
|
||||||
|
// returns an empty string if there are none.
|
||||||
|
func (e *Event) Message() string {
|
||||||
|
if len(e.Arguments) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return e.Arguments[len(e.Arguments)-1]
|
||||||
|
}
|
14
vendor/github.com/42wim/go-ircevent/irc_test_fuzz.go
generated
vendored
Normal file
14
vendor/github.com/42wim/go-ircevent/irc_test_fuzz.go
generated
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// +build gofuzz
|
||||||
|
|
||||||
|
package irc
|
||||||
|
|
||||||
|
func Fuzz(data []byte) int {
|
||||||
|
b := bytes.NewBuffer(data)
|
||||||
|
event, err := parseToEvent(b.String())
|
||||||
|
if err == nil {
|
||||||
|
irc := IRC("go-eventirc", "go-eventirc")
|
||||||
|
irc.RunCallbacks(event)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
14
vendor/github.com/BurntSushi/toml/COPYING
generated
vendored
Normal file
14
vendor/github.com/BurntSushi/toml/COPYING
generated
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
Version 2, December 2004
|
||||||
|
|
||||||
|
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
90
vendor/github.com/BurntSushi/toml/cmd/toml-test-decoder/main.go
generated
vendored
Normal file
90
vendor/github.com/BurntSushi/toml/cmd/toml-test-decoder/main.go
generated
vendored
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
// Command toml-test-decoder satisfies the toml-test interface for testing
|
||||||
|
// TOML decoders. Namely, it accepts TOML on stdin and outputs JSON on stdout.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
|
||||||
|
flag.Usage = usage
|
||||||
|
flag.Parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
log.Printf("Usage: %s < toml-file\n", path.Base(os.Args[0]))
|
||||||
|
flag.PrintDefaults()
|
||||||
|
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if flag.NArg() != 0 {
|
||||||
|
flag.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmp interface{}
|
||||||
|
if _, err := toml.DecodeReader(os.Stdin, &tmp); err != nil {
|
||||||
|
log.Fatalf("Error decoding TOML: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
typedTmp := translate(tmp)
|
||||||
|
if err := json.NewEncoder(os.Stdout).Encode(typedTmp); err != nil {
|
||||||
|
log.Fatalf("Error encoding JSON: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func translate(tomlData interface{}) interface{} {
|
||||||
|
switch orig := tomlData.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
typed := make(map[string]interface{}, len(orig))
|
||||||
|
for k, v := range orig {
|
||||||
|
typed[k] = translate(v)
|
||||||
|
}
|
||||||
|
return typed
|
||||||
|
case []map[string]interface{}:
|
||||||
|
typed := make([]map[string]interface{}, len(orig))
|
||||||
|
for i, v := range orig {
|
||||||
|
typed[i] = translate(v).(map[string]interface{})
|
||||||
|
}
|
||||||
|
return typed
|
||||||
|
case []interface{}:
|
||||||
|
typed := make([]interface{}, len(orig))
|
||||||
|
for i, v := range orig {
|
||||||
|
typed[i] = translate(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't really need to tag arrays, but let's be future proof.
|
||||||
|
// (If TOML ever supports tuples, we'll need this.)
|
||||||
|
return tag("array", typed)
|
||||||
|
case time.Time:
|
||||||
|
return tag("datetime", orig.Format("2006-01-02T15:04:05Z"))
|
||||||
|
case bool:
|
||||||
|
return tag("bool", fmt.Sprintf("%v", orig))
|
||||||
|
case int64:
|
||||||
|
return tag("integer", fmt.Sprintf("%d", orig))
|
||||||
|
case float64:
|
||||||
|
return tag("float", fmt.Sprintf("%v", orig))
|
||||||
|
case string:
|
||||||
|
return tag("string", orig)
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(fmt.Sprintf("Unknown type: %T", tomlData))
|
||||||
|
}
|
||||||
|
|
||||||
|
func tag(typeName string, data interface{}) map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"type": typeName,
|
||||||
|
"value": data,
|
||||||
|
}
|
||||||
|
}
|
131
vendor/github.com/BurntSushi/toml/cmd/toml-test-encoder/main.go
generated
vendored
Normal file
131
vendor/github.com/BurntSushi/toml/cmd/toml-test-encoder/main.go
generated
vendored
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// Command toml-test-encoder satisfies the toml-test interface for testing
|
||||||
|
// TOML encoders. Namely, it accepts JSON on stdin and outputs TOML on stdout.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
|
||||||
|
flag.Usage = usage
|
||||||
|
flag.Parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
log.Printf("Usage: %s < json-file\n", path.Base(os.Args[0]))
|
||||||
|
flag.PrintDefaults()
|
||||||
|
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if flag.NArg() != 0 {
|
||||||
|
flag.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmp interface{}
|
||||||
|
if err := json.NewDecoder(os.Stdin).Decode(&tmp); err != nil {
|
||||||
|
log.Fatalf("Error decoding JSON: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tomlData := translate(tmp)
|
||||||
|
if err := toml.NewEncoder(os.Stdout).Encode(tomlData); err != nil {
|
||||||
|
log.Fatalf("Error encoding TOML: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func translate(typedJson interface{}) interface{} {
|
||||||
|
switch v := typedJson.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
if len(v) == 2 && in("type", v) && in("value", v) {
|
||||||
|
return untag(v)
|
||||||
|
}
|
||||||
|
m := make(map[string]interface{}, len(v))
|
||||||
|
for k, v2 := range v {
|
||||||
|
m[k] = translate(v2)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
case []interface{}:
|
||||||
|
tabArray := make([]map[string]interface{}, len(v))
|
||||||
|
for i := range v {
|
||||||
|
if m, ok := translate(v[i]).(map[string]interface{}); ok {
|
||||||
|
tabArray[i] = m
|
||||||
|
} else {
|
||||||
|
log.Fatalf("JSON arrays may only contain objects. This " +
|
||||||
|
"corresponds to only tables being allowed in " +
|
||||||
|
"TOML table arrays.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tabArray
|
||||||
|
}
|
||||||
|
log.Fatalf("Unrecognized JSON format '%T'.", typedJson)
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
func untag(typed map[string]interface{}) interface{} {
|
||||||
|
t := typed["type"].(string)
|
||||||
|
v := typed["value"]
|
||||||
|
switch t {
|
||||||
|
case "string":
|
||||||
|
return v.(string)
|
||||||
|
case "integer":
|
||||||
|
v := v.(string)
|
||||||
|
n, err := strconv.Atoi(v)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Could not parse '%s' as integer: %s", v, err)
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
case "float":
|
||||||
|
v := v.(string)
|
||||||
|
f, err := strconv.ParseFloat(v, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Could not parse '%s' as float64: %s", v, err)
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
case "datetime":
|
||||||
|
v := v.(string)
|
||||||
|
t, err := time.Parse("2006-01-02T15:04:05Z", v)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Could not parse '%s' as a datetime: %s", v, err)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
case "bool":
|
||||||
|
v := v.(string)
|
||||||
|
switch v {
|
||||||
|
case "true":
|
||||||
|
return true
|
||||||
|
case "false":
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
log.Fatalf("Could not parse '%s' as a boolean.", v)
|
||||||
|
case "array":
|
||||||
|
v := v.([]interface{})
|
||||||
|
array := make([]interface{}, len(v))
|
||||||
|
for i := range v {
|
||||||
|
if m, ok := v[i].(map[string]interface{}); ok {
|
||||||
|
array[i] = untag(m)
|
||||||
|
} else {
|
||||||
|
log.Fatalf("Arrays may only contain other arrays or "+
|
||||||
|
"primitive values, but found a '%T'.", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
log.Fatalf("Unrecognized tag type '%s'.", t)
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
func in(key string, m map[string]interface{}) bool {
|
||||||
|
_, ok := m[key]
|
||||||
|
return ok
|
||||||
|
}
|
61
vendor/github.com/BurntSushi/toml/cmd/tomlv/main.go
generated
vendored
Normal file
61
vendor/github.com/BurntSushi/toml/cmd/tomlv/main.go
generated
vendored
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// Command tomlv validates TOML documents and prints each key's type.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagTypes = false
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
|
||||||
|
flag.BoolVar(&flagTypes, "types", flagTypes,
|
||||||
|
"When set, the types of every defined key will be shown.")
|
||||||
|
|
||||||
|
flag.Usage = usage
|
||||||
|
flag.Parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
log.Printf("Usage: %s toml-file [ toml-file ... ]\n",
|
||||||
|
path.Base(os.Args[0]))
|
||||||
|
flag.PrintDefaults()
|
||||||
|
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if flag.NArg() < 1 {
|
||||||
|
flag.Usage()
|
||||||
|
}
|
||||||
|
for _, f := range flag.Args() {
|
||||||
|
var tmp interface{}
|
||||||
|
md, err := toml.DecodeFile(f, &tmp)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error in '%s': %s", f, err)
|
||||||
|
}
|
||||||
|
if flagTypes {
|
||||||
|
printTypes(md)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printTypes(md toml.MetaData) {
|
||||||
|
tabw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
for _, key := range md.Keys() {
|
||||||
|
fmt.Fprintf(tabw, "%s%s\t%s\n",
|
||||||
|
strings.Repeat(" ", len(key)-1), key, md.Type(key...))
|
||||||
|
}
|
||||||
|
tabw.Flush()
|
||||||
|
}
|
509
vendor/github.com/BurntSushi/toml/decode.go
generated
vendored
Normal file
509
vendor/github.com/BurntSushi/toml/decode.go
generated
vendored
Normal file
@ -0,0 +1,509 @@
|
|||||||
|
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)
|
||||||
|
}
|
121
vendor/github.com/BurntSushi/toml/decode_meta.go
generated
vendored
Normal file
121
vendor/github.com/BurntSushi/toml/decode_meta.go
generated
vendored
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
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
|
||||||
|
}
|
27
vendor/github.com/BurntSushi/toml/doc.go
generated
vendored
Normal file
27
vendor/github.com/BurntSushi/toml/doc.go
generated
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
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
|
568
vendor/github.com/BurntSushi/toml/encode.go
generated
vendored
Normal file
568
vendor/github.com/BurntSushi/toml/encode.go
generated
vendored
Normal file
@ -0,0 +1,568 @@
|
|||||||
|
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
|
||||||
|
}
|
19
vendor/github.com/BurntSushi/toml/encoding_types.go
generated
vendored
Normal file
19
vendor/github.com/BurntSushi/toml/encoding_types.go
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// +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
|
18
vendor/github.com/BurntSushi/toml/encoding_types_1.1.go
generated
vendored
Normal file
18
vendor/github.com/BurntSushi/toml/encoding_types_1.1.go
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// +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
|
||||||
|
}
|
953
vendor/github.com/BurntSushi/toml/lex.go
generated
vendored
Normal file
953
vendor/github.com/BurntSushi/toml/lex.go
generated
vendored
Normal file
@ -0,0 +1,953 @@
|
|||||||
|
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)
|
||||||
|
}
|
592
vendor/github.com/BurntSushi/toml/parse.go
generated
vendored
Normal file
592
vendor/github.com/BurntSushi/toml/parse.go
generated
vendored
Normal file
@ -0,0 +1,592 @@
|
|||||||
|
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
|
||||||
|
}
|
91
vendor/github.com/BurntSushi/toml/type_check.go
generated
vendored
Normal file
91
vendor/github.com/BurntSushi/toml/type_check.go
generated
vendored
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
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
|
||||||
|
}
|
242
vendor/github.com/BurntSushi/toml/type_fields.go
generated
vendored
Normal file
242
vendor/github.com/BurntSushi/toml/type_fields.go
generated
vendored
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
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
|
||||||
|
}
|
22
vendor/github.com/GeertJohan/go.rice/LICENSE
generated
vendored
Normal file
22
vendor/github.com/GeertJohan/go.rice/LICENSE
generated
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
Copyright (c) 2013, Geert-Johan Riemer
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
138
vendor/github.com/GeertJohan/go.rice/appended.go
generated
vendored
Normal file
138
vendor/github.com/GeertJohan/go.rice/appended.go
generated
vendored
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package rice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/daaku/go.zipexe"
|
||||||
|
"github.com/kardianos/osext"
|
||||||
|
)
|
||||||
|
|
||||||
|
// appendedBox defines an appended box
|
||||||
|
type appendedBox struct {
|
||||||
|
Name string // box name
|
||||||
|
Files map[string]*appendedFile // appended files (*zip.File) by full path
|
||||||
|
}
|
||||||
|
|
||||||
|
type appendedFile struct {
|
||||||
|
zipFile *zip.File
|
||||||
|
dir bool
|
||||||
|
dirInfo *appendedDirInfo
|
||||||
|
children []*appendedFile
|
||||||
|
content []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendedBoxes is a public register of appendes boxes
|
||||||
|
var appendedBoxes = make(map[string]*appendedBox)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// find if exec is appended
|
||||||
|
thisFile, err := osext.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return // not appended or cant find self executable
|
||||||
|
}
|
||||||
|
closer, rd, err := zipexe.OpenCloser(thisFile)
|
||||||
|
if err != nil {
|
||||||
|
return // not appended
|
||||||
|
}
|
||||||
|
defer closer.Close()
|
||||||
|
|
||||||
|
for _, f := range rd.File {
|
||||||
|
// get box and file name from f.Name
|
||||||
|
fileParts := strings.SplitN(strings.TrimLeft(filepath.ToSlash(f.Name), "/"), "/", 2)
|
||||||
|
boxName := fileParts[0]
|
||||||
|
var fileName string
|
||||||
|
if len(fileParts) > 1 {
|
||||||
|
fileName = fileParts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// find box or create new one if doesn't exist
|
||||||
|
box := appendedBoxes[boxName]
|
||||||
|
if box == nil {
|
||||||
|
box = &appendedBox{
|
||||||
|
Name: boxName,
|
||||||
|
Files: make(map[string]*appendedFile),
|
||||||
|
}
|
||||||
|
appendedBoxes[boxName] = box
|
||||||
|
}
|
||||||
|
|
||||||
|
// create and add file to box
|
||||||
|
af := &appendedFile{
|
||||||
|
zipFile: f,
|
||||||
|
}
|
||||||
|
if f.Comment == "dir" {
|
||||||
|
af.dir = true
|
||||||
|
af.dirInfo = &appendedDirInfo{
|
||||||
|
name: filepath.Base(af.zipFile.Name),
|
||||||
|
//++ TODO: use zip modtime when that is set correctly: af.zipFile.ModTime()
|
||||||
|
time: time.Now(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// this is a file, we need it's contents so we can create a bytes.Reader when the file is opened
|
||||||
|
// make a new byteslice
|
||||||
|
af.content = make([]byte, af.zipFile.FileInfo().Size())
|
||||||
|
// ignore reading empty files from zip (empty file still is a valid file to be read though!)
|
||||||
|
if len(af.content) > 0 {
|
||||||
|
// open io.ReadCloser
|
||||||
|
rc, err := af.zipFile.Open()
|
||||||
|
if err != nil {
|
||||||
|
af.content = nil // this will cause an error when the file is being opened or seeked (which is good)
|
||||||
|
// TODO: it's quite blunt to just log this stuff. but this is in init, so rice.Debug can't be changed yet..
|
||||||
|
log.Printf("error opening appended file %s: %v", af.zipFile.Name, err)
|
||||||
|
} else {
|
||||||
|
_, err = rc.Read(af.content)
|
||||||
|
rc.Close()
|
||||||
|
if err != nil {
|
||||||
|
af.content = nil // this will cause an error when the file is being opened or seeked (which is good)
|
||||||
|
// TODO: it's quite blunt to just log this stuff. but this is in init, so rice.Debug can't be changed yet..
|
||||||
|
log.Printf("error reading data for appended file %s: %v", af.zipFile.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add appendedFile to box file list
|
||||||
|
box.Files[fileName] = af
|
||||||
|
|
||||||
|
// add to parent dir (if any)
|
||||||
|
dirName := filepath.Dir(fileName)
|
||||||
|
if dirName == "." {
|
||||||
|
dirName = ""
|
||||||
|
}
|
||||||
|
if fileName != "" { // don't make box root dir a child of itself
|
||||||
|
if dir := box.Files[dirName]; dir != nil {
|
||||||
|
dir.children = append(dir.children, af)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// implements os.FileInfo.
|
||||||
|
// used for Readdir()
|
||||||
|
type appendedDirInfo struct {
|
||||||
|
name string
|
||||||
|
time time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adi *appendedDirInfo) Name() string {
|
||||||
|
return adi.name
|
||||||
|
}
|
||||||
|
func (adi *appendedDirInfo) Size() int64 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
func (adi *appendedDirInfo) Mode() os.FileMode {
|
||||||
|
return os.ModeDir
|
||||||
|
}
|
||||||
|
func (adi *appendedDirInfo) ModTime() time.Time {
|
||||||
|
return adi.time
|
||||||
|
}
|
||||||
|
func (adi *appendedDirInfo) IsDir() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (adi *appendedDirInfo) Sys() interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
337
vendor/github.com/GeertJohan/go.rice/box.go
generated
vendored
Normal file
337
vendor/github.com/GeertJohan/go.rice/box.go
generated
vendored
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
package rice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/GeertJohan/go.rice/embedded"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Box abstracts a directory for resources/files.
|
||||||
|
// It can either load files from disk, or from embedded code (when `rice --embed` was ran).
|
||||||
|
type Box struct {
|
||||||
|
name string
|
||||||
|
absolutePath string
|
||||||
|
embed *embedded.EmbeddedBox
|
||||||
|
appendd *appendedBox
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultLocateOrder = []LocateMethod{LocateEmbedded, LocateAppended, LocateFS}
|
||||||
|
|
||||||
|
func findBox(name string, order []LocateMethod) (*Box, error) {
|
||||||
|
b := &Box{name: name}
|
||||||
|
|
||||||
|
// no support for absolute paths since gopath can be different on different machines.
|
||||||
|
// therefore, required box must be located relative to package requiring it.
|
||||||
|
if filepath.IsAbs(name) {
|
||||||
|
return nil, errors.New("given name/path is absolute")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
for _, method := range order {
|
||||||
|
switch method {
|
||||||
|
case LocateEmbedded:
|
||||||
|
if embed := embedded.EmbeddedBoxes[name]; embed != nil {
|
||||||
|
b.embed = embed
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case LocateAppended:
|
||||||
|
appendedBoxName := strings.Replace(name, `/`, `-`, -1)
|
||||||
|
if appendd := appendedBoxes[appendedBoxName]; appendd != nil {
|
||||||
|
b.appendd = appendd
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case LocateFS:
|
||||||
|
// resolve absolute directory path
|
||||||
|
err := b.resolveAbsolutePathFromCaller()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// check if absolutePath exists on filesystem
|
||||||
|
info, err := os.Stat(b.absolutePath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// check if absolutePath is actually a directory
|
||||||
|
if !info.IsDir() {
|
||||||
|
err = errors.New("given name/path is not a directory")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
case LocateWorkingDirectory:
|
||||||
|
// resolve absolute directory path
|
||||||
|
err := b.resolveAbsolutePathFromWorkingDirectory()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// check if absolutePath exists on filesystem
|
||||||
|
info, err := os.Stat(b.absolutePath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// check if absolutePath is actually a directory
|
||||||
|
if !info.IsDir() {
|
||||||
|
err = errors.New("given name/path is not a directory")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
err = fmt.Errorf("could not locate box %q", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindBox returns a Box instance for given name.
|
||||||
|
// When the given name is a relative path, it's base path will be the calling pkg/cmd's source root.
|
||||||
|
// When the given name is absolute, it's absolute. derp.
|
||||||
|
// Make sure the path doesn't contain any sensitive information as it might be placed into generated go source (embedded).
|
||||||
|
func FindBox(name string) (*Box, error) {
|
||||||
|
return findBox(name, defaultLocateOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustFindBox returns a Box instance for given name, like FindBox does.
|
||||||
|
// It does not return an error, instead it panics when an error occurs.
|
||||||
|
func MustFindBox(name string) *Box {
|
||||||
|
box, err := findBox(name, defaultLocateOrder)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return box
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is injected as a mutable function literal so that we can mock it out in
|
||||||
|
// tests and return a fixed test file.
|
||||||
|
var resolveAbsolutePathFromCaller = func(name string, nStackFrames int) (string, error) {
|
||||||
|
_, callingGoFile, _, ok := runtime.Caller(nStackFrames)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.New("couldn't find caller on stack")
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve to proper path
|
||||||
|
pkgDir := filepath.Dir(callingGoFile)
|
||||||
|
// fix for go cover
|
||||||
|
const coverPath = "_test/_obj_test"
|
||||||
|
if !filepath.IsAbs(pkgDir) {
|
||||||
|
if i := strings.Index(pkgDir, coverPath); i >= 0 {
|
||||||
|
pkgDir = pkgDir[:i] + pkgDir[i+len(coverPath):] // remove coverPath
|
||||||
|
pkgDir = filepath.Join(os.Getenv("GOPATH"), "src", pkgDir) // make absolute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filepath.Join(pkgDir, name), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Box) resolveAbsolutePathFromCaller() error {
|
||||||
|
path, err := resolveAbsolutePathFromCaller(b.name, 4)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.absolutePath = path
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Box) resolveAbsolutePathFromWorkingDirectory() error {
|
||||||
|
path, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.absolutePath = filepath.Join(path, b.name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmbedded indicates wether this box was embedded into the application
|
||||||
|
func (b *Box) IsEmbedded() bool {
|
||||||
|
return b.embed != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAppended indicates wether this box was appended to the application
|
||||||
|
func (b *Box) IsAppended() bool {
|
||||||
|
return b.appendd != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time returns how actual the box is.
|
||||||
|
// When the box is embedded, it's value is saved in the embedding code.
|
||||||
|
// When the box is live, this methods returns time.Now()
|
||||||
|
func (b *Box) Time() time.Time {
|
||||||
|
if b.IsEmbedded() {
|
||||||
|
return b.embed.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
//++ TODO: return time for appended box
|
||||||
|
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open opens a File from the box
|
||||||
|
// If there is an error, it will be of type *os.PathError.
|
||||||
|
func (b *Box) Open(name string) (*File, error) {
|
||||||
|
if Debug {
|
||||||
|
fmt.Printf("Open(%s)\n", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.IsEmbedded() {
|
||||||
|
if Debug {
|
||||||
|
fmt.Println("Box is embedded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// trim prefix (paths are relative to box)
|
||||||
|
name = strings.TrimLeft(name, "/")
|
||||||
|
if Debug {
|
||||||
|
fmt.Printf("Trying %s\n", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// search for file
|
||||||
|
ef := b.embed.Files[name]
|
||||||
|
if ef == nil {
|
||||||
|
if Debug {
|
||||||
|
fmt.Println("Didn't find file in embed")
|
||||||
|
}
|
||||||
|
// file not found, try dir
|
||||||
|
ed := b.embed.Dirs[name]
|
||||||
|
if ed == nil {
|
||||||
|
if Debug {
|
||||||
|
fmt.Println("Didn't find dir in embed")
|
||||||
|
}
|
||||||
|
// dir not found, error out
|
||||||
|
return nil, &os.PathError{
|
||||||
|
Op: "open",
|
||||||
|
Path: name,
|
||||||
|
Err: os.ErrNotExist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if Debug {
|
||||||
|
fmt.Println("Found dir. Returning virtual dir")
|
||||||
|
}
|
||||||
|
vd := newVirtualDir(ed)
|
||||||
|
return &File{virtualD: vd}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// box is embedded
|
||||||
|
if Debug {
|
||||||
|
fmt.Println("Found file. Returning virtual file")
|
||||||
|
}
|
||||||
|
vf := newVirtualFile(ef)
|
||||||
|
return &File{virtualF: vf}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.IsAppended() {
|
||||||
|
// trim prefix (paths are relative to box)
|
||||||
|
name = strings.TrimLeft(name, "/")
|
||||||
|
|
||||||
|
// search for file
|
||||||
|
appendedFile := b.appendd.Files[name]
|
||||||
|
if appendedFile == nil {
|
||||||
|
return nil, &os.PathError{
|
||||||
|
Op: "open",
|
||||||
|
Path: name,
|
||||||
|
Err: os.ErrNotExist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create new file
|
||||||
|
f := &File{
|
||||||
|
appendedF: appendedFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this file is a directory, we want to be able to read and seek
|
||||||
|
if !appendedFile.dir {
|
||||||
|
// looks like malformed data in zip, error now
|
||||||
|
if appendedFile.content == nil {
|
||||||
|
return nil, &os.PathError{
|
||||||
|
Op: "open",
|
||||||
|
Path: "name",
|
||||||
|
Err: errors.New("error reading data from zip file"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// create new bytes.Reader
|
||||||
|
f.appendedFileReader = bytes.NewReader(appendedFile.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// all done
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// perform os open
|
||||||
|
if Debug {
|
||||||
|
fmt.Printf("Using os.Open(%s)", filepath.Join(b.absolutePath, name))
|
||||||
|
}
|
||||||
|
file, err := os.Open(filepath.Join(b.absolutePath, name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &File{realF: file}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns the content of the file with given name as []byte.
|
||||||
|
func (b *Box) Bytes(name string) ([]byte, error) {
|
||||||
|
file, err := b.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
content, err := ioutil.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustBytes returns the content of the file with given name as []byte.
|
||||||
|
// panic's on error.
|
||||||
|
func (b *Box) MustBytes(name string) []byte {
|
||||||
|
bts, err := b.Bytes(name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return bts
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the content of the file with given name as string.
|
||||||
|
func (b *Box) String(name string) (string, error) {
|
||||||
|
// check if box is embedded, optimized fast path
|
||||||
|
if b.IsEmbedded() {
|
||||||
|
// find file in embed
|
||||||
|
ef := b.embed.Files[name]
|
||||||
|
if ef == nil {
|
||||||
|
return "", os.ErrNotExist
|
||||||
|
}
|
||||||
|
// return as string
|
||||||
|
return ef.Content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bts, err := b.Bytes(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(bts), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustString returns the content of the file with given name as string.
|
||||||
|
// panic's on error.
|
||||||
|
func (b *Box) MustString(name string) string {
|
||||||
|
str, err := b.String(name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the box
|
||||||
|
func (b *Box) Name() string {
|
||||||
|
return b.name
|
||||||
|
}
|
39
vendor/github.com/GeertJohan/go.rice/config.go
generated
vendored
Normal file
39
vendor/github.com/GeertJohan/go.rice/config.go
generated
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package rice
|
||||||
|
|
||||||
|
// LocateMethod defines how a box is located.
|
||||||
|
type LocateMethod int
|
||||||
|
|
||||||
|
const (
|
||||||
|
LocateFS = LocateMethod(iota) // Locate on the filesystem according to package path.
|
||||||
|
LocateAppended // Locate boxes appended to the executable.
|
||||||
|
LocateEmbedded // Locate embedded boxes.
|
||||||
|
LocateWorkingDirectory // Locate on the binary working directory
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config allows customizing the box lookup behavior.
|
||||||
|
type Config struct {
|
||||||
|
// LocateOrder defines the priority order that boxes are searched for. By
|
||||||
|
// default, the package global FindBox searches for embedded boxes first,
|
||||||
|
// then appended boxes, and then finally boxes on the filesystem. That
|
||||||
|
// search order may be customized by provided the ordered list here. Leaving
|
||||||
|
// out a particular method will omit that from the search space. For
|
||||||
|
// example, []LocateMethod{LocateEmbedded, LocateAppended} will never search
|
||||||
|
// the filesystem for boxes.
|
||||||
|
LocateOrder []LocateMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindBox searches for boxes using the LocateOrder of the config.
|
||||||
|
func (c *Config) FindBox(boxName string) (*Box, error) {
|
||||||
|
return findBox(boxName, c.LocateOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustFindBox searches for boxes using the LocateOrder of the config, like
|
||||||
|
// FindBox does. It does not return an error, instead it panics when an error
|
||||||
|
// occurs.
|
||||||
|
func (c *Config) MustFindBox(boxName string) *Box {
|
||||||
|
box, err := findBox(boxName, c.LocateOrder)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return box
|
||||||
|
}
|
4
vendor/github.com/GeertJohan/go.rice/debug.go
generated
vendored
Normal file
4
vendor/github.com/GeertJohan/go.rice/debug.go
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
package rice
|
||||||
|
|
||||||
|
// Debug can be set to true to enable debugging.
|
||||||
|
var Debug = false
|
90
vendor/github.com/GeertJohan/go.rice/embedded.go
generated
vendored
Normal file
90
vendor/github.com/GeertJohan/go.rice/embedded.go
generated
vendored
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package rice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/GeertJohan/go.rice/embedded"
|
||||||
|
)
|
||||||
|
|
||||||
|
// re-type to make exported methods invisible to user (godoc)
|
||||||
|
// they're not required for the user
|
||||||
|
// embeddedDirInfo implements os.FileInfo
|
||||||
|
type embeddedDirInfo embedded.EmbeddedDir
|
||||||
|
|
||||||
|
// Name returns the base name of the directory
|
||||||
|
// (implementing os.FileInfo)
|
||||||
|
func (ed *embeddedDirInfo) Name() string {
|
||||||
|
return ed.Filename
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size always returns 0
|
||||||
|
// (implementing os.FileInfo)
|
||||||
|
func (ed *embeddedDirInfo) Size() int64 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode returns the file mode bits
|
||||||
|
// (implementing os.FileInfo)
|
||||||
|
func (ed *embeddedDirInfo) Mode() os.FileMode {
|
||||||
|
return os.FileMode(0555 | os.ModeDir) // dr-xr-xr-x
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModTime returns the modification time
|
||||||
|
// (implementing os.FileInfo)
|
||||||
|
func (ed *embeddedDirInfo) ModTime() time.Time {
|
||||||
|
return ed.DirModTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir returns the abbreviation for Mode().IsDir() (always true)
|
||||||
|
// (implementing os.FileInfo)
|
||||||
|
func (ed *embeddedDirInfo) IsDir() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sys returns the underlying data source (always nil)
|
||||||
|
// (implementing os.FileInfo)
|
||||||
|
func (ed *embeddedDirInfo) Sys() interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-type to make exported methods invisible to user (godoc)
|
||||||
|
// they're not required for the user
|
||||||
|
// embeddedFileInfo implements os.FileInfo
|
||||||
|
type embeddedFileInfo embedded.EmbeddedFile
|
||||||
|
|
||||||
|
// Name returns the base name of the file
|
||||||
|
// (implementing os.FileInfo)
|
||||||
|
func (ef *embeddedFileInfo) Name() string {
|
||||||
|
return ef.Filename
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the length in bytes for regular files; system-dependent for others
|
||||||
|
// (implementing os.FileInfo)
|
||||||
|
func (ef *embeddedFileInfo) Size() int64 {
|
||||||
|
return int64(len(ef.Content))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode returns the file mode bits
|
||||||
|
// (implementing os.FileInfo)
|
||||||
|
func (ef *embeddedFileInfo) Mode() os.FileMode {
|
||||||
|
return os.FileMode(0555) // r-xr-xr-x
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModTime returns the modification time
|
||||||
|
// (implementing os.FileInfo)
|
||||||
|
func (ef *embeddedFileInfo) ModTime() time.Time {
|
||||||
|
return ef.FileModTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir returns the abbreviation for Mode().IsDir() (always false)
|
||||||
|
// (implementing os.FileInfo)
|
||||||
|
func (ef *embeddedFileInfo) IsDir() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sys returns the underlying data source (always nil)
|
||||||
|
// (implementing os.FileInfo)
|
||||||
|
func (ef *embeddedFileInfo) Sys() interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
80
vendor/github.com/GeertJohan/go.rice/embedded/embedded.go
generated
vendored
Normal file
80
vendor/github.com/GeertJohan/go.rice/embedded/embedded.go
generated
vendored
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// Package embedded defines embedded data types that are shared between the go.rice package and generated code.
|
||||||
|
package embedded
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
EmbedTypeGo = 0
|
||||||
|
EmbedTypeSyso = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// EmbeddedBox defines an embedded box
|
||||||
|
type EmbeddedBox struct {
|
||||||
|
Name string // box name
|
||||||
|
Time time.Time // embed time
|
||||||
|
EmbedType int // kind of embedding
|
||||||
|
Files map[string]*EmbeddedFile // ALL embedded files by full path
|
||||||
|
Dirs map[string]*EmbeddedDir // ALL embedded dirs by full path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link creates the ChildDirs and ChildFiles links in all EmbeddedDir's
|
||||||
|
func (e *EmbeddedBox) Link() {
|
||||||
|
for path, ed := range e.Dirs {
|
||||||
|
fmt.Println(path)
|
||||||
|
ed.ChildDirs = make([]*EmbeddedDir, 0)
|
||||||
|
ed.ChildFiles = make([]*EmbeddedFile, 0)
|
||||||
|
}
|
||||||
|
for path, ed := range e.Dirs {
|
||||||
|
parentDirpath, _ := filepath.Split(path)
|
||||||
|
if strings.HasSuffix(parentDirpath, "/") {
|
||||||
|
parentDirpath = parentDirpath[:len(parentDirpath)-1]
|
||||||
|
}
|
||||||
|
parentDir := e.Dirs[parentDirpath]
|
||||||
|
if parentDir == nil {
|
||||||
|
panic("parentDir `" + parentDirpath + "` is missing in embedded box")
|
||||||
|
}
|
||||||
|
parentDir.ChildDirs = append(parentDir.ChildDirs, ed)
|
||||||
|
}
|
||||||
|
for path, ef := range e.Files {
|
||||||
|
dirpath, _ := filepath.Split(path)
|
||||||
|
if strings.HasSuffix(dirpath, "/") {
|
||||||
|
dirpath = dirpath[:len(dirpath)-1]
|
||||||
|
}
|
||||||
|
dir := e.Dirs[dirpath]
|
||||||
|
if dir == nil {
|
||||||
|
panic("dir `" + dirpath + "` is missing in embedded box")
|
||||||
|
}
|
||||||
|
dir.ChildFiles = append(dir.ChildFiles, ef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmbeddedDir is instanced in the code generated by the rice tool and contains all necicary information about an embedded file
|
||||||
|
type EmbeddedDir struct {
|
||||||
|
Filename string
|
||||||
|
DirModTime time.Time
|
||||||
|
ChildDirs []*EmbeddedDir // direct childs, as returned by virtualDir.Readdir()
|
||||||
|
ChildFiles []*EmbeddedFile // direct childs, as returned by virtualDir.Readdir()
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmbeddedFile is instanced in the code generated by the rice tool and contains all necicary information about an embedded file
|
||||||
|
type EmbeddedFile struct {
|
||||||
|
Filename string // filename
|
||||||
|
FileModTime time.Time
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmbeddedBoxes is a public register of embedded boxes
|
||||||
|
var EmbeddedBoxes = make(map[string]*EmbeddedBox)
|
||||||
|
|
||||||
|
// RegisterEmbeddedBox registers an EmbeddedBox
|
||||||
|
func RegisterEmbeddedBox(name string, box *EmbeddedBox) {
|
||||||
|
if _, exists := EmbeddedBoxes[name]; exists {
|
||||||
|
panic(fmt.Sprintf("EmbeddedBox with name `%s` exists already", name))
|
||||||
|
}
|
||||||
|
EmbeddedBoxes[name] = box
|
||||||
|
}
|
69
vendor/github.com/GeertJohan/go.rice/example/example.go
generated
vendored
Normal file
69
vendor/github.com/GeertJohan/go.rice/example/example.go
generated
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/GeertJohan/go.rice"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
conf := rice.Config{
|
||||||
|
LocateOrder: []rice.LocateMethod{rice.LocateEmbedded, rice.LocateAppended, rice.LocateFS},
|
||||||
|
}
|
||||||
|
box, err := conf.FindBox("example-files")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error opening rice.Box: %s\n", err)
|
||||||
|
}
|
||||||
|
// spew.Dump(box)
|
||||||
|
|
||||||
|
contentString, err := box.String("file.txt")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not read file contents as string: %s\n", err)
|
||||||
|
}
|
||||||
|
log.Printf("Read some file contents as string:\n%s\n", contentString)
|
||||||
|
|
||||||
|
contentBytes, err := box.Bytes("file.txt")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not read file contents as byteSlice: %s\n", err)
|
||||||
|
}
|
||||||
|
log.Printf("Read some file contents as byteSlice:\n%s\n", hex.Dump(contentBytes))
|
||||||
|
|
||||||
|
file, err := box.Open("file.txt")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not open file: %s\n", err)
|
||||||
|
}
|
||||||
|
spew.Dump(file)
|
||||||
|
|
||||||
|
// find/create a rice.Box
|
||||||
|
templateBox, err := rice.FindBox("example-templates")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
// get file contents as string
|
||||||
|
templateString, err := templateBox.String("message.tmpl")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
// parse and execute the template
|
||||||
|
tmplMessage, err := template.New("message").Parse(templateString)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
tmplMessage.Execute(os.Stdout, map[string]string{"Message": "Hello, world!"})
|
||||||
|
|
||||||
|
http.Handle("/", http.FileServer(box.HTTPBox()))
|
||||||
|
go func() {
|
||||||
|
fmt.Println("Serving files on :8080, press ctrl-C to exit")
|
||||||
|
err := http.ListenAndServe(":8080", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error serving files: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
select {}
|
||||||
|
}
|
144
vendor/github.com/GeertJohan/go.rice/file.go
generated
vendored
Normal file
144
vendor/github.com/GeertJohan/go.rice/file.go
generated
vendored
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package rice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// File implements the io.Reader, io.Seeker, io.Closer and http.File interfaces
|
||||||
|
type File struct {
|
||||||
|
// File abstracts file methods so the user doesn't see the difference between rice.virtualFile, rice.virtualDir and os.File
|
||||||
|
// TODO: maybe use internal File interface and four implementations: *os.File, appendedFile, virtualFile, virtualDir
|
||||||
|
|
||||||
|
// real file on disk
|
||||||
|
realF *os.File
|
||||||
|
|
||||||
|
// when embedded (go)
|
||||||
|
virtualF *virtualFile
|
||||||
|
virtualD *virtualDir
|
||||||
|
|
||||||
|
// when appended (zip)
|
||||||
|
appendedF *appendedFile
|
||||||
|
appendedFileReader *bytes.Reader
|
||||||
|
// TODO: is appendedFileReader subject of races? Might need a lock here..
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close is like (*os.File).Close()
|
||||||
|
// Visit http://golang.org/pkg/os/#File.Close for more information
|
||||||
|
func (f *File) Close() error {
|
||||||
|
if f.appendedF != nil {
|
||||||
|
if f.appendedFileReader == nil {
|
||||||
|
return errors.New("already closed")
|
||||||
|
}
|
||||||
|
f.appendedFileReader = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if f.virtualF != nil {
|
||||||
|
return f.virtualF.close()
|
||||||
|
}
|
||||||
|
if f.virtualD != nil {
|
||||||
|
return f.virtualD.close()
|
||||||
|
}
|
||||||
|
return f.realF.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat is like (*os.File).Stat()
|
||||||
|
// Visit http://golang.org/pkg/os/#File.Stat for more information
|
||||||
|
func (f *File) Stat() (os.FileInfo, error) {
|
||||||
|
if f.appendedF != nil {
|
||||||
|
if f.appendedF.dir {
|
||||||
|
return f.appendedF.dirInfo, nil
|
||||||
|
}
|
||||||
|
if f.appendedFileReader == nil {
|
||||||
|
return nil, errors.New("file is closed")
|
||||||
|
}
|
||||||
|
return f.appendedF.zipFile.FileInfo(), nil
|
||||||
|
}
|
||||||
|
if f.virtualF != nil {
|
||||||
|
return f.virtualF.stat()
|
||||||
|
}
|
||||||
|
if f.virtualD != nil {
|
||||||
|
return f.virtualD.stat()
|
||||||
|
}
|
||||||
|
return f.realF.Stat()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Readdir is like (*os.File).Readdir()
|
||||||
|
// Visit http://golang.org/pkg/os/#File.Readdir for more information
|
||||||
|
func (f *File) Readdir(count int) ([]os.FileInfo, error) {
|
||||||
|
if f.appendedF != nil {
|
||||||
|
if f.appendedF.dir {
|
||||||
|
fi := make([]os.FileInfo, 0, len(f.appendedF.children))
|
||||||
|
for _, childAppendedFile := range f.appendedF.children {
|
||||||
|
if childAppendedFile.dir {
|
||||||
|
fi = append(fi, childAppendedFile.dirInfo)
|
||||||
|
} else {
|
||||||
|
fi = append(fi, childAppendedFile.zipFile.FileInfo())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fi, nil
|
||||||
|
}
|
||||||
|
//++ TODO: is os.ErrInvalid the correct error for Readdir on file?
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
if f.virtualF != nil {
|
||||||
|
return f.virtualF.readdir(count)
|
||||||
|
}
|
||||||
|
if f.virtualD != nil {
|
||||||
|
return f.virtualD.readdir(count)
|
||||||
|
}
|
||||||
|
return f.realF.Readdir(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read is like (*os.File).Read()
|
||||||
|
// Visit http://golang.org/pkg/os/#File.Read for more information
|
||||||
|
func (f *File) Read(bts []byte) (int, error) {
|
||||||
|
if f.appendedF != nil {
|
||||||
|
if f.appendedFileReader == nil {
|
||||||
|
return 0, &os.PathError{
|
||||||
|
Op: "read",
|
||||||
|
Path: filepath.Base(f.appendedF.zipFile.Name),
|
||||||
|
Err: errors.New("file is closed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if f.appendedF.dir {
|
||||||
|
return 0, &os.PathError{
|
||||||
|
Op: "read",
|
||||||
|
Path: filepath.Base(f.appendedF.zipFile.Name),
|
||||||
|
Err: errors.New("is a directory"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f.appendedFileReader.Read(bts)
|
||||||
|
}
|
||||||
|
if f.virtualF != nil {
|
||||||
|
return f.virtualF.read(bts)
|
||||||
|
}
|
||||||
|
if f.virtualD != nil {
|
||||||
|
return f.virtualD.read(bts)
|
||||||
|
}
|
||||||
|
return f.realF.Read(bts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek is like (*os.File).Seek()
|
||||||
|
// Visit http://golang.org/pkg/os/#File.Seek for more information
|
||||||
|
func (f *File) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
if f.appendedF != nil {
|
||||||
|
if f.appendedFileReader == nil {
|
||||||
|
return 0, &os.PathError{
|
||||||
|
Op: "seek",
|
||||||
|
Path: filepath.Base(f.appendedF.zipFile.Name),
|
||||||
|
Err: errors.New("file is closed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f.appendedFileReader.Seek(offset, whence)
|
||||||
|
}
|
||||||
|
if f.virtualF != nil {
|
||||||
|
return f.virtualF.seek(offset, whence)
|
||||||
|
}
|
||||||
|
if f.virtualD != nil {
|
||||||
|
return f.virtualD.seek(offset, whence)
|
||||||
|
}
|
||||||
|
return f.realF.Seek(offset, whence)
|
||||||
|
}
|
21
vendor/github.com/GeertJohan/go.rice/http.go
generated
vendored
Normal file
21
vendor/github.com/GeertJohan/go.rice/http.go
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package rice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPBox implements http.FileSystem which allows the use of Box with a http.FileServer.
|
||||||
|
// e.g.: http.Handle("/", http.FileServer(rice.MustFindBox("http-files").HTTPBox()))
|
||||||
|
type HTTPBox struct {
|
||||||
|
*Box
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPBox creates a new HTTPBox from an existing Box
|
||||||
|
func (b *Box) HTTPBox() *HTTPBox {
|
||||||
|
return &HTTPBox{b}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open returns a File using the http.File interface
|
||||||
|
func (hb *HTTPBox) Open(name string) (http.File, error) {
|
||||||
|
return hb.Box.Open(name)
|
||||||
|
}
|
172
vendor/github.com/GeertJohan/go.rice/rice/append.go
generated
vendored
Normal file
172
vendor/github.com/GeertJohan/go.rice/rice/append.go
generated
vendored
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"fmt"
|
||||||
|
"go/build"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/daaku/go.zipexe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func operationAppend(pkgs []*build.Package) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
_, err := exec.LookPath("zip")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("#### WARNING ! ####")
|
||||||
|
fmt.Println("`rice append` is known not to work under windows because the `zip` command is not available. Please let me know if you got this to work (and how).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARKED FOR DELETION
|
||||||
|
// This is actually not required, the append command now has the option --exec required.
|
||||||
|
// // check if package is a command
|
||||||
|
// if !pkg.IsCommand() {
|
||||||
|
// fmt.Println("Error: can not append to non-main package. Please follow instructions at github.com/GeertJohan/go.rice")
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// create tmp zipfile
|
||||||
|
tmpZipfileName := filepath.Join(os.TempDir(), fmt.Sprintf("ricebox-%d-%s.zip", time.Now().Unix(), randomString(10)))
|
||||||
|
verbosef("Will create tmp zipfile: %s\n", tmpZipfileName)
|
||||||
|
tmpZipfile, err := os.Create(tmpZipfileName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error creating tmp zipfile: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
tmpZipfile.Close()
|
||||||
|
os.Remove(tmpZipfileName)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// find abs path for binary file
|
||||||
|
binfileName, err := filepath.Abs(flags.Append.Executable)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error finding absolute path for executable to append: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
verbosef("Will append to file: %s\n", binfileName)
|
||||||
|
|
||||||
|
// check that command doesn't already have zip appended
|
||||||
|
if rd, _ := zipexe.Open(binfileName); rd != nil {
|
||||||
|
fmt.Printf("Cannot append to already appended executable. Please remove %s and build a fresh one.\n", binfileName)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open binfile
|
||||||
|
binfile, err := os.OpenFile(binfileName, os.O_WRONLY, os.ModeAppend)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error: unable to open executable file: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create zip.Writer
|
||||||
|
zipWriter := zip.NewWriter(tmpZipfile)
|
||||||
|
|
||||||
|
for _, pkg := range pkgs {
|
||||||
|
// find boxes for this command
|
||||||
|
boxMap := findBoxes(pkg)
|
||||||
|
|
||||||
|
// notify user when no calls to rice.FindBox are made (is this an error and therefore os.Exit(1) ?
|
||||||
|
if len(boxMap) == 0 {
|
||||||
|
fmt.Printf("no calls to rice.FindBox() or rice.MustFindBox() found in import path `%s`\n", pkg.ImportPath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
verbosef("\n")
|
||||||
|
|
||||||
|
for boxname := range boxMap {
|
||||||
|
appendedBoxName := strings.Replace(boxname, `/`, `-`, -1)
|
||||||
|
|
||||||
|
// walk box path's and insert files
|
||||||
|
boxPath := filepath.Clean(filepath.Join(pkg.Dir, boxname))
|
||||||
|
filepath.Walk(boxPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if info == nil {
|
||||||
|
fmt.Printf("Error: box \"%s\" not found on disk\n", path)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
// create zipFilename
|
||||||
|
zipFileName := filepath.Join(appendedBoxName, strings.TrimPrefix(path, boxPath))
|
||||||
|
// write directories as empty file with comment "dir"
|
||||||
|
if info.IsDir() {
|
||||||
|
_, err := zipWriter.CreateHeader(&zip.FileHeader{
|
||||||
|
Name: zipFileName,
|
||||||
|
Comment: "dir",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error creating dir in tmp zip: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// create zipFileWriter
|
||||||
|
zipFileHeader, err := zip.FileInfoHeader(info)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error creating zip FileHeader: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
zipFileHeader.Name = zipFileName
|
||||||
|
zipFileWriter, err := zipWriter.CreateHeader(zipFileHeader)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error creating file in tmp zip: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
srcFile, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error opening file to append: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
_, err = io.Copy(zipFileWriter, srcFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error copying file contents to zip: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
srcFile.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = zipWriter.Close()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error closing tmp zipfile: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tmpZipfile.Sync()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error syncing tmp zipfile: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
_, err = tmpZipfile.Seek(0, 0)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error seeking tmp zipfile: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
_, err = binfile.Seek(0, 2)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error seeking bin file: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(binfile, tmpZipfile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error appending zipfile to executable: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
zipA := exec.Command("zip", "-A", binfileName)
|
||||||
|
err = zipA.Run()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error setting zip offset: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
33
vendor/github.com/GeertJohan/go.rice/rice/clean.go
generated
vendored
Normal file
33
vendor/github.com/GeertJohan/go.rice/rice/clean.go
generated
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/build"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func operationClean(pkg *build.Package) {
|
||||||
|
filepath.Walk(pkg.Dir, func(filename string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error walking pkg dir to clean files: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
verbosef("checking file '%s'\n", filename)
|
||||||
|
if filepath.Base(filename) == "rice-box.go" ||
|
||||||
|
strings.HasSuffix(filename, ".rice-box.go") ||
|
||||||
|
strings.HasSuffix(filename, ".rice-box.syso") {
|
||||||
|
err := os.Remove(filename)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error removing file (%s): %s\n", filename, err)
|
||||||
|
os.Exit(-1)
|
||||||
|
}
|
||||||
|
verbosef("removed file '%s'\n", filename)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
158
vendor/github.com/GeertJohan/go.rice/rice/embed-go.go
generated
vendored
Normal file
158
vendor/github.com/GeertJohan/go.rice/rice/embed-go.go
generated
vendored
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"go/build"
|
||||||
|
"go/format"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const boxFilename = "rice-box.go"
|
||||||
|
|
||||||
|
func operationEmbedGo(pkg *build.Package) {
|
||||||
|
|
||||||
|
boxMap := findBoxes(pkg)
|
||||||
|
|
||||||
|
// notify user when no calls to rice.FindBox are made (is this an error and therefore os.Exit(1) ?
|
||||||
|
if len(boxMap) == 0 {
|
||||||
|
fmt.Println("no calls to rice.FindBox() found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
verbosef("\n")
|
||||||
|
var boxes []*boxDataType
|
||||||
|
|
||||||
|
for boxname := range boxMap {
|
||||||
|
// find path and filename for this box
|
||||||
|
boxPath := filepath.Join(pkg.Dir, boxname)
|
||||||
|
|
||||||
|
// Check to see if the path for the box is a symbolic link. If so, simply
|
||||||
|
// box what the symbolic link points to. Note: the filepath.Walk function
|
||||||
|
// will NOT follow any nested symbolic links. This only handles the case
|
||||||
|
// where the root of the box is a symbolic link.
|
||||||
|
symPath, serr := os.Readlink(boxPath)
|
||||||
|
if serr == nil {
|
||||||
|
boxPath = symPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// verbose info
|
||||||
|
verbosef("embedding box '%s' to '%s'\n", boxname, boxFilename)
|
||||||
|
|
||||||
|
// read box metadata
|
||||||
|
boxInfo, ierr := os.Stat(boxPath)
|
||||||
|
if ierr != nil {
|
||||||
|
fmt.Printf("Error: unable to access box at %s\n", boxPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create box datastructure (used by template)
|
||||||
|
box := &boxDataType{
|
||||||
|
BoxName: boxname,
|
||||||
|
UnixNow: boxInfo.ModTime().Unix(),
|
||||||
|
Files: make([]*fileDataType, 0),
|
||||||
|
Dirs: make(map[string]*dirDataType),
|
||||||
|
}
|
||||||
|
|
||||||
|
if !boxInfo.IsDir() {
|
||||||
|
fmt.Printf("Error: Box %s must point to a directory but points to %s instead\n",
|
||||||
|
boxname, boxPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill box datastructure with file data
|
||||||
|
filepath.Walk(boxPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error walking box: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := strings.TrimPrefix(path, boxPath)
|
||||||
|
filename = strings.Replace(filename, "\\", "/", -1)
|
||||||
|
filename = strings.TrimPrefix(filename, "/")
|
||||||
|
if info.IsDir() {
|
||||||
|
dirData := &dirDataType{
|
||||||
|
Identifier: "dir" + nextIdentifier(),
|
||||||
|
FileName: filename,
|
||||||
|
ModTime: info.ModTime().Unix(),
|
||||||
|
ChildFiles: make([]*fileDataType, 0),
|
||||||
|
ChildDirs: make([]*dirDataType, 0),
|
||||||
|
}
|
||||||
|
verbosef("\tincludes dir: '%s'\n", dirData.FileName)
|
||||||
|
box.Dirs[dirData.FileName] = dirData
|
||||||
|
|
||||||
|
// add tree entry (skip for root, it'll create a recursion)
|
||||||
|
if dirData.FileName != "" {
|
||||||
|
pathParts := strings.Split(dirData.FileName, "/")
|
||||||
|
parentDir := box.Dirs[strings.Join(pathParts[:len(pathParts)-1], "/")]
|
||||||
|
parentDir.ChildDirs = append(parentDir.ChildDirs, dirData)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileData := &fileDataType{
|
||||||
|
Identifier: "file" + nextIdentifier(),
|
||||||
|
FileName: filename,
|
||||||
|
ModTime: info.ModTime().Unix(),
|
||||||
|
}
|
||||||
|
verbosef("\tincludes file: '%s'\n", fileData.FileName)
|
||||||
|
fileData.Content, err = ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error reading file content while walking box: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
box.Files = append(box.Files, fileData)
|
||||||
|
|
||||||
|
// add tree entry
|
||||||
|
pathParts := strings.Split(fileData.FileName, "/")
|
||||||
|
parentDir := box.Dirs[strings.Join(pathParts[:len(pathParts)-1], "/")]
|
||||||
|
if parentDir == nil {
|
||||||
|
fmt.Printf("Error: parent of %s is not within the box\n", path)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
parentDir.ChildFiles = append(parentDir.ChildFiles, fileData)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
boxes = append(boxes, box)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
embedSourceUnformated := bytes.NewBuffer(make([]byte, 0))
|
||||||
|
|
||||||
|
// execute template to buffer
|
||||||
|
err := tmplEmbeddedBox.Execute(
|
||||||
|
embedSourceUnformated,
|
||||||
|
embedFileDataType{pkg.Name, boxes},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error writing embedded box to file (template execute): %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// format the source code
|
||||||
|
embedSource, err := format.Source(embedSourceUnformated.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error formatting embedSource: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create go file for box
|
||||||
|
boxFile, err := os.Create(filepath.Join(pkg.Dir, boxFilename))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error creating embedded box file: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer boxFile.Close()
|
||||||
|
|
||||||
|
// write source to file
|
||||||
|
_, err = io.Copy(boxFile, bytes.NewBuffer(embedSource))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error writing embedSource to file: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
204
vendor/github.com/GeertJohan/go.rice/rice/embed-syso.go
generated
vendored
Normal file
204
vendor/github.com/GeertJohan/go.rice/rice/embed-syso.go
generated
vendored
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
"fmt"
|
||||||
|
"go/build"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/GeertJohan/go.rice/embedded"
|
||||||
|
"github.com/akavel/rsrc/coff"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sizedReader struct {
|
||||||
|
*bytes.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sizedReader) Size() int64 {
|
||||||
|
return int64(s.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmplEmbeddedSysoHelper *template.Template
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
tmplEmbeddedSysoHelper, err = template.New("embeddedSysoHelper").Parse(`package {{.Package}}
|
||||||
|
// ############# GENERATED CODE #####################
|
||||||
|
// ## This file was generated by the rice tool.
|
||||||
|
// ## Do not edit unless you know what you're doing.
|
||||||
|
// ##################################################
|
||||||
|
|
||||||
|
// extern char _bricebox_{{.Symname}}[], _ericebox_{{.Symname}};
|
||||||
|
// int get_{{.Symname}}_length() {
|
||||||
|
// return &_ericebox_{{.Symname}} - _bricebox_{{.Symname}};
|
||||||
|
// }
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
"github.com/GeertJohan/go.rice/embedded"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ptr := unsafe.Pointer(&C._bricebox_{{.Symname}})
|
||||||
|
bts := C.GoBytes(ptr, C.get_{{.Symname}}_length())
|
||||||
|
embeddedBox := &embedded.EmbeddedBox{}
|
||||||
|
err := gob.NewDecoder(bytes.NewReader(bts)).Decode(embeddedBox)
|
||||||
|
if err != nil {
|
||||||
|
panic("error decoding embedded box: "+err.Error())
|
||||||
|
}
|
||||||
|
embeddedBox.Link()
|
||||||
|
embedded.RegisterEmbeddedBox(embeddedBox.Name, embeddedBox)
|
||||||
|
}`)
|
||||||
|
if err != nil {
|
||||||
|
panic("could not parse template embeddedSysoHelper: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type embeddedSysoHelperData struct {
|
||||||
|
Package string
|
||||||
|
Symname string
|
||||||
|
}
|
||||||
|
|
||||||
|
func operationEmbedSyso(pkg *build.Package) {
|
||||||
|
|
||||||
|
regexpSynameReplacer := regexp.MustCompile(`[^a-z0-9_]`)
|
||||||
|
|
||||||
|
boxMap := findBoxes(pkg)
|
||||||
|
|
||||||
|
// notify user when no calls to rice.FindBox are made (is this an error and therefore os.Exit(1) ?
|
||||||
|
if len(boxMap) == 0 {
|
||||||
|
fmt.Println("no calls to rice.FindBox() found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
verbosef("\n")
|
||||||
|
|
||||||
|
for boxname := range boxMap {
|
||||||
|
// find path and filename for this box
|
||||||
|
boxPath := filepath.Join(pkg.Dir, boxname)
|
||||||
|
boxFilename := strings.Replace(boxname, "/", "-", -1)
|
||||||
|
boxFilename = strings.Replace(boxFilename, "..", "back", -1)
|
||||||
|
boxFilename = strings.Replace(boxFilename, ".", "-", -1)
|
||||||
|
|
||||||
|
// verbose info
|
||||||
|
verbosef("embedding box '%s'\n", boxname)
|
||||||
|
verbosef("\tto file %s\n", boxFilename)
|
||||||
|
|
||||||
|
// read box metadata
|
||||||
|
boxInfo, ierr := os.Stat(boxPath)
|
||||||
|
if ierr != nil {
|
||||||
|
fmt.Printf("Error: unable to access box at %s\n", boxPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create box datastructure (used by template)
|
||||||
|
box := &embedded.EmbeddedBox{
|
||||||
|
Name: boxname,
|
||||||
|
Time: boxInfo.ModTime(),
|
||||||
|
EmbedType: embedded.EmbedTypeSyso,
|
||||||
|
Files: make(map[string]*embedded.EmbeddedFile),
|
||||||
|
Dirs: make(map[string]*embedded.EmbeddedDir),
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill box datastructure with file data
|
||||||
|
filepath.Walk(boxPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error walking box: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := strings.TrimPrefix(path, boxPath)
|
||||||
|
filename = strings.Replace(filename, "\\", "/", -1)
|
||||||
|
filename = strings.TrimPrefix(filename, "/")
|
||||||
|
if info.IsDir() {
|
||||||
|
embeddedDir := &embedded.EmbeddedDir{
|
||||||
|
Filename: filename,
|
||||||
|
DirModTime: info.ModTime(),
|
||||||
|
}
|
||||||
|
verbosef("\tincludes dir: '%s'\n", embeddedDir.Filename)
|
||||||
|
box.Dirs[embeddedDir.Filename] = embeddedDir
|
||||||
|
|
||||||
|
// add tree entry (skip for root, it'll create a recursion)
|
||||||
|
if embeddedDir.Filename != "" {
|
||||||
|
pathParts := strings.Split(embeddedDir.Filename, "/")
|
||||||
|
parentDir := box.Dirs[strings.Join(pathParts[:len(pathParts)-1], "/")]
|
||||||
|
parentDir.ChildDirs = append(parentDir.ChildDirs, embeddedDir)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
embeddedFile := &embedded.EmbeddedFile{
|
||||||
|
Filename: filename,
|
||||||
|
FileModTime: info.ModTime(),
|
||||||
|
Content: "",
|
||||||
|
}
|
||||||
|
verbosef("\tincludes file: '%s'\n", embeddedFile.Filename)
|
||||||
|
contentBytes, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error reading file content while walking box: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
embeddedFile.Content = string(contentBytes)
|
||||||
|
box.Files[embeddedFile.Filename] = embeddedFile
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// encode embedded box to gob file
|
||||||
|
boxGobBuf := &bytes.Buffer{}
|
||||||
|
err := gob.NewEncoder(boxGobBuf).Encode(box)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error encoding box to gob: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
verbosef("gob-encoded embeddedBox is %d bytes large\n", boxGobBuf.Len())
|
||||||
|
|
||||||
|
// write coff
|
||||||
|
symname := regexpSynameReplacer.ReplaceAllString(boxname, "_")
|
||||||
|
createCoffSyso(boxname, symname, "386", boxGobBuf.Bytes())
|
||||||
|
createCoffSyso(boxname, symname, "amd64", boxGobBuf.Bytes())
|
||||||
|
|
||||||
|
// write go
|
||||||
|
sysoHelperData := embeddedSysoHelperData{
|
||||||
|
Package: pkg.Name,
|
||||||
|
Symname: symname,
|
||||||
|
}
|
||||||
|
fileSysoHelper, err := os.Create(boxFilename + ".rice-box.go")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error creating syso helper: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err = tmplEmbeddedSysoHelper.Execute(fileSysoHelper, sysoHelperData)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error executing tmplEmbeddedSysoHelper: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createCoffSyso(boxFilename string, symname string, arch string, data []byte) {
|
||||||
|
boxCoff := coff.NewRDATA()
|
||||||
|
switch arch {
|
||||||
|
case "386":
|
||||||
|
case "amd64":
|
||||||
|
boxCoff.FileHeader.Machine = 0x8664
|
||||||
|
default:
|
||||||
|
panic("invalid arch")
|
||||||
|
}
|
||||||
|
boxCoff.AddData("_bricebox_"+symname, sizedReader{bytes.NewReader(data)})
|
||||||
|
boxCoff.AddData("_ericebox_"+symname, io.NewSectionReader(strings.NewReader("\000\000"), 0, 2)) // TODO: why? copied from rsrc, which copied it from as-generated
|
||||||
|
boxCoff.Freeze()
|
||||||
|
err := writeCoff(boxCoff, boxFilename+"_"+arch+".rice-box.syso")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error writing %s coff/.syso: %v\n", arch, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
150
vendor/github.com/GeertJohan/go.rice/rice/find.go
generated
vendored
Normal file
150
vendor/github.com/GeertJohan/go.rice/rice/find.go
generated
vendored
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
"go/build"
|
||||||
|
"go/parser"
|
||||||
|
"go/token"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func badArgument(fileset *token.FileSet, p token.Pos) {
|
||||||
|
pos := fileset.Position(p)
|
||||||
|
filename := pos.Filename
|
||||||
|
base, err := os.Getwd()
|
||||||
|
if err == nil {
|
||||||
|
rpath, perr := filepath.Rel(base, pos.Filename)
|
||||||
|
if perr == nil {
|
||||||
|
filename = rpath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg := fmt.Sprintf("%s:%d: Error: found call to rice.FindBox, "+
|
||||||
|
"but argument must be a string literal.\n", filename, pos.Line)
|
||||||
|
fmt.Println(msg)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findBoxes(pkg *build.Package) map[string]bool {
|
||||||
|
// create map of boxes to embed
|
||||||
|
var boxMap = make(map[string]bool)
|
||||||
|
|
||||||
|
// create one list of files for this package
|
||||||
|
filenames := make([]string, 0, len(pkg.GoFiles)+len(pkg.CgoFiles))
|
||||||
|
filenames = append(filenames, pkg.GoFiles...)
|
||||||
|
filenames = append(filenames, pkg.CgoFiles...)
|
||||||
|
|
||||||
|
// loop over files, search for rice.FindBox(..) calls
|
||||||
|
for _, filename := range filenames {
|
||||||
|
// find full filepath
|
||||||
|
fullpath := filepath.Join(pkg.Dir, filename)
|
||||||
|
if strings.HasSuffix(filename, "rice-box.go") {
|
||||||
|
// Ignore *.rice-box.go files
|
||||||
|
verbosef("skipping file %q\n", fullpath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
verbosef("scanning file %q\n", fullpath)
|
||||||
|
|
||||||
|
fset := token.NewFileSet()
|
||||||
|
f, err := parser.ParseFile(fset, fullpath, nil, 0)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var riceIsImported bool
|
||||||
|
ricePkgName := "rice"
|
||||||
|
for _, imp := range f.Imports {
|
||||||
|
if strings.HasSuffix(imp.Path.Value, "go.rice\"") {
|
||||||
|
if imp.Name != nil {
|
||||||
|
ricePkgName = imp.Name.Name
|
||||||
|
}
|
||||||
|
riceIsImported = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !riceIsImported {
|
||||||
|
// Rice wasn't imported, so we won't find a box.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ricePkgName == "_" {
|
||||||
|
// Rice pkg is unnamed, so we won't find a box.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inspect AST, looking for calls to (Must)?FindBox.
|
||||||
|
// First parameter of the func must be a basic literal.
|
||||||
|
// Identifiers won't be resolved.
|
||||||
|
var nextIdentIsBoxFunc bool
|
||||||
|
var nextBasicLitParamIsBoxName bool
|
||||||
|
var boxCall token.Pos
|
||||||
|
var variableToRemember string
|
||||||
|
var validVariablesForBoxes map[string]bool = make(map[string]bool)
|
||||||
|
|
||||||
|
ast.Inspect(f, func(node ast.Node) bool {
|
||||||
|
if node == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch x := node.(type) {
|
||||||
|
// this case fixes the var := func() style assignments, not assignments to vars declared separately from the assignment.
|
||||||
|
case *ast.AssignStmt:
|
||||||
|
var assign = node.(*ast.AssignStmt)
|
||||||
|
name, found := assign.Lhs[0].(*ast.Ident)
|
||||||
|
if found {
|
||||||
|
variableToRemember = name.Name
|
||||||
|
composite, first := assign.Rhs[0].(*ast.CompositeLit)
|
||||||
|
if first {
|
||||||
|
riceSelector, second := composite.Type.(*ast.SelectorExpr)
|
||||||
|
|
||||||
|
if second {
|
||||||
|
callCorrect := riceSelector.Sel.Name == "Config"
|
||||||
|
packageName, third := riceSelector.X.(*ast.Ident)
|
||||||
|
|
||||||
|
if third && callCorrect && packageName.Name == ricePkgName {
|
||||||
|
validVariablesForBoxes[name.Name] = true
|
||||||
|
verbosef("\tfound variable, saving to scan for boxes: %q\n", name.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *ast.Ident:
|
||||||
|
if nextIdentIsBoxFunc || ricePkgName == "." {
|
||||||
|
nextIdentIsBoxFunc = false
|
||||||
|
if x.Name == "FindBox" || x.Name == "MustFindBox" {
|
||||||
|
nextBasicLitParamIsBoxName = true
|
||||||
|
boxCall = x.Pos()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if x.Name == ricePkgName || validVariablesForBoxes[x.Name] {
|
||||||
|
nextIdentIsBoxFunc = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *ast.BasicLit:
|
||||||
|
if nextBasicLitParamIsBoxName {
|
||||||
|
if x.Kind == token.STRING {
|
||||||
|
nextBasicLitParamIsBoxName = false
|
||||||
|
// trim "" or ``
|
||||||
|
name := x.Value[1 : len(x.Value)-1]
|
||||||
|
boxMap[name] = true
|
||||||
|
verbosef("\tfound box %q\n", name)
|
||||||
|
} else {
|
||||||
|
badArgument(fset, boxCall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if nextIdentIsBoxFunc {
|
||||||
|
nextIdentIsBoxFunc = false
|
||||||
|
}
|
||||||
|
if nextBasicLitParamIsBoxName {
|
||||||
|
badArgument(fset, boxCall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return boxMap
|
||||||
|
}
|
80
vendor/github.com/GeertJohan/go.rice/rice/flags.go
generated
vendored
Normal file
80
vendor/github.com/GeertJohan/go.rice/rice/flags.go
generated
vendored
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/build"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
goflags "github.com/jessevdk/go-flags" // rename import to `goflags` (file scope) so we can use `var flags` (package scope)
|
||||||
|
)
|
||||||
|
|
||||||
|
// flags
|
||||||
|
var flags struct {
|
||||||
|
Verbose bool `long:"verbose" short:"v" description:"Show verbose debug information"`
|
||||||
|
ImportPaths []string `long:"import-path" short:"i" description:"Import path(s) to use. Using PWD when left empty. Specify multiple times for more import paths to append"`
|
||||||
|
|
||||||
|
Append struct {
|
||||||
|
Executable string `long:"exec" description:"Executable to append" required:"true"`
|
||||||
|
} `command:"append"`
|
||||||
|
|
||||||
|
EmbedGo struct{} `command:"embed-go" alias:"embed"`
|
||||||
|
EmbedSyso struct{} `command:"embed-syso"`
|
||||||
|
Clean struct{} `command:"clean"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// flags parser
|
||||||
|
var flagsParser *goflags.Parser
|
||||||
|
|
||||||
|
// initFlags parses the given flags.
|
||||||
|
// when the user asks for help (-h or --help): the application exists with status 0
|
||||||
|
// when unexpected flags is given: the application exits with status 1
|
||||||
|
func parseArguments() {
|
||||||
|
// create flags parser in global var, for flagsParser.Active.Name (operation)
|
||||||
|
flagsParser = goflags.NewParser(&flags, goflags.Default)
|
||||||
|
|
||||||
|
// parse flags
|
||||||
|
args, err := flagsParser.Parse()
|
||||||
|
if err != nil {
|
||||||
|
// assert the err to be a flags.Error
|
||||||
|
flagError := err.(*goflags.Error)
|
||||||
|
if flagError.Type == goflags.ErrHelp {
|
||||||
|
// user asked for help on flags.
|
||||||
|
// program can exit successfully
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
if flagError.Type == goflags.ErrUnknownFlag {
|
||||||
|
fmt.Println("Use --help to view available options.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if flagError.Type == goflags.ErrRequired {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("Error parsing flags: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// error on left-over arguments
|
||||||
|
if len(args) > 0 {
|
||||||
|
fmt.Printf("Unexpected arguments: %s\nUse --help to view available options.", args)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// default ImportPath to pwd when not set
|
||||||
|
if len(flags.ImportPaths) == 0 {
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error getting pwd: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
verbosef("using pwd as import path\n")
|
||||||
|
// find non-absolute path for this pwd
|
||||||
|
pkg, err := build.ImportDir(pwd, build.FindOnly)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error using current directory as import path: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
flags.ImportPaths = append(flags.ImportPaths, pkg.ImportPath)
|
||||||
|
verbosef("using import paths: %s\n", flags.ImportPaths)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
14
vendor/github.com/GeertJohan/go.rice/rice/identifier.go
generated
vendored
Normal file
14
vendor/github.com/GeertJohan/go.rice/rice/identifier.go
generated
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/GeertJohan/go.incremental"
|
||||||
|
)
|
||||||
|
|
||||||
|
var identifierCount incremental.Uint64
|
||||||
|
|
||||||
|
func nextIdentifier() string {
|
||||||
|
num := identifierCount.Next()
|
||||||
|
return strconv.FormatUint(num, 36) // 0123456789abcdefghijklmnopqrstuvwxyz
|
||||||
|
}
|
68
vendor/github.com/GeertJohan/go.rice/rice/main.go
generated
vendored
Normal file
68
vendor/github.com/GeertJohan/go.rice/rice/main.go
generated
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/build"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// parser arguments
|
||||||
|
parseArguments()
|
||||||
|
|
||||||
|
// find package for path
|
||||||
|
var pkgs []*build.Package
|
||||||
|
for _, importPath := range flags.ImportPaths {
|
||||||
|
pkg := pkgForPath(importPath)
|
||||||
|
pkgs = append(pkgs, pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// switch on the operation to perform
|
||||||
|
switch flagsParser.Active.Name {
|
||||||
|
case "embed", "embed-go":
|
||||||
|
for _, pkg := range pkgs {
|
||||||
|
operationEmbedGo(pkg)
|
||||||
|
}
|
||||||
|
case "embed-syso":
|
||||||
|
log.Println("WARNING: embedding .syso is experimental..")
|
||||||
|
for _, pkg := range pkgs {
|
||||||
|
operationEmbedSyso(pkg)
|
||||||
|
}
|
||||||
|
case "append":
|
||||||
|
operationAppend(pkgs)
|
||||||
|
case "clean":
|
||||||
|
for _, pkg := range pkgs {
|
||||||
|
operationClean(pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// all done
|
||||||
|
verbosef("\n")
|
||||||
|
verbosef("rice finished successfully\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to get *build.Package for given path
|
||||||
|
func pkgForPath(path string) *build.Package {
|
||||||
|
// get pwd for relative imports
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error getting pwd (required for relative imports): %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// read full package information
|
||||||
|
pkg, err := build.Import(path, pwd, 0)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error reading package: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkg
|
||||||
|
}
|
||||||
|
|
||||||
|
func verbosef(format string, stuff ...interface{}) {
|
||||||
|
if flags.Verbose {
|
||||||
|
log.Printf(format, stuff...)
|
||||||
|
}
|
||||||
|
}
|
98
vendor/github.com/GeertJohan/go.rice/rice/templates.go
generated
vendored
Normal file
98
vendor/github.com/GeertJohan/go.rice/rice/templates.go
generated
vendored
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tmplEmbeddedBox *template.Template
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// parse embedded box template
|
||||||
|
tmplEmbeddedBox, err = template.New("embeddedBox").Parse(`package {{.Package}}
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/GeertJohan/go.rice/embedded"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
{{range .Boxes}}
|
||||||
|
func init() {
|
||||||
|
|
||||||
|
// define files
|
||||||
|
{{range .Files}}{{.Identifier}} := &embedded.EmbeddedFile{
|
||||||
|
Filename: ` + "`" + `{{.FileName}}` + "`" + `,
|
||||||
|
FileModTime: time.Unix({{.ModTime}}, 0),
|
||||||
|
Content: string({{.Content | printf "%q"}}),
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
// define dirs
|
||||||
|
{{range .Dirs}}{{.Identifier}} := &embedded.EmbeddedDir{
|
||||||
|
Filename: ` + "`" + `{{.FileName}}` + "`" + `,
|
||||||
|
DirModTime: time.Unix({{.ModTime}}, 0),
|
||||||
|
ChildFiles: []*embedded.EmbeddedFile{
|
||||||
|
{{range .ChildFiles}}{{.Identifier}}, // {{.FileName}}
|
||||||
|
{{end}}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
// link ChildDirs
|
||||||
|
{{range .Dirs}}{{.Identifier}}.ChildDirs = []*embedded.EmbeddedDir{
|
||||||
|
{{range .ChildDirs}}{{.Identifier}}, // {{.FileName}}
|
||||||
|
{{end}}
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
// register embeddedBox
|
||||||
|
embedded.RegisterEmbeddedBox(` + "`" + `{{.BoxName}}` + "`" + `, &embedded.EmbeddedBox{
|
||||||
|
Name: ` + "`" + `{{.BoxName}}` + "`" + `,
|
||||||
|
Time: time.Unix({{.UnixNow}}, 0),
|
||||||
|
Dirs: map[string]*embedded.EmbeddedDir{
|
||||||
|
{{range .Dirs}}"{{.FileName}}": {{.Identifier}},
|
||||||
|
{{end}}
|
||||||
|
},
|
||||||
|
Files: map[string]*embedded.EmbeddedFile{
|
||||||
|
{{range .Files}}"{{.FileName}}": {{.Identifier}},
|
||||||
|
{{end}}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
{{end}}`)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error parsing embedded box template: %s\n", err)
|
||||||
|
os.Exit(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type embedFileDataType struct {
|
||||||
|
Package string
|
||||||
|
Boxes []*boxDataType
|
||||||
|
}
|
||||||
|
|
||||||
|
type boxDataType struct {
|
||||||
|
BoxName string
|
||||||
|
UnixNow int64
|
||||||
|
Files []*fileDataType
|
||||||
|
Dirs map[string]*dirDataType
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileDataType struct {
|
||||||
|
Identifier string
|
||||||
|
FileName string
|
||||||
|
Content []byte
|
||||||
|
ModTime int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type dirDataType struct {
|
||||||
|
Identifier string
|
||||||
|
FileName string
|
||||||
|
Content []byte
|
||||||
|
ModTime int64
|
||||||
|
ChildDirs []*dirDataType
|
||||||
|
ChildFiles []*fileDataType
|
||||||
|
}
|
22
vendor/github.com/GeertJohan/go.rice/rice/util.go
generated
vendored
Normal file
22
vendor/github.com/GeertJohan/go.rice/rice/util.go
generated
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// randomString generates a pseudo-random alpha-numeric string with given length.
|
||||||
|
func randomString(length int) string {
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
k := make([]rune, length)
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
c := rand.Intn(35)
|
||||||
|
if c < 10 {
|
||||||
|
c += 48 // numbers (0-9) (0+48 == 48 == '0', 9+48 == 57 == '9')
|
||||||
|
} else {
|
||||||
|
c += 87 // lower case alphabets (a-z) (10+87 == 97 == 'a', 35+87 == 122 = 'z')
|
||||||
|
}
|
||||||
|
k[i] = rune(c)
|
||||||
|
}
|
||||||
|
return string(k)
|
||||||
|
}
|
42
vendor/github.com/GeertJohan/go.rice/rice/writecoff.go
generated
vendored
Normal file
42
vendor/github.com/GeertJohan/go.rice/rice/writecoff.go
generated
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/akavel/rsrc/binutil"
|
||||||
|
"github.com/akavel/rsrc/coff"
|
||||||
|
)
|
||||||
|
|
||||||
|
// copied from github.com/akavel/rsrc
|
||||||
|
// LICENSE: MIT
|
||||||
|
// Copyright 2013-2014 The rsrc Authors. (https://github.com/akavel/rsrc/blob/master/AUTHORS)
|
||||||
|
func writeCoff(coff *coff.Coff, fnameout string) error {
|
||||||
|
out, err := os.Create(fnameout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
w := binutil.Writer{W: out}
|
||||||
|
|
||||||
|
// write the resulting file to disk
|
||||||
|
binutil.Walk(coff, func(v reflect.Value, path string) error {
|
||||||
|
if binutil.Plain(v.Kind()) {
|
||||||
|
w.WriteLE(v.Interface())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
vv, ok := v.Interface().(binutil.SizedReader)
|
||||||
|
if ok {
|
||||||
|
w.WriteFromSized(vv)
|
||||||
|
return binutil.WALK_SKIP
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if w.Err != nil {
|
||||||
|
return fmt.Errorf("Error writing output file: %s", w.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
19
vendor/github.com/GeertJohan/go.rice/sort.go
generated
vendored
Normal file
19
vendor/github.com/GeertJohan/go.rice/sort.go
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package rice
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
// SortByName allows an array of os.FileInfo objects
|
||||||
|
// to be easily sorted by filename using sort.Sort(SortByName(array))
|
||||||
|
type SortByName []os.FileInfo
|
||||||
|
|
||||||
|
func (f SortByName) Len() int { return len(f) }
|
||||||
|
func (f SortByName) Less(i, j int) bool { return f[i].Name() < f[j].Name() }
|
||||||
|
func (f SortByName) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
|
||||||
|
|
||||||
|
// SortByModified allows an array of os.FileInfo objects
|
||||||
|
// to be easily sorted by modified date using sort.Sort(SortByModified(array))
|
||||||
|
type SortByModified []os.FileInfo
|
||||||
|
|
||||||
|
func (f SortByModified) Len() int { return len(f) }
|
||||||
|
func (f SortByModified) Less(i, j int) bool { return f[i].ModTime().Unix() > f[j].ModTime().Unix() }
|
||||||
|
func (f SortByModified) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
|
252
vendor/github.com/GeertJohan/go.rice/virtual.go
generated
vendored
Normal file
252
vendor/github.com/GeertJohan/go.rice/virtual.go
generated
vendored
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
package rice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/GeertJohan/go.rice/embedded"
|
||||||
|
)
|
||||||
|
|
||||||
|
//++ TODO: IDEA: merge virtualFile and virtualDir, this decreases work done by rice.File
|
||||||
|
|
||||||
|
// Error indicating some function is not implemented yet (but available to satisfy an interface)
|
||||||
|
var ErrNotImplemented = errors.New("not implemented yet")
|
||||||
|
|
||||||
|
// virtualFile is a 'stateful' virtual file.
|
||||||
|
// virtualFile wraps an *EmbeddedFile for a call to Box.Open() and virtualizes 'read cursor' (offset) and 'closing'.
|
||||||
|
// virtualFile is only internally visible and should be exposed through rice.File
|
||||||
|
type virtualFile struct {
|
||||||
|
*embedded.EmbeddedFile // the actual embedded file, embedded to obtain methods
|
||||||
|
offset int64 // read position on the virtual file
|
||||||
|
closed bool // closed when true
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new virtualFile for given EmbeddedFile
|
||||||
|
func newVirtualFile(ef *embedded.EmbeddedFile) *virtualFile {
|
||||||
|
vf := &virtualFile{
|
||||||
|
EmbeddedFile: ef,
|
||||||
|
offset: 0,
|
||||||
|
closed: false,
|
||||||
|
}
|
||||||
|
return vf
|
||||||
|
}
|
||||||
|
|
||||||
|
//++ TODO check for nil pointers in all these methods. When so: return os.PathError with Err: os.ErrInvalid
|
||||||
|
|
||||||
|
func (vf *virtualFile) close() error {
|
||||||
|
if vf.closed {
|
||||||
|
return &os.PathError{
|
||||||
|
Op: "close",
|
||||||
|
Path: vf.EmbeddedFile.Filename,
|
||||||
|
Err: errors.New("already closed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vf.EmbeddedFile = nil
|
||||||
|
vf.closed = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vf *virtualFile) stat() (os.FileInfo, error) {
|
||||||
|
if vf.closed {
|
||||||
|
return nil, &os.PathError{
|
||||||
|
Op: "stat",
|
||||||
|
Path: vf.EmbeddedFile.Filename,
|
||||||
|
Err: errors.New("bad file descriptor"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (*embeddedFileInfo)(vf.EmbeddedFile), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vf *virtualFile) readdir(count int) ([]os.FileInfo, error) {
|
||||||
|
if vf.closed {
|
||||||
|
return nil, &os.PathError{
|
||||||
|
Op: "readdir",
|
||||||
|
Path: vf.EmbeddedFile.Filename,
|
||||||
|
Err: errors.New("bad file descriptor"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//TODO: return proper error for a readdir() call on a file
|
||||||
|
return nil, ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vf *virtualFile) read(bts []byte) (int, error) {
|
||||||
|
if vf.closed {
|
||||||
|
return 0, &os.PathError{
|
||||||
|
Op: "read",
|
||||||
|
Path: vf.EmbeddedFile.Filename,
|
||||||
|
Err: errors.New("bad file descriptor"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
end := vf.offset + int64(len(bts))
|
||||||
|
|
||||||
|
if end >= int64(len(vf.Content)) {
|
||||||
|
// end of file, so return what we have + EOF
|
||||||
|
n := copy(bts, vf.Content[vf.offset:])
|
||||||
|
vf.offset = 0
|
||||||
|
return n, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
n := copy(bts, vf.Content[vf.offset:end])
|
||||||
|
vf.offset += int64(n)
|
||||||
|
return n, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vf *virtualFile) seek(offset int64, whence int) (int64, error) {
|
||||||
|
if vf.closed {
|
||||||
|
return 0, &os.PathError{
|
||||||
|
Op: "seek",
|
||||||
|
Path: vf.EmbeddedFile.Filename,
|
||||||
|
Err: errors.New("bad file descriptor"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var e error
|
||||||
|
|
||||||
|
//++ TODO: check if this is correct implementation for seek
|
||||||
|
switch whence {
|
||||||
|
case os.SEEK_SET:
|
||||||
|
//++ check if new offset isn't out of bounds, set e when it is, then break out of switch
|
||||||
|
vf.offset = offset
|
||||||
|
case os.SEEK_CUR:
|
||||||
|
//++ check if new offset isn't out of bounds, set e when it is, then break out of switch
|
||||||
|
vf.offset += offset
|
||||||
|
case os.SEEK_END:
|
||||||
|
//++ check if new offset isn't out of bounds, set e when it is, then break out of switch
|
||||||
|
vf.offset = int64(len(vf.EmbeddedFile.Content)) - offset
|
||||||
|
}
|
||||||
|
|
||||||
|
if e != nil {
|
||||||
|
return 0, &os.PathError{
|
||||||
|
Op: "seek",
|
||||||
|
Path: vf.Filename,
|
||||||
|
Err: e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return vf.offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// virtualDir is a 'stateful' virtual directory.
|
||||||
|
// virtualDir wraps an *EmbeddedDir for a call to Box.Open() and virtualizes 'closing'.
|
||||||
|
// virtualDir is only internally visible and should be exposed through rice.File
|
||||||
|
type virtualDir struct {
|
||||||
|
*embedded.EmbeddedDir
|
||||||
|
offset int // readdir position on the directory
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new virtualDir for given EmbeddedDir
|
||||||
|
func newVirtualDir(ed *embedded.EmbeddedDir) *virtualDir {
|
||||||
|
vd := &virtualDir{
|
||||||
|
EmbeddedDir: ed,
|
||||||
|
offset: 0,
|
||||||
|
closed: false,
|
||||||
|
}
|
||||||
|
return vd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vd *virtualDir) close() error {
|
||||||
|
//++ TODO: needs sync mutex?
|
||||||
|
if vd.closed {
|
||||||
|
return &os.PathError{
|
||||||
|
Op: "close",
|
||||||
|
Path: vd.EmbeddedDir.Filename,
|
||||||
|
Err: errors.New("already closed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vd.closed = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vd *virtualDir) stat() (os.FileInfo, error) {
|
||||||
|
if vd.closed {
|
||||||
|
return nil, &os.PathError{
|
||||||
|
Op: "stat",
|
||||||
|
Path: vd.EmbeddedDir.Filename,
|
||||||
|
Err: errors.New("bad file descriptor"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (*embeddedDirInfo)(vd.EmbeddedDir), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vd *virtualDir) readdir(n int) (fi []os.FileInfo, err error) {
|
||||||
|
|
||||||
|
if vd.closed {
|
||||||
|
return nil, &os.PathError{
|
||||||
|
Op: "readdir",
|
||||||
|
Path: vd.EmbeddedDir.Filename,
|
||||||
|
Err: errors.New("bad file descriptor"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build up the array of our contents
|
||||||
|
var files []os.FileInfo
|
||||||
|
|
||||||
|
// Add the child directories
|
||||||
|
for _, child := range vd.ChildDirs {
|
||||||
|
child.Filename = filepath.Base(child.Filename)
|
||||||
|
files = append(files, (*embeddedDirInfo)(child))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the child files
|
||||||
|
for _, child := range vd.ChildFiles {
|
||||||
|
child.Filename = filepath.Base(child.Filename)
|
||||||
|
files = append(files, (*embeddedFileInfo)(child))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort it by filename (lexical order)
|
||||||
|
sort.Sort(SortByName(files))
|
||||||
|
|
||||||
|
// Return all contents if that's what is requested
|
||||||
|
if n <= 0 {
|
||||||
|
vd.offset = 0
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user has requested past the end of our list
|
||||||
|
// return what we can and send an EOF
|
||||||
|
if vd.offset+n >= len(files) {
|
||||||
|
offset := vd.offset
|
||||||
|
vd.offset = 0
|
||||||
|
return files[offset:], io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := vd.offset
|
||||||
|
vd.offset += n
|
||||||
|
return files[offset : offset+n], nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vd *virtualDir) read(bts []byte) (int, error) {
|
||||||
|
if vd.closed {
|
||||||
|
return 0, &os.PathError{
|
||||||
|
Op: "read",
|
||||||
|
Path: vd.EmbeddedDir.Filename,
|
||||||
|
Err: errors.New("bad file descriptor"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, &os.PathError{
|
||||||
|
Op: "read",
|
||||||
|
Path: vd.EmbeddedDir.Filename,
|
||||||
|
Err: errors.New("is a directory"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vd *virtualDir) seek(offset int64, whence int) (int64, error) {
|
||||||
|
if vd.closed {
|
||||||
|
return 0, &os.PathError{
|
||||||
|
Op: "seek",
|
||||||
|
Path: vd.EmbeddedDir.Filename,
|
||||||
|
Err: errors.New("bad file descriptor"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, &os.PathError{
|
||||||
|
Op: "seek",
|
||||||
|
Path: vd.Filename,
|
||||||
|
Err: errors.New("is a directory"),
|
||||||
|
}
|
||||||
|
}
|
122
vendor/github.com/GeertJohan/go.rice/walk.go
generated
vendored
Normal file
122
vendor/github.com/GeertJohan/go.rice/walk.go
generated
vendored
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package rice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Walk is like filepath.Walk()
|
||||||
|
// Visit http://golang.org/pkg/path/filepath/#Walk for more information
|
||||||
|
func (b *Box) Walk(path string, walkFn filepath.WalkFunc) error {
|
||||||
|
|
||||||
|
pathFile, err := b.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer pathFile.Close()
|
||||||
|
|
||||||
|
pathInfo, err := pathFile.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.IsAppended() || b.IsEmbedded() {
|
||||||
|
return b.walk(path, pathInfo, walkFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't have any embedded or appended box so use live filesystem mode
|
||||||
|
return filepath.Walk(b.absolutePath+string(os.PathSeparator)+path, func(path string, info os.FileInfo, err error) error {
|
||||||
|
|
||||||
|
// Strip out the box name from the returned paths
|
||||||
|
path = strings.TrimPrefix(path, b.absolutePath+string(os.PathSeparator))
|
||||||
|
return walkFn(path, info, err)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk recursively descends path.
|
||||||
|
// See walk() in $GOROOT/src/pkg/path/filepath/path.go
|
||||||
|
func (b *Box) walk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
|
||||||
|
|
||||||
|
err := walkFn(path, info, nil)
|
||||||
|
if err != nil {
|
||||||
|
if info.IsDir() && err == filepath.SkipDir {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
names, err := b.readDirNames(path)
|
||||||
|
if err != nil {
|
||||||
|
return walkFn(path, info, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
|
||||||
|
filename := filepath.Join(path, name)
|
||||||
|
fileObject, err := b.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fileObject.Close()
|
||||||
|
|
||||||
|
fileInfo, err := fileObject.Stat()
|
||||||
|
if err != nil {
|
||||||
|
if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = b.walk(filename, fileInfo, walkFn)
|
||||||
|
if err != nil {
|
||||||
|
if !fileInfo.IsDir() || err != filepath.SkipDir {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// readDirNames reads the directory named by path and returns a sorted list of directory entries.
|
||||||
|
// See readDirNames() in $GOROOT/pkg/path/filepath/path.go
|
||||||
|
func (b *Box) readDirNames(path string) ([]string, error) {
|
||||||
|
|
||||||
|
f, err := b.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
stat, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stat.IsDir() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
infos, err := f.Readdir(0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var names []string
|
||||||
|
|
||||||
|
for _, info := range infos {
|
||||||
|
names = append(names, info.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(names)
|
||||||
|
return names, nil
|
||||||
|
|
||||||
|
}
|
26
vendor/github.com/Philipp15b/go-steam/LICENSE.txt
generated
vendored
Normal file
26
vendor/github.com/Philipp15b/go-steam/LICENSE.txt
generated
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
Copyright (c) 2014 The go-steam 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.
|
||||||
|
* The names of its contributors may not 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.
|
178
vendor/github.com/Philipp15b/go-steam/auth.go
generated
vendored
Normal file
178
vendor/github.com/Philipp15b/go-steam/auth.go
generated
vendored
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
package steam
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
. "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 {
|
||||||
|
client *Client
|
||||||
|
details *LogOnDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
type SentryHash []byte
|
||||||
|
|
||||||
|
type LogOnDetails struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
AuthCode string
|
||||||
|
TwoFactorCode string
|
||||||
|
SentryFileHash SentryHash
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// 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.
|
||||||
|
func (a *Auth) LogOn(details *LogOnDetails) {
|
||||||
|
if len(details.Username) == 0 || len(details.Password) == 0 {
|
||||||
|
panic("Username and password must be set!")
|
||||||
|
}
|
||||||
|
|
||||||
|
logon := new(CMsgClientLogon)
|
||||||
|
logon.AccountName = &details.Username
|
||||||
|
logon.Password = &details.Password
|
||||||
|
if details.AuthCode != "" {
|
||||||
|
logon.AuthCode = proto.String(details.AuthCode)
|
||||||
|
}
|
||||||
|
if details.TwoFactorCode != "" {
|
||||||
|
logon.TwoFactorCode = proto.String(details.TwoFactorCode)
|
||||||
|
}
|
||||||
|
logon.ClientLanguage = proto.String("english")
|
||||||
|
logon.ProtocolVersion = proto.Uint32(MsgClientLogon_CurrentProtocol)
|
||||||
|
logon.ShaSentryfile = details.SentryFileHash
|
||||||
|
|
||||||
|
atomic.StoreUint64(&a.client.steamId, uint64(NewIdAdv(0, 1, int32(EUniverse_Public), int32(EAccountType_Individual))))
|
||||||
|
|
||||||
|
a.client.Write(NewClientMsgProtobuf(EMsg_ClientLogon, logon))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) HandlePacket(packet *Packet) {
|
||||||
|
switch packet.EMsg {
|
||||||
|
case EMsg_ClientLogOnResponse:
|
||||||
|
a.handleLogOnResponse(packet)
|
||||||
|
case EMsg_ClientNewLoginKey:
|
||||||
|
a.handleLoginKey(packet)
|
||||||
|
case EMsg_ClientSessionToken:
|
||||||
|
case EMsg_ClientLoggedOff:
|
||||||
|
a.handleLoggedOff(packet)
|
||||||
|
case EMsg_ClientUpdateMachineAuth:
|
||||||
|
a.handleUpdateMachineAuth(packet)
|
||||||
|
case EMsg_ClientAccountInfo:
|
||||||
|
a.handleAccountInfo(packet)
|
||||||
|
case EMsg_ClientWalletInfoUpdate:
|
||||||
|
case EMsg_ClientRequestWebAPIAuthenticateUserNonceResponse:
|
||||||
|
case EMsg_ClientMarketingMessageUpdate:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) handleLogOnResponse(packet *Packet) {
|
||||||
|
if !packet.IsProto {
|
||||||
|
a.client.Fatalf("Got non-proto logon response!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := new(CMsgClientLogonResponse)
|
||||||
|
msg := packet.ReadProtoMsg(body)
|
||||||
|
|
||||||
|
result := EResult(body.GetEresult())
|
||||||
|
if result == EResult_OK {
|
||||||
|
atomic.StoreInt32(&a.client.sessionId, msg.Header.Proto.GetClientSessionid())
|
||||||
|
atomic.StoreUint64(&a.client.steamId, msg.Header.Proto.GetSteamid())
|
||||||
|
a.client.Web.webLoginKey = *body.WebapiAuthenticateUserNonce
|
||||||
|
|
||||||
|
go a.client.heartbeatLoop(time.Duration(body.GetOutOfGameHeartbeatSeconds()))
|
||||||
|
|
||||||
|
a.client.Emit(&LoggedOnEvent{
|
||||||
|
Result: EResult(body.GetEresult()),
|
||||||
|
ExtendedResult: EResult(body.GetEresultExtended()),
|
||||||
|
OutOfGameSecsPerHeartbeat: body.GetOutOfGameHeartbeatSeconds(),
|
||||||
|
InGameSecsPerHeartbeat: body.GetInGameHeartbeatSeconds(),
|
||||||
|
PublicIp: body.GetPublicIp(),
|
||||||
|
ServerTime: body.GetRtime32ServerTime(),
|
||||||
|
AccountFlags: EAccountFlags(body.GetAccountFlags()),
|
||||||
|
ClientSteamId: SteamId(body.GetClientSuppliedSteamid()),
|
||||||
|
EmailDomain: body.GetEmailDomain(),
|
||||||
|
CellId: body.GetCellId(),
|
||||||
|
CellIdPingThreshold: body.GetCellIdPingThreshold(),
|
||||||
|
Steam2Ticket: body.GetSteam2Ticket(),
|
||||||
|
UsePics: body.GetUsePics(),
|
||||||
|
WebApiUserNonce: body.GetWebapiAuthenticateUserNonce(),
|
||||||
|
IpCountryCode: body.GetIpCountryCode(),
|
||||||
|
VanityUrl: body.GetVanityUrl(),
|
||||||
|
NumLoginFailuresToMigrate: body.GetCountLoginfailuresToMigrate(),
|
||||||
|
NumDisconnectsToMigrate: body.GetCountDisconnectsToMigrate(),
|
||||||
|
})
|
||||||
|
} else if result == EResult_Fail || result == EResult_ServiceUnavailable || result == EResult_TryAnotherCM {
|
||||||
|
// some error on Steam's side, we'll get an EOF later
|
||||||
|
} else {
|
||||||
|
a.client.Emit(&LogOnFailedEvent{
|
||||||
|
Result: EResult(body.GetEresult()),
|
||||||
|
})
|
||||||
|
a.client.Disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) handleLoginKey(packet *Packet) {
|
||||||
|
body := new(CMsgClientNewLoginKey)
|
||||||
|
packet.ReadProtoMsg(body)
|
||||||
|
a.client.Write(NewClientMsgProtobuf(EMsg_ClientNewLoginKeyAccepted, &CMsgClientNewLoginKeyAccepted{
|
||||||
|
UniqueId: proto.Uint32(body.GetUniqueId()),
|
||||||
|
}))
|
||||||
|
a.client.Emit(&LoginKeyEvent{
|
||||||
|
UniqueId: body.GetUniqueId(),
|
||||||
|
LoginKey: body.GetLoginKey(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) handleLoggedOff(packet *Packet) {
|
||||||
|
result := EResult_Invalid
|
||||||
|
if packet.IsProto {
|
||||||
|
body := new(CMsgClientLoggedOff)
|
||||||
|
packet.ReadProtoMsg(body)
|
||||||
|
result = EResult(body.GetEresult())
|
||||||
|
} else {
|
||||||
|
body := new(MsgClientLoggedOff)
|
||||||
|
packet.ReadClientMsg(body)
|
||||||
|
result = body.Result
|
||||||
|
}
|
||||||
|
a.client.Emit(&LoggedOffEvent{Result: result})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) handleUpdateMachineAuth(packet *Packet) {
|
||||||
|
body := new(CMsgClientUpdateMachineAuth)
|
||||||
|
packet.ReadProtoMsg(body)
|
||||||
|
hash := sha1.New()
|
||||||
|
hash.Write(packet.Data)
|
||||||
|
sha := hash.Sum(nil)
|
||||||
|
|
||||||
|
msg := NewClientMsgProtobuf(EMsg_ClientUpdateMachineAuthResponse, &CMsgClientUpdateMachineAuthResponse{
|
||||||
|
ShaFile: sha,
|
||||||
|
})
|
||||||
|
msg.SetTargetJobId(packet.SourceJobId)
|
||||||
|
a.client.Write(msg)
|
||||||
|
|
||||||
|
a.client.Emit(&MachineAuthUpdateEvent{sha})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) handleAccountInfo(packet *Packet) {
|
||||||
|
body := new(CMsgClientAccountInfo)
|
||||||
|
packet.ReadProtoMsg(body)
|
||||||
|
a.client.Emit(&AccountInfoEvent{
|
||||||
|
PersonaName: body.GetPersonaName(),
|
||||||
|
Country: body.GetIpCountry(),
|
||||||
|
CountAuthedComputers: body.GetCountAuthedComputers(),
|
||||||
|
AccountFlags: EAccountFlags(body.GetAccountFlags()),
|
||||||
|
FacebookId: body.GetFacebookId(),
|
||||||
|
FacebookName: body.GetFacebookName(),
|
||||||
|
})
|
||||||
|
}
|
53
vendor/github.com/Philipp15b/go-steam/auth_events.go
generated
vendored
Normal file
53
vendor/github.com/Philipp15b/go-steam/auth_events.go
generated
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package steam
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/Philipp15b/go-steam/protocol/steamlang"
|
||||||
|
. "github.com/Philipp15b/go-steam/steamid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoggedOnEvent struct {
|
||||||
|
Result EResult
|
||||||
|
ExtendedResult EResult
|
||||||
|
OutOfGameSecsPerHeartbeat int32
|
||||||
|
InGameSecsPerHeartbeat int32
|
||||||
|
PublicIp uint32
|
||||||
|
ServerTime uint32
|
||||||
|
AccountFlags EAccountFlags
|
||||||
|
ClientSteamId SteamId `json:",string"`
|
||||||
|
EmailDomain string
|
||||||
|
CellId uint32
|
||||||
|
CellIdPingThreshold uint32
|
||||||
|
Steam2Ticket []byte
|
||||||
|
UsePics bool
|
||||||
|
WebApiUserNonce string
|
||||||
|
IpCountryCode string
|
||||||
|
VanityUrl string
|
||||||
|
NumLoginFailuresToMigrate int32
|
||||||
|
NumDisconnectsToMigrate int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogOnFailedEvent struct {
|
||||||
|
Result EResult
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginKeyEvent struct {
|
||||||
|
UniqueId uint32
|
||||||
|
LoginKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoggedOffEvent struct {
|
||||||
|
Result EResult
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineAuthUpdateEvent struct {
|
||||||
|
Hash []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccountInfoEvent struct {
|
||||||
|
PersonaName string
|
||||||
|
Country string
|
||||||
|
CountAuthedComputers int32
|
||||||
|
AccountFlags EAccountFlags
|
||||||
|
FacebookId uint64 `json:",string"`
|
||||||
|
FacebookName string
|
||||||
|
}
|
383
vendor/github.com/Philipp15b/go-steam/client.go
generated
vendored
Normal file
383
vendor/github.com/Philipp15b/go-steam/client.go
generated
vendored
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
package steam
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"hash/crc32"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Philipp15b/go-steam/cryptoutil"
|
||||||
|
"github.com/Philipp15b/go-steam/netutil"
|
||||||
|
. "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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Represents a client to the Steam network.
|
||||||
|
// Always poll events from the channel returned by Events() or receiving messages will stop.
|
||||||
|
// All access, unless otherwise noted, should be threadsafe.
|
||||||
|
//
|
||||||
|
// When a FatalErrorEvent is emitted, the connection is automatically closed. The same client can be used to reconnect.
|
||||||
|
// Other errors don't have any effect.
|
||||||
|
type Client struct {
|
||||||
|
// these need to be 64 bit aligned for sync/atomic on 32bit
|
||||||
|
sessionId int32
|
||||||
|
_ uint32
|
||||||
|
steamId uint64
|
||||||
|
currentJobId uint64
|
||||||
|
|
||||||
|
Auth *Auth
|
||||||
|
Social *Social
|
||||||
|
Web *Web
|
||||||
|
Notifications *Notifications
|
||||||
|
Trading *Trading
|
||||||
|
GC *GameCoordinator
|
||||||
|
|
||||||
|
events chan interface{}
|
||||||
|
handlers []PacketHandler
|
||||||
|
handlersMutex sync.RWMutex
|
||||||
|
|
||||||
|
tempSessionKey []byte
|
||||||
|
|
||||||
|
ConnectionTimeout time.Duration
|
||||||
|
|
||||||
|
mutex sync.RWMutex // guarding conn and writeChan
|
||||||
|
conn connection
|
||||||
|
writeChan chan IMsg
|
||||||
|
writeBuf *bytes.Buffer
|
||||||
|
heartbeat *time.Ticker
|
||||||
|
}
|
||||||
|
|
||||||
|
type PacketHandler interface {
|
||||||
|
HandlePacket(*Packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient() *Client {
|
||||||
|
client := &Client{
|
||||||
|
events: make(chan interface{}, 3),
|
||||||
|
writeBuf: new(bytes.Buffer),
|
||||||
|
}
|
||||||
|
client.Auth = &Auth{client: client}
|
||||||
|
client.RegisterPacketHandler(client.Auth)
|
||||||
|
client.Social = newSocial(client)
|
||||||
|
client.RegisterPacketHandler(client.Social)
|
||||||
|
client.Web = &Web{client: client}
|
||||||
|
client.RegisterPacketHandler(client.Web)
|
||||||
|
client.Notifications = newNotifications(client)
|
||||||
|
client.RegisterPacketHandler(client.Notifications)
|
||||||
|
client.Trading = &Trading{client: client}
|
||||||
|
client.RegisterPacketHandler(client.Trading)
|
||||||
|
client.GC = newGC(client)
|
||||||
|
client.RegisterPacketHandler(client.GC)
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the event channel. By convention all events are pointers, except for errors.
|
||||||
|
// It is never closed.
|
||||||
|
func (c *Client) Events() <-chan interface{} {
|
||||||
|
return c.events
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Emit(event interface{}) {
|
||||||
|
c.events <- event
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emits a FatalErrorEvent formatted with fmt.Errorf and disconnects.
|
||||||
|
func (c *Client) Fatalf(format string, a ...interface{}) {
|
||||||
|
c.Emit(FatalErrorEvent(fmt.Errorf(format, a...)))
|
||||||
|
c.Disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emits an error formatted with fmt.Errorf.
|
||||||
|
func (c *Client) Errorf(format string, a ...interface{}) {
|
||||||
|
c.Emit(fmt.Errorf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registers a PacketHandler that receives all incoming packets.
|
||||||
|
func (c *Client) RegisterPacketHandler(handler PacketHandler) {
|
||||||
|
c.handlersMutex.Lock()
|
||||||
|
defer c.handlersMutex.Unlock()
|
||||||
|
c.handlers = append(c.handlers, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetNextJobId() JobId {
|
||||||
|
return JobId(atomic.AddUint64(&c.currentJobId, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SteamId() SteamId {
|
||||||
|
return SteamId(atomic.LoadUint64(&c.steamId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SessionId() int32 {
|
||||||
|
return atomic.LoadInt32(&c.sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Connected() bool {
|
||||||
|
c.mutex.RLock()
|
||||||
|
defer c.mutex.RUnlock()
|
||||||
|
return c.conn != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connects to a random Steam server and returns its address.
|
||||||
|
// If this client is already connected, it is disconnected first.
|
||||||
|
// This method tries to use an address from the Steam Directory and falls
|
||||||
|
// back to the built-in server list if the Steam Directory can't be reached.
|
||||||
|
// If you want to connect to a specific server, use `ConnectTo`.
|
||||||
|
func (c *Client) Connect() *netutil.PortAddr {
|
||||||
|
var server *netutil.PortAddr
|
||||||
|
if steamDirectoryCache.IsInitialized() {
|
||||||
|
server = steamDirectoryCache.GetRandomCM()
|
||||||
|
} else {
|
||||||
|
server = GetRandomCM()
|
||||||
|
}
|
||||||
|
c.ConnectTo(server)
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connects to a specific server.
|
||||||
|
// You may want to use one of the `GetRandom*CM()` functions in this package.
|
||||||
|
// If this client is already connected, it is disconnected first.
|
||||||
|
func (c *Client) ConnectTo(addr *netutil.PortAddr) {
|
||||||
|
c.ConnectToBind(addr, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connects to a specific server, and binds to a specified local IP
|
||||||
|
// If this client is already connected, it is disconnected first.
|
||||||
|
func (c *Client) ConnectToBind(addr *netutil.PortAddr, local *net.TCPAddr) {
|
||||||
|
c.Disconnect()
|
||||||
|
|
||||||
|
conn, err := dialTCP(local, addr.ToTCPAddr())
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("Connect failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.conn = conn
|
||||||
|
c.writeChan = make(chan IMsg, 5)
|
||||||
|
|
||||||
|
go c.readLoop()
|
||||||
|
go c.writeLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Disconnect() {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
if c.conn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.conn.Close()
|
||||||
|
c.conn = nil
|
||||||
|
if c.heartbeat != nil {
|
||||||
|
c.heartbeat.Stop()
|
||||||
|
}
|
||||||
|
close(c.writeChan)
|
||||||
|
c.Emit(&DisconnectedEvent{})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a message to the send queue. Modifications to the given message after
|
||||||
|
// writing are not allowed (possible race conditions).
|
||||||
|
//
|
||||||
|
// Writes to this client when not connected are ignored.
|
||||||
|
func (c *Client) Write(msg IMsg) {
|
||||||
|
if cm, ok := msg.(IClientMsg); ok {
|
||||||
|
cm.SetSessionId(c.SessionId())
|
||||||
|
cm.SetSteamId(c.SteamId())
|
||||||
|
}
|
||||||
|
c.mutex.RLock()
|
||||||
|
defer c.mutex.RUnlock()
|
||||||
|
if c.conn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.writeChan <- msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) readLoop() {
|
||||||
|
for {
|
||||||
|
// This *should* be atomic on most platforms, but the Go spec doesn't guarantee it
|
||||||
|
c.mutex.RLock()
|
||||||
|
conn := c.conn
|
||||||
|
c.mutex.RUnlock()
|
||||||
|
if conn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
packet, err := conn.Read()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("Error reading from the connection: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.handlePacket(packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) writeLoop() {
|
||||||
|
for {
|
||||||
|
c.mutex.RLock()
|
||||||
|
conn := c.conn
|
||||||
|
c.mutex.RUnlock()
|
||||||
|
if conn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, ok := <-c.writeChan
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := msg.Serialize(c.writeBuf)
|
||||||
|
if err != nil {
|
||||||
|
c.writeBuf.Reset()
|
||||||
|
c.Fatalf("Error serializing message %v: %v", msg, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.Write(c.writeBuf.Bytes())
|
||||||
|
|
||||||
|
c.writeBuf.Reset()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("Error writing message %v: %v", msg, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) heartbeatLoop(seconds time.Duration) {
|
||||||
|
if c.heartbeat != nil {
|
||||||
|
c.heartbeat.Stop()
|
||||||
|
}
|
||||||
|
c.heartbeat = time.NewTicker(seconds * time.Second)
|
||||||
|
for {
|
||||||
|
_, ok := <-c.heartbeat.C
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
c.Write(NewClientMsgProtobuf(EMsg_ClientHeartBeat, new(CMsgClientHeartBeat)))
|
||||||
|
}
|
||||||
|
c.heartbeat = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handlePacket(packet *Packet) {
|
||||||
|
switch packet.EMsg {
|
||||||
|
case EMsg_ChannelEncryptRequest:
|
||||||
|
c.handleChannelEncryptRequest(packet)
|
||||||
|
case EMsg_ChannelEncryptResult:
|
||||||
|
c.handleChannelEncryptResult(packet)
|
||||||
|
case EMsg_Multi:
|
||||||
|
c.handleMulti(packet)
|
||||||
|
case EMsg_ClientCMList:
|
||||||
|
c.handleClientCMList(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.handlersMutex.RLock()
|
||||||
|
defer c.handlersMutex.RUnlock()
|
||||||
|
for _, handler := range c.handlers {
|
||||||
|
handler.HandlePacket(packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleChannelEncryptRequest(packet *Packet) {
|
||||||
|
body := NewMsgChannelEncryptRequest()
|
||||||
|
packet.ReadMsg(body)
|
||||||
|
|
||||||
|
if body.Universe != EUniverse_Public {
|
||||||
|
c.Fatalf("Invalid univserse %v!", body.Universe)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.tempSessionKey = make([]byte, 32)
|
||||||
|
rand.Read(c.tempSessionKey)
|
||||||
|
encryptedKey := cryptoutil.RSAEncrypt(GetPublicKey(EUniverse_Public), c.tempSessionKey)
|
||||||
|
|
||||||
|
payload := new(bytes.Buffer)
|
||||||
|
payload.Write(encryptedKey)
|
||||||
|
binary.Write(payload, binary.LittleEndian, crc32.ChecksumIEEE(encryptedKey))
|
||||||
|
payload.WriteByte(0)
|
||||||
|
payload.WriteByte(0)
|
||||||
|
payload.WriteByte(0)
|
||||||
|
payload.WriteByte(0)
|
||||||
|
|
||||||
|
c.Write(NewMsg(NewMsgChannelEncryptResponse(), payload.Bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleChannelEncryptResult(packet *Packet) {
|
||||||
|
body := NewMsgChannelEncryptResult()
|
||||||
|
packet.ReadMsg(body)
|
||||||
|
|
||||||
|
if body.Result != EResult_OK {
|
||||||
|
c.Fatalf("Encryption failed: %v", body.Result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.conn.SetEncryptionKey(c.tempSessionKey)
|
||||||
|
c.tempSessionKey = nil
|
||||||
|
|
||||||
|
c.Emit(&ConnectedEvent{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleMulti(packet *Packet) {
|
||||||
|
body := new(CMsgMulti)
|
||||||
|
packet.ReadProtoMsg(body)
|
||||||
|
|
||||||
|
payload := body.GetMessageBody()
|
||||||
|
|
||||||
|
if body.GetSizeUnzipped() > 0 {
|
||||||
|
r, err := gzip.NewReader(bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
c.Errorf("handleMulti: Error while decompressing: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err = ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
c.Errorf("handleMulti: Error while decompressing: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pr := bytes.NewReader(payload)
|
||||||
|
for pr.Len() > 0 {
|
||||||
|
var length uint32
|
||||||
|
binary.Read(pr, binary.LittleEndian, &length)
|
||||||
|
packetData := make([]byte, length)
|
||||||
|
pr.Read(packetData)
|
||||||
|
p, err := NewPacket(packetData)
|
||||||
|
if err != nil {
|
||||||
|
c.Errorf("Error reading packet in Multi msg %v: %v", packet, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.handlePacket(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleClientCMList(packet *Packet) {
|
||||||
|
body := new(CMsgClientCMList)
|
||||||
|
packet.ReadProtoMsg(body)
|
||||||
|
|
||||||
|
l := make([]*netutil.PortAddr, 0)
|
||||||
|
for i, ip := range body.GetCmAddresses() {
|
||||||
|
l = append(l, &netutil.PortAddr{
|
||||||
|
readIp(ip),
|
||||||
|
uint16(body.GetCmPorts()[i]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Emit(&ClientCMListEvent{l})
|
||||||
|
}
|
||||||
|
|
||||||
|
func readIp(ip uint32) net.IP {
|
||||||
|
r := make(net.IP, 4)
|
||||||
|
r[3] = byte(ip)
|
||||||
|
r[2] = byte(ip >> 8)
|
||||||
|
r[1] = byte(ip >> 16)
|
||||||
|
r[0] = byte(ip >> 24)
|
||||||
|
return r
|
||||||
|
}
|
20
vendor/github.com/Philipp15b/go-steam/client_events.go
generated
vendored
Normal file
20
vendor/github.com/Philipp15b/go-steam/client_events.go
generated
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package steam
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Philipp15b/go-steam/netutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// When this event is emitted by the Client, the connection is automatically closed.
|
||||||
|
// This may be caused by a network error, for example.
|
||||||
|
type FatalErrorEvent error
|
||||||
|
|
||||||
|
type ConnectedEvent struct{}
|
||||||
|
|
||||||
|
type DisconnectedEvent struct{}
|
||||||
|
|
||||||
|
// A list of connection manager addresses to connect to in the future.
|
||||||
|
// You should always save them and then select one of these
|
||||||
|
// instead of the builtin ones for the next connection.
|
||||||
|
type ClientCMListEvent struct {
|
||||||
|
Addresses []*netutil.PortAddr
|
||||||
|
}
|
35
vendor/github.com/Philipp15b/go-steam/community/community.go
generated
vendored
Normal file
35
vendor/github.com/Philipp15b/go-steam/community/community.go
generated
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package community
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
const cookiePath = "https://steamcommunity.com/"
|
||||||
|
|
||||||
|
func SetCookies(client *http.Client, sessionId, steamLogin, steamLoginSecure string) {
|
||||||
|
if client.Jar == nil {
|
||||||
|
client.Jar, _ = cookiejar.New(new(cookiejar.Options))
|
||||||
|
}
|
||||||
|
base, err := url.Parse(cookiePath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
client.Jar.SetCookies(base, []*http.Cookie{
|
||||||
|
// It seems that, for some reason, Steam tries to URL-decode the cookie.
|
||||||
|
&http.Cookie{
|
||||||
|
Name: "sessionid",
|
||||||
|
Value: url.QueryEscape(sessionId),
|
||||||
|
},
|
||||||
|
// steamLogin is already URL-encoded.
|
||||||
|
&http.Cookie{
|
||||||
|
Name: "steamLogin",
|
||||||
|
Value: steamLogin,
|
||||||
|
},
|
||||||
|
&http.Cookie{
|
||||||
|
Name: "steamLoginSecure",
|
||||||
|
Value: steamLoginSecure,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
127
vendor/github.com/Philipp15b/go-steam/connection.go
generated
vendored
Normal file
127
vendor/github.com/Philipp15b/go-steam/connection.go
generated
vendored
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package steam
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/Philipp15b/go-steam/cryptoutil"
|
||||||
|
. "github.com/Philipp15b/go-steam/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
type connection interface {
|
||||||
|
Read() (*Packet, error)
|
||||||
|
Write([]byte) error
|
||||||
|
Close() error
|
||||||
|
SetEncryptionKey([]byte)
|
||||||
|
IsEncrypted() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
const tcpConnectionMagic uint32 = 0x31305456 // "VT01"
|
||||||
|
|
||||||
|
type tcpConnection struct {
|
||||||
|
conn *net.TCPConn
|
||||||
|
ciph cipher.Block
|
||||||
|
cipherMutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func dialTCP(laddr, raddr *net.TCPAddr) (*tcpConnection, error) {
|
||||||
|
conn, err := net.DialTCP("tcp", laddr, raddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tcpConnection{
|
||||||
|
conn: conn,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *tcpConnection) Read() (*Packet, error) {
|
||||||
|
// All packets begin with a packet length
|
||||||
|
var packetLen uint32
|
||||||
|
err := binary.Read(c.conn, binary.LittleEndian, &packetLen)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// A magic value follows for validation
|
||||||
|
var packetMagic uint32
|
||||||
|
err = binary.Read(c.conn, binary.LittleEndian, &packetMagic)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if packetMagic != tcpConnectionMagic {
|
||||||
|
return nil, fmt.Errorf("Invalid connection magic! Expected %d, got %d!", tcpConnectionMagic, packetMagic)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, packetLen, packetLen)
|
||||||
|
_, err = io.ReadFull(c.conn, buf)
|
||||||
|
if err == io.ErrUnexpectedEOF {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Packets after ChannelEncryptResult are encrypted
|
||||||
|
c.cipherMutex.RLock()
|
||||||
|
if c.ciph != nil {
|
||||||
|
buf = cryptoutil.SymmetricDecrypt(c.ciph, buf)
|
||||||
|
}
|
||||||
|
c.cipherMutex.RUnlock()
|
||||||
|
|
||||||
|
return NewPacket(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes a message. This may only be used by one goroutine at a time.
|
||||||
|
func (c *tcpConnection) Write(message []byte) error {
|
||||||
|
c.cipherMutex.RLock()
|
||||||
|
if c.ciph != nil {
|
||||||
|
message = cryptoutil.SymmetricEncrypt(c.ciph, message)
|
||||||
|
}
|
||||||
|
c.cipherMutex.RUnlock()
|
||||||
|
|
||||||
|
err := binary.Write(c.conn, binary.LittleEndian, uint32(len(message)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = binary.Write(c.conn, binary.LittleEndian, tcpConnectionMagic)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.conn.Write(message)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *tcpConnection) Close() error {
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *tcpConnection) SetEncryptionKey(key []byte) {
|
||||||
|
c.cipherMutex.Lock()
|
||||||
|
defer c.cipherMutex.Unlock()
|
||||||
|
if key == nil {
|
||||||
|
c.ciph = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(key) != 32 {
|
||||||
|
panic("Connection AES key is not 32 bytes long!")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
c.ciph, err = aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *tcpConnection) IsEncrypted() bool {
|
||||||
|
c.cipherMutex.RLock()
|
||||||
|
defer c.cipherMutex.RUnlock()
|
||||||
|
return c.ciph != nil
|
||||||
|
}
|
38
vendor/github.com/Philipp15b/go-steam/cryptoutil/cryptoutil.go
generated
vendored
Normal file
38
vendor/github.com/Philipp15b/go-steam/cryptoutil/cryptoutil.go
generated
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package cryptoutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Performs an encryption using AES/CBC/PKCS7
|
||||||
|
// with a random IV prepended using AES/ECB/None.
|
||||||
|
func SymmetricEncrypt(ciph cipher.Block, src []byte) []byte {
|
||||||
|
// get a random IV and ECB encrypt it
|
||||||
|
iv := make([]byte, aes.BlockSize, aes.BlockSize)
|
||||||
|
_, err := rand.Read(iv)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
encryptedIv := make([]byte, aes.BlockSize, aes.BlockSize)
|
||||||
|
newECBEncrypter(ciph).CryptBlocks(encryptedIv, iv)
|
||||||
|
|
||||||
|
// pad it, copy the IV to the first 16 bytes and encrypt the rest with CBC
|
||||||
|
encrypted := padPKCS7WithIV(src)
|
||||||
|
copy(encrypted, encryptedIv)
|
||||||
|
cipher.NewCBCEncrypter(ciph, iv).CryptBlocks(encrypted[aes.BlockSize:], encrypted[aes.BlockSize:])
|
||||||
|
return encrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypts data from the reader using AES/CBC/PKCS7 with an IV
|
||||||
|
// prepended using AES/ECB/None. The src slice may not be used anymore.
|
||||||
|
func SymmetricDecrypt(ciph cipher.Block, src []byte) []byte {
|
||||||
|
iv := src[:aes.BlockSize]
|
||||||
|
newECBDecrypter(ciph).CryptBlocks(iv, iv)
|
||||||
|
|
||||||
|
data := src[aes.BlockSize:]
|
||||||
|
cipher.NewCBCDecrypter(ciph, iv).CryptBlocks(data, data)
|
||||||
|
|
||||||
|
return unpadPKCS7(data)
|
||||||
|
}
|
68
vendor/github.com/Philipp15b/go-steam/cryptoutil/ecb.go
generated
vendored
Normal file
68
vendor/github.com/Philipp15b/go-steam/cryptoutil/ecb.go
generated
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package cryptoutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/cipher"
|
||||||
|
)
|
||||||
|
|
||||||
|
// From this code review: https://codereview.appspot.com/7860047/
|
||||||
|
// by fasmat for the Go crypto/cipher package
|
||||||
|
|
||||||
|
type ecb struct {
|
||||||
|
b cipher.Block
|
||||||
|
blockSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newECB(b cipher.Block) *ecb {
|
||||||
|
return &ecb{
|
||||||
|
b: b,
|
||||||
|
blockSize: b.BlockSize(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ecbEncrypter ecb
|
||||||
|
|
||||||
|
// NewECBEncrypter returns a BlockMode which encrypts in electronic code book
|
||||||
|
// mode, using the given Block.
|
||||||
|
func newECBEncrypter(b cipher.Block) cipher.BlockMode {
|
||||||
|
return (*ecbEncrypter)(newECB(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ecbEncrypter) BlockSize() int { return x.blockSize }
|
||||||
|
|
||||||
|
func (x *ecbEncrypter) CryptBlocks(dst, src []byte) {
|
||||||
|
if len(src)%x.blockSize != 0 {
|
||||||
|
panic("cryptoutil/ecb: input not full blocks")
|
||||||
|
}
|
||||||
|
if len(dst) < len(src) {
|
||||||
|
panic("cryptoutil/ecb: output smaller than input")
|
||||||
|
}
|
||||||
|
for len(src) > 0 {
|
||||||
|
x.b.Encrypt(dst, src[:x.blockSize])
|
||||||
|
src = src[x.blockSize:]
|
||||||
|
dst = dst[x.blockSize:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ecbDecrypter ecb
|
||||||
|
|
||||||
|
// newECBDecrypter returns a BlockMode which decrypts in electronic code book
|
||||||
|
// mode, using the given Block.
|
||||||
|
func newECBDecrypter(b cipher.Block) cipher.BlockMode {
|
||||||
|
return (*ecbDecrypter)(newECB(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ecbDecrypter) BlockSize() int { return x.blockSize }
|
||||||
|
|
||||||
|
func (x *ecbDecrypter) CryptBlocks(dst, src []byte) {
|
||||||
|
if len(src)%x.blockSize != 0 {
|
||||||
|
panic("cryptoutil/ecb: input not full blocks")
|
||||||
|
}
|
||||||
|
if len(dst) < len(src) {
|
||||||
|
panic("cryptoutil/ecb: output smaller than input")
|
||||||
|
}
|
||||||
|
for len(src) > 0 {
|
||||||
|
x.b.Decrypt(dst, src[:x.blockSize])
|
||||||
|
src = src[x.blockSize:]
|
||||||
|
dst = dst[x.blockSize:]
|
||||||
|
}
|
||||||
|
}
|
25
vendor/github.com/Philipp15b/go-steam/cryptoutil/pkcs7.go
generated
vendored
Normal file
25
vendor/github.com/Philipp15b/go-steam/cryptoutil/pkcs7.go
generated
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package cryptoutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Returns a new byte array padded with PKCS7 and prepended
|
||||||
|
// with empty space of the AES block size (16 bytes) for the IV.
|
||||||
|
func padPKCS7WithIV(src []byte) []byte {
|
||||||
|
missing := aes.BlockSize - (len(src) % aes.BlockSize)
|
||||||
|
newSize := len(src) + aes.BlockSize + missing
|
||||||
|
dest := make([]byte, newSize, newSize)
|
||||||
|
copy(dest[aes.BlockSize:], src)
|
||||||
|
|
||||||
|
padding := byte(missing)
|
||||||
|
for i := newSize - missing; i < newSize; i++ {
|
||||||
|
dest[i] = padding
|
||||||
|
}
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpadPKCS7(src []byte) []byte {
|
||||||
|
padLen := src[len(src)-1]
|
||||||
|
return src[:len(src)-int(padLen)]
|
||||||
|
}
|
31
vendor/github.com/Philipp15b/go-steam/cryptoutil/rsa.go
generated
vendored
Normal file
31
vendor/github.com/Philipp15b/go-steam/cryptoutil/rsa.go
generated
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package cryptoutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parses a DER encoded RSA public key
|
||||||
|
func ParseASN1RSAPublicKey(derBytes []byte) (*rsa.PublicKey, error) {
|
||||||
|
key, err := x509.ParsePKIXPublicKey(derBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pubKey, ok := key.(*rsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("not an RSA public key")
|
||||||
|
}
|
||||||
|
return pubKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypts a message with the given public key using RSA-OAEP and the sha1 hash function.
|
||||||
|
func RSAEncrypt(pub *rsa.PublicKey, msg []byte) []byte {
|
||||||
|
b, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, pub, msg, nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
53
vendor/github.com/Philipp15b/go-steam/doc.go
generated
vendored
Normal file
53
vendor/github.com/Philipp15b/go-steam/doc.go
generated
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
This package allows you to automate actions on Valve's Steam network. It is a Go port of SteamKit.
|
||||||
|
|
||||||
|
To login, you'll have to create a new Client first. Then connect to the Steam network
|
||||||
|
and wait for a ConnectedCallback. Then you may call the Login method in the Auth module
|
||||||
|
with your login information. This is covered in more detail in the method's documentation. After you've
|
||||||
|
received the LoggedOnEvent, you should set your persona state to online to receive friend lists etc.
|
||||||
|
|
||||||
|
Example code
|
||||||
|
|
||||||
|
You can also find a running example in the `gsbot` package.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/Philipp15b/go-steam"
|
||||||
|
"github.com/Philipp15b/go-steam/protocol/steamlang"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
myLoginInfo := new(steam.LogOnDetails)
|
||||||
|
myLoginInfo.Username = "Your username"
|
||||||
|
myLoginInfo.Password = "Your password"
|
||||||
|
|
||||||
|
client := steam.NewClient()
|
||||||
|
client.Connect()
|
||||||
|
for event := range client.Events() {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *steam.ConnectedEvent:
|
||||||
|
client.Auth.LogOn(myLoginInfo)
|
||||||
|
case *steam.MachineAuthUpdateEvent:
|
||||||
|
ioutil.WriteFile("sentry", e.Hash, 0666)
|
||||||
|
case *steam.LoggedOnEvent:
|
||||||
|
client.Social.SetPersonaState(steamlang.EPersonaState_Online)
|
||||||
|
case steam.FatalErrorEvent:
|
||||||
|
log.Print(e)
|
||||||
|
case error:
|
||||||
|
log.Print(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Events
|
||||||
|
|
||||||
|
go-steam emits events that can be read via Client.Events(). Although the channel has the type interface{},
|
||||||
|
only types from this package ending with "Event" and errors will be emitted.
|
||||||
|
|
||||||
|
*/
|
||||||
|
package steam
|
3651
vendor/github.com/Philipp15b/go-steam/dota/protocol/protobuf/base.pb.go
generated
vendored
Normal file
3651
vendor/github.com/Philipp15b/go-steam/dota/protocol/protobuf/base.pb.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
18413
vendor/github.com/Philipp15b/go-steam/dota/protocol/protobuf/dota_client.pb.go
generated
vendored
Normal file
18413
vendor/github.com/Philipp15b/go-steam/dota/protocol/protobuf/dota_client.pb.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6123
vendor/github.com/Philipp15b/go-steam/dota/protocol/protobuf/dota_client_fantasy.pb.go
generated
vendored
Normal file
6123
vendor/github.com/Philipp15b/go-steam/dota/protocol/protobuf/dota_client_fantasy.pb.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
10997
vendor/github.com/Philipp15b/go-steam/dota/protocol/protobuf/dota_common.pb.go
generated
vendored
Normal file
10997
vendor/github.com/Philipp15b/go-steam/dota/protocol/protobuf/dota_common.pb.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4441
vendor/github.com/Philipp15b/go-steam/dota/protocol/protobuf/econ.pb.go
generated
vendored
Normal file
4441
vendor/github.com/Philipp15b/go-steam/dota/protocol/protobuf/econ.pb.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1825
vendor/github.com/Philipp15b/go-steam/dota/protocol/protobuf/gcsdk.pb.go
generated
vendored
Normal file
1825
vendor/github.com/Philipp15b/go-steam/dota/protocol/protobuf/gcsdk.pb.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
579
vendor/github.com/Philipp15b/go-steam/dota/protocol/protobuf/system.pb.go
generated
vendored
Normal file
579
vendor/github.com/Philipp15b/go-steam/dota/protocol/protobuf/system.pb.go
generated
vendored
Normal file
@ -0,0 +1,579 @@
|
|||||||
|
// Code generated by protoc-gen-go.
|
||||||
|
// source: gcsystemmsgs.proto
|
||||||
|
// DO NOT EDIT!
|
||||||
|
|
||||||
|
package protobuf
|
||||||
|
|
||||||
|
import proto "github.com/golang/protobuf/proto"
|
||||||
|
import fmt "fmt"
|
||||||
|
import math "math"
|
||||||
|
|
||||||
|
// Reference imports to suppress errors if they are not otherwise used.
|
||||||
|
var _ = proto.Marshal
|
||||||
|
var _ = fmt.Errorf
|
||||||
|
var _ = math.Inf
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the proto package protobuf is being compiled against.
|
||||||
|
const _ = proto.ProtoPackageIsVersion1
|
||||||
|
|
||||||
|
type EGCSystemMsg int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
EGCSystemMsg_k_EGCMsgInvalid EGCSystemMsg = 0
|
||||||
|
EGCSystemMsg_k_EGCMsgMulti EGCSystemMsg = 1
|
||||||
|
EGCSystemMsg_k_EGCMsgGenericReply EGCSystemMsg = 10
|
||||||
|
EGCSystemMsg_k_EGCMsgSystemBase EGCSystemMsg = 50
|
||||||
|
EGCSystemMsg_k_EGCMsgAchievementAwarded EGCSystemMsg = 51
|
||||||
|
EGCSystemMsg_k_EGCMsgConCommand EGCSystemMsg = 52
|
||||||
|
EGCSystemMsg_k_EGCMsgStartPlaying EGCSystemMsg = 53
|
||||||
|
EGCSystemMsg_k_EGCMsgStopPlaying EGCSystemMsg = 54
|
||||||
|
EGCSystemMsg_k_EGCMsgStartGameserver EGCSystemMsg = 55
|
||||||
|
EGCSystemMsg_k_EGCMsgStopGameserver EGCSystemMsg = 56
|
||||||
|
EGCSystemMsg_k_EGCMsgWGRequest EGCSystemMsg = 57
|
||||||
|
EGCSystemMsg_k_EGCMsgWGResponse EGCSystemMsg = 58
|
||||||
|
EGCSystemMsg_k_EGCMsgGetUserGameStatsSchema EGCSystemMsg = 59
|
||||||
|
EGCSystemMsg_k_EGCMsgGetUserGameStatsSchemaResponse EGCSystemMsg = 60
|
||||||
|
EGCSystemMsg_k_EGCMsgGetUserStatsDEPRECATED EGCSystemMsg = 61
|
||||||
|
EGCSystemMsg_k_EGCMsgGetUserStatsResponse EGCSystemMsg = 62
|
||||||
|
EGCSystemMsg_k_EGCMsgAppInfoUpdated EGCSystemMsg = 63
|
||||||
|
EGCSystemMsg_k_EGCMsgValidateSession EGCSystemMsg = 64
|
||||||
|
EGCSystemMsg_k_EGCMsgValidateSessionResponse EGCSystemMsg = 65
|
||||||
|
EGCSystemMsg_k_EGCMsgLookupAccountFromInput EGCSystemMsg = 66
|
||||||
|
EGCSystemMsg_k_EGCMsgSendHTTPRequest EGCSystemMsg = 67
|
||||||
|
EGCSystemMsg_k_EGCMsgSendHTTPRequestResponse EGCSystemMsg = 68
|
||||||
|
EGCSystemMsg_k_EGCMsgPreTestSetup EGCSystemMsg = 69
|
||||||
|
EGCSystemMsg_k_EGCMsgRecordSupportAction EGCSystemMsg = 70
|
||||||
|
EGCSystemMsg_k_EGCMsgGetAccountDetails_DEPRECATED EGCSystemMsg = 71
|
||||||
|
EGCSystemMsg_k_EGCMsgReceiveInterAppMessage EGCSystemMsg = 73
|
||||||
|
EGCSystemMsg_k_EGCMsgFindAccounts EGCSystemMsg = 74
|
||||||
|
EGCSystemMsg_k_EGCMsgPostAlert EGCSystemMsg = 75
|
||||||
|
EGCSystemMsg_k_EGCMsgGetLicenses EGCSystemMsg = 76
|
||||||
|
EGCSystemMsg_k_EGCMsgGetUserStats EGCSystemMsg = 77
|
||||||
|
EGCSystemMsg_k_EGCMsgGetCommands EGCSystemMsg = 78
|
||||||
|
EGCSystemMsg_k_EGCMsgGetCommandsResponse EGCSystemMsg = 79
|
||||||
|
EGCSystemMsg_k_EGCMsgAddFreeLicense EGCSystemMsg = 80
|
||||||
|
EGCSystemMsg_k_EGCMsgAddFreeLicenseResponse EGCSystemMsg = 81
|
||||||
|
EGCSystemMsg_k_EGCMsgGetIPLocation EGCSystemMsg = 82
|
||||||
|
EGCSystemMsg_k_EGCMsgGetIPLocationResponse EGCSystemMsg = 83
|
||||||
|
EGCSystemMsg_k_EGCMsgSystemStatsSchema EGCSystemMsg = 84
|
||||||
|
EGCSystemMsg_k_EGCMsgGetSystemStats EGCSystemMsg = 85
|
||||||
|
EGCSystemMsg_k_EGCMsgGetSystemStatsResponse EGCSystemMsg = 86
|
||||||
|
EGCSystemMsg_k_EGCMsgSendEmail EGCSystemMsg = 87
|
||||||
|
EGCSystemMsg_k_EGCMsgSendEmailResponse EGCSystemMsg = 88
|
||||||
|
EGCSystemMsg_k_EGCMsgGetEmailTemplate EGCSystemMsg = 89
|
||||||
|
EGCSystemMsg_k_EGCMsgGetEmailTemplateResponse EGCSystemMsg = 90
|
||||||
|
EGCSystemMsg_k_EGCMsgGrantGuestPass EGCSystemMsg = 91
|
||||||
|
EGCSystemMsg_k_EGCMsgGrantGuestPassResponse EGCSystemMsg = 92
|
||||||
|
EGCSystemMsg_k_EGCMsgGetAccountDetails EGCSystemMsg = 93
|
||||||
|
EGCSystemMsg_k_EGCMsgGetAccountDetailsResponse EGCSystemMsg = 94
|
||||||
|
EGCSystemMsg_k_EGCMsgGetPersonaNames EGCSystemMsg = 95
|
||||||
|
EGCSystemMsg_k_EGCMsgGetPersonaNamesResponse EGCSystemMsg = 96
|
||||||
|
EGCSystemMsg_k_EGCMsgMultiplexMsg EGCSystemMsg = 97
|
||||||
|
EGCSystemMsg_k_EGCMsgWebAPIRegisterInterfaces EGCSystemMsg = 101
|
||||||
|
EGCSystemMsg_k_EGCMsgWebAPIJobRequest EGCSystemMsg = 102
|
||||||
|
EGCSystemMsg_k_EGCMsgWebAPIJobRequestHttpResponse EGCSystemMsg = 104
|
||||||
|
EGCSystemMsg_k_EGCMsgWebAPIJobRequestForwardResponse EGCSystemMsg = 105
|
||||||
|
EGCSystemMsg_k_EGCMsgMemCachedGet EGCSystemMsg = 200
|
||||||
|
EGCSystemMsg_k_EGCMsgMemCachedGetResponse EGCSystemMsg = 201
|
||||||
|
EGCSystemMsg_k_EGCMsgMemCachedSet EGCSystemMsg = 202
|
||||||
|
EGCSystemMsg_k_EGCMsgMemCachedDelete EGCSystemMsg = 203
|
||||||
|
EGCSystemMsg_k_EGCMsgMemCachedStats EGCSystemMsg = 204
|
||||||
|
EGCSystemMsg_k_EGCMsgMemCachedStatsResponse EGCSystemMsg = 205
|
||||||
|
EGCSystemMsg_k_EGCMsgSQLStats EGCSystemMsg = 210
|
||||||
|
EGCSystemMsg_k_EGCMsgSQLStatsResponse EGCSystemMsg = 211
|
||||||
|
EGCSystemMsg_k_EGCMsgMasterSetDirectory EGCSystemMsg = 220
|
||||||
|
EGCSystemMsg_k_EGCMsgMasterSetDirectoryResponse EGCSystemMsg = 221
|
||||||
|
EGCSystemMsg_k_EGCMsgMasterSetWebAPIRouting EGCSystemMsg = 222
|
||||||
|
EGCSystemMsg_k_EGCMsgMasterSetWebAPIRoutingResponse EGCSystemMsg = 223
|
||||||
|
EGCSystemMsg_k_EGCMsgMasterSetClientMsgRouting EGCSystemMsg = 224
|
||||||
|
EGCSystemMsg_k_EGCMsgMasterSetClientMsgRoutingResponse EGCSystemMsg = 225
|
||||||
|
EGCSystemMsg_k_EGCMsgSetOptions EGCSystemMsg = 226
|
||||||
|
EGCSystemMsg_k_EGCMsgSetOptionsResponse EGCSystemMsg = 227
|
||||||
|
EGCSystemMsg_k_EGCMsgSystemBase2 EGCSystemMsg = 500
|
||||||
|
EGCSystemMsg_k_EGCMsgGetPurchaseTrustStatus EGCSystemMsg = 501
|
||||||
|
EGCSystemMsg_k_EGCMsgGetPurchaseTrustStatusResponse EGCSystemMsg = 502
|
||||||
|
EGCSystemMsg_k_EGCMsgUpdateSession EGCSystemMsg = 503
|
||||||
|
EGCSystemMsg_k_EGCMsgGCAccountVacStatusChange EGCSystemMsg = 504
|
||||||
|
EGCSystemMsg_k_EGCMsgCheckFriendship EGCSystemMsg = 505
|
||||||
|
EGCSystemMsg_k_EGCMsgCheckFriendshipResponse EGCSystemMsg = 506
|
||||||
|
EGCSystemMsg_k_EGCMsgGetPartnerAccountLink EGCSystemMsg = 507
|
||||||
|
EGCSystemMsg_k_EGCMsgGetPartnerAccountLinkResponse EGCSystemMsg = 508
|
||||||
|
EGCSystemMsg_k_EGCMsgVSReportedSuspiciousActivity EGCSystemMsg = 509
|
||||||
|
EGCSystemMsg_k_EGCMsgDPPartnerMicroTxns EGCSystemMsg = 512
|
||||||
|
EGCSystemMsg_k_EGCMsgDPPartnerMicroTxnsResponse EGCSystemMsg = 513
|
||||||
|
EGCSystemMsg_k_EGCMsgGetIPASN EGCSystemMsg = 514
|
||||||
|
EGCSystemMsg_k_EGCMsgGetIPASNResponse EGCSystemMsg = 515
|
||||||
|
EGCSystemMsg_k_EGCMsgGetAppFriendsList EGCSystemMsg = 516
|
||||||
|
EGCSystemMsg_k_EGCMsgGetAppFriendsListResponse EGCSystemMsg = 517
|
||||||
|
)
|
||||||
|
|
||||||
|
var EGCSystemMsg_name = map[int32]string{
|
||||||
|
0: "k_EGCMsgInvalid",
|
||||||
|
1: "k_EGCMsgMulti",
|
||||||
|
10: "k_EGCMsgGenericReply",
|
||||||
|
50: "k_EGCMsgSystemBase",
|
||||||
|
51: "k_EGCMsgAchievementAwarded",
|
||||||
|
52: "k_EGCMsgConCommand",
|
||||||
|
53: "k_EGCMsgStartPlaying",
|
||||||
|
54: "k_EGCMsgStopPlaying",
|
||||||
|
55: "k_EGCMsgStartGameserver",
|
||||||
|
56: "k_EGCMsgStopGameserver",
|
||||||
|
57: "k_EGCMsgWGRequest",
|
||||||
|
58: "k_EGCMsgWGResponse",
|
||||||
|
59: "k_EGCMsgGetUserGameStatsSchema",
|
||||||
|
60: "k_EGCMsgGetUserGameStatsSchemaResponse",
|
||||||
|
61: "k_EGCMsgGetUserStatsDEPRECATED",
|
||||||
|
62: "k_EGCMsgGetUserStatsResponse",
|
||||||
|
63: "k_EGCMsgAppInfoUpdated",
|
||||||
|
64: "k_EGCMsgValidateSession",
|
||||||
|
65: "k_EGCMsgValidateSessionResponse",
|
||||||
|
66: "k_EGCMsgLookupAccountFromInput",
|
||||||
|
67: "k_EGCMsgSendHTTPRequest",
|
||||||
|
68: "k_EGCMsgSendHTTPRequestResponse",
|
||||||
|
69: "k_EGCMsgPreTestSetup",
|
||||||
|
70: "k_EGCMsgRecordSupportAction",
|
||||||
|
71: "k_EGCMsgGetAccountDetails_DEPRECATED",
|
||||||
|
73: "k_EGCMsgReceiveInterAppMessage",
|
||||||
|
74: "k_EGCMsgFindAccounts",
|
||||||
|
75: "k_EGCMsgPostAlert",
|
||||||
|
76: "k_EGCMsgGetLicenses",
|
||||||
|
77: "k_EGCMsgGetUserStats",
|
||||||
|
78: "k_EGCMsgGetCommands",
|
||||||
|
79: "k_EGCMsgGetCommandsResponse",
|
||||||
|
80: "k_EGCMsgAddFreeLicense",
|
||||||
|
81: "k_EGCMsgAddFreeLicenseResponse",
|
||||||
|
82: "k_EGCMsgGetIPLocation",
|
||||||
|
83: "k_EGCMsgGetIPLocationResponse",
|
||||||
|
84: "k_EGCMsgSystemStatsSchema",
|
||||||
|
85: "k_EGCMsgGetSystemStats",
|
||||||
|
86: "k_EGCMsgGetSystemStatsResponse",
|
||||||
|
87: "k_EGCMsgSendEmail",
|
||||||
|
88: "k_EGCMsgSendEmailResponse",
|
||||||
|
89: "k_EGCMsgGetEmailTemplate",
|
||||||
|
90: "k_EGCMsgGetEmailTemplateResponse",
|
||||||
|
91: "k_EGCMsgGrantGuestPass",
|
||||||
|
92: "k_EGCMsgGrantGuestPassResponse",
|
||||||
|
93: "k_EGCMsgGetAccountDetails",
|
||||||
|
94: "k_EGCMsgGetAccountDetailsResponse",
|
||||||
|
95: "k_EGCMsgGetPersonaNames",
|
||||||
|
96: "k_EGCMsgGetPersonaNamesResponse",
|
||||||
|
97: "k_EGCMsgMultiplexMsg",
|
||||||
|
101: "k_EGCMsgWebAPIRegisterInterfaces",
|
||||||
|
102: "k_EGCMsgWebAPIJobRequest",
|
||||||
|
104: "k_EGCMsgWebAPIJobRequestHttpResponse",
|
||||||
|
105: "k_EGCMsgWebAPIJobRequestForwardResponse",
|
||||||
|
200: "k_EGCMsgMemCachedGet",
|
||||||
|
201: "k_EGCMsgMemCachedGetResponse",
|
||||||
|
202: "k_EGCMsgMemCachedSet",
|
||||||
|
203: "k_EGCMsgMemCachedDelete",
|
||||||
|
204: "k_EGCMsgMemCachedStats",
|
||||||
|
205: "k_EGCMsgMemCachedStatsResponse",
|
||||||
|
210: "k_EGCMsgSQLStats",
|
||||||
|
211: "k_EGCMsgSQLStatsResponse",
|
||||||
|
220: "k_EGCMsgMasterSetDirectory",
|
||||||
|
221: "k_EGCMsgMasterSetDirectoryResponse",
|
||||||
|
222: "k_EGCMsgMasterSetWebAPIRouting",
|
||||||
|
223: "k_EGCMsgMasterSetWebAPIRoutingResponse",
|
||||||
|
224: "k_EGCMsgMasterSetClientMsgRouting",
|
||||||
|
225: "k_EGCMsgMasterSetClientMsgRoutingResponse",
|
||||||
|
226: "k_EGCMsgSetOptions",
|
||||||
|
227: "k_EGCMsgSetOptionsResponse",
|
||||||
|
500: "k_EGCMsgSystemBase2",
|
||||||
|
501: "k_EGCMsgGetPurchaseTrustStatus",
|
||||||
|
502: "k_EGCMsgGetPurchaseTrustStatusResponse",
|
||||||
|
503: "k_EGCMsgUpdateSession",
|
||||||
|
504: "k_EGCMsgGCAccountVacStatusChange",
|
||||||
|
505: "k_EGCMsgCheckFriendship",
|
||||||
|
506: "k_EGCMsgCheckFriendshipResponse",
|
||||||
|
507: "k_EGCMsgGetPartnerAccountLink",
|
||||||
|
508: "k_EGCMsgGetPartnerAccountLinkResponse",
|
||||||
|
509: "k_EGCMsgVSReportedSuspiciousActivity",
|
||||||
|
512: "k_EGCMsgDPPartnerMicroTxns",
|
||||||
|
513: "k_EGCMsgDPPartnerMicroTxnsResponse",
|
||||||
|
514: "k_EGCMsgGetIPASN",
|
||||||
|
515: "k_EGCMsgGetIPASNResponse",
|
||||||
|
516: "k_EGCMsgGetAppFriendsList",
|
||||||
|
517: "k_EGCMsgGetAppFriendsListResponse",
|
||||||
|
}
|
||||||
|
var EGCSystemMsg_value = map[string]int32{
|
||||||
|
"k_EGCMsgInvalid": 0,
|
||||||
|
"k_EGCMsgMulti": 1,
|
||||||
|
"k_EGCMsgGenericReply": 10,
|
||||||
|
"k_EGCMsgSystemBase": 50,
|
||||||
|
"k_EGCMsgAchievementAwarded": 51,
|
||||||
|
"k_EGCMsgConCommand": 52,
|
||||||
|
"k_EGCMsgStartPlaying": 53,
|
||||||
|
"k_EGCMsgStopPlaying": 54,
|
||||||
|
"k_EGCMsgStartGameserver": 55,
|
||||||
|
"k_EGCMsgStopGameserver": 56,
|
||||||
|
"k_EGCMsgWGRequest": 57,
|
||||||
|
"k_EGCMsgWGResponse": 58,
|
||||||
|
"k_EGCMsgGetUserGameStatsSchema": 59,
|
||||||
|
"k_EGCMsgGetUserGameStatsSchemaResponse": 60,
|
||||||
|
"k_EGCMsgGetUserStatsDEPRECATED": 61,
|
||||||
|
"k_EGCMsgGetUserStatsResponse": 62,
|
||||||
|
"k_EGCMsgAppInfoUpdated": 63,
|
||||||
|
"k_EGCMsgValidateSession": 64,
|
||||||
|
"k_EGCMsgValidateSessionResponse": 65,
|
||||||
|
"k_EGCMsgLookupAccountFromInput": 66,
|
||||||
|
"k_EGCMsgSendHTTPRequest": 67,
|
||||||
|
"k_EGCMsgSendHTTPRequestResponse": 68,
|
||||||
|
"k_EGCMsgPreTestSetup": 69,
|
||||||
|
"k_EGCMsgRecordSupportAction": 70,
|
||||||
|
"k_EGCMsgGetAccountDetails_DEPRECATED": 71,
|
||||||
|
"k_EGCMsgReceiveInterAppMessage": 73,
|
||||||
|
"k_EGCMsgFindAccounts": 74,
|
||||||
|
"k_EGCMsgPostAlert": 75,
|
||||||
|
"k_EGCMsgGetLicenses": 76,
|
||||||
|
"k_EGCMsgGetUserStats": 77,
|
||||||
|
"k_EGCMsgGetCommands": 78,
|
||||||
|
"k_EGCMsgGetCommandsResponse": 79,
|
||||||
|
"k_EGCMsgAddFreeLicense": 80,
|
||||||
|
"k_EGCMsgAddFreeLicenseResponse": 81,
|
||||||
|
"k_EGCMsgGetIPLocation": 82,
|
||||||
|
"k_EGCMsgGetIPLocationResponse": 83,
|
||||||
|
"k_EGCMsgSystemStatsSchema": 84,
|
||||||
|
"k_EGCMsgGetSystemStats": 85,
|
||||||
|
"k_EGCMsgGetSystemStatsResponse": 86,
|
||||||
|
"k_EGCMsgSendEmail": 87,
|
||||||
|
"k_EGCMsgSendEmailResponse": 88,
|
||||||
|
"k_EGCMsgGetEmailTemplate": 89,
|
||||||
|
"k_EGCMsgGetEmailTemplateResponse": 90,
|
||||||
|
"k_EGCMsgGrantGuestPass": 91,
|
||||||
|
"k_EGCMsgGrantGuestPassResponse": 92,
|
||||||
|
"k_EGCMsgGetAccountDetails": 93,
|
||||||
|
"k_EGCMsgGetAccountDetailsResponse": 94,
|
||||||
|
"k_EGCMsgGetPersonaNames": 95,
|
||||||
|
"k_EGCMsgGetPersonaNamesResponse": 96,
|
||||||
|
"k_EGCMsgMultiplexMsg": 97,
|
||||||
|
"k_EGCMsgWebAPIRegisterInterfaces": 101,
|
||||||
|
"k_EGCMsgWebAPIJobRequest": 102,
|
||||||
|
"k_EGCMsgWebAPIJobRequestHttpResponse": 104,
|
||||||
|
"k_EGCMsgWebAPIJobRequestForwardResponse": 105,
|
||||||
|
"k_EGCMsgMemCachedGet": 200,
|
||||||
|
"k_EGCMsgMemCachedGetResponse": 201,
|
||||||
|
"k_EGCMsgMemCachedSet": 202,
|
||||||
|
"k_EGCMsgMemCachedDelete": 203,
|
||||||
|
"k_EGCMsgMemCachedStats": 204,
|
||||||
|
"k_EGCMsgMemCachedStatsResponse": 205,
|
||||||
|
"k_EGCMsgSQLStats": 210,
|
||||||
|
"k_EGCMsgSQLStatsResponse": 211,
|
||||||
|
"k_EGCMsgMasterSetDirectory": 220,
|
||||||
|
"k_EGCMsgMasterSetDirectoryResponse": 221,
|
||||||
|
"k_EGCMsgMasterSetWebAPIRouting": 222,
|
||||||
|
"k_EGCMsgMasterSetWebAPIRoutingResponse": 223,
|
||||||
|
"k_EGCMsgMasterSetClientMsgRouting": 224,
|
||||||
|
"k_EGCMsgMasterSetClientMsgRoutingResponse": 225,
|
||||||
|
"k_EGCMsgSetOptions": 226,
|
||||||
|
"k_EGCMsgSetOptionsResponse": 227,
|
||||||
|
"k_EGCMsgSystemBase2": 500,
|
||||||
|
"k_EGCMsgGetPurchaseTrustStatus": 501,
|
||||||
|
"k_EGCMsgGetPurchaseTrustStatusResponse": 502,
|
||||||
|
"k_EGCMsgUpdateSession": 503,
|
||||||
|
"k_EGCMsgGCAccountVacStatusChange": 504,
|
||||||
|
"k_EGCMsgCheckFriendship": 505,
|
||||||
|
"k_EGCMsgCheckFriendshipResponse": 506,
|
||||||
|
"k_EGCMsgGetPartnerAccountLink": 507,
|
||||||
|
"k_EGCMsgGetPartnerAccountLinkResponse": 508,
|
||||||
|
"k_EGCMsgVSReportedSuspiciousActivity": 509,
|
||||||
|
"k_EGCMsgDPPartnerMicroTxns": 512,
|
||||||
|
"k_EGCMsgDPPartnerMicroTxnsResponse": 513,
|
||||||
|
"k_EGCMsgGetIPASN": 514,
|
||||||
|
"k_EGCMsgGetIPASNResponse": 515,
|
||||||
|
"k_EGCMsgGetAppFriendsList": 516,
|
||||||
|
"k_EGCMsgGetAppFriendsListResponse": 517,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x EGCSystemMsg) Enum() *EGCSystemMsg {
|
||||||
|
p := new(EGCSystemMsg)
|
||||||
|
*p = x
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
func (x EGCSystemMsg) String() string {
|
||||||
|
return proto.EnumName(EGCSystemMsg_name, int32(x))
|
||||||
|
}
|
||||||
|
func (x *EGCSystemMsg) UnmarshalJSON(data []byte) error {
|
||||||
|
value, err := proto.UnmarshalJSONEnum(EGCSystemMsg_value, data, "EGCSystemMsg")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = EGCSystemMsg(value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (EGCSystemMsg) EnumDescriptor() ([]byte, []int) { return system_fileDescriptor0, []int{0} }
|
||||||
|
|
||||||
|
type ESOMsg int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
ESOMsg_k_ESOMsg_Create ESOMsg = 21
|
||||||
|
ESOMsg_k_ESOMsg_Update ESOMsg = 22
|
||||||
|
ESOMsg_k_ESOMsg_Destroy ESOMsg = 23
|
||||||
|
ESOMsg_k_ESOMsg_CacheSubscribed ESOMsg = 24
|
||||||
|
ESOMsg_k_ESOMsg_CacheUnsubscribed ESOMsg = 25
|
||||||
|
ESOMsg_k_ESOMsg_UpdateMultiple ESOMsg = 26
|
||||||
|
ESOMsg_k_ESOMsg_CacheSubscriptionRefresh ESOMsg = 28
|
||||||
|
ESOMsg_k_ESOMsg_CacheSubscribedUpToDate ESOMsg = 29
|
||||||
|
)
|
||||||
|
|
||||||
|
var ESOMsg_name = map[int32]string{
|
||||||
|
21: "k_ESOMsg_Create",
|
||||||
|
22: "k_ESOMsg_Update",
|
||||||
|
23: "k_ESOMsg_Destroy",
|
||||||
|
24: "k_ESOMsg_CacheSubscribed",
|
||||||
|
25: "k_ESOMsg_CacheUnsubscribed",
|
||||||
|
26: "k_ESOMsg_UpdateMultiple",
|
||||||
|
28: "k_ESOMsg_CacheSubscriptionRefresh",
|
||||||
|
29: "k_ESOMsg_CacheSubscribedUpToDate",
|
||||||
|
}
|
||||||
|
var ESOMsg_value = map[string]int32{
|
||||||
|
"k_ESOMsg_Create": 21,
|
||||||
|
"k_ESOMsg_Update": 22,
|
||||||
|
"k_ESOMsg_Destroy": 23,
|
||||||
|
"k_ESOMsg_CacheSubscribed": 24,
|
||||||
|
"k_ESOMsg_CacheUnsubscribed": 25,
|
||||||
|
"k_ESOMsg_UpdateMultiple": 26,
|
||||||
|
"k_ESOMsg_CacheSubscriptionRefresh": 28,
|
||||||
|
"k_ESOMsg_CacheSubscribedUpToDate": 29,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x ESOMsg) Enum() *ESOMsg {
|
||||||
|
p := new(ESOMsg)
|
||||||
|
*p = x
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
func (x ESOMsg) String() string {
|
||||||
|
return proto.EnumName(ESOMsg_name, int32(x))
|
||||||
|
}
|
||||||
|
func (x *ESOMsg) UnmarshalJSON(data []byte) error {
|
||||||
|
value, err := proto.UnmarshalJSONEnum(ESOMsg_value, data, "ESOMsg")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = ESOMsg(value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (ESOMsg) EnumDescriptor() ([]byte, []int) { return system_fileDescriptor0, []int{1} }
|
||||||
|
|
||||||
|
type EGCBaseClientMsg int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
EGCBaseClientMsg_k_EMsgGCPingRequest EGCBaseClientMsg = 3001
|
||||||
|
EGCBaseClientMsg_k_EMsgGCPingResponse EGCBaseClientMsg = 3002
|
||||||
|
EGCBaseClientMsg_k_EMsgGCClientWelcome EGCBaseClientMsg = 4004
|
||||||
|
EGCBaseClientMsg_k_EMsgGCServerWelcome EGCBaseClientMsg = 4005
|
||||||
|
EGCBaseClientMsg_k_EMsgGCClientHello EGCBaseClientMsg = 4006
|
||||||
|
EGCBaseClientMsg_k_EMsgGCServerHello EGCBaseClientMsg = 4007
|
||||||
|
EGCBaseClientMsg_k_EMsgGCClientConnectionStatus EGCBaseClientMsg = 4009
|
||||||
|
EGCBaseClientMsg_k_EMsgGCServerConnectionStatus EGCBaseClientMsg = 4010
|
||||||
|
)
|
||||||
|
|
||||||
|
var EGCBaseClientMsg_name = map[int32]string{
|
||||||
|
3001: "k_EMsgGCPingRequest",
|
||||||
|
3002: "k_EMsgGCPingResponse",
|
||||||
|
4004: "k_EMsgGCClientWelcome",
|
||||||
|
4005: "k_EMsgGCServerWelcome",
|
||||||
|
4006: "k_EMsgGCClientHello",
|
||||||
|
4007: "k_EMsgGCServerHello",
|
||||||
|
4009: "k_EMsgGCClientConnectionStatus",
|
||||||
|
4010: "k_EMsgGCServerConnectionStatus",
|
||||||
|
}
|
||||||
|
var EGCBaseClientMsg_value = map[string]int32{
|
||||||
|
"k_EMsgGCPingRequest": 3001,
|
||||||
|
"k_EMsgGCPingResponse": 3002,
|
||||||
|
"k_EMsgGCClientWelcome": 4004,
|
||||||
|
"k_EMsgGCServerWelcome": 4005,
|
||||||
|
"k_EMsgGCClientHello": 4006,
|
||||||
|
"k_EMsgGCServerHello": 4007,
|
||||||
|
"k_EMsgGCClientConnectionStatus": 4009,
|
||||||
|
"k_EMsgGCServerConnectionStatus": 4010,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x EGCBaseClientMsg) Enum() *EGCBaseClientMsg {
|
||||||
|
p := new(EGCBaseClientMsg)
|
||||||
|
*p = x
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
func (x EGCBaseClientMsg) String() string {
|
||||||
|
return proto.EnumName(EGCBaseClientMsg_name, int32(x))
|
||||||
|
}
|
||||||
|
func (x *EGCBaseClientMsg) UnmarshalJSON(data []byte) error {
|
||||||
|
value, err := proto.UnmarshalJSONEnum(EGCBaseClientMsg_value, data, "EGCBaseClientMsg")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = EGCBaseClientMsg(value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (EGCBaseClientMsg) EnumDescriptor() ([]byte, []int) { return system_fileDescriptor0, []int{2} }
|
||||||
|
|
||||||
|
type EGCToGCMsg int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
EGCToGCMsg_k_EGCToGCMsgMasterAck EGCToGCMsg = 150
|
||||||
|
EGCToGCMsg_k_EGCToGCMsgMasterAckResponse EGCToGCMsg = 151
|
||||||
|
EGCToGCMsg_k_EGCToGCMsgRouted EGCToGCMsg = 152
|
||||||
|
EGCToGCMsg_k_EGCToGCMsgRoutedReply EGCToGCMsg = 153
|
||||||
|
EGCToGCMsg_k_EMsgGCUpdateSubGCSessionInfo EGCToGCMsg = 154
|
||||||
|
EGCToGCMsg_k_EMsgGCRequestSubGCSessionInfo EGCToGCMsg = 155
|
||||||
|
EGCToGCMsg_k_EMsgGCRequestSubGCSessionInfoResponse EGCToGCMsg = 156
|
||||||
|
EGCToGCMsg_k_EGCToGCMsgMasterStartupComplete EGCToGCMsg = 157
|
||||||
|
EGCToGCMsg_k_EMsgGCToGCSOCacheSubscribe EGCToGCMsg = 158
|
||||||
|
EGCToGCMsg_k_EMsgGCToGCSOCacheUnsubscribe EGCToGCMsg = 159
|
||||||
|
EGCToGCMsg_k_EMsgGCToGCLoadSessionSOCache EGCToGCMsg = 160
|
||||||
|
EGCToGCMsg_k_EMsgGCToGCLoadSessionSOCacheResponse EGCToGCMsg = 161
|
||||||
|
EGCToGCMsg_k_EMsgGCToGCUpdateSessionStats EGCToGCMsg = 162
|
||||||
|
)
|
||||||
|
|
||||||
|
var EGCToGCMsg_name = map[int32]string{
|
||||||
|
150: "k_EGCToGCMsgMasterAck",
|
||||||
|
151: "k_EGCToGCMsgMasterAckResponse",
|
||||||
|
152: "k_EGCToGCMsgRouted",
|
||||||
|
153: "k_EGCToGCMsgRoutedReply",
|
||||||
|
154: "k_EMsgGCUpdateSubGCSessionInfo",
|
||||||
|
155: "k_EMsgGCRequestSubGCSessionInfo",
|
||||||
|
156: "k_EMsgGCRequestSubGCSessionInfoResponse",
|
||||||
|
157: "k_EGCToGCMsgMasterStartupComplete",
|
||||||
|
158: "k_EMsgGCToGCSOCacheSubscribe",
|
||||||
|
159: "k_EMsgGCToGCSOCacheUnsubscribe",
|
||||||
|
160: "k_EMsgGCToGCLoadSessionSOCache",
|
||||||
|
161: "k_EMsgGCToGCLoadSessionSOCacheResponse",
|
||||||
|
162: "k_EMsgGCToGCUpdateSessionStats",
|
||||||
|
}
|
||||||
|
var EGCToGCMsg_value = map[string]int32{
|
||||||
|
"k_EGCToGCMsgMasterAck": 150,
|
||||||
|
"k_EGCToGCMsgMasterAckResponse": 151,
|
||||||
|
"k_EGCToGCMsgRouted": 152,
|
||||||
|
"k_EGCToGCMsgRoutedReply": 153,
|
||||||
|
"k_EMsgGCUpdateSubGCSessionInfo": 154,
|
||||||
|
"k_EMsgGCRequestSubGCSessionInfo": 155,
|
||||||
|
"k_EMsgGCRequestSubGCSessionInfoResponse": 156,
|
||||||
|
"k_EGCToGCMsgMasterStartupComplete": 157,
|
||||||
|
"k_EMsgGCToGCSOCacheSubscribe": 158,
|
||||||
|
"k_EMsgGCToGCSOCacheUnsubscribe": 159,
|
||||||
|
"k_EMsgGCToGCLoadSessionSOCache": 160,
|
||||||
|
"k_EMsgGCToGCLoadSessionSOCacheResponse": 161,
|
||||||
|
"k_EMsgGCToGCUpdateSessionStats": 162,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x EGCToGCMsg) Enum() *EGCToGCMsg {
|
||||||
|
p := new(EGCToGCMsg)
|
||||||
|
*p = x
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
func (x EGCToGCMsg) String() string {
|
||||||
|
return proto.EnumName(EGCToGCMsg_name, int32(x))
|
||||||
|
}
|
||||||
|
func (x *EGCToGCMsg) UnmarshalJSON(data []byte) error {
|
||||||
|
value, err := proto.UnmarshalJSONEnum(EGCToGCMsg_value, data, "EGCToGCMsg")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = EGCToGCMsg(value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (EGCToGCMsg) EnumDescriptor() ([]byte, []int) { return system_fileDescriptor0, []int{3} }
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
proto.RegisterEnum("EGCSystemMsg", EGCSystemMsg_name, EGCSystemMsg_value)
|
||||||
|
proto.RegisterEnum("ESOMsg", ESOMsg_name, ESOMsg_value)
|
||||||
|
proto.RegisterEnum("EGCBaseClientMsg", EGCBaseClientMsg_name, EGCBaseClientMsg_value)
|
||||||
|
proto.RegisterEnum("EGCToGCMsg", EGCToGCMsg_name, EGCToGCMsg_value)
|
||||||
|
}
|
||||||
|
|
||||||
|
var system_fileDescriptor0 = []byte{
|
||||||
|
// 1475 bytes of a gzipped FileDescriptorProto
|
||||||
|
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x84, 0x57, 0x59, 0x73, 0x1b, 0xc5,
|
||||||
|
0x13, 0xcf, 0x96, 0xfc, 0xff, 0x3f, 0x4c, 0x41, 0xd1, 0x99, 0xc4, 0x47, 0x12, 0x27, 0x4a, 0x42,
|
||||||
|
0x0e, 0x62, 0xa8, 0x3c, 0x84, 0xfb, 0x46, 0x91, 0x64, 0x5b, 0x41, 0x8e, 0x15, 0x4b, 0xb6, 0xb9,
|
||||||
|
0xcd, 0x7a, 0x35, 0xb6, 0xb6, 0x2c, 0xed, 0x2c, 0x33, 0xbb, 0x26, 0x7e, 0x0b, 0xd7, 0x57, 0xe0,
|
||||||
|
0xbe, 0x8b, 0xa3, 0xe0, 0x1b, 0xc0, 0x27, 0xe0, 0x7c, 0x81, 0x57, 0xee, 0x7c, 0x01, 0x1e, 0xb8,
|
||||||
|
0x21, 0x55, 0xf4, 0xee, 0xce, 0xce, 0xce, 0x4a, 0xb2, 0x79, 0x93, 0xe6, 0xd7, 0xdd, 0xd3, 0xdd,
|
||||||
|
0xd3, 0xfd, 0xeb, 0x5e, 0x42, 0xd7, 0x1d, 0xb9, 0x25, 0x03, 0xd6, 0xeb, 0xc9, 0x75, 0x79, 0xda,
|
||||||
|
0x17, 0x3c, 0xe0, 0x53, 0x97, 0x47, 0xc9, 0x55, 0xd5, 0x99, 0x72, 0x33, 0x3e, 0x9f, 0x93, 0xeb,
|
||||||
|
0x74, 0x0f, 0xb9, 0x66, 0x63, 0x05, 0x4f, 0xf0, 0x77, 0xcd, 0xdb, 0xb4, 0xbb, 0x6e, 0x1b, 0x76,
|
||||||
|
0xd1, 0xdd, 0xe4, 0xea, 0xf4, 0x70, 0x2e, 0xec, 0x06, 0x2e, 0x58, 0x74, 0x82, 0xec, 0x4d, 0x8f,
|
||||||
|
0x66, 0x98, 0xc7, 0x84, 0xeb, 0x2c, 0x30, 0xbf, 0xbb, 0x05, 0x84, 0x8e, 0x11, 0x9a, 0x22, 0x89,
|
||||||
|
0xd9, 0xb3, 0xb6, 0x64, 0x70, 0x86, 0x1e, 0x22, 0xfb, 0xd3, 0xf3, 0x92, 0xd3, 0x71, 0xd9, 0x26,
|
||||||
|
0xeb, 0x31, 0x2f, 0x28, 0x3d, 0x69, 0x8b, 0x36, 0x6b, 0xc3, 0x8d, 0xa6, 0x5e, 0x99, 0x7b, 0x65,
|
||||||
|
0xde, 0xeb, 0xd9, 0x5e, 0x1b, 0x6e, 0x32, 0x6f, 0x6a, 0x06, 0xb6, 0x08, 0x1a, 0x5d, 0x7b, 0xcb,
|
||||||
|
0xf5, 0xd6, 0xe1, 0x66, 0x3a, 0x4e, 0xf6, 0x64, 0x08, 0xf7, 0x53, 0xe0, 0x16, 0x7a, 0x80, 0x8c,
|
||||||
|
0xe7, 0x54, 0x66, 0xec, 0x1e, 0x93, 0x4c, 0x6c, 0x32, 0x01, 0xb7, 0xd2, 0xfd, 0x64, 0xcc, 0xd4,
|
||||||
|
0x32, 0xb0, 0xdb, 0xe8, 0x28, 0xd9, 0x9d, 0x62, 0xcb, 0x33, 0x0b, 0xec, 0x89, 0x90, 0xc9, 0x00,
|
||||||
|
0x6e, 0x37, 0x5d, 0x8b, 0x8e, 0xa5, 0xcf, 0x3d, 0x0c, 0xe9, 0x0e, 0x7a, 0x94, 0x1c, 0xca, 0x92,
|
||||||
|
0x10, 0x2c, 0xa2, 0x99, 0xc8, 0x1a, 0x5e, 0x19, 0xc8, 0xa6, 0xd3, 0x61, 0x3d, 0x1b, 0xee, 0xa4,
|
||||||
|
0x53, 0xe4, 0xc4, 0xce, 0x32, 0xda, 0xde, 0x5d, 0x43, 0xec, 0xc5, 0x72, 0x95, 0x6a, 0x63, 0xa1,
|
||||||
|
0x5a, 0x2e, 0xb5, 0xaa, 0x15, 0xb8, 0x9b, 0x1e, 0x26, 0x93, 0xc3, 0x64, 0xb4, 0x95, 0x7b, 0xcc,
|
||||||
|
0x00, 0x4b, 0xbe, 0x5f, 0xf3, 0xd6, 0xf8, 0xa2, 0xdf, 0xb6, 0x03, 0x4c, 0xf2, 0xbd, 0x66, 0x66,
|
||||||
|
0x96, 0xa2, 0xc7, 0xc5, 0xe3, 0x26, 0x93, 0xd2, 0xe5, 0x1e, 0xdc, 0x47, 0xaf, 0x25, 0xc5, 0x6d,
|
||||||
|
0x40, 0x6d, 0xbd, 0x64, 0xfa, 0x58, 0xe7, 0x7c, 0x23, 0xf4, 0x4b, 0x8e, 0xc3, 0x43, 0x2f, 0x98,
|
||||||
|
0x16, 0xbc, 0x57, 0xf3, 0xfc, 0x30, 0x80, 0xb3, 0xb9, 0xfc, 0x33, 0xaf, 0x3d, 0xdb, 0x6a, 0x35,
|
||||||
|
0xd2, 0x64, 0x96, 0xcd, 0x5b, 0xfa, 0x40, 0x7d, 0x4b, 0xc5, 0x7c, 0xf4, 0x86, 0x60, 0x2d, 0x04,
|
||||||
|
0x9b, 0x2c, 0x08, 0x7d, 0xa8, 0xd2, 0x22, 0x39, 0x90, 0x22, 0x0b, 0xcc, 0xe1, 0xa2, 0xdd, 0x0c,
|
||||||
|
0x7d, 0x9f, 0x8b, 0xa0, 0xe4, 0x04, 0x51, 0x14, 0xd3, 0xf4, 0x3a, 0x72, 0xcc, 0x48, 0x90, 0xf2,
|
||||||
|
0xae, 0xc2, 0x02, 0xdb, 0xed, 0xca, 0x15, 0x23, 0x95, 0x33, 0x66, 0x28, 0x68, 0x8a, 0xb9, 0x9b,
|
||||||
|
0xac, 0xe6, 0x05, 0x4c, 0x60, 0xd2, 0xe6, 0x30, 0x6c, 0x7b, 0x9d, 0x41, 0xcd, 0x74, 0x64, 0xda,
|
||||||
|
0xf5, 0xda, 0xca, 0x9c, 0x84, 0x73, 0x66, 0xad, 0x34, 0xb8, 0x0c, 0x4a, 0x5d, 0x26, 0x02, 0xb8,
|
||||||
|
0xdf, 0x2c, 0x4a, 0xbc, 0xbe, 0xee, 0x3a, 0x0c, 0x23, 0x92, 0x50, 0xcf, 0x77, 0x4c, 0xf6, 0x70,
|
||||||
|
0x30, 0xd7, 0xa7, 0xa2, 0x2a, 0x5f, 0xc2, 0x79, 0x33, 0x56, 0x03, 0xd0, 0x69, 0x9a, 0xcf, 0x3d,
|
||||||
|
0x75, 0xbb, 0x3d, 0x2d, 0x18, 0x53, 0x17, 0x42, 0xc3, 0x8c, 0x2e, 0x8f, 0x69, 0xfd, 0x0b, 0x74,
|
||||||
|
0x1f, 0x19, 0x35, 0x2e, 0xa8, 0x35, 0xea, 0xdc, 0xb1, 0xe3, 0x34, 0x2e, 0xd0, 0x23, 0xe4, 0xe0,
|
||||||
|
0x50, 0x48, 0x6b, 0x37, 0xe9, 0x41, 0xb2, 0x2f, 0xdf, 0xe9, 0x66, 0xe5, 0xb7, 0x4c, 0xe7, 0xd0,
|
||||||
|
0x82, 0x21, 0x01, 0x8b, 0x7d, 0x95, 0x6e, 0x60, 0xda, 0xfc, 0x92, 0x99, 0xe0, 0xa8, 0x50, 0xaa,
|
||||||
|
0x3d, 0x7c, 0x41, 0x58, 0xce, 0xdd, 0x9a, 0x1e, 0x6b, 0xad, 0x07, 0xe8, 0x24, 0x99, 0x30, 0x2c,
|
||||||
|
0xc7, 0x68, 0x8b, 0xf5, 0xfc, 0x2e, 0x16, 0x33, 0x3c, 0x48, 0x8f, 0x91, 0xc3, 0xdb, 0xa1, 0xda,
|
||||||
|
0xc6, 0x43, 0x39, 0xcf, 0x85, 0xed, 0x05, 0x33, 0x51, 0x75, 0x36, 0x6c, 0x29, 0xe1, 0xe1, 0x9c,
|
||||||
|
0xe7, 0x39, 0x4c, 0xeb, 0x3f, 0x62, 0xba, 0x38, 0x50, 0x82, 0xf0, 0x28, 0x3d, 0x4e, 0x8e, 0x6c,
|
||||||
|
0x0b, 0x6b, 0x2b, 0x8f, 0x99, 0x5d, 0x84, 0x62, 0x0d, 0x26, 0x24, 0xf7, 0xec, 0xf3, 0x11, 0x5d,
|
||||||
|
0xc1, 0x8a, 0xd9, 0x45, 0x7d, 0xa0, 0xb6, 0xf0, 0xb8, 0x59, 0x72, 0x31, 0x6f, 0xfb, 0x5d, 0x76,
|
||||||
|
0x11, 0x7f, 0x83, 0x6d, 0xe6, 0x61, 0x99, 0xad, 0x96, 0x1a, 0xb5, 0x05, 0xb6, 0xee, 0xe2, 0x23,
|
||||||
|
0x88, 0xb8, 0x03, 0xd6, 0x6c, 0x07, 0x2f, 0x61, 0x66, 0x2e, 0x13, 0xa9, 0x73, 0x7c, 0x35, 0x6d,
|
||||||
|
0xe4, 0x35, 0xb3, 0xd1, 0xfa, 0xd1, 0xd9, 0x20, 0xf0, 0xb5, 0x1f, 0x1d, 0x7a, 0x3d, 0x39, 0xb9,
|
||||||
|
0x9d, 0xe4, 0x34, 0x17, 0xd1, 0x04, 0xd0, 0xc2, 0x2e, 0xd6, 0x64, 0xe6, 0x34, 0xeb, 0x95, 0x6d,
|
||||||
|
0x2c, 0xa7, 0x36, 0x86, 0x08, 0x9f, 0x58, 0x58, 0x93, 0x93, 0xc3, 0x20, 0xad, 0xfc, 0xa9, 0x35,
|
||||||
|
0x54, 0x1b, 0xa9, 0x03, 0x3e, 0xb3, 0x30, 0x9a, 0xf1, 0x01, 0xa8, 0xc2, 0xba, 0x0c, 0x0b, 0xe3,
|
||||||
|
0x73, 0x0b, 0xb3, 0x3d, 0x36, 0xa8, 0x18, 0x57, 0xeb, 0x17, 0x16, 0x66, 0xfb, 0xd0, 0x70, 0x50,
|
||||||
|
0x5f, 0xfd, 0xa5, 0x85, 0xf5, 0x0a, 0xba, 0x30, 0x2f, 0xd4, 0x13, 0xdd, 0xaf, 0x2c, 0x2c, 0x86,
|
||||||
|
0x89, 0xfe, 0x63, 0xad, 0xf5, 0xb5, 0x85, 0x3d, 0xae, 0xc7, 0xe2, 0x9c, 0x1d, 0xbd, 0x00, 0x7a,
|
||||||
|
0x5b, 0x71, 0x05, 0x73, 0x02, 0x2e, 0xb6, 0xe0, 0x1b, 0x8b, 0x9e, 0x24, 0x47, 0xb7, 0x17, 0xd0,
|
||||||
|
0x96, 0xbe, 0xcd, 0x3b, 0x99, 0x0a, 0xaa, 0xc7, 0xe5, 0x61, 0x10, 0x4d, 0xc6, 0xef, 0x2c, 0x7c,
|
||||||
|
0x8a, 0x13, 0x3b, 0x0b, 0x69, 0x8b, 0xdf, 0x5b, 0xf4, 0x44, 0x56, 0xa8, 0x5a, 0xb8, 0xdc, 0x75,
|
||||||
|
0x71, 0x6c, 0x47, 0x94, 0xa9, 0x8c, 0xfe, 0x60, 0xd1, 0xd3, 0xe4, 0xd4, 0x7f, 0xca, 0x69, 0xbb,
|
||||||
|
0x3f, 0x5a, 0x48, 0x78, 0xd9, 0x8a, 0xc0, 0x82, 0x79, 0x3f, 0xe2, 0x15, 0x09, 0x3f, 0xe5, 0x92,
|
||||||
|
0x91, 0x01, 0x5a, 0xf3, 0x72, 0xb4, 0x76, 0xec, 0x19, 0x5c, 0x2e, 0xce, 0xc0, 0x2f, 0x05, 0x33,
|
||||||
|
0xfa, 0xa8, 0x21, 0x42, 0xe1, 0x74, 0x10, 0x6a, 0x89, 0x10, 0x47, 0x07, 0xe6, 0x3c, 0x94, 0xf0,
|
||||||
|
0x6b, 0xc1, 0x8c, 0x7e, 0xb8, 0x90, 0xbe, 0xeb, 0xb7, 0x02, 0xb2, 0x80, 0x26, 0xc7, 0x64, 0x80,
|
||||||
|
0xa6, 0x93, 0xf2, 0xf7, 0x02, 0xb6, 0x70, 0xc6, 0x23, 0x65, 0xd5, 0xc1, 0x4b, 0xb6, 0x93, 0x18,
|
||||||
|
0x29, 0x77, 0x6c, 0x0f, 0x87, 0xc7, 0x1f, 0x05, 0xb3, 0xe4, 0xca, 0x1d, 0xe6, 0x6c, 0x4c, 0x0b,
|
||||||
|
0x4c, 0x4a, 0x5b, 0x76, 0x5c, 0x1f, 0xfe, 0x2c, 0x60, 0x13, 0x16, 0xb7, 0x41, 0xb5, 0x1b, 0x7f,
|
||||||
|
0x15, 0x90, 0x70, 0x4c, 0x22, 0x6e, 0xe0, 0x3a, 0x83, 0xeb, 0x96, 0xba, 0xb2, 0xee, 0x7a, 0x1b,
|
||||||
|
0xf0, 0x77, 0x01, 0x97, 0x8c, 0xe3, 0x3b, 0xca, 0x68, 0x7b, 0xff, 0x14, 0xe8, 0xa9, 0xac, 0x6d,
|
||||||
|
0x97, 0x9a, 0xb8, 0xb4, 0xe1, 0xec, 0xc4, 0x62, 0x0e, 0xa5, 0xef, 0x3a, 0x2e, 0x0f, 0x65, 0x34,
|
||||||
|
0x47, 0x37, 0xdd, 0x60, 0x0b, 0xae, 0x14, 0xcc, 0xe7, 0xa8, 0x34, 0x94, 0xd5, 0x39, 0xd7, 0x11,
|
||||||
|
0xbc, 0x75, 0x11, 0xdf, 0xeb, 0xd2, 0x88, 0x59, 0x9b, 0x83, 0x02, 0xfa, 0xd2, 0xa7, 0x46, 0xcc,
|
||||||
|
0xde, 0x88, 0xa7, 0x49, 0xa9, 0x79, 0x1e, 0x9e, 0x1e, 0x31, 0x7b, 0x23, 0x3d, 0xd6, 0x5a, 0xcf,
|
||||||
|
0x8c, 0xe0, 0xca, 0x98, 0xe3, 0x51, 0xdf, 0x57, 0x19, 0xaa, 0x23, 0x55, 0xc1, 0xb3, 0x23, 0x66,
|
||||||
|
0x7d, 0x0e, 0xe0, 0xda, 0xce, 0x73, 0x23, 0x53, 0x3f, 0x5b, 0xe4, 0xff, 0xd5, 0xe6, 0x7c, 0xb6,
|
||||||
|
0xdf, 0xc6, 0xbf, 0x57, 0xca, 0x82, 0x45, 0x53, 0x61, 0x34, 0x77, 0x98, 0x3c, 0x35, 0x8c, 0xd1,
|
||||||
|
0xbd, 0xb1, 0xcb, 0xc9, 0x61, 0x05, 0x99, 0x4a, 0xf0, 0x2d, 0x18, 0x57, 0x94, 0xa8, 0xf4, 0x23,
|
||||||
|
0x1e, 0x68, 0x86, 0xab, 0xd2, 0x11, 0xee, 0x2a, 0xae, 0x57, 0x13, 0x6a, 0xc7, 0x35, 0xd0, 0x45,
|
||||||
|
0x4f, 0x66, 0xf8, 0x3e, 0x45, 0xe9, 0xe6, 0x45, 0x29, 0x2f, 0xc3, 0x7e, 0x35, 0x16, 0x06, 0x4d,
|
||||||
|
0xfb, 0xc9, 0xd8, 0x5d, 0x13, 0x4c, 0x76, 0x60, 0x52, 0x51, 0xf7, 0x50, 0x0f, 0x16, 0xfd, 0x16,
|
||||||
|
0xaf, 0x44, 0xde, 0x1f, 0x9c, 0xba, 0x62, 0x11, 0xc0, 0xcc, 0x44, 0xed, 0xa1, 0x3b, 0x51, 0x75,
|
||||||
|
0x4f, 0x5c, 0xb3, 0x8d, 0xb8, 0x25, 0x13, 0x2a, 0xff, 0x68, 0x5c, 0xd1, 0xa6, 0x81, 0xa8, 0xe4,
|
||||||
|
0x7d, 0x3c, 0xae, 0xda, 0x20, 0x86, 0x12, 0x4b, 0xcb, 0xac, 0xeb, 0xf0, 0x1e, 0x83, 0x77, 0x8a,
|
||||||
|
0x26, 0xd6, 0x8c, 0x77, 0xe8, 0x14, 0x7b, 0xb7, 0x68, 0x5e, 0x96, 0xe8, 0xcd, 0xb2, 0x6e, 0x97,
|
||||||
|
0xc3, 0x7b, 0x39, 0x24, 0xd1, 0x4a, 0x90, 0xf7, 0x8b, 0xaa, 0x89, 0x0d, 0x1d, 0xfc, 0x12, 0xf0,
|
||||||
|
0x58, 0xbc, 0xd9, 0xa9, 0x26, 0xfe, 0x20, 0x27, 0x94, 0xa8, 0x0f, 0x08, 0x7d, 0x58, 0x9c, 0xba,
|
||||||
|
0x5c, 0x20, 0x04, 0xe3, 0x6f, 0xf1, 0xb8, 0x3a, 0x74, 0x2f, 0xab, 0xff, 0x09, 0x4b, 0x95, 0x9c,
|
||||||
|
0x0d, 0x78, 0xde, 0xd2, 0x0d, 0xd6, 0x8f, 0xe9, 0x24, 0xbc, 0x90, 0x31, 0x96, 0x92, 0x89, 0x38,
|
||||||
|
0x0d, 0x1f, 0xf4, 0xc5, 0x6c, 0xa8, 0xe4, 0x80, 0xe4, 0x53, 0xe8, 0x25, 0xcb, 0x74, 0x55, 0x51,
|
||||||
|
0x48, 0xb8, 0x1a, 0x79, 0x1d, 0xf3, 0x48, 0xb4, 0x99, 0xc3, 0xcb, 0x96, 0xa2, 0x81, 0x58, 0x48,
|
||||||
|
0xbd, 0xc8, 0x80, 0xd4, 0x2b, 0x16, 0xbd, 0x21, 0x9e, 0xa1, 0x3b, 0x49, 0x69, 0x7f, 0x5f, 0xcd,
|
||||||
|
0x98, 0x3b, 0x17, 0x53, 0xfc, 0x2d, 0x14, 0xfa, 0xb8, 0x47, 0xfa, 0xf1, 0xd4, 0x7b, 0x2d, 0x9d,
|
||||||
|
0xa8, 0xb1, 0xd5, 0x48, 0xb4, 0x39, 0x9f, 0xaf, 0x28, 0x78, 0x3d, 0x17, 0x83, 0x21, 0x62, 0x14,
|
||||||
|
0x36, 0xbc, 0x31, 0x20, 0x54, 0xe7, 0x76, 0x5b, 0x79, 0xa6, 0xe4, 0xe1, 0xcd, 0x74, 0xf6, 0xec,
|
||||||
|
0x20, 0xa4, 0x23, 0x78, 0x6b, 0xc0, 0x62, 0x8e, 0x81, 0x93, 0xd9, 0xfa, 0xb6, 0x75, 0xf6, 0x7f,
|
||||||
|
0xb3, 0xd6, 0x25, 0x6b, 0xd7, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x05, 0xab, 0xaf, 0x14, 0xda,
|
||||||
|
0x0e, 0x00, 0x00,
|
||||||
|
}
|
188
vendor/github.com/Philipp15b/go-steam/economy/inventory/inventory.go
generated
vendored
Normal file
188
vendor/github.com/Philipp15b/go-steam/economy/inventory/inventory.go
generated
vendored
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
Includes inventory types as used in the trade package
|
||||||
|
*/
|
||||||
|
package inventory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/Philipp15b/go-steam/jsont"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GenericInventory map[uint32]map[uint64]*Inventory
|
||||||
|
|
||||||
|
func NewGenericInventory() GenericInventory {
|
||||||
|
iMap := make(map[uint32]map[uint64]*Inventory)
|
||||||
|
return GenericInventory(iMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get inventory for specified AppId and ContextId
|
||||||
|
func (i *GenericInventory) Get(appId uint32, contextId uint64) (*Inventory, error) {
|
||||||
|
iMap := (map[uint32]map[uint64]*Inventory)(*i)
|
||||||
|
iMap2, ok := iMap[appId]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("inventory for specified appId not found")
|
||||||
|
}
|
||||||
|
inv, ok := iMap2[contextId]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("inventory for specified contextId not found")
|
||||||
|
}
|
||||||
|
return inv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *GenericInventory) Add(appId uint32, contextId uint64, inv *Inventory) {
|
||||||
|
iMap := (map[uint32]map[uint64]*Inventory)(*i)
|
||||||
|
iMap2, ok := iMap[appId]
|
||||||
|
if !ok {
|
||||||
|
iMap2 = make(map[uint64]*Inventory)
|
||||||
|
iMap[appId] = iMap2
|
||||||
|
}
|
||||||
|
iMap2[contextId] = inv
|
||||||
|
}
|
||||||
|
|
||||||
|
type Inventory struct {
|
||||||
|
Items Items `json:"rgInventory"`
|
||||||
|
Currencies Currencies `json:"rgCurrency"`
|
||||||
|
Descriptions Descriptions `json:"rgDescriptions"`
|
||||||
|
AppInfo *AppInfo `json:"rgAppInfo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Items key is an AssetId
|
||||||
|
type Items map[string]*Item
|
||||||
|
|
||||||
|
func (i *Items) ToMap() map[string]*Item {
|
||||||
|
return (map[string]*Item)(*i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Items) Get(assetId uint64) (*Item, error) {
|
||||||
|
iMap := (map[string]*Item)(*i)
|
||||||
|
if item, ok := iMap[strconv.FormatUint(assetId, 10)]; ok {
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("item not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Items) UnmarshalJSON(data []byte) error {
|
||||||
|
if bytes.Equal(data, []byte("[]")) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, (*map[string]*Item)(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Currencies map[string]*Currency
|
||||||
|
|
||||||
|
func (c *Currencies) ToMap() map[string]*Currency {
|
||||||
|
return (map[string]*Currency)(*c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Currencies) UnmarshalJSON(data []byte) error {
|
||||||
|
if bytes.Equal(data, []byte("[]")) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, (*map[string]*Currency)(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descriptions key format is %d_%d, first %d is ClassId, second is InstanceId
|
||||||
|
type Descriptions map[string]*Description
|
||||||
|
|
||||||
|
func (d *Descriptions) ToMap() map[string]*Description {
|
||||||
|
return (map[string]*Description)(*d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Descriptions) Get(classId uint64, instanceId uint64) (*Description, error) {
|
||||||
|
dMap := (map[string]*Description)(*d)
|
||||||
|
descId := fmt.Sprintf("%v_%v", classId, instanceId)
|
||||||
|
if desc, ok := dMap[descId]; ok {
|
||||||
|
return desc, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("description not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Descriptions) UnmarshalJSON(data []byte) error {
|
||||||
|
if bytes.Equal(data, []byte("[]")) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, (*map[string]*Description)(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Item struct {
|
||||||
|
Id uint64 `json:",string"`
|
||||||
|
ClassId uint64 `json:",string"`
|
||||||
|
InstanceId uint64 `json:",string"`
|
||||||
|
Amount uint64 `json:",string"`
|
||||||
|
Pos uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type Currency struct {
|
||||||
|
Id uint64 `json:",string"`
|
||||||
|
ClassId uint64 `json:",string"`
|
||||||
|
IsCurrency bool `json:"is_currency"`
|
||||||
|
Pos uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type Description struct {
|
||||||
|
AppId uint32 `json:",string"`
|
||||||
|
ClassId uint64 `json:",string"`
|
||||||
|
InstanceId uint64 `json:",string"`
|
||||||
|
|
||||||
|
IconUrl string `json:"icon_url"`
|
||||||
|
IconUrlLarge string `json:"icon_url_large"`
|
||||||
|
IconDragUrl string `json:"icon_drag_url"`
|
||||||
|
|
||||||
|
Name string
|
||||||
|
MarketName string `json:"market_name"`
|
||||||
|
MarketHashName string `json:"market_hash_name"`
|
||||||
|
|
||||||
|
// Colors in hex, for example `B2B2B2`
|
||||||
|
NameColor string `json:"name_color"`
|
||||||
|
BackgroundColor string `json:"background_color"`
|
||||||
|
|
||||||
|
Type string
|
||||||
|
|
||||||
|
Tradable jsont.UintBool
|
||||||
|
Marketable jsont.UintBool
|
||||||
|
Commodity jsont.UintBool
|
||||||
|
MarketTradableRestriction uint32 `json:"market_tradable_restriction,string"`
|
||||||
|
|
||||||
|
Descriptions DescriptionLines
|
||||||
|
Actions []*Action
|
||||||
|
// Application-specific data, like "def_index" and "quality" for TF2
|
||||||
|
AppData map[string]string
|
||||||
|
Tags []*Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
type DescriptionLines []*DescriptionLine
|
||||||
|
|
||||||
|
func (d *DescriptionLines) UnmarshalJSON(data []byte) error {
|
||||||
|
if bytes.Equal(data, []byte(`""`)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, (*[]*DescriptionLine)(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
type DescriptionLine struct {
|
||||||
|
Value string
|
||||||
|
Type *string // Is `html` for HTML descriptions
|
||||||
|
Color *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action struct {
|
||||||
|
Name string
|
||||||
|
Link string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppInfo struct {
|
||||||
|
AppId uint32
|
||||||
|
Name string
|
||||||
|
Icon string
|
||||||
|
Link string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tag struct {
|
||||||
|
InternalName string `json:internal_name`
|
||||||
|
Name string
|
||||||
|
Category string
|
||||||
|
CategoryName string `json:category_name`
|
||||||
|
}
|
79
vendor/github.com/Philipp15b/go-steam/economy/inventory/inventory_apps.go
generated
vendored
Normal file
79
vendor/github.com/Philipp15b/go-steam/economy/inventory/inventory_apps.go
generated
vendored
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package inventory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/Philipp15b/go-steam/steamid"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InventoryApps map[string]*InventoryApp
|
||||||
|
|
||||||
|
func (i *InventoryApps) Get(appId uint32) (*InventoryApp, error) {
|
||||||
|
iMap := (map[string]*InventoryApp)(*i)
|
||||||
|
if inventoryApp, ok := iMap[strconv.FormatUint(uint64(appId), 10)]; ok {
|
||||||
|
return inventoryApp, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("inventory app not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *InventoryApps) ToMap() map[string]*InventoryApp {
|
||||||
|
return (map[string]*InventoryApp)(*i)
|
||||||
|
}
|
||||||
|
|
||||||
|
type InventoryApp struct {
|
||||||
|
AppId uint32
|
||||||
|
Name string
|
||||||
|
Icon string
|
||||||
|
Link string
|
||||||
|
AssetCount uint32 `json:"asset_count"`
|
||||||
|
InventoryLogo string `json:"inventory_logo"`
|
||||||
|
TradePermissions string `json:"trade_permissions"`
|
||||||
|
Contexts Contexts `json:"rgContexts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Contexts map[string]*Context
|
||||||
|
|
||||||
|
func (c *Contexts) Get(contextId uint64) (*Context, error) {
|
||||||
|
cMap := (map[string]*Context)(*c)
|
||||||
|
if context, ok := cMap[strconv.FormatUint(contextId, 10)]; ok {
|
||||||
|
return context, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("context not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Contexts) ToMap() map[string]*Context {
|
||||||
|
return (map[string]*Context)(*c)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
ContextId uint64 `json:"id,string"`
|
||||||
|
AssetCount uint32 `json:"asset_count"`
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetInventoryApps(client *http.Client, steamId steamid.SteamId) (InventoryApps, error) {
|
||||||
|
resp, err := http.Get("http://steamcommunity.com/profiles/" + steamId.ToString() + "/inventory/")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
respBody, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reg := regexp.MustCompile("var g_rgAppContextData = (.*?);")
|
||||||
|
inventoryAppsMatches := reg.FindSubmatch(respBody)
|
||||||
|
if inventoryAppsMatches == nil {
|
||||||
|
return nil, fmt.Errorf("profile inventory not found in steam response")
|
||||||
|
}
|
||||||
|
var inventoryApps InventoryApps
|
||||||
|
if err = json.Unmarshal(inventoryAppsMatches[1], &inventoryApps); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return inventoryApps, nil
|
||||||
|
}
|
28
vendor/github.com/Philipp15b/go-steam/economy/inventory/own.go
generated
vendored
Normal file
28
vendor/github.com/Philipp15b/go-steam/economy/inventory/own.go
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package inventory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetPartialOwnInventory(client *http.Client, contextId uint64, appId uint32, start *uint) (*PartialInventory, error) {
|
||||||
|
// TODO: the "trading" parameter can be left off to return non-tradable items too
|
||||||
|
url := fmt.Sprintf("http://steamcommunity.com/my/inventory/json/%d/%d?trading=1", appId, contextId)
|
||||||
|
if start != nil {
|
||||||
|
url += "&start=" + strconv.FormatUint(uint64(*start), 10)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return DoInventoryRequest(client, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOwnInventory(client *http.Client, contextId uint64, appId uint32) (*Inventory, error) {
|
||||||
|
return GetFullInventory(func() (*PartialInventory, error) {
|
||||||
|
return GetPartialOwnInventory(client, contextId, appId, nil)
|
||||||
|
}, func(start uint) (*PartialInventory, error) {
|
||||||
|
return GetPartialOwnInventory(client, contextId, appId, &start)
|
||||||
|
})
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user